css shennanigans

This commit is contained in:
Will McGugan
2021-11-01 16:42:27 +00:00
parent 66d7066196
commit eff879b211
7 changed files with 220 additions and 83 deletions

View File

@@ -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" }

View File

@@ -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:

View File

@@ -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

View File

@@ -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"))

View File

@@ -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)

View File

@@ -84,5 +84,4 @@ def tokenize(code: str) -> Iterable[Token]:
elif name == "eof":
break
expect = get_state(name, expect)
print(token)
yield token

View File

@@ -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)