mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
css shennanigans
This commit is contained in:
@@ -20,7 +20,7 @@ classifiers = [
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
rich = "^10.7.0"
|
||||
rich = "^10.12.0"
|
||||
#rich = {git = "git@github.com:willmcgugan/rich", rev = "link-id"}
|
||||
typing-extensions = { version = "^3.10.0", python = "<3.8" }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, overload, TYPE_CHECKING
|
||||
from typing import Iterator, overload, TYPE_CHECKING
|
||||
from weakref import ref
|
||||
|
||||
import rich.repr
|
||||
@@ -59,7 +59,7 @@ class NodeList:
|
||||
del self._widget_refs[:]
|
||||
self.__widgets = None
|
||||
|
||||
def __iter__(self) -> Iterable[DOMNode]:
|
||||
def __iter__(self) -> Iterator[DOMNode]:
|
||||
for widget_ref in self._widget_refs:
|
||||
widget = widget_ref()
|
||||
if widget is not None:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from rich import print
|
||||
import rich.repr
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
@@ -44,7 +43,8 @@ class Selector:
|
||||
type: SelectorType = SelectorType.TYPE
|
||||
pseudo_classes: list[str] = field(default_factory=list)
|
||||
specificity: Specificity3 = field(default_factory=lambda: (0, 0, 0))
|
||||
_name_lower: str = ""
|
||||
_name_lower: str = field(default="", repr=False)
|
||||
advance: int = 0
|
||||
|
||||
@property
|
||||
def css(self) -> str:
|
||||
@@ -101,11 +101,22 @@ class Declaration:
|
||||
tokens: list[Token] = field(default_factory=list)
|
||||
|
||||
|
||||
@rich.repr.auto(angular=True)
|
||||
@dataclass
|
||||
class SelectorSet:
|
||||
selectors: list[Selector] = field(default_factory=list)
|
||||
specificity: Specificity3 = (0, 0, 0)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
SAME = CombinatorType.SAME
|
||||
for selector, next_selector in zip(self.selectors, self.selectors[1:]):
|
||||
selector.advance = 0 if next_selector.combinator == SAME else 1
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
selectors = RuleSet._selector_to_css(self.selectors)
|
||||
yield selectors
|
||||
yield None, self.specificity
|
||||
|
||||
@classmethod
|
||||
def from_selectors(cls, selectors: list[list[Selector]]) -> Iterable[SelectorSet]:
|
||||
for selector_list in selectors:
|
||||
@@ -124,7 +135,7 @@ class RuleSet:
|
||||
styles: Styles = field(default_factory=Styles)
|
||||
|
||||
@classmethod
|
||||
def selector_to_css(cls, selectors: list[Selector]) -> str:
|
||||
def _selector_to_css(cls, selectors: list[Selector]) -> str:
|
||||
tokens: list[str] = []
|
||||
for selector in selectors:
|
||||
if selector.combinator == CombinatorType.DESCENDENT:
|
||||
@@ -135,11 +146,14 @@ class RuleSet:
|
||||
return "".join(tokens).strip()
|
||||
|
||||
@property
|
||||
def css(self) -> str:
|
||||
selectors = ", ".join(
|
||||
self.selector_to_css(selector_set.selectors)
|
||||
def selectors(self):
|
||||
return ", ".join(
|
||||
self._selector_to_css(selector_set.selectors)
|
||||
for selector_set in self.selector_set
|
||||
)
|
||||
|
||||
@property
|
||||
def css(self) -> str:
|
||||
declarations = "\n".join(f" {line}" for line in self.styles.css_lines)
|
||||
css = f"{selectors} {{\n{declarations}\n}}"
|
||||
css = f"{self.selectors} {{\n{declarations}\n}}"
|
||||
return css
|
||||
|
||||
@@ -5,6 +5,7 @@ from rich import print
|
||||
from typing import Iterator, Iterable
|
||||
|
||||
from .tokenize import tokenize, Token
|
||||
from .tokenizer import EOFError
|
||||
|
||||
from .model import (
|
||||
Declaration,
|
||||
@@ -29,6 +30,54 @@ SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = {
|
||||
}
|
||||
|
||||
|
||||
def parse_selectors(css_selectors: str) -> list[SelectorSet]:
|
||||
|
||||
tokens = iter(tokenize(css_selectors))
|
||||
|
||||
get_selector = SELECTOR_MAP.get
|
||||
combinator: CombinatorType | None = CombinatorType.DESCENDENT
|
||||
selectors: list[Selector] = []
|
||||
rule_selectors: list[list[Selector]] = []
|
||||
|
||||
while True:
|
||||
try:
|
||||
token = next(tokens)
|
||||
print(token)
|
||||
except EOFError:
|
||||
break
|
||||
if token.name == "pseudo_class":
|
||||
selectors[-1].pseudo_classes.append(token.value.lstrip(":"))
|
||||
elif token.name == "whitespace":
|
||||
if combinator is None or combinator == CombinatorType.SAME:
|
||||
combinator = CombinatorType.DESCENDENT
|
||||
elif token.name == "new_selector":
|
||||
rule_selectors.append(selectors[:])
|
||||
selectors.clear()
|
||||
combinator = None
|
||||
elif token.name == "declaration_set_start":
|
||||
break
|
||||
elif token.name == "combinator_child":
|
||||
combinator = CombinatorType.CHILD
|
||||
else:
|
||||
_selector, specificity = get_selector(
|
||||
token.name, (SelectorType.TYPE, (0, 0, 0))
|
||||
)
|
||||
selectors.append(
|
||||
Selector(
|
||||
name=token.value.lstrip(".#"),
|
||||
combinator=combinator or CombinatorType.DESCENDENT,
|
||||
type=_selector,
|
||||
specificity=specificity,
|
||||
)
|
||||
)
|
||||
combinator = CombinatorType.SAME
|
||||
if selectors:
|
||||
rule_selectors.append(selectors[:])
|
||||
|
||||
selector_set = list(SelectorSet.from_selectors(rule_selectors))
|
||||
return selector_set
|
||||
|
||||
|
||||
def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
|
||||
|
||||
rule_set = RuleSet()
|
||||
@@ -109,37 +158,41 @@ def parse(css: str) -> Iterable[RuleSet]:
|
||||
yield from parse_rule_set(tokens, token)
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# test = """
|
||||
|
||||
# App View {
|
||||
# text: red;
|
||||
# }
|
||||
|
||||
# .foo.bar baz:focus, #egg .foo.baz {
|
||||
# /* ignore me, I'm a comment */
|
||||
# display: block;
|
||||
# visibility: visible;
|
||||
# border: solid green !important;
|
||||
# outline: red;
|
||||
# padding: 1 2;
|
||||
# margin: 5;
|
||||
# text: bold red on magenta
|
||||
# text-color: green;
|
||||
# text-background: white
|
||||
# docks: foo bar bar
|
||||
# dock-group: foo
|
||||
# dock-edge: top
|
||||
# offset-x: 4
|
||||
# offset-y: 5
|
||||
# }"""
|
||||
|
||||
# from .stylesheet import Stylesheet
|
||||
|
||||
# print(test)
|
||||
# print()
|
||||
# stylesheet = Stylesheet()
|
||||
# stylesheet.parse(test)
|
||||
# print(stylesheet)
|
||||
# print()
|
||||
# print(stylesheet.css)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test = """
|
||||
|
||||
App View {
|
||||
text: red;
|
||||
}
|
||||
|
||||
.foo.bar baz:focus, #egg .foo.baz {
|
||||
/* ignore me, I'm a comment */
|
||||
display: block;
|
||||
visibility: visible;
|
||||
border: solid green !important;
|
||||
outline: red;
|
||||
padding: 1 2;
|
||||
margin: 5;
|
||||
text: bold red on magenta
|
||||
text-color: green;
|
||||
text-background: white
|
||||
docks: foo bar bar
|
||||
dock-group: foo
|
||||
dock-edge: top
|
||||
offset-x: 4
|
||||
offset-y: 5
|
||||
}"""
|
||||
|
||||
from .stylesheet import Stylesheet
|
||||
|
||||
print(test)
|
||||
print()
|
||||
stylesheet = Stylesheet()
|
||||
stylesheet.parse(test)
|
||||
print(stylesheet)
|
||||
print()
|
||||
print(stylesheet.css)
|
||||
print(parse_selectors("Foo > Bar.baz { foo: bar"))
|
||||
|
||||
@@ -4,7 +4,7 @@ import rich.repr
|
||||
|
||||
from ..dom import DOMNode
|
||||
from .errors import StylesheetError
|
||||
from .model import CombinatorType, RuleSet, Selector, SelectorType
|
||||
from .model import CombinatorType, RuleSet, Selector
|
||||
from .parse import parse
|
||||
from .styles import Styles
|
||||
from .types import Specificity3
|
||||
@@ -45,49 +45,100 @@ class Stylesheet:
|
||||
styles: list[tuple[Specificity3, Styles]] = []
|
||||
|
||||
for rule in self.rules:
|
||||
print(rule)
|
||||
self.apply_rule(rule, node)
|
||||
|
||||
def apply_rule(self, rule: RuleSet, node: DOMNode) -> None:
|
||||
for selector_set in rule.selector_set:
|
||||
if self.check_selectors(selector_set.selectors, node):
|
||||
print(rule.css)
|
||||
print(selector_set, repr(node))
|
||||
|
||||
def check_selectors(self, selectors: list[Selector], node: DOMNode) -> bool:
|
||||
node_path = node.css_path
|
||||
nodes = iter(node_path)
|
||||
|
||||
node_siblings = next(nodes, None)
|
||||
if node_siblings is None:
|
||||
return False
|
||||
node, siblings = node_siblings
|
||||
|
||||
SAME = CombinatorType.SAME
|
||||
DESCENDENT = CombinatorType.DESCENDENT
|
||||
CHILD = CombinatorType.CHILD
|
||||
|
||||
try:
|
||||
for selector in selectors:
|
||||
if selector.combinator == SAME:
|
||||
if not selector.check(node):
|
||||
return False
|
||||
elif selector.combinator == DESCENDENT:
|
||||
while True:
|
||||
node, siblings = next(nodes)
|
||||
if selector.check(node):
|
||||
break
|
||||
elif selector.combinator == CHILD:
|
||||
node, siblings = next(nodes)
|
||||
if not selector.check(node):
|
||||
return False
|
||||
except StopIteration:
|
||||
return False
|
||||
css_path = node.css_path
|
||||
path_count = len(css_path)
|
||||
selector_count = len(selectors)
|
||||
|
||||
return True
|
||||
stack: list[tuple[int, int]] = [(0, 0)]
|
||||
|
||||
push = stack.append
|
||||
pop = stack.pop
|
||||
selector_index = 0
|
||||
|
||||
while stack:
|
||||
selector_index, node_index = stack[-1]
|
||||
if selector_index == selector_count:
|
||||
return node_index + 1 == path_count
|
||||
if node_index == path_count:
|
||||
pop()
|
||||
continue
|
||||
selector = selectors[selector_index]
|
||||
path_node = css_path[node_index]
|
||||
combinator = selector.combinator
|
||||
stack[-1] = (selector_index, node_index)
|
||||
|
||||
if combinator == SAME:
|
||||
# Check current node again
|
||||
if selector.check(path_node):
|
||||
stack[-1] = (selector_index + 1, node_index + selector.advance)
|
||||
else:
|
||||
pop()
|
||||
elif combinator == DESCENDENT:
|
||||
# Find a matching descendent
|
||||
if selector.check(path_node):
|
||||
pop()
|
||||
push((selector_index + 1, node_index + selector.advance + 1))
|
||||
push((selector_index + 1, node_index + selector.advance))
|
||||
else:
|
||||
stack[-1] = (selector_index, node_index + selector.advance + 1)
|
||||
elif combinator == CHILD:
|
||||
# Match the next node
|
||||
if selector.check(path_node):
|
||||
stack[-1] = (selector_index + 1, node_index + selector.advance)
|
||||
else:
|
||||
pop()
|
||||
return False
|
||||
|
||||
# def search(selector_index: int, node_index: int) -> bool:
|
||||
# nodes = iter(enumerate(css_path[node_index:], node_index))
|
||||
# try:
|
||||
# node_index, node = next(nodes)
|
||||
# for selector_index in range(selector_index, selector_count):
|
||||
# selector = selectors[selector_index]
|
||||
# combinator = selector.combinator
|
||||
# if combinator == SAME:
|
||||
# # Check current node again
|
||||
# if not selector.check(node):
|
||||
# return False
|
||||
# elif combinator == DESCENDENT:
|
||||
# # Find a matching descendent
|
||||
# while True:
|
||||
# node_index, node = next(nodes)
|
||||
# if selector.check(node) and search(
|
||||
# selector_index + 1, node_index
|
||||
# ):
|
||||
# break
|
||||
# elif combinator == CHILD:
|
||||
# # Match the next node
|
||||
# node_index, node = next(nodes)
|
||||
# if not selector.check(node):
|
||||
# return False
|
||||
# except StopIteration:
|
||||
# return False
|
||||
# return True
|
||||
|
||||
# return search(0, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
from rich.traceback import install
|
||||
|
||||
install(show_locals=True)
|
||||
|
||||
class Widget(DOMNode):
|
||||
pass
|
||||
|
||||
@@ -105,16 +156,24 @@ if __name__ == "__main__":
|
||||
|
||||
widget1 = Widget(id="widget1")
|
||||
widget2 = Widget(id="widget2")
|
||||
sidebar = Widget()
|
||||
sidebar = Widget(id="sidebar")
|
||||
sidebar.add_class("float")
|
||||
|
||||
helpbar = Widget()
|
||||
helpbar = Widget(id="helpbar")
|
||||
helpbar.add_class("float")
|
||||
|
||||
main_view.add_child(widget1)
|
||||
main_view.add_child(widget2)
|
||||
main_view.add_child(sidebar)
|
||||
|
||||
sub_view = View(id="sub")
|
||||
sub_view.add_class("-subview")
|
||||
main_view.add_child(sub_view)
|
||||
|
||||
tooltip = Widget(id="tooltip")
|
||||
tooltip.add_class("float")
|
||||
sub_view.add_child(tooltip)
|
||||
|
||||
help = Widget(id="markdown")
|
||||
help_view.add_child(help)
|
||||
help_view.add_child(helpbar)
|
||||
@@ -125,17 +184,21 @@ if __name__ == "__main__":
|
||||
|
||||
CSS = """
|
||||
|
||||
/*
|
||||
App > View {
|
||||
text: red;
|
||||
}*/
|
||||
|
||||
App > View.-subview {
|
||||
outline: heavy
|
||||
}
|
||||
|
||||
Widget.float {
|
||||
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
stylesheet = Stylesheet()
|
||||
stylesheet.parse(CSS)
|
||||
print(app._all_children())
|
||||
|
||||
stylesheet.apply(sidebar)
|
||||
# stylesheet = Stylesheet()
|
||||
# stylesheet.parse(CSS)
|
||||
|
||||
# stylesheet.apply(sub_view)
|
||||
|
||||
@@ -84,5 +84,4 @@ def tokenize(code: str) -> Iterable[Token]:
|
||||
elif name == "eof":
|
||||
break
|
||||
expect = get_state(name, expect)
|
||||
print(token)
|
||||
yield token
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
from rich.highlighter import ReprHighlighter
|
||||
import rich.repr
|
||||
from rich.pretty import Pretty
|
||||
@@ -54,14 +56,14 @@ class DOMNode(MessagePump):
|
||||
return self.__class__.__name__.lower()
|
||||
|
||||
@property
|
||||
def css_path(self) -> list[tuple[DOMNode, list[DOMNode]]]:
|
||||
result: list[tuple[DOMNode, list[DOMNode]]] = [(self, self.children[:])]
|
||||
def css_path(self) -> list[DOMNode]:
|
||||
result: list[DOMNode] = [self]
|
||||
append = result.append
|
||||
|
||||
node: DOMNode = self
|
||||
while isinstance(node._parent, DOMNode):
|
||||
node = node._parent
|
||||
append((node, node.children[:]))
|
||||
append(node)
|
||||
return result[::-1]
|
||||
|
||||
@property
|
||||
@@ -82,6 +84,12 @@ class DOMNode(MessagePump):
|
||||
self.children._append(node)
|
||||
node.set_parent(self)
|
||||
|
||||
def _all_children(self) -> Iterable[DOMNode]:
|
||||
children = {self, *self.children}
|
||||
for child in self.children:
|
||||
children.update(child._all_children())
|
||||
return children
|
||||
|
||||
def has_class(self, *class_names: str) -> bool:
|
||||
return self._classes.issuperset(class_names)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user