mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
DOM query
This commit is contained in:
66
src/textual/css/match.py
Normal file
66
src/textual/css/match.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, TYPE_CHECKING
|
||||
from .model import CombinatorType, Selector, SelectorSet, SelectorType
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..dom import DOMNode
|
||||
|
||||
|
||||
def match(selector_sets: Iterable[SelectorSet], node: DOMNode):
|
||||
for selector_set in selector_sets:
|
||||
if _check_selectors(selector_set.selectors, node):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _check_selectors(selectors: list[Selector], node: DOMNode) -> bool:
|
||||
|
||||
SAME = CombinatorType.SAME
|
||||
DESCENDENT = CombinatorType.DESCENDENT
|
||||
CHILD = CombinatorType.CHILD
|
||||
|
||||
css_path = node.css_path
|
||||
path_count = len(css_path)
|
||||
selector_count = len(selectors)
|
||||
|
||||
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
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from rich import print
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Iterator, Iterable
|
||||
|
||||
from .tokenize import tokenize, Token
|
||||
@@ -30,7 +31,8 @@ SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = {
|
||||
}
|
||||
|
||||
|
||||
def parse_selectors(css_selectors: str) -> list[SelectorSet]:
|
||||
@lru_cache(maxsize=1024)
|
||||
def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
|
||||
|
||||
tokens = iter(tokenize(css_selectors))
|
||||
|
||||
@@ -42,7 +44,6 @@ def parse_selectors(css_selectors: str) -> list[SelectorSet]:
|
||||
while True:
|
||||
try:
|
||||
token = next(tokens)
|
||||
print(token)
|
||||
except EOFError:
|
||||
break
|
||||
if token.name == "pseudo_class":
|
||||
@@ -74,7 +75,7 @@ def parse_selectors(css_selectors: str) -> list[SelectorSet]:
|
||||
if selectors:
|
||||
rule_selectors.append(selectors[:])
|
||||
|
||||
selector_set = list(SelectorSet.from_selectors(rule_selectors))
|
||||
selector_set = tuple(SelectorSet.from_selectors(rule_selectors))
|
||||
return selector_set
|
||||
|
||||
|
||||
|
||||
47
src/textual/css/query.py
Normal file
47
src/textual/css/query.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import rich.repr
|
||||
|
||||
from typing import Iterable, Iterator, TYPE_CHECKING
|
||||
|
||||
|
||||
from .match import match
|
||||
from .parse import parse_selectors
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..dom import DOMNode
|
||||
|
||||
|
||||
@rich.repr.auto(angular=True)
|
||||
class DOMQuery:
|
||||
def __init__(
|
||||
self,
|
||||
node: DOMNode | None = None,
|
||||
selector: str | None = None,
|
||||
nodes: Iterable[DOMNode] | None = None,
|
||||
) -> None:
|
||||
|
||||
self._nodes: list[DOMNode]
|
||||
if nodes is not None:
|
||||
self._nodes = list(nodes)
|
||||
elif node is not None:
|
||||
self._nodes = list(node.walk_children())
|
||||
else:
|
||||
self._nodes = []
|
||||
|
||||
if selector is not None:
|
||||
selector_set = parse_selectors(selector)
|
||||
self._nodes = [_node for _node in self._nodes if match(selector_set, _node)]
|
||||
|
||||
def __iter__(self) -> Iterator[DOMNode]:
|
||||
return iter(self._nodes)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield self._nodes
|
||||
|
||||
def filter(self, selector: str) -> DOMQuery:
|
||||
selector_set = parse_selectors(selector)
|
||||
query = DOMQuery()
|
||||
query._nodes = [_node for _node in self._nodes if match(selector_set, _node)]
|
||||
return query
|
||||
@@ -1,13 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import rich.repr
|
||||
|
||||
from ..dom import DOMNode
|
||||
|
||||
from .errors import StylesheetError
|
||||
from .match import _check_selectors
|
||||
from .model import CombinatorType, RuleSet, Selector
|
||||
from .parse import parse
|
||||
from .styles import Styles
|
||||
from .types import Specificity3
|
||||
from ..dom import DOMNode
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
@@ -49,89 +53,9 @@ class Stylesheet:
|
||||
|
||||
def apply_rule(self, rule: RuleSet, node: DOMNode) -> None:
|
||||
for selector_set in rule.selector_set:
|
||||
if self.check_selectors(selector_set.selectors, node):
|
||||
if _check_selectors(selector_set.selectors, node):
|
||||
print(selector_set, repr(node))
|
||||
|
||||
def check_selectors(self, selectors: list[Selector], node: DOMNode) -> bool:
|
||||
|
||||
SAME = CombinatorType.SAME
|
||||
DESCENDENT = CombinatorType.DESCENDENT
|
||||
CHILD = CombinatorType.CHILD
|
||||
|
||||
css_path = node.css_path
|
||||
path_count = len(css_path)
|
||||
selector_count = len(selectors)
|
||||
|
||||
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__":
|
||||
|
||||
@@ -171,7 +95,7 @@ if __name__ == "__main__":
|
||||
main_view.add_child(sub_view)
|
||||
|
||||
tooltip = Widget(id="tooltip")
|
||||
tooltip.add_class("float")
|
||||
tooltip.add_class("float", "transient")
|
||||
sub_view.add_child(tooltip)
|
||||
|
||||
help = Widget(id="markdown")
|
||||
@@ -196,7 +120,22 @@ if __name__ == "__main__":
|
||||
|
||||
"""
|
||||
|
||||
print(app._all_children())
|
||||
from .query import DOMQuery
|
||||
|
||||
# print(DOMQuery(selector="App", nodes=[sub_view]))
|
||||
|
||||
tests = [
|
||||
"App > View",
|
||||
"Widget.float",
|
||||
".float.transient",
|
||||
]
|
||||
|
||||
for test in tests:
|
||||
print("")
|
||||
print(f"[b]{test}")
|
||||
print(app.query(test))
|
||||
|
||||
# print(app.query("View"))
|
||||
|
||||
# stylesheet = Stylesheet()
|
||||
# stylesheet.parse(CSS)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable
|
||||
from typing import Iterable, Iterator, TYPE_CHECKING
|
||||
|
||||
from rich.highlighter import ReprHighlighter
|
||||
import rich.repr
|
||||
@@ -12,6 +12,10 @@ from .message_pump import MessagePump
|
||||
from ._node_list import NodeList
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .css.query import DOMQuery
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class DOMNode(MessagePump):
|
||||
"""A node in a hierarchy of things forming the UI.
|
||||
@@ -84,11 +88,28 @@ 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 walk_children(self, with_self: bool = True) -> Iterable[DOMNode]:
|
||||
|
||||
stack: list[Iterator[DOMNode]] = [iter(self.children)]
|
||||
pop = stack.pop
|
||||
push = stack.append
|
||||
|
||||
if with_self:
|
||||
yield self
|
||||
|
||||
while stack:
|
||||
node = next(stack[-1], None)
|
||||
if node is None:
|
||||
pop()
|
||||
else:
|
||||
yield node
|
||||
if node.children:
|
||||
push(iter(node.children))
|
||||
|
||||
def query(self, selector: str) -> DOMQuery:
|
||||
from .css.query import DOMQuery
|
||||
|
||||
return DOMQuery(self, selector)
|
||||
|
||||
def has_class(self, *class_names: str) -> bool:
|
||||
return self._classes.issuperset(class_names)
|
||||
|
||||
Reference in New Issue
Block a user