specificity

This commit is contained in:
Will McGugan
2021-10-27 15:33:38 +01:00
parent 0732247d1a
commit 3e74b5e0b0
4 changed files with 147 additions and 96 deletions

View File

@@ -20,7 +20,7 @@ class BoxProperty:
DEFAULT = ("", Style())
def __set_name__(self, owner: Styles, name: str) -> None:
self.internal_name = f"_{name}"
self.internal_name = f"_rule_{name}"
_type, edge = name.split("_")
self._type = _type
self.edge = edge
@@ -134,7 +134,7 @@ class StyleProperty:
DEFAULT_STYLE = Style()
def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_{name}"
self._internal_name = f"_rule_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style:
return getattr(obj, self._internal_name) or self.DEFAULT_STYLE
@@ -154,7 +154,7 @@ class StyleProperty:
class SpacingProperty:
def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_{name}"
self._internal_name = f"_rule_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Spacing:
return getattr(obj, self._internal_name) or NULL_SPACING
@@ -169,29 +169,29 @@ class DocksProperty:
def __get__(
self, obj: Styles, objtype: type[Styles] | None = None
) -> tuple[str, ...]:
return obj._docks or ()
return obj._rule_docks or ()
def __set__(self, obj: Styles, docks: str | Iterable[str]) -> tuple[str, ...]:
if isinstance(docks, str):
_docks = tuple(name.lower().strip() for name in docks.split(" "))
else:
_docks = tuple(docks)
obj._docks = _docks
obj._rule_docks = _docks
return _docks
class DockGroupProperty:
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
return obj._dock_group or ""
return obj._rule_dock_group or ""
def __set__(self, obj: Styles, spacing: str | None) -> str | None:
obj._dock_group = spacing
obj._rule_dock_group = spacing
return spacing
class OffsetProperty:
def __set_name__(self, owner: Styles, name: str) -> None:
self._internal_name = f"_{name}"
self._internal_name = f"_rule_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Offset:
return getattr(obj, self._internal_name) or Offset()
@@ -204,12 +204,12 @@ class OffsetProperty:
class DockEdgeProperty:
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
return obj._dock_edge or ""
return obj._rule_dock_edge or ""
def __set__(self, obj: Styles, edge: str | None) -> str | None:
if isinstance(edge, str) and edge not in VALID_EDGE:
raise ValueError(f"dock edge must be one of {friendly_list(VALID_EDGE)}")
obj._dock_edge = edge
obj._rule_dock_edge = edge
return edge
@@ -235,7 +235,7 @@ class StringProperty:
def __set_name__(self, owner: Styles, name: str) -> None:
self._name = name
self._internal_name = f"_{name}"
self._internal_name = f"_rule_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
return getattr(obj, self._internal_name, None) or self._default
@@ -253,7 +253,7 @@ class StringProperty:
class NameProperty:
def __set_name__(self, owner: Styles, name: str) -> None:
self._name = name
self._internal_name = f"_{name}"
self._internal_name = f"_rule_{name}"
def __get__(self, obj: Styles, objtype: type[Styles] | None) -> str:
return getattr(obj, self._internal_name, None) or ""
@@ -268,7 +268,7 @@ class NameProperty:
class NameListProperty:
def __set_name__(self, owner: Styles, name: str) -> None:
self._name = name
self._internal_name = f"_{name}"
self._internal_name = f"_rule_{name}"
def __get__(
self, obj: Styles, objtype: type[Styles] | None = None

View File

@@ -4,7 +4,7 @@ from rich import print
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
from typing import Iterable
from .styles import Styles
from .tokenize import Token
@@ -35,6 +35,7 @@ class Selector:
combinator: CombinatorType = CombinatorType.SAME
selector: SelectorType = SelectorType.TYPE
pseudo_classes: list[str] = field(default_factory=list)
specificity: tuple[int, int, int] = field(default_factory=lambda: (0, 0, 0))
@property
def css(self) -> str:
@@ -58,9 +59,26 @@ class Declaration:
raise NotImplementedError
@dataclass
class SelectorSet:
selectors: list[Selector] = field(default_factory=list)
specificity: tuple[int, int, int] = (0, 0, 0)
@classmethod
def from_selectors(cls, selectors: list[list[Selector]]) -> Iterable[SelectorSet]:
for selector_list in selectors:
id_total = class_total = type_total = 0
for selector in selector_list:
_id, _class, _type = selector.specificity
id_total += _id
class_total += _class
type_total += _type
yield SelectorSet(selector_list, (id_total, class_total, type_total))
@dataclass
class RuleSet:
selectors: list[list[Selector]] = field(default_factory=list)
selector_set: list[SelectorSet] = field(default_factory=list)
styles: Styles = field(default_factory=Styles)
@classmethod
@@ -77,7 +95,8 @@ class RuleSet:
@property
def css(self) -> str:
selectors = ", ".join(
self.selector_to_css(selector) for selector in self.selectors
self.selector_to_css(selector_set.selectors)
for selector_set in self.selector_set
)
declarations = "\n".join(f" {line}" for line in self.styles.css_lines)
css = f"{selectors} {{\n{declarations}\n}}"

View File

@@ -6,19 +6,26 @@ from typing import Iterator, Iterable
from .tokenize import tokenize, Token
from .model import Declaration, RuleSet, Selector, CombinatorType, SelectorType
from .model import (
Declaration,
RuleSet,
Selector,
CombinatorType,
SelectorSet,
SelectorType,
)
from ._styles_builder import StylesBuilder
SELECTOR_MAP = {
"selector": SelectorType.TYPE,
"selector_start": SelectorType.TYPE,
"selector_class": SelectorType.CLASS,
"selector_start_class": SelectorType.CLASS,
"selector_id": SelectorType.ID,
"selector_start_id": SelectorType.ID,
"selector_universal": SelectorType.UNIVERSAL,
"selector_start_universal": SelectorType.UNIVERSAL,
SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = {
"selector": (SelectorType.TYPE, (0, 0, 1)),
"selector_start": (SelectorType.TYPE, (0, 0, 1)),
"selector_class": (SelectorType.CLASS, (0, 1, 0)),
"selector_start_class": (SelectorType.CLASS, (0, 1, 0)),
"selector_id": (SelectorType.ID, (1, 0, 0)),
"selector_start_id": (SelectorType.ID, (1, 0, 0)),
"selector_universal": (SelectorType.UNIVERSAL, (0, 0, 0)),
"selector_start_universal": (SelectorType.UNIVERSAL, (0, 0, 0)),
}
@@ -45,11 +52,15 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
elif token.name == "declaration_set_start":
break
else:
_selector, specificity = get_selector(
token.name, (SelectorType.TYPE, (0, 0, 0))
)
selectors.append(
Selector(
name=token.value.lstrip(".#"),
combinator=combinator,
selector=get_selector(token.name, SelectorType.TYPE),
selector=_selector,
specificity=specificity,
)
)
combinator = CombinatorType.SAME
@@ -79,7 +90,9 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
if declaration.tokens:
styles_builder.add_declaration(declaration)
rule_set = RuleSet(rule_selectors, styles_builder.styles)
rule_set = RuleSet(
list(SelectorSet.from_selectors(rule_selectors)), styles_builder.styles
)
yield rule_set
@@ -96,7 +109,7 @@ def parse(css: str) -> Iterable[RuleSet]:
if __name__ == "__main__":
test = """
.foo.bar baz:focus, #egg {
.foo.bar baz:focus, #egg .foo.baz {
/* ignore me, I'm a comment */
display: block;
visibility: visible;

View File

@@ -35,36 +35,36 @@ from .types import Display, Visibility
@dataclass
class Styles:
_display: Display | None = None
_visibility: Visibility | None = None
_layout: str | None = None
_rule_display: Display | None = None
_rule_visibility: Visibility | None = None
_rule_layout: str | None = None
_text: Style | None = None
_rule_text: Style | None = None
_padding: Spacing | None = None
_margin: Spacing | None = None
_offset: Offset | None = None
_rule_padding: Spacing | None = None
_rule_margin: Spacing | None = None
_rule_offset: Offset | None = None
_border_top: tuple[str, Style] | None = None
_border_right: tuple[str, Style] | None = None
_border_bottom: tuple[str, Style] | None = None
_border_left: tuple[str, Style] | None = None
_rule_border_top: tuple[str, Style] | None = None
_rule_border_right: tuple[str, Style] | None = None
_rule_border_bottom: tuple[str, Style] | None = None
_rule_border_left: tuple[str, Style] | None = None
_outline_top: tuple[str, Style] | None = None
_outline_right: tuple[str, Style] | None = None
_outline_bottom: tuple[str, Style] | None = None
_outline_left: tuple[str, Style] | None = None
_rule_outline_top: tuple[str, Style] | None = None
_rule_outline_right: tuple[str, Style] | None = None
_rule_outline_bottom: tuple[str, Style] | None = None
_rule_outline_left: tuple[str, Style] | None = None
_size: int | None = None
_fraction: int | None = None
_min_size: int | None = None
_rule_size: int | None = None
_rule_fraction: int | None = None
_rule_min_size: int | None = None
_dock_group: str | None = None
_dock_edge: str | None = None
_docks: tuple[str, ...] | None = None
_rule_dock_group: str | None = None
_rule_dock_edge: str | None = None
_rule_docks: tuple[str, ...] | None = None
_layers: str | None = None
_layer: tuple[str, ...] | None = None
_rule_layers: str | None = None
_rule_layer: tuple[str, ...] | None = None
important: set[str] = field(default_factory=set)
@@ -111,6 +111,19 @@ class Styles:
"""Check if an outline is present."""
return any(edge for edge, _style in self.outline)
def extract_rules(
self, specificity: tuple[int, int, int]
) -> dict[str, tuple[object, tuple[int, int, int, int]]]:
is_important = self.important.__contains__
return {
rule_name: (
getattr(self, rule_name),
(int(is_important(rule_name)), *specificity),
)
for rule_name in RULE_NAMES
if getattr(self, f"_rule_{rule_name}") is not None
}
def __rich_repr__(self) -> rich.repr.Result:
if self.has_border:
yield "border", self.border
@@ -142,73 +155,73 @@ class Styles:
else:
append(f"{name}: {value};")
if self._display is not None:
append_declaration("display", self._display)
if self._visibility is not None:
append_declaration("visibility", self._visibility)
if self._text is not None:
append_declaration("text", str(self._text))
if self._padding is not None:
append_declaration("padding", self._padding.packed)
if self._margin is not None:
append_declaration("margin", self._margin.packed)
if self._rule_display is not None:
append_declaration("display", self._rule_display)
if self._rule_visibility is not None:
append_declaration("visibility", self._rule_visibility)
if self._rule_text is not None:
append_declaration("text", str(self._rule_text))
if self._rule_padding is not None:
append_declaration("padding", self._rule_padding.packed)
if self._rule_margin is not None:
append_declaration("margin", self._rule_margin.packed)
if (
self._border_top is not None
and self._border_top == self._border_right
and self._border_right == self._border_bottom
and self._border_bottom == self._border_left
self._rule_border_top is not None
and self._rule_border_top == self._rule_border_right
and self._rule_border_right == self._rule_border_bottom
and self._rule_border_bottom == self._rule_border_left
):
_type, style = self._border_top
_type, style = self._rule_border_top
append_declaration("border", f"{_type} {style}")
else:
if self._border_top is not None:
_type, style = self._border_top
if self._rule_border_top is not None:
_type, style = self._rule_border_top
append_declaration("border-top", f"{_type} {style}")
if self._border_right is not None:
_type, style = self._border_right
if self._rule_border_right is not None:
_type, style = self._rule_border_right
append_declaration("border-right", f"{_type} {style}")
if self._border_bottom is not None:
_type, style = self._border_bottom
if self._rule_border_bottom is not None:
_type, style = self._rule_border_bottom
append_declaration("border-bottom", f"{_type} {style}")
if self._border_left is not None:
_type, style = self._border_left
if self._rule_border_left is not None:
_type, style = self._rule_border_left
append_declaration("border-left", f"{_type} {style}")
if (
self._outline_top is not None
and self._outline_top == self._outline_right
and self._outline_right == self._outline_bottom
and self._outline_bottom == self._outline_left
self._rule_outline_top is not None
and self._rule_outline_top == self._rule_outline_right
and self._rule_outline_right == self._rule_outline_bottom
and self._rule_outline_bottom == self._rule_outline_left
):
_type, style = self._outline_top
_type, style = self._rule_outline_top
append_declaration("outline", f"{_type} {style}")
else:
if self._outline_top is not None:
_type, style = self._outline_top
if self._rule_outline_top is not None:
_type, style = self._rule_outline_top
append_declaration("outline-top", f"{_type} {style}")
if self._outline_right is not None:
_type, style = self._outline_right
if self._rule_outline_right is not None:
_type, style = self._rule_outline_right
append_declaration("outline-right", f"{_type} {style}")
if self._outline_bottom is not None:
_type, style = self._outline_bottom
if self._rule_outline_bottom is not None:
_type, style = self._rule_outline_bottom
append_declaration("outline-bottom", f"{_type} {style}")
if self._outline_left is not None:
_type, style = self._outline_left
if self._rule_outline_left is not None:
_type, style = self._rule_outline_left
append_declaration("outline-left", f"{_type} {style}")
if self.offset:
x, y = self.offset
append_declaration("offset", f"{x} {y}")
if self._dock_group:
append_declaration("dock-group", self._dock_group)
if self._docks:
append_declaration("docks", " ".join(self._docks))
if self._dock_edge:
append_declaration("dock-edge", self._dock_edge)
if self._layers is not None:
if self._rule_dock_group:
append_declaration("dock-group", self._rule_dock_group)
if self._rule_docks:
append_declaration("docks", " ".join(self._rule_docks))
if self._rule_dock_edge:
append_declaration("dock-edge", self._rule_dock_edge)
if self._rule_layers is not None:
append_declaration("layers", " ".join(self.layers))
if self._layer is not None:
if self._rule_layer is not None:
append_declaration("layer", self.layer)
lines.sort()
@@ -219,6 +232,8 @@ class Styles:
return "\n".join(self.css_lines)
RULE_NAMES = {name[6:] for name in dir(Styles) if name.startswith("_rule_")}
if __name__ == "__main__":
styles = Styles()
@@ -235,3 +250,7 @@ if __name__ == "__main__":
print(styles)
print(styles.css)
print(dir(styles))
print(RULE_NAMES)
print(styles.extract_rules((0, 1, 0)))