mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
declaration parser
This commit is contained in:
@@ -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;
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__(
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user