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; layout: dock;
docks: side=left/1 header=top footer=bottom; docks: side=left/1 header=top footer=bottom;
layers: base panels; layers: base panels;
@@ -37,3 +46,4 @@ App > DockView {
dock-group: header; dock-group: header;
text: on #20639b; text: on #20639b;
} }
*/

View File

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

View File

@@ -15,7 +15,7 @@ ANSI_SEQUENCES: Dict[str, Tuple[Keys, ...]] = {
"\x06": (Keys.ControlF,), # Control-F (cursor forward) "\x06": (Keys.ControlF,), # Control-F (cursor forward)
"\x07": (Keys.ControlG,), # Control-G "\x07": (Keys.ControlG,), # Control-G
"\x08": (Keys.ControlH,), # Control-H (8) (Identical to '\b') "\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') "\x0a": (Keys.ControlJ,), # Control-J (10) (Identical to '\n')
"\x0b": (Keys.ControlK,), # Control-K (delete until end of line; vertical tab) "\x0b": (Keys.ControlK,), # Control-K (delete until end of line; vertical tab)
"\x0c": (Keys.ControlL,), # Control-L (clear; form feed) "\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 # Handle input events that haven't been forwarded
# If the event has been forwarded it may have bubbled up back to the App # If the event has been forwarded it may have bubbled up back to the App
if isinstance(event, events.Mount): if isinstance(event, events.Mount):
view = DockView() view = View()
self.register(self, view) self.register(self, view)
await self.push_view(view) await self.push_view(view)
await super().on_event(event) await super().on_event(event)

View File

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

View File

@@ -5,7 +5,8 @@ from rich import print
from functools import lru_cache from functools import lru_cache
from typing import Iterator, Iterable 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 .tokenizer import EOFError
from .model import ( from .model import (
@@ -157,6 +158,44 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
yield rule_set 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]: def parse(css: str, path: str) -> Iterable[RuleSet]:
tokens = iter(tokenize(css, path)) tokens = iter(tokenize(css, path))
@@ -206,3 +245,10 @@ def parse(css: str, path: str) -> Iterable[RuleSet]:
if __name__ == "__main__": if __name__ == "__main__":
print(parse_selectors("Foo > Bar.baz { foo: bar")) 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 _rule_transitions: dict[str, Transition] | None = None
_layout_required: bool = False
_repaint_required: bool = False
important: set[str] = field(default_factory=set) important: set[str] = field(default_factory=set)
display = StringProperty(VALID_DISPLAY, "block") display = StringProperty(VALID_DISPLAY, "block")
@@ -130,6 +133,15 @@ class Styles:
layers = NameListProperty() layers = NameListProperty()
transitions = TransitionsProperty() 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 @property
def has_border(self) -> bool: def has_border(self) -> bool:
"""Check in a border is present.""" """Check in a border is present."""

View File

@@ -40,6 +40,13 @@ expect_declaration = Expect(
declaration_set_end=r"\}", 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( expect_declaration_content = Expect(
declaration_end=r"\n|;", declaration_end=r"\n|;",
whitespace=r"\s+", whitespace=r"\s+",
@@ -55,34 +62,65 @@ expect_declaration_content = Expect(
) )
_STATES = { class StateTokenizer:
"selector_start": expect_selector_continue, EXPECT = expect_selector
"selector_start_id": expect_selector_continue, STATE_MAP = {
"selector_start_class": expect_selector_continue, "selector_start": expect_selector_continue,
"selector_start_universal": expect_selector_continue, "selector_start_id": expect_selector_continue,
"selector_id": expect_selector_continue, "selector_start_class": expect_selector_continue,
"selector_class": expect_selector_continue, "selector_start_universal": expect_selector_continue,
"selector_universal": expect_selector_continue, "selector_id": expect_selector_continue,
"declaration_set_start": expect_declaration, "selector_class": expect_selector_continue,
"declaration_name": expect_declaration_content, "selector_universal": expect_selector_continue,
"declaration_end": expect_declaration, "declaration_set_start": expect_declaration,
"declaration_set_end": expect_selector, "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]: class DeclarationStateTokenizer(StateTokenizer):
tokenizer = Tokenizer(code, path=path) EXPECT = expect_declaration_solo
expect = expect_selector STATE_MAP = {
get_token = tokenizer.get_token "declaration_name": expect_declaration_content,
get_state = _STATES.get "declaration_end": expect_declaration_solo,
while True: }
token = get_token(expect)
name = token.name
if name == "comment_start": tokenize = StateTokenizer()
tokenizer.skip_to(expect_comment_end) tokenize_declarations = DeclarationStateTokenizer()
continue
elif name == "eof":
break # def tokenize(
expect = get_state(name, expect) # code: str, path: str, *, expect: Expect = expect_selector
yield token # ) -> 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 @rich.repr.auto
class View(Widget): class View(Widget):
layout_factory: ClassVar[Callable[[], Layout]] CSS = """
docks: main=top
"""
def __init__( def __init__(self, name: str | None = None, id: str | None = None) -> None:
self, layout: Layout = None, name: str | None = None, id: str | None = None
) -> None: from .layouts.dock import DockLayout
self._layout: Layout = layout or self.layout_factory()
self._layout: Layout = DockLayout()
self.mouse_over: Widget | None = None self.mouse_over: Widget | None = None
self.widgets: set[Widget] = set() self.widgets: set[Widget] = set()
@@ -56,7 +59,6 @@ class View(Widget):
Offset(), Offset(),
[], [],
) )
super().__init__(name=name, id=id) super().__init__(name=name, id=id)
def __init_subclass__( def __init_subclass__(

View File

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