dom: make DomState a class

This commit is contained in:
Corentin Sechet 2023-01-18 12:43:43 +01:00
parent f8728af173
commit befed88628
6 changed files with 106 additions and 94 deletions

38
stylo/dom.py Normal file
View File

@ -0,0 +1,38 @@
from cssselect2 import ElementWrapper
HOVER = 0x01
FOCUS = 0x02
_PSEUDO_CLASSES_FLAGS = {
"hover": HOVER,
"focus": FOCUS,
}
def get_pseudo_class_flag(pseudo_class: str) -> int:
return _PSEUDO_CLASSES_FLAGS.get(pseudo_class, 0)
class DomState:
def __init__(self, *args: tuple[ElementWrapper, int]) -> None:
self._hash: int | None = None
self._element_states = dict(args)
def __eq__(self, right: object) -> bool:
if not isinstance(right, DomState):
return False
return self._element_states == right._element_states
def __contains__(self, other: "DomState") -> bool:
for element, element_state in other._element_states.items():
included_element_state = self._element_states.get(element, 0)
if (element_state & included_element_state) != element_state:
return False
return True
def __hash__(self) -> int:
if self._hash is None:
self._hash = frozenset(self._element_states.items()).__hash__()
return self._hash

View File

@ -4,21 +4,11 @@ from cssselect2 import ElementWrapper
from cssselect2.compiler import CompiledSelector
from cssselect2.parser import CombinedSelector, CompoundSelector, PseudoClassSelector, Selector
from stylo.dom import DomState, get_pseudo_class_flag
TSelector = TypeVar("TSelector")
HOVER = 0x01
FOCUS = 0x02
_PSEUDO_CLASSES_FLAGS = {
"hover": HOVER,
"focus": FOCUS,
}
DomState = frozenset[tuple[ElementWrapper, int]]
def compile_selector(selector: Any) -> CompiledSelector:
if isinstance(selector, Selector):
tree = selector.parsed_tree
@ -33,29 +23,20 @@ def compile_selector(selector: Any) -> CompiledSelector:
def get_matching_state(selector: Selector, node: ElementWrapper) -> DomState:
state: set[tuple[ElementWrapper, int]] = set()
state: dict[ElementWrapper, int] = {}
tree = selector.parsed_tree
while tree:
node_state = _get_matching_state(tree)
if node_state != 0:
state.add((node, node_state))
state[node] = node_state
if not isinstance(tree, CombinedSelector):
break
node, tree = _get_parent_match(node, tree)
return frozenset(state)
def matches_state(selector: Selector, node: ElementWrapper, state: DomState) -> bool:
for parent_node, node_state in get_matching_state(selector, node):
expected_state = next((state_it for node_it, state_it in state if node_it == parent_node), 0)
if (expected_state & node_state) != node_state:
return False
return True
return DomState(*state.items())
def _get_matching_state(tree: Any) -> int:
@ -69,7 +50,7 @@ def _get_matching_state(tree: Any) -> int:
if not isinstance(selector, PseudoClassSelector):
continue
state |= _PSEUDO_CLASSES_FLAGS.get(selector.name, 0)
state |= get_pseudo_class_flag(selector.name)
return state

View File

@ -6,8 +6,9 @@ from cssselect2 import ElementWrapper, Matcher
from cssselect2.parser import Selector
from tinycss2 import parse_stylesheet
from stylo.dom import DomState
from stylo.nodes import Declaration, Node, QualifiedRule
from stylo.selector import DomState, compile_selector, get_matching_state, matches_state
from stylo.selector import compile_selector, get_matching_state
from stylo.source_map import SourceMap
Match = tuple[Selector, QualifiedRule]
@ -25,7 +26,7 @@ class StyledNode:
def get_style(self, state: DomState) -> dict[str, Declaration]:
declarations: dict[str, Declaration] = {}
for selector, rule in self._matches:
if not matches_state(selector, self._node, state):
if get_matching_state(selector, self._node) not in state:
continue
for declaration in rule.declarations:

25
tests/test_dom.py Normal file
View File

@ -0,0 +1,25 @@
from typing import cast
from cssselect2.tree import ElementWrapper
from stylo.dom import FOCUS, HOVER, DomState
def test_contains() -> None:
node = cast(ElementWrapper, object())
state = DomState()
assert state in DomState()
assert state in DomState((node, HOVER))
assert state in DomState((node, HOVER | FOCUS))
state = DomState((node, HOVER))
assert state not in DomState()
assert state in DomState((node, HOVER))
assert state in DomState((node, HOVER | FOCUS))
state = DomState((node, HOVER | FOCUS))
assert state not in DomState()
assert state not in DomState((node, HOVER))
assert state not in DomState((node, FOCUS))
assert state in DomState((node, HOVER | FOCUS))

View File

@ -1,75 +1,42 @@
from cssselect2.parser import Selector
from cssselect2.parser import parse as parse_selector
from cssselect2.tree import ElementWrapper
from html5lib import parse as parse_html
from stylo.selector import FOCUS, HOVER, DomState, compile_selector, get_matching_state, matches_state
_TEST_HTML = ElementWrapper.from_html_root(
parse_html(
"""
<html>
<body>
<ul class="list">
<li class="item">
<a class="link"></a>
<div class="description"></div>
</li>
</ul>
</body>
</html>
"""
)
)
def _state(*args: tuple[ElementWrapper, int]) -> DomState:
return frozenset(args)
def _parse(selector: str) -> tuple[Selector, ElementWrapper]:
parsed_selector = next(parse_selector(selector))
node = _TEST_HTML.query(compile_selector(parsed_selector))
return parsed_selector, node
from stylo.dom import FOCUS, HOVER, DomState
from stylo.selector import compile_selector, get_matching_state
def test_get_matching_states() -> None:
def _matching_state(selector: str) -> DomState:
selector, node = _parse(selector)
return get_matching_state(selector, node)
root = ElementWrapper.from_html_root(
parse_html(
"""
<html>
<body>
<ul class="list">
<li class="item">
<a class="link"></a>
<div class="description"></div>
<a class="link test"></a>
</li>
</ul>
</body>
</html>
"""
)
)
item = _TEST_HTML.query(".item")
link = _TEST_HTML.query(".link")
def _state(selector: str) -> DomState:
parsed_selector = next(parse_selector(selector))
node = root.query(compile_selector(parsed_selector))
return get_matching_state(parsed_selector, node)
assert _matching_state(".item") == _state()
item = root.query(".item")
link = root.query(".link")
assert _matching_state(".item:hover .link") == _state((item, HOVER))
assert _matching_state(".item:hover > .link") == _state((item, HOVER))
assert _matching_state(".link:hover + .description") == _state((link, HOVER))
assert _matching_state(".link:hover ~ .description") == _state((link, HOVER))
assert _state(".item:hover .link") == DomState((item, HOVER))
assert _state(".item:hover > .link") == DomState((item, HOVER))
assert _state(".link:hover + .description") == DomState((link, HOVER))
assert _state(".link:hover ~ .description") == DomState((link, HOVER))
assert _matching_state(".item:hover > .link:focus") == _state((item, HOVER), (link, FOCUS))
assert _matching_state(".item:hover:focus") == _state((item, HOVER | FOCUS))
def test_matches_state() -> None:
selector, node = _parse(".item")
assert matches_state(selector, node, _state())
assert matches_state(selector, node, _state((node, HOVER)))
selector, node = _parse(".item:hover")
assert not matches_state(selector, node, _state())
assert matches_state(selector, node, _state((node, HOVER)))
selector, node = _parse(".item:hover:focus")
assert not matches_state(selector, node, _state())
assert not matches_state(selector, node, _state((node, HOVER)))
assert not matches_state(selector, node, _state((node, FOCUS)))
assert matches_state(selector, node, _state((node, HOVER | FOCUS)))
item = _TEST_HTML.query(".item")
selector, node = _parse(".item:hover .link")
assert not matches_state(selector, node, _state())
assert not matches_state(selector, node, _state((node, HOVER)))
assert matches_state(selector, node, _state((item, HOVER)))
# assert matches_state(selector, node, _state((link, HOVER))) optimisation qu'on pourrait faire ici
assert _state(".item:hover > .link:focus") == DomState((item, HOVER), (link, FOCUS))
assert _state(".item:hover:focus") == DomState((item, HOVER | FOCUS))

View File

@ -3,8 +3,8 @@ from io import StringIO
from cssselect2.tree import ElementWrapper
from html5lib import parse
from stylo.dom import HOVER, DomState
from stylo.nodes import Declaration
from stylo.selector import HOVER
from stylo.stylesheet import Stylesheet
@ -52,9 +52,9 @@ def test_style_pseudo_class() -> None:
)
link = root.query("a")
styled_node = stylesheet.style(link)
normal_style = styled_node.get_style(frozenset())
normal_style = styled_node.get_style(DomState())
_assert_style_equals(normal_style, {"background": "blue", "color": "red"})
hover_style = styled_node.get_style(frozenset([(link, HOVER)]))
hover_style = styled_node.get_style(DomState((link, HOVER)))
_assert_style_equals(hover_style, {"background": "red", "color": "red"})