dom: make DomState a class
This commit is contained in:
parent
f8728af173
commit
befed88628
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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))
|
|
@ -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))
|
||||
|
|
|
@ -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"})
|
||||
|
|
Loading…
Reference in New Issue