declaration parser

This commit is contained in:
Will McGugan
2021-12-18 17:35:59 +00:00
parent fe40123ea7
commit e87f561428
10 changed files with 169 additions and 42 deletions

View File

@@ -1,4 +1,13 @@
App > DockView {
App > View {
docks: main=top;
}
#sidebar {
dock-group: main;
}
/*
App > View {
layout: dock;
docks: side=left/1 header=top footer=bottom;
layers: base panels;
@@ -37,3 +46,4 @@ App > DockView {
dock-group: header;
text: on #20639b;
}
*/

View File

@@ -6,7 +6,7 @@ class BasicApp(App):
"""A basic app demonstrating CSS"""
def on_load(self):
self.bind("t", "toggle_class('#sidebar', '-active')")
self.bind("tab", "toggle_class('#sidebar', '-active')")
def on_mount(self):
"""Build layout here."""

View File

@@ -15,7 +15,7 @@ ANSI_SEQUENCES: Dict[str, Tuple[Keys, ...]] = {
"\x06": (Keys.ControlF,), # Control-F (cursor forward)
"\x07": (Keys.ControlG,), # Control-G
"\x08": (Keys.ControlH,), # Control-H (8) (Identical to '\b')
"\x09": (Keys.ControlI,), # Control-I (9) (Identical to '\t')
"\x09": (Keys.Tab,), # Control-I (9) (Identical to '\t')
"\x0a": (Keys.ControlJ,), # Control-J (10) (Identical to '\n')
"\x0b": (Keys.ControlK,), # Control-K (delete until end of line; vertical tab)
"\x0c": (Keys.ControlL,), # Control-L (clear; form feed)

View File

@@ -498,7 +498,7 @@ class App(DOMNode):
# Handle input events that haven't been forwarded
# If the event has been forwarded it may have bubbled up back to the App
if isinstance(event, events.Mount):
view = DockView()
view = View()
self.register(self, view)
await self.push_view(view)
await super().on_event(event)

View File

@@ -67,6 +67,7 @@ class ScalarProperty:
if new_value is not None and new_value.is_percent:
new_value = Scalar(float(new_value.value), self.percent_unit, Unit.WIDTH)
setattr(obj, self.internal_name, new_value)
obj.refresh()
return value
@@ -100,6 +101,7 @@ class BoxProperty:
else:
new_value = (_type, Style.from_color(Color.parse(color)))
setattr(obj, self.internal_name, new_value)
obj.refresh()
return border
@@ -151,6 +153,7 @@ class BorderProperty:
| None,
) -> None:
top, right, bottom, left = self._properties
obj.refresh()
if border is None:
setattr(obj, top, None)
setattr(obj, right, None)
@@ -207,6 +210,7 @@ class StyleProperty:
return style
def __set__(self, obj: Styles, style: Style | str | None) -> Style | str | None:
obj.refresh()
if style is None:
setattr(obj, self._color_name, None)
setattr(obj, self._bgcolor_name, None)
@@ -232,6 +236,7 @@ class SpacingProperty:
return getattr(obj, self._internal_name) or NULL_SPACING
def __set__(self, obj: Styles, spacing: SpacingDimensions) -> Spacing:
obj.refresh(True)
spacing = Spacing.unpack(spacing)
setattr(obj, self._internal_name, spacing)
return spacing
@@ -246,6 +251,7 @@ class DocksProperty:
def __set__(
self, obj: Styles, docks: Iterable[DockGroup] | None
) -> Iterable[DockGroup] | None:
obj.refresh(True)
if docks is None:
obj._rule_docks = None
else:
@@ -258,6 +264,7 @@ class DockGroupProperty:
return obj._rule_dock_group or ""
def __set__(self, obj: Styles, spacing: str | None) -> str | None:
obj.refresh(True)
obj._rule_dock_group = spacing
return spacing
@@ -274,6 +281,7 @@ class OffsetProperty:
def __set__(
self, obj: Styles, offset: tuple[int | str, int | str]
) -> tuple[int | str, int | str]:
obj.refresh(True)
x, y = offset
scalar_x = (
Scalar.parse(x, Unit.WIDTH)
@@ -299,6 +307,7 @@ class IntegerProperty:
return getattr(obj, self._internal_name, 0)
def __set__(self, obj: Styles, value: int | None) -> int | None:
obj.refresh()
if not isinstance(value, int):
raise StyleTypeError(f"{self._name} must be a str")
setattr(obj, self._internal_name, value)
@@ -318,6 +327,7 @@ class StringProperty:
return getattr(obj, self._internal_name, None) or self._default
def __set__(self, obj: Styles, value: str | None = None) -> str | None:
obj.refresh()
if value is not None:
if value not in self._valid_values:
raise StyleValueError(
@@ -336,6 +346,7 @@ class NameProperty:
return getattr(obj, self._internal_name) or ""
def __set__(self, obj: Styles, name: str | None) -> str | None:
obj.refresh(True)
if not isinstance(name, str):
raise StyleTypeError(f"{self._name} must be a str")
setattr(obj, self._internal_name, name)
@@ -355,6 +366,7 @@ class NameListProperty:
def __set__(
self, obj: Styles, names: str | tuple[str] | None = None
) -> str | tuple[str] | None:
obj.refresh(True)
names_value: tuple[str, ...] | None = None
if isinstance(names, str):
names_value = tuple(name.strip().lower() for name in names.split(" "))
@@ -375,6 +387,7 @@ class ColorProperty:
return getattr(obj, self._internal_name, None) or Color.default()
def __set__(self, obj: Styles, color: Color | str | None) -> Color | str | None:
obj.refresh()
if color is None:
setattr(self, self._internal_name, None)
else:
@@ -409,6 +422,7 @@ class StyleFlagsProperty:
return getattr(obj, self._internal_name, None) or Style.null()
def __set__(self, obj: Styles, style_flags: str | None) -> str | None:
obj.refresh()
if style_flags is None:
setattr(self, self._internal_name, None)
else:

View File

@@ -5,7 +5,8 @@ from rich import print
from functools import lru_cache
from typing import Iterator, Iterable
from .tokenize import tokenize, Token
from .styles import Styles
from .tokenize import tokenize, tokenize_declarations, Token
from .tokenizer import EOFError
from .model import (
@@ -157,6 +158,44 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
yield rule_set
def parse_declarations(css: str, path: str) -> Styles:
tokens = iter(tokenize_declarations(css, path))
styles_builder = StylesBuilder()
declaration: Declaration | None = None
errors: list[tuple[Token, str]] = []
while True:
token = next(tokens, None)
if token is None:
break
token_name = token.name
if token_name in ("whitespace", "declaration_end", "eof"):
continue
if token_name == "declaration_name":
if declaration and declaration.tokens:
try:
styles_builder.add_declaration(declaration)
except DeclarationError as error:
errors.append((error.token, error.message))
declaration = Declaration(token, "")
declaration.name = token.value.rstrip(":")
elif token_name == "declaration_set_end":
break
else:
if declaration:
declaration.tokens.append(token)
if declaration and declaration.tokens:
try:
styles_builder.add_declaration(declaration)
except DeclarationError as error:
errors.append((error.token, error.message))
return styles_builder.styles
def parse(css: str, path: str) -> Iterable[RuleSet]:
tokens = iter(tokenize(css, path))
@@ -206,3 +245,10 @@ def parse(css: str, path: str) -> Iterable[RuleSet]:
if __name__ == "__main__":
print(parse_selectors("Foo > Bar.baz { foo: bar"))
CSS = """
text: on red;
docks: main=top;
"""
print(parse_declarations(CSS, "foo"))

View File

@@ -91,6 +91,9 @@ class Styles:
_rule_transitions: dict[str, Transition] | None = None
_layout_required: bool = False
_repaint_required: bool = False
important: set[str] = field(default_factory=set)
display = StringProperty(VALID_DISPLAY, "block")
@@ -130,6 +133,15 @@ class Styles:
layers = NameListProperty()
transitions = TransitionsProperty()
def refresh(self, layout: bool = False) -> None:
self._repaint_required = True
self._layout_required = layout
def check_refresh(self) -> tuple[bool, bool]:
result = (self._repaint_required, self._layout_required)
self._repaint_required = self._layout_required = False
return result
@property
def has_border(self) -> bool:
"""Check in a border is present."""

View File

@@ -40,6 +40,13 @@ expect_declaration = Expect(
declaration_set_end=r"\}",
)
expect_declaration_solo = Expect(
whitespace=r"\s+",
comment_start=r"\/\*",
declaration_name=r"[a-zA-Z_\-]+\:",
declaration_set_end=r"\}",
).expect_eof(True)
expect_declaration_content = Expect(
declaration_end=r"\n|;",
whitespace=r"\s+",
@@ -55,34 +62,65 @@ expect_declaration_content = Expect(
)
_STATES = {
"selector_start": expect_selector_continue,
"selector_start_id": expect_selector_continue,
"selector_start_class": expect_selector_continue,
"selector_start_universal": expect_selector_continue,
"selector_id": expect_selector_continue,
"selector_class": expect_selector_continue,
"selector_universal": expect_selector_continue,
"declaration_set_start": expect_declaration,
"declaration_name": expect_declaration_content,
"declaration_end": expect_declaration,
"declaration_set_end": expect_selector,
}
class StateTokenizer:
EXPECT = expect_selector
STATE_MAP = {
"selector_start": expect_selector_continue,
"selector_start_id": expect_selector_continue,
"selector_start_class": expect_selector_continue,
"selector_start_universal": expect_selector_continue,
"selector_id": expect_selector_continue,
"selector_class": expect_selector_continue,
"selector_universal": expect_selector_continue,
"declaration_set_start": expect_declaration,
"declaration_name": expect_declaration_content,
"declaration_end": expect_declaration,
"declaration_set_end": expect_selector,
}
def __call__(self, code: str, path: str) -> Iterable[Token]:
tokenizer = Tokenizer(code, path=path)
expect = self.EXPECT
get_token = tokenizer.get_token
get_state = self.STATE_MAP.get
while True:
token = get_token(expect)
name = token.name
if name == "comment_start":
tokenizer.skip_to(expect_comment_end)
continue
elif name == "eof":
break
expect = get_state(name, expect)
yield token
def tokenize(code: str, path: str) -> Iterable[Token]:
tokenizer = Tokenizer(code, path=path)
expect = expect_selector
get_token = tokenizer.get_token
get_state = _STATES.get
while True:
token = get_token(expect)
class DeclarationStateTokenizer(StateTokenizer):
EXPECT = expect_declaration_solo
STATE_MAP = {
"declaration_name": expect_declaration_content,
"declaration_end": expect_declaration_solo,
}
name = token.name
if name == "comment_start":
tokenizer.skip_to(expect_comment_end)
continue
elif name == "eof":
break
expect = get_state(name, expect)
yield token
tokenize = StateTokenizer()
tokenize_declarations = DeclarationStateTokenizer()
# def tokenize(
# code: str, path: str, *, expect: Expect = expect_selector
# ) -> Iterable[Token]:
# tokenizer = Tokenizer(code, path=path)
# # expect = expect_selector
# get_token = tokenizer.get_token
# get_state = _STATES.get
# while True:
# token = get_token(expect)
# name = token.name
# if name == "comment_start":
# tokenizer.skip_to(expect_comment_end)
# continue
# elif name == "eof":
# break
# expect = get_state(name, expect)
# yield token

View File

@@ -39,12 +39,15 @@ class LayoutProperty:
@rich.repr.auto
class View(Widget):
layout_factory: ClassVar[Callable[[], Layout]]
CSS = """
docks: main=top
"""
def __init__(
self, layout: Layout = None, name: str | None = None, id: str | None = None
) -> None:
self._layout: Layout = layout or self.layout_factory()
def __init__(self, name: str | None = None, id: str | None = None) -> None:
from .layouts.dock import DockLayout
self._layout: Layout = DockLayout()
self.mouse_over: Widget | None = None
self.widgets: set[Widget] = set()
@@ -56,7 +59,6 @@ class View(Widget):
Offset(),
[],
)
super().__init__(name=name, id=id)
def __init_subclass__(

View File

@@ -48,7 +48,7 @@ class RenderCache(NamedTuple):
@property
def cursor_line(self) -> int | None:
for index, line in enumerate(self.lines):
for text, style, control in line:
for _text, style, _control in line:
if style and style._meta and style.meta.get("cursor", False):
return index
return None
@@ -59,6 +59,10 @@ class Widget(DOMNode):
_counts: ClassVar[dict[str, int]] = {}
can_focus: bool = False
CSS = """
dock-group: main;
"""
def __init__(self, name: str | None = None, id: str | None = None) -> None:
if name is None:
class_name = self.__class__.__name__
@@ -279,13 +283,14 @@ class Widget(DOMNode):
self.refresh()
async def on_idle(self, event: events.Idle) -> None:
if self.check_layout():
repaint, layout = self.styles.check_refresh()
if layout or self.check_layout():
self.log("layout required")
self.render_cache = None
self.reset_check_repaint()
self.reset_check_layout()
await self.emit(Layout(self))
elif self.check_repaint():
elif repaint or self.check_repaint():
self.log("repaint required")
self.render_cache = None
self.reset_check_repaint()