Source code for rational.styling.style

"""``Style``: an immutable, typed bundle of CSS declarations.

``None`` means "unspecified" (never CSS ``none``). ``a | b`` merges right-biased:
every property set in ``b`` wins; nested pseudo-state and responsive styles are
merged recursively.
"""

from __future__ import annotations

import re
from collections.abc import Mapping
from dataclasses import dataclass, field
from types import MappingProxyType
from typing import Any, Literal, TypeAlias

Length: TypeAlias = int | float | str
Color: TypeAlias = str

UNSAFE_VALUE = re.compile(r"[;{}<>]|/\*")
RAW_PROPERTY = re.compile(r"^-{0,2}[a-zA-Z][a-zA-Z0-9-]*$")


[docs] class StyleError(ValueError): """An invalid or unsafe style value."""
PSEUDO_FIELDS = ("hover", "focus", "focus_visible", "active", "disabled", "placeholder") MAPPING_FIELDS = ("viewport", "container", "raw")
[docs] @dataclass(frozen=True, eq=True, repr=False) class Style: # layout display: str | None = None layout: Literal["vertical", "horizontal"] | None = None direction: str | None = None wrap: bool | str | None = None justify: str | None = None align: str | None = None align_self: str | None = None gap: Length | None = None row_gap: Length | None = None column_gap: Length | None = None grow: int | float | None = None shrink: int | float | None = None basis: Length | None = None columns: int | str | None = None rows: int | str | None = None column_span: int | str | None = None position: str | None = None top: Length | None = None right: Length | None = None bottom: Length | None = None left: Length | None = None inset: Length | None = None z_index: int | None = None overflow: str | None = None overflow_x: str | None = None overflow_y: str | None = None # spacing padding: Length | None = None padding_x: Length | None = None padding_y: Length | None = None padding_top: Length | None = None padding_right: Length | None = None padding_bottom: Length | None = None padding_left: Length | None = None margin: Length | None = None margin_x: Length | None = None margin_y: Length | None = None margin_top: Length | None = None margin_right: Length | None = None margin_bottom: Length | None = None margin_left: Length | None = None # sizing width: Length | None = None height: Length | None = None min_width: Length | None = None max_width: Length | None = None min_height: Length | None = None max_height: Length | None = None aspect_ratio: str | float | None = None # typography font_family: str | None = None font_size: Length | None = None font_weight: int | str | None = None line_height: float | str | None = None letter_spacing: Length | None = None text_align: str | None = None text_transform: str | None = None text_decoration: str | None = None white_space: str | None = None color: Color | None = None # surface background: Color | None = None border: bool | str | None = None border_top: bool | str | None = None border_right: bool | str | None = None border_bottom: bool | str | None = None border_left: bool | str | None = None border_color: Color | None = None border_width: Length | None = None border_style: str | None = None border_radius: Length | None = None outline: str | None = None shadow: str | None = None opacity: float | None = None # behavior cursor: str | None = None transition: bool | str | None = None transform: str | None = None pointer_events: str | None = None user_select: str | None = None list_style: str | None = None object_fit: str | None = None container_type: str | None = None container_name: str | None = None # nested hover: Style | None = None focus: Style | None = None focus_visible: Style | None = None active: Style | None = None disabled: Style | None = None placeholder: Style | None = None viewport: Mapping[str, Style] | None = None container: Mapping[str, Style] | None = None raw: Mapping[str, str] | None = None _items: tuple[tuple[str, Any], ...] = field(init=False, repr=False, compare=False) _key: tuple[Any, ...] = field(init=False, repr=False, compare=False) _hash: int = field(init=False, repr=False, compare=False) def __post_init__(self) -> None: # One pass: validate eagerly (so errors point at the line that built the style), # freeze mappings, and cache the set fields and the value key. Styles are immutable, # so hashing, equality and compiler cache lookups never walk all fields again. items: list[tuple[str, Any]] = [] for name, value in self.__dict__.items(): if value is None: continue if isinstance(value, str) and UNSAFE_VALUE.search(value): raise StyleError(f"unsafe characters in {name}={value!r}") if name in MAPPING_FIELDS: value = MappingProxyType(dict(value)) object.__setattr__(self, name, value) items.append((name, value)) for prop, raw_value in (self.raw or {}).items(): if not RAW_PROPERTY.match(prop): raise StyleError(f"invalid raw CSS property {prop!r}") if UNSAFE_VALUE.search(str(raw_value)): raise StyleError(f"unsafe characters in raw[{prop!r}]={raw_value!r}") key = tuple((name, _key_of(value)) for name, value in items) object.__setattr__(self, "_items", tuple(items)) object.__setattr__(self, "_key", key) object.__setattr__(self, "_hash", hash(key)) def __or__(self, other: Style | None) -> Style: if other is None: return self if not isinstance(other, Style): return NotImplemented cache_key = (self._key, other._key) cached = _MERGE_CACHE.get(cache_key) if cached is not None: return cached merged = dict(self._items) for name, b in other._items: a = merged.get(name) if a is None: merged[name] = b elif name in PSEUDO_FIELDS: merged[name] = a | b elif name in ("viewport", "container"): merged[name] = {k: (a[k] | b[k]) if k in a and k in b else b.get(k, a.get(k)) for k in {**a, **b}} elif name == "raw": merged[name] = {**a, **b} else: merged[name] = b result = Style(**merged) if len(_MERGE_CACHE) >= _MERGE_CACHE_SIZE: _MERGE_CACHE.clear() _MERGE_CACHE[cache_key] = result return result def __ror__(self, other: Style | None) -> Style: if other is None: return self return NotImplemented
[docs] def items(self) -> tuple[tuple[str, Any], ...]: """The set (non-``None``) fields, in declaration order.""" return self._items
[docs] def key(self) -> tuple[Any, ...]: """A hashable value identity: equal styles have equal keys.""" return self._key
def __hash__(self) -> int: return self._hash def __eq__(self, other: object) -> bool: return isinstance(other, Style) and self._key == other._key def __repr__(self) -> str: return "Style(" + ", ".join(f"{k}={v!r}" for k, v in self._items) + ")" def __bool__(self) -> bool: return bool(self._items)
_MERGE_CACHE: dict[tuple[Any, Any], Style] = {} _MERGE_CACHE_SIZE = 4096 def _key_of(value: Any) -> Any: if isinstance(value, Style): return value._key if isinstance(value, Mapping): return tuple(sorted((k, _key_of(v)) for k, v in value.items())) return value def merge(*styles: Style | None) -> Style | None: out: Style | None = None for s in styles: if s is not None: out = s if out is None else out | s return out