mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
eof
This commit is contained in:
@@ -35,4 +35,4 @@ class MyApp(App):
|
|||||||
await self.call_later(get_markdown, "richreadme.md")
|
await self.call_later(get_markdown, "richreadme.md")
|
||||||
|
|
||||||
|
|
||||||
MyApp.run(title="Simple App", log="textual.log")
|
MyApp.run(title="Simple App", log="textual.log", css_file="theme.css")
|
||||||
|
|||||||
7
examples/theme.css
Normal file
7
examples/theme.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Header {
|
||||||
|
border: solid #122233;
|
||||||
|
}
|
||||||
|
|
||||||
|
App > View > Widget {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from functools import partial
|
|
||||||
from typing import Any, Callable, ClassVar, Type, TypeVar
|
from typing import Any, Callable, ClassVar, Type, TypeVar
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ from .geometry import Offset, Region
|
|||||||
from . import log
|
from . import log
|
||||||
from ._callback import invoke
|
from ._callback import invoke
|
||||||
from ._context import active_app
|
from ._context import active_app
|
||||||
|
from .css.stylesheet import Stylesheet
|
||||||
from ._event_broker import extract_handler_actions, NoHandler
|
from ._event_broker import extract_handler_actions, NoHandler
|
||||||
from .driver import Driver
|
from .driver import Driver
|
||||||
from .layouts.dock import DockLayout, Dock
|
from .layouts.dock import DockLayout, Dock
|
||||||
@@ -66,6 +67,7 @@ class App(MessagePump):
|
|||||||
log: str = "",
|
log: str = "",
|
||||||
log_verbosity: int = 1,
|
log_verbosity: int = 1,
|
||||||
title: str = "Textual Application",
|
title: str = "Textual Application",
|
||||||
|
css_file: str | None = None,
|
||||||
):
|
):
|
||||||
"""The Textual Application base class
|
"""The Textual Application base class
|
||||||
|
|
||||||
@@ -104,6 +106,10 @@ class App(MessagePump):
|
|||||||
self.bindings.bind("ctrl+c", "quit", show=False, allow_forward=False)
|
self.bindings.bind("ctrl+c", "quit", show=False, allow_forward=False)
|
||||||
self._refresh_required = False
|
self._refresh_required = False
|
||||||
|
|
||||||
|
self.stylesheet = Stylesheet()
|
||||||
|
|
||||||
|
self.css_file = css_file
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
title: Reactive[str] = Reactive("Textual")
|
title: Reactive[str] = Reactive("Textual")
|
||||||
@@ -124,6 +130,9 @@ class App(MessagePump):
|
|||||||
def view(self) -> DockView:
|
def view(self) -> DockView:
|
||||||
return self._view_stack[-1]
|
return self._view_stack[-1]
|
||||||
|
|
||||||
|
def load_css(self, filename: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
def log(self, *args: Any, verbosity: int = 1, **kwargs) -> None:
|
def log(self, *args: Any, verbosity: int = 1, **kwargs) -> None:
|
||||||
"""Write to logs.
|
"""Write to logs.
|
||||||
|
|
||||||
@@ -269,6 +278,13 @@ class App(MessagePump):
|
|||||||
log("---")
|
log("---")
|
||||||
log(f"driver={self.driver_class}")
|
log(f"driver={self.driver_class}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.css_file is not None:
|
||||||
|
self.stylesheet.read(self.css_file)
|
||||||
|
print(self.stylesheet.css)
|
||||||
|
except Exception:
|
||||||
|
self.panic()
|
||||||
|
|
||||||
load_event = events.Load(sender=self)
|
load_event = events.Load(sender=self)
|
||||||
await self.dispatch_message(load_event)
|
await self.dispatch_message(load_event)
|
||||||
await self.post_message(events.Mount(self))
|
await self.post_message(events.Mount(self))
|
||||||
|
|||||||
@@ -36,6 +36,18 @@ class Selector:
|
|||||||
selector: SelectorType = SelectorType.TYPE
|
selector: SelectorType = SelectorType.TYPE
|
||||||
pseudo_classes: list[str] = field(default_factory=list)
|
pseudo_classes: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def css(self) -> str:
|
||||||
|
psuedo_suffix = "".join(f":{name}" for name in self.pseudo_classes)
|
||||||
|
if self.selector == SelectorType.UNIVERSAL:
|
||||||
|
return "*"
|
||||||
|
elif self.selector == SelectorType.TYPE:
|
||||||
|
return f"{self.name}{psuedo_suffix}"
|
||||||
|
elif self.selector == SelectorType.CLASS:
|
||||||
|
return f".{self.name}{psuedo_suffix}"
|
||||||
|
else:
|
||||||
|
return f"#{self.name}{psuedo_suffix}"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Declaration:
|
class Declaration:
|
||||||
@@ -50,3 +62,23 @@ class Declaration:
|
|||||||
class RuleSet:
|
class RuleSet:
|
||||||
selectors: list[list[Selector]] = field(default_factory=list)
|
selectors: list[list[Selector]] = field(default_factory=list)
|
||||||
styles: Styles = field(default_factory=Styles)
|
styles: Styles = field(default_factory=Styles)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def selector_to_css(cls, selectors: list[Selector]) -> str:
|
||||||
|
tokens: list[str] = []
|
||||||
|
for selector in selectors:
|
||||||
|
if selector.combinator == CombinatorType.DESCENDENT:
|
||||||
|
tokens.append(" ")
|
||||||
|
elif selector.combinator == CombinatorType.CHILD:
|
||||||
|
tokens.append(" > ")
|
||||||
|
tokens.append(selector.css)
|
||||||
|
return "".join(tokens).strip()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def css(self) -> str:
|
||||||
|
selectors = ", ".join(
|
||||||
|
self.selector_to_css(selector) for selector in self.selectors
|
||||||
|
)
|
||||||
|
declarations = "\n".join(f" {line}" for line in self.styles.css_lines)
|
||||||
|
css = f"{selectors} {{\n{declarations}\n}}"
|
||||||
|
return css
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
|
|||||||
if declaration.tokens:
|
if declaration.tokens:
|
||||||
rule_set.styles.add_declaration(declaration)
|
rule_set.styles.add_declaration(declaration)
|
||||||
|
|
||||||
print(rule_set)
|
|
||||||
yield rule_set
|
yield rule_set
|
||||||
|
|
||||||
|
|
||||||
@@ -87,16 +86,24 @@ def parse(css: str) -> Iterable[RuleSet]:
|
|||||||
token = next(tokens, None)
|
token = next(tokens, None)
|
||||||
if token is None:
|
if token is None:
|
||||||
break
|
break
|
||||||
if token.name.startswith("selector_start_"):
|
if token.name.startswith("selector_start"):
|
||||||
yield from parse_rule_set(tokens, token)
|
yield from parse_rule_set(tokens, token)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
test = """
|
test = """
|
||||||
.foo.bar baz:focus, #egg {
|
.foo.bar baz:focus, #egg {
|
||||||
|
display: block
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
border: solid green !important;
|
border: solid green !important;
|
||||||
outline: red
|
outline: red;
|
||||||
|
padding: 1 2;
|
||||||
|
margin: 5
|
||||||
}"""
|
}"""
|
||||||
for obj in parse(test):
|
|
||||||
print(obj)
|
from .stylesheet import Stylesheet
|
||||||
|
|
||||||
|
stylesheet = Stylesheet()
|
||||||
|
stylesheet.parse(test)
|
||||||
|
print(stylesheet)
|
||||||
|
print(stylesheet.css)
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import cast, TYPE_CHECKING
|
from typing import cast, Sequence, TYPE_CHECKING
|
||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
|
import rich.repr
|
||||||
from rich.color import ANSI_COLOR_NAMES, Color
|
from rich.color import ANSI_COLOR_NAMES, Color
|
||||||
|
|
||||||
from ._error_tools import friendly_list
|
from ._error_tools import friendly_list
|
||||||
|
from ..geometry import Spacing, SpacingDimensions
|
||||||
from .tokenize import Token
|
from .tokenize import Token
|
||||||
from .types import Visibility
|
from .types import Display, Visibility
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .model import Declaration
|
from .model import Declaration
|
||||||
@@ -21,68 +23,347 @@ class DeclarationError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
VALID_VISIBILITY = {"visible", "hidden"}
|
VALID_VISIBILITY = {"visible", "hidden"}
|
||||||
|
VALID_DISPLAY = {"block", "none"}
|
||||||
VALID_BORDER = {"rounded", "solid", "double", "dashed", "heavy", "inner", "outer"}
|
VALID_BORDER = {"rounded", "solid", "double", "dashed", "heavy", "inner", "outer"}
|
||||||
|
|
||||||
|
|
||||||
|
NULL_SPACING = Spacing(0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class _BoxSetter:
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
self.internal_name = f"_{name}"
|
||||||
|
_type, edge = name.split("_")
|
||||||
|
self._type = _type
|
||||||
|
self.edge = edge
|
||||||
|
|
||||||
|
def __get__(self, obj: Styles, objtype=None) -> tuple[str, str] | None:
|
||||||
|
value = getattr(obj, self.internal_name)
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
_type, color = value
|
||||||
|
return (_type, color.name)
|
||||||
|
|
||||||
|
def __set__(
|
||||||
|
self, obj: Styles, border: tuple[str, str] | None
|
||||||
|
) -> tuple[str, Color] | None:
|
||||||
|
if border is None:
|
||||||
|
new_value = None
|
||||||
|
else:
|
||||||
|
_type, color = border
|
||||||
|
if isinstance(color, Color):
|
||||||
|
new_value = (_type, color)
|
||||||
|
else:
|
||||||
|
new_value = (_type, Color.parse(color))
|
||||||
|
setattr(obj, self.internal_name, new_value)
|
||||||
|
return new_value
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Styles:
|
class Styles:
|
||||||
|
|
||||||
visibility: Visibility | None = None
|
_display: Display | None = None
|
||||||
|
_visibility: Visibility | None = None
|
||||||
|
|
||||||
border_top: tuple[str, Color] | None = None
|
_padding: Spacing | None = None
|
||||||
border_right: tuple[str, Color] | None = None
|
_margin: Spacing | None = None
|
||||||
border_bottom: tuple[str, Color] | None = None
|
|
||||||
border_left: tuple[str, Color] | None = None
|
|
||||||
|
|
||||||
outline_top: tuple[str, Color] | None = None
|
_border_top: tuple[str, Color] | None = None
|
||||||
outline_right: tuple[str, Color] | None = None
|
_border_right: tuple[str, Color] | None = None
|
||||||
outline_bottom: tuple[str, Color] | None = None
|
_border_bottom: tuple[str, Color] | None = None
|
||||||
outline_left: tuple[str, Color] | None = None
|
_border_left: tuple[str, Color] | None = None
|
||||||
|
|
||||||
important: set[str] = field(default_factory=set)
|
_outline_top: tuple[str, Color] | None = None
|
||||||
|
_outline_right: tuple[str, Color] | None = None
|
||||||
|
_outline_bottom: tuple[str, Color] | None = None
|
||||||
|
_outline_left: tuple[str, Color] | None = None
|
||||||
|
|
||||||
|
_important: set[str] = field(default_factory=set)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display(self) -> Display:
|
||||||
|
return self._display or "block"
|
||||||
|
|
||||||
|
@display.setter
|
||||||
|
def display(self, display: Display) -> None:
|
||||||
|
if display not in VALID_DISPLAY:
|
||||||
|
raise ValueError(f"display must be one of {friendly_list(VALID_DISPLAY)}")
|
||||||
|
self._display = display
|
||||||
|
|
||||||
|
@property
|
||||||
|
def visibility(self) -> Visibility:
|
||||||
|
return self._visibility or "visible"
|
||||||
|
|
||||||
|
@visibility.setter
|
||||||
|
def visibility(self, visibility: Visibility) -> None:
|
||||||
|
if visibility not in VALID_VISIBILITY:
|
||||||
|
raise ValueError(
|
||||||
|
f"visibility must be one of {friendly_list(VALID_VISIBILITY)}"
|
||||||
|
)
|
||||||
|
self._visibility = visibility
|
||||||
|
|
||||||
|
@property
|
||||||
|
def padding(self) -> Spacing:
|
||||||
|
return self._padding or NULL_SPACING
|
||||||
|
|
||||||
|
@padding.setter
|
||||||
|
def padding(self, padding: SpacingDimensions) -> None:
|
||||||
|
self._padding = Spacing.unpack(padding)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def margin(self) -> Spacing:
|
||||||
|
return self._margin or NULL_SPACING
|
||||||
|
|
||||||
|
@margin.setter
|
||||||
|
def margin(self, padding: SpacingDimensions) -> None:
|
||||||
|
self._margin = Spacing.unpack(padding)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def border(
|
||||||
|
self,
|
||||||
|
) -> tuple[
|
||||||
|
tuple[str, str] | None,
|
||||||
|
tuple[str, str] | None,
|
||||||
|
tuple[str, str] | None,
|
||||||
|
tuple[str, str] | None,
|
||||||
|
]:
|
||||||
|
return (
|
||||||
|
self.border_top,
|
||||||
|
self.border_right,
|
||||||
|
self.border_bottom,
|
||||||
|
self.border_left,
|
||||||
|
)
|
||||||
|
|
||||||
|
@border.setter
|
||||||
|
def border(
|
||||||
|
self, border: Sequence[tuple[str, str] | None] | tuple[str, str] | None
|
||||||
|
) -> None:
|
||||||
|
if border is None:
|
||||||
|
self._border_top = (
|
||||||
|
self._border_right
|
||||||
|
) = self._border_bottom = self._border_left = None
|
||||||
|
return
|
||||||
|
if isinstance(border, tuple):
|
||||||
|
self.border_top = (
|
||||||
|
self.border_right
|
||||||
|
) = self.border_bottom = self.border_left = border
|
||||||
|
return
|
||||||
|
count = len(border)
|
||||||
|
if count == 1:
|
||||||
|
self.border_top = (
|
||||||
|
self.border_right
|
||||||
|
) = self.border_bottom = self.border_left = border[0]
|
||||||
|
elif count == 2:
|
||||||
|
self.border_top = self.border_right = border[0]
|
||||||
|
self.border_bottom = self.border_left = border[1]
|
||||||
|
elif count == 4:
|
||||||
|
top, right, bottom, left = border
|
||||||
|
self.border_top = top
|
||||||
|
self.border_right = right
|
||||||
|
self.border_bottom = bottom
|
||||||
|
self.border_left = left
|
||||||
|
else:
|
||||||
|
raise ValueError("expected 1, 2, or 4 values")
|
||||||
|
|
||||||
|
border_top = _BoxSetter()
|
||||||
|
border_right = _BoxSetter()
|
||||||
|
border_bottom = _BoxSetter()
|
||||||
|
border_left = _BoxSetter()
|
||||||
|
|
||||||
|
outline_top = _BoxSetter()
|
||||||
|
outline_right = _BoxSetter()
|
||||||
|
outline_bottom = _BoxSetter()
|
||||||
|
outline_left = _BoxSetter()
|
||||||
|
|
||||||
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
|
yield "display", self.display, "block"
|
||||||
|
yield "visibility", self.visibility, "visible"
|
||||||
|
yield "padding", self.padding, NULL_SPACING
|
||||||
|
yield "margin", self.padding, NULL_SPACING
|
||||||
|
|
||||||
|
yield "border_top", self.border_top, None
|
||||||
|
yield "border_right", self.border_right, None
|
||||||
|
yield "border_bottom", self.border_bottom, None
|
||||||
|
yield "border_left", self.border_left, None
|
||||||
|
|
||||||
|
yield "outline_top", self.outline_top, None
|
||||||
|
yield "outline_right", self.outline_right, None
|
||||||
|
yield "outline_bottom", self.outline_bottom, None
|
||||||
|
yield "outline_left", self.outline_left, None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def css_lines(self) -> list[str]:
|
||||||
|
lines: list[str] = []
|
||||||
|
append = lines.append
|
||||||
|
|
||||||
|
def append_declaration(name: str, value: str) -> None:
|
||||||
|
if name in self._important:
|
||||||
|
append(f"{name}: {value} !important;")
|
||||||
|
else:
|
||||||
|
append(f"{name}: {value};")
|
||||||
|
|
||||||
|
if self._display is not None:
|
||||||
|
append_declaration("display", self._display)
|
||||||
|
|
||||||
|
if self._visibility is not None:
|
||||||
|
append_declaration("visibility", self._visibility)
|
||||||
|
|
||||||
|
if self._padding is not None:
|
||||||
|
append_declaration("padding", self._padding.packed)
|
||||||
|
|
||||||
|
if self._margin is not None:
|
||||||
|
append_declaration("margin", self._margin.packed)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self._border_top != None
|
||||||
|
and self._border_top == self._border_right
|
||||||
|
and self._border_right == self._border_bottom
|
||||||
|
and self._border_bottom == self._border_left
|
||||||
|
):
|
||||||
|
_type, color = self._border_top
|
||||||
|
append_declaration("border", f"{_type} {color.name}")
|
||||||
|
else:
|
||||||
|
|
||||||
|
if self._border_top is not None:
|
||||||
|
_type, color = self._border_top
|
||||||
|
append_declaration("border-top", f"{_type} {color.name}")
|
||||||
|
|
||||||
|
if self._border_right is not None:
|
||||||
|
_type, color = self._border_right
|
||||||
|
append_declaration("border-right", f"{_type} {color.name}")
|
||||||
|
|
||||||
|
if self._border_bottom is not None:
|
||||||
|
_type, color = self._border_bottom
|
||||||
|
append_declaration("border-bottom", f"{_type} {color.name}")
|
||||||
|
|
||||||
|
if self._border_left is not None:
|
||||||
|
_type, color = self._border_left
|
||||||
|
append_declaration("border-left", f"{_type} {color.name}")
|
||||||
|
|
||||||
|
if (
|
||||||
|
self._outline_top != None
|
||||||
|
and self._outline_top == self._outline_right
|
||||||
|
and self._outline_right == self._outline_bottom
|
||||||
|
and self._outline_bottom == self._outline_left
|
||||||
|
):
|
||||||
|
_type, color = self._outline_top
|
||||||
|
append_declaration("outline", f"{_type} {color.name}")
|
||||||
|
else:
|
||||||
|
|
||||||
|
if self._outline_top is not None:
|
||||||
|
_type, color = self._outline_top
|
||||||
|
append_declaration("outline-top", f"{_type} {color.name}")
|
||||||
|
|
||||||
|
if self._outline_right is not None:
|
||||||
|
_type, color = self._outline_right
|
||||||
|
append_declaration("outline-right", f"{_type} {color.name}")
|
||||||
|
|
||||||
|
if self._outline_bottom is not None:
|
||||||
|
_type, color = self._outline_bottom
|
||||||
|
append_declaration("outline-bottom", f"{_type} {color.name}")
|
||||||
|
|
||||||
|
if self._outline_left is not None:
|
||||||
|
_type, color = self._outline_left
|
||||||
|
append_declaration("outline-left", f"{_type} {color.name}")
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
def error(self, name: str, token: Token, msg: str) -> None:
|
def error(self, name: str, token: Token, msg: str) -> None:
|
||||||
raise DeclarationError(name, token, msg)
|
line, col = token.location
|
||||||
|
raise DeclarationError(name, token, f"{msg} (line {line + 1}, col {col + 1})")
|
||||||
|
|
||||||
def add_declaration(self, declaration: Declaration) -> None:
|
def add_declaration(self, declaration: Declaration) -> None:
|
||||||
|
|
||||||
print(declaration)
|
|
||||||
if not declaration.tokens:
|
if not declaration.tokens:
|
||||||
return
|
return
|
||||||
process_method = getattr(self, f"process_{declaration.name.replace('-', '_')}")
|
process_method = getattr(self, f"process_{declaration.name.replace('-', '_')}")
|
||||||
tokens = declaration.tokens
|
tokens = declaration.tokens
|
||||||
if tokens[-1].name == "important":
|
if tokens[-1].name == "important":
|
||||||
tokens = tokens[:-1]
|
tokens = tokens[:-1]
|
||||||
self.important.add(declaration.name)
|
self._important.add(declaration.name)
|
||||||
if process_method is not None:
|
if process_method is not None:
|
||||||
process_method(declaration.name, tokens)
|
process_method(declaration.name, tokens)
|
||||||
|
|
||||||
def _parse_border(self, tokens: list[Token]) -> tuple[str, Color]:
|
def process_display(self, name: str, tokens: list[Token]) -> None:
|
||||||
color = Color.default()
|
|
||||||
border_type = "solid"
|
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
location, name, value = token
|
location, name, value = token
|
||||||
if name == "token":
|
if name == "token":
|
||||||
|
value = value.lower()
|
||||||
|
if value in VALID_DISPLAY:
|
||||||
|
self._display = cast(Display, value)
|
||||||
|
else:
|
||||||
|
self.error(
|
||||||
|
name,
|
||||||
|
token,
|
||||||
|
f"invalid value for display (received {value!r}, expected {friendly_list(VALID_DISPLAY)})",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.error(name, token, f"invalid token {value!r} in this context")
|
||||||
|
|
||||||
|
def process_visibility(self, name: str, tokens: list[Token]) -> None:
|
||||||
|
for token in tokens:
|
||||||
|
location, name, value = token
|
||||||
|
if name == "token":
|
||||||
|
value = value.lower()
|
||||||
|
if value in VALID_VISIBILITY:
|
||||||
|
self._visibility = cast(Visibility, value)
|
||||||
|
else:
|
||||||
|
self.error(
|
||||||
|
name,
|
||||||
|
token,
|
||||||
|
f"invalid value for visibility (received {value!r}, expected {friendly_list(VALID_VISIBILITY)})",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.error(name, token, f"invalid token {value!r} in this context")
|
||||||
|
|
||||||
|
def _process_space(self, name: str, tokens: list[Token]) -> None:
|
||||||
|
space: list[int] = []
|
||||||
|
append = space.append
|
||||||
|
for token in tokens:
|
||||||
|
location, toke_name, value = token
|
||||||
|
if toke_name == "number":
|
||||||
|
append(int(value))
|
||||||
|
else:
|
||||||
|
self.error(name, token, f"unexpected token {value!r} in declaration")
|
||||||
|
if len(space) not in (1, 2, 4):
|
||||||
|
self.error(
|
||||||
|
name, tokens[0], f"1, 2, or 4 values expected (received {len(space)})"
|
||||||
|
)
|
||||||
|
setattr(self, f"_{name}", Spacing.unpack(cast(SpacingDimensions, tuple(space))))
|
||||||
|
|
||||||
|
def process_padding(self, name: str, tokens: list[Token]) -> None:
|
||||||
|
self._process_space(name, tokens)
|
||||||
|
|
||||||
|
def process_margin(self, name: str, tokens: list[Token]) -> None:
|
||||||
|
self._process_space(name, tokens)
|
||||||
|
|
||||||
|
def _parse_border(self, name: str, tokens: list[Token]) -> tuple[str, Color]:
|
||||||
|
color = Color.default()
|
||||||
|
border_type = "solid"
|
||||||
|
for token in tokens:
|
||||||
|
location, token_name, value = token
|
||||||
|
if token_name == "token":
|
||||||
if value in ANSI_COLOR_NAMES:
|
if value in ANSI_COLOR_NAMES:
|
||||||
color = Color.parse(value)
|
color = Color.parse(value)
|
||||||
elif value in VALID_BORDER:
|
elif value in VALID_BORDER:
|
||||||
border_type = value
|
border_type = value
|
||||||
else:
|
else:
|
||||||
self.error(name, token, f"unknown token {value!r} in declaration")
|
self.error(name, token, f"unknown token {value!r} in declaration")
|
||||||
elif name == "color":
|
elif token_name == "color":
|
||||||
color = Color.parse(value)
|
color = Color.parse(value)
|
||||||
else:
|
else:
|
||||||
self.error(name, token, f"unexpected token {value!r} in declaration")
|
self.error(name, token, f"unexpected token {value!r} in declaration")
|
||||||
return (border_type, color)
|
return (border_type, color)
|
||||||
|
|
||||||
def _process_border(self, edge: str, name: str, tokens: list[Token]) -> None:
|
def _process_border(self, edge: str, name: str, tokens: list[Token]) -> None:
|
||||||
border = self._parse_border(tokens)
|
border = self._parse_border("border", tokens)
|
||||||
setattr(self, f"border_{edge}", border)
|
setattr(self, f"_border_{edge}", border)
|
||||||
|
|
||||||
def process_border(self, name: str, tokens: list[Token]) -> None:
|
def process_border(self, name: str, tokens: list[Token]) -> None:
|
||||||
border = self._parse_border(tokens)
|
border = self._parse_border("border", tokens)
|
||||||
self.border_top = self.border_right = border
|
self._border_top = self._border_right = border
|
||||||
self.border_bottom = self.border_left = border
|
self._border_bottom = self._border_left = border
|
||||||
|
|
||||||
def process_border_top(self, name: str, tokens: list[Token]) -> None:
|
def process_border_top(self, name: str, tokens: list[Token]) -> None:
|
||||||
self._process_border("top", name, tokens)
|
self._process_border("top", name, tokens)
|
||||||
@@ -97,13 +378,13 @@ class Styles:
|
|||||||
self._process_border("left", name, tokens)
|
self._process_border("left", name, tokens)
|
||||||
|
|
||||||
def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None:
|
def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None:
|
||||||
border = self._parse_border(tokens)
|
border = self._parse_border("outline", tokens)
|
||||||
setattr(self, f"outline_{edge}", border)
|
setattr(self, f"_outline_{edge}", border)
|
||||||
|
|
||||||
def process_outline(self, name: str, tokens: list[Token]) -> None:
|
def process_outline(self, name: str, tokens: list[Token]) -> None:
|
||||||
border = self._parse_border(tokens)
|
border = self._parse_border("outline", tokens)
|
||||||
self.outline_top = self.outline_right = border
|
self._outline_top = self._outline_right = border
|
||||||
self.outline_bottom = self.outline_left = border
|
self._outline_bottom = self._outline_left = border
|
||||||
|
|
||||||
def process_outline_top(self, name: str, tokens: list[Token]) -> None:
|
def process_outline_top(self, name: str, tokens: list[Token]) -> None:
|
||||||
self._process_outline("top", name, tokens)
|
self._process_outline("top", name, tokens)
|
||||||
@@ -117,18 +398,15 @@ class Styles:
|
|||||||
def process_outline_left(self, name: str, tokens: list[Token]) -> None:
|
def process_outline_left(self, name: str, tokens: list[Token]) -> None:
|
||||||
self._process_outline("left", name, tokens)
|
self._process_outline("left", name, tokens)
|
||||||
|
|
||||||
def process_visibility(self, name: str, tokens: list[Token]) -> None:
|
|
||||||
for token in tokens:
|
if __name__ == "__main__":
|
||||||
location, name, value = token
|
styles = Styles()
|
||||||
if name == "token":
|
|
||||||
value = value.lower()
|
styles.display = "none"
|
||||||
if value in VALID_VISIBILITY:
|
styles.visibility = "hidden"
|
||||||
self.visibility = cast(Visibility, value)
|
styles.border = ("solid", "rgb(10,20,30)")
|
||||||
else:
|
styles.outline_right = ("solid", "red")
|
||||||
self.error(
|
|
||||||
name,
|
from rich import print
|
||||||
token,
|
|
||||||
f"invalid value for visibility (received {value!r}, expected {friendly_list(VALID_VISIBILITY)})",
|
print(styles)
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.error(name, token, f"invalid token {value!r} in this context")
|
|
||||||
|
|||||||
43
src/textual/css/stylesheet.py
Normal file
43
src/textual/css/stylesheet.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import rich.repr
|
||||||
|
|
||||||
|
from .model import RuleSet
|
||||||
|
from .parse import parse
|
||||||
|
|
||||||
|
|
||||||
|
class StylesheetError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@rich.repr.auto
|
||||||
|
class Stylesheet:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.rules: list[RuleSet] = []
|
||||||
|
|
||||||
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
|
yield self.rules
|
||||||
|
|
||||||
|
@property
|
||||||
|
def css(self) -> str:
|
||||||
|
return "\n\n".join(rule_set.css for rule_set in self.rules)
|
||||||
|
|
||||||
|
def read(self, filename: str) -> None:
|
||||||
|
try:
|
||||||
|
with open(filename, "rt") as css_file:
|
||||||
|
css = css_file.read()
|
||||||
|
del css_file
|
||||||
|
except Exception as error:
|
||||||
|
raise StylesheetError(f"unable to read {filename!r}; {error}") from None
|
||||||
|
try:
|
||||||
|
rules = list(parse(css))
|
||||||
|
except Exception as error:
|
||||||
|
raise StylesheetError(f"failed to parse {filename!r}; {error}") from None
|
||||||
|
self.rules.extend(rules)
|
||||||
|
|
||||||
|
def parse(self, css: str) -> None:
|
||||||
|
try:
|
||||||
|
rules = list(parse(css))
|
||||||
|
except Exception as error:
|
||||||
|
raise StylesheetError(f"failed to parse css; {error}") from None
|
||||||
|
self.rules.extend(rules)
|
||||||
@@ -45,7 +45,7 @@ expect_declaration_content = Expect(
|
|||||||
whitespace=r"\s+",
|
whitespace=r"\s+",
|
||||||
comment_start=r"\/\*",
|
comment_start=r"\/\*",
|
||||||
percentage=r"\d+\%",
|
percentage=r"\d+\%",
|
||||||
number=r"\d+\.?\d+",
|
number=r"\d+\.?\d*",
|
||||||
color=r"\#[0-9a-f]{6}|color\[0-9]{1,3}\|rgb\([\d\s,]+\)",
|
color=r"\#[0-9a-f]{6}|color\[0-9]{1,3}\|rgb\([\d\s,]+\)",
|
||||||
token="[a-zA-Z_-]+",
|
token="[a-zA-Z_-]+",
|
||||||
string=r"\".*?\"",
|
string=r"\".*?\"",
|
||||||
@@ -82,7 +82,6 @@ def tokenize(code: str) -> Iterable[Token]:
|
|||||||
elif name == "eof":
|
elif name == "eof":
|
||||||
break
|
break
|
||||||
expect = get_state(name, expect)
|
expect = get_state(name, expect)
|
||||||
print(token)
|
|
||||||
yield token
|
yield token
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -74,15 +74,14 @@ class Tokenizer:
|
|||||||
if value is not None:
|
if value is not None:
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
token = Token((line_no, col_no), name, value)
|
||||||
return Token((line_no, col_no), name, value)
|
col_no += len(value)
|
||||||
finally:
|
if col_no >= len(line):
|
||||||
col_no += len(value)
|
line_no += 1
|
||||||
if col_no >= len(line):
|
col_no = 0
|
||||||
line_no += 1
|
self.line_no = line_no
|
||||||
col_no = 0
|
self.col_no = col_no
|
||||||
self.line_no = line_no
|
return token
|
||||||
self.col_no = col_no
|
|
||||||
|
|
||||||
def skip_to(self, expect: Expect) -> Token:
|
def skip_to(self, expect: Expect) -> Token:
|
||||||
line_no = self.line_no
|
line_no = self.line_no
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
Visibility = Literal["visible", "hidden", "initial", "inherit"]
|
Visibility = Literal["visible", "hidden", "initial", "inherit"]
|
||||||
|
Display = Literal["block", "none"]
|
||||||
|
|||||||
@@ -457,6 +457,16 @@ class Spacing(NamedTuple):
|
|||||||
"""Bottom right space."""
|
"""Bottom right space."""
|
||||||
return (self.right, self.bottom)
|
return (self.right, self.bottom)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def packed(self) -> str:
|
||||||
|
top, right, bottom, left = self
|
||||||
|
if top == right == bottom == left:
|
||||||
|
return f"{top}"
|
||||||
|
if (top, right) == (bottom, left):
|
||||||
|
return f"{top}, {right}"
|
||||||
|
else:
|
||||||
|
return f"{top}, {right}, {bottom}, {left}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def unpack(cls, pad: SpacingDimensions) -> Spacing:
|
def unpack(cls, pad: SpacingDimensions) -> Spacing:
|
||||||
"""Unpack padding specified in CSS style."""
|
"""Unpack padding specified in CSS style."""
|
||||||
|
|||||||
Reference in New Issue
Block a user