Source code for domonic.style

"""
domonic.style
=============

CSSOM-flavoured style helpers for domonic.

This module provides ``CSSStyleDeclaration``, stylesheet lists, rule objects,
and the basic structures needed to attach style data to DOM nodes and
documents.
"""

from __future__ import annotations

import ast
import functools
import math
import re
from re import M, findall, finditer
from typing import Any, Callable

from . import _cssom
from .utils import Utils

COMMENT_REGEXP = r"/\*[\s\S]*?\*/"
_IMPORTANT_RE = re.compile(r"\s*!\s*important\s*$", re.IGNORECASE)
_KNOWN_CSS_PROPERTIES: set[str] | None = None
_CSS_STYLE_INTERNAL_NAMES = {
    "cssText",
    "parentRule",
    "parentStyleSheet",
    "_css_text",
    "_declared_properties",
    "_members_checked",
    "_parent_node",
    "_suspend_style_sync",
}
_ADDITIONAL_CSS_PROPERTY_NAMES = {
    "accent-color",
    "align-tracks",
    "anchor-name",
    "anchor-scope",
    "animation-composition",
    "appearance",
    "aspect-ratio",
    "backdrop-filter",
    "block-size",
    "border-block",
    "border-block-color",
    "border-block-end",
    "border-block-end-color",
    "border-block-end-style",
    "border-block-end-width",
    "border-block-start",
    "border-block-start-color",
    "border-block-start-style",
    "border-block-start-width",
    "border-block-style",
    "border-block-width",
    "border-inline",
    "border-inline-color",
    "border-inline-end",
    "border-inline-end-color",
    "border-inline-end-style",
    "border-inline-end-width",
    "border-inline-start",
    "border-inline-start-color",
    "border-inline-start-style",
    "border-inline-start-width",
    "border-inline-style",
    "border-inline-width",
    "container",
    "container-name",
    "container-type",
    "contain-intrinsic-block-size",
    "contain-intrinsic-height",
    "contain-intrinsic-inline-size",
    "contain-intrinsic-size",
    "contain-intrinsic-width",
    "content-visibility",
    "field-sizing",
    "font-palette",
    "font-size-adjust",
    "font-synthesis-position",
    "font-synthesis-small-caps",
    "font-synthesis-style",
    "font-synthesis-weight",
    "hyphenate-character",
    "image-rendering",
    "initial-letter",
    "inline-size",
    "inset",
    "inset-block",
    "inset-block-end",
    "inset-block-start",
    "inset-inline",
    "inset-inline-end",
    "inset-inline-start",
    "interpolate-size",
    "justify-tracks",
    "line-clamp",
    "margin-block",
    "margin-block-end",
    "margin-block-start",
    "margin-inline",
    "margin-inline-end",
    "margin-inline-start",
    "mask-border",
    "mask-clip",
    "mask-composite",
    "mask-image",
    "mask-mode",
    "mask-origin",
    "mask-position",
    "mask-repeat",
    "mask-size",
    "max-block-size",
    "max-inline-size",
    "min-block-size",
    "min-inline-size",
    "object-fit",
    "object-position",
    "offset",
    "offset-anchor",
    "offset-distance",
    "offset-path",
    "offset-position",
    "offset-rotate",
    "overflow-anchor",
    "overflow-block",
    "overflow-clip-margin",
    "overflow-inline",
    "overscroll-behavior",
    "overscroll-behavior-block",
    "overscroll-behavior-inline",
    "overscroll-behavior-x",
    "overscroll-behavior-y",
    "padding-block",
    "padding-block-end",
    "padding-block-start",
    "padding-inline",
    "padding-inline-end",
    "padding-inline-start",
    "place-content",
    "place-items",
    "place-self",
    "position-anchor",
    "position-area",
    "position-try",
    "position-try-fallbacks",
    "position-try-order",
    "position-visibility",
    "print-color-adjust",
    "rotate",
    "scale",
    "scrollbar-color",
    "scrollbar-gutter",
    "scrollbar-width",
    "text-box",
    "text-box-edge",
    "text-box-trim",
    "text-decoration-skip-ink",
    "text-emphasis",
    "text-emphasis-color",
    "text-emphasis-position",
    "text-emphasis-style",
    "text-size-adjust",
    "text-wrap",
    "text-wrap-mode",
    "text-wrap-style",
    "timeline-scope",
    "touch-action",
    "translate",
    "view-timeline",
    "view-timeline-axis",
    "view-timeline-inset",
    "view-timeline-name",
    "view-transition-class",
    "view-transition-name",
}


@functools.lru_cache(maxsize=1024)
def _css_property_name(name: str) -> str:
    """Normalize DOM style names to CSS property names.

    Pure function of *name* over a small, fixed universe of property/attribute
    spellings, called on every style read (``getPropertyValue``, the ``style``
    IDL, ``getComputedStyle``) -- cached so the regex-driven case conversion
    it delegates to only ever runs once per distinct name.
    """
    if name in ("cssFloat", "float"):
        return "float"
    if name.startswith("--"):
        return name
    return Utils.case_kebab(name)


@functools.lru_cache(maxsize=1024)
def _css_attribute_name(name: str) -> str:
    """Normalize CSS property names to DOM style attribute names. See
    ``_css_property_name`` -- cached for the same reason."""
    if name == "float":
        return "cssFloat"
    if name.startswith("--"):
        return name
    return Utils.case_camel(name)


def _strip_css_comments(css_text: str) -> str:
    return re.sub(COMMENT_REGEXP, "", css_text or "")


def _split_css_declarations(css_text: str) -> list[str]:
    declarations: list[str] = []
    start = 0
    quote = None
    escaped = False
    depth = 0
    for index, char in enumerate(css_text or ""):
        if quote is not None:
            if escaped:
                escaped = False
            elif char == "\\":
                escaped = True
            elif char == quote:
                quote = None
            continue
        if char in ("'", '"'):
            quote = char
            continue
        if char in "([{":
            depth += 1
            continue
        if char in ")]}" and depth > 0:
            depth -= 1
            continue
        if char == ";" and depth == 0:
            item = css_text[start:index].strip()
            if item:
                declarations.append(item)
            start = index + 1
    item = (css_text or "")[start:].strip()
    if item:
        declarations.append(item)
    return declarations


#: each 2-value logical axis shorthand ("start end", or one value for both)
#: -> its two physical longhands, in start-then-end order. Only LTR,
#: horizontal writing-mode is modelled (inline -> left/right, block ->
#: top/bottom), matching this project's existing lack of RTL/bidi support.
_LOGICAL_AXIS_SHORTHANDS: dict[str, tuple[str, str]] = {
    "padding-inline": ("padding-left", "padding-right"),
    "padding-block": ("padding-top", "padding-bottom"),
    "margin-inline": ("margin-left", "margin-right"),
    "margin-block": ("margin-top", "margin-bottom"),
    "inset-inline": ("left", "right"),
    "inset-block": ("top", "bottom"),
}

#: each single-value logical longhand -> the one physical longhand it aliases.
_LOGICAL_ALIAS_LONGHANDS: dict[str, str] = {
    "padding-inline-start": "padding-left",
    "padding-inline-end": "padding-right",
    "padding-block-start": "padding-top",
    "padding-block-end": "padding-bottom",
    "margin-inline-start": "margin-left",
    "margin-inline-end": "margin-right",
    "margin-block-start": "margin-top",
    "margin-block-end": "margin-bottom",
    "inset-inline-start": "left",
    "inset-inline-end": "right",
    "inset-block-start": "top",
    "inset-block-end": "bottom",
}


#: properties whose value CSS 2.1 requires to be non-negative (8.4's
#: "Negative values for padding are not allowed", 8.5.3's own words for
#: border-width, and each property's own definition for the rest). A
#: syntactically invalid value is dropped per the cascade -- as if that one
#: declaration were never written -- not applied literally or clamped.
_NON_NEGATIVE_LENGTH_PROPERTIES = frozenset(
    {
        "padding",
        "padding-top",
        "padding-right",
        "padding-bottom",
        "padding-left",
        "padding-inline",
        "padding-block",
        "padding-inline-start",
        "padding-inline-end",
        "padding-block-start",
        "padding-block-end",
        "border-width",
        "border-top-width",
        "border-right-width",
        "border-bottom-width",
        "border-left-width",
        "outline-width",
        "width",
        "height",
        "min-width",
        "min-height",
        "font-size",
    }
)
#: a single token that is unambiguously a negative length/percentage (not
#: ``var()``/``calc()``, which need substitution before they can be judged).
_NEGATIVE_LENGTH_TOKEN_RE = re.compile(
    r"^-(?:\d+\.?\d*|\.\d+)(px|em|rem|%|in|cm|mm|q|pt|pc|vw|vh|vmin|vmax|ch|ex)?$", re.I
)


def _is_invalid_non_negative_declaration(name: str, value: str) -> bool:
    if name not in _NON_NEGATIVE_LENGTH_PROPERTIES:
        return False
    low = value.lower()
    if "var(" in low or "calc(" in low:
        return False
    return any(_NEGATIVE_LENGTH_TOKEN_RE.match(token) for token in value.split())


def _parse_css_declarations(css_text: str) -> list[tuple[str, str, str]]:
    entries: list[tuple[str, str, str]] = []
    index_by_name: dict[str, int] = {}

    def emit(name: str, value: str, priority: str) -> None:
        # a later declaration for the same (already-physical) name replaces
        # an earlier one rather than sitting alongside it, keeping its first
        # position -- e.g. a repeated ``padding-inline`` in the same rule, or
        # a plain longhand and the logical property that aliases it.
        existing = index_by_name.get(name)
        if existing is None:
            index_by_name[name] = len(entries)
            entries.append((name, value, priority))
        else:
            entries[existing] = (name, value, priority)

    for declaration in _split_css_declarations(css_text):
        if ":" not in declaration:
            continue
        name, value = declaration.split(":", 1)
        name = _css_property_name(name.strip())
        value = value.strip()
        if not name:
            continue
        priority = ""
        if _IMPORTANT_RE.search(value):
            value = _IMPORTANT_RE.sub("", value).strip()
            priority = "important"
        # a syntactically invalid declaration is dropped entirely -- the
        # cascade behaves as if it were never written, leaving whatever the
        # property's next-highest-priority declaration (or initial value)
        # was, not this literal invalid value.
        if _is_invalid_non_negative_declaration(name, value):
            continue
        # logical properties are expanded into their physical longhand(s)
        # here, at the tokenizer level, so the expansion rides on the same
        # cascade priority (source order, !important) as the declaration it
        # came from, rather than a physical longhand from some unrelated,
        # lower-priority rule winning just by already having a value.
        axis = _LOGICAL_AXIS_SHORTHANDS.get(name)
        if axis is not None:
            parts = value.split()
            if len(parts) == 1:
                emit(axis[0], parts[0], priority)
                emit(axis[1], parts[0], priority)
                continue
            if len(parts) == 2:
                emit(axis[0], parts[0], priority)
                emit(axis[1], parts[1], priority)
                continue
            # unrecognised shape -- keep verbatim rather than guess
            emit(name, value, priority)
            continue
        alias = _LOGICAL_ALIAS_LONGHANDS.get(name)
        emit(alias if alias is not None else name, value, priority)
    return entries


#: <length>-valued properties where a bare ``0`` is serialised as ``0px`` (the
#: CSSOM normalises unit-optional zero). ``line-height`` is excluded -- a
#: unitless number there is a multiplier, not a length.
_ZERO_LENGTH_PROPERTIES = frozenset(
    {
        "margin",
        "margin-top",
        "margin-right",
        "margin-bottom",
        "margin-left",
        "padding",
        "padding-top",
        "padding-right",
        "padding-bottom",
        "padding-left",
        "inset",
        "top",
        "right",
        "bottom",
        "left",
        "width",
        "height",
        "min-width",
        "min-height",
        "max-width",
        "max-height",
        "border-width",
        "border-top-width",
        "border-right-width",
        "border-bottom-width",
        "border-left-width",
        "outline-width",
        "column-rule-width",
        "column-width",
        "column-gap",
        "row-gap",
        "gap",
        "letter-spacing",
        "word-spacing",
        "text-indent",
        "outline-offset",
        "border-radius",
        "border-top-left-radius",
        "border-top-right-radius",
        "border-bottom-right-radius",
        "border-bottom-left-radius",
    }
)
_BARE_ZERO_RE = re.compile(r"(?<![\w.#])0(?![\w.%])")

# The non-``None`` per-property defaults that ``Style.__init__`` used to assign
# one-by-one through the property/decorator machinery (~380 assignments, ~430
# us per ``Style()``). It is now built lazily: the camelCase getters
# (``style_get_decorator``) fall back to this map when a property was never
# set on the instance, so ``el.style.fontSize`` still reports ``"medium"`` etc.
# ``None`` defaults are not listed -- the getter already renders a missing /
# ``None`` value as ``"none"``. Keyed by the camelCase attribute name (the
# getter function name), matching how the old assignments were spelled.
_STYLE_INITIAL_VALUES: dict[str, Any] = {
    "alignContent": "normal",
    "alignItems": "normal",
    "alignSelf": "auto",
    "animation": "normal",
    "animationDelay": 0,
    "animationDirection": "normal",
    "animationDuration": 0,
    "animationIterationCount": 1,
    "animationPlayState": "running",
    "animationTimingFunction": "ease",
    "backgroundAttachment": "scroll",
    "border": "medium none black",
    "borderBottom": "medium none black",
    "borderBottomLeftRadius": "0px",
    "borderBottomRightRadius": "0px",
    "borderRadius": "0px",
    "borderTopLeftRadius": "0px",
    "borderTopRightRadius": "0px",
    "columnGap": "normal",
    "fontSize": "medium",
    "fontStyle": "normal",
    "fontWeight": "normal",
    "height": "auto",
    "justifyContent": "normal",
    "left": "auto",
    "margin": "0px",
    "marginBottom": "0px",
    "marginLeft": "0px",
    "marginRight": "0px",
    "marginTop": "0px",
    "overflow": "visible",
    "padding": "0px",
    "paddingBottom": "0px",
    "paddingLeft": "0px",
    "paddingRight": "0px",
    "paddingTop": "0px",
    "pageBreakAfter": "auto",
    "pageBreakBefore": "auto",
    "pageBreakInside": "auto",
    "right": "auto",
    "tableLayout": "auto",
    "textAlign": "left",
    "textAlignLast": "auto",
    "textOverflow": "clip",
    "transitionDelay": 0,
    "transitionDuration": 0,
    "visibility": "visible",
    "whiteSpace": "normal",
    "width": "auto",
    "wordBreak": "normal",
    "wordWrap": "normal",
    "zIndex": "auto",
}


def _normalize_zero_lengths(property_name: str, value: str) -> str:
    if property_name in _ZERO_LENGTH_PROPERTIES and "0" in value:
        return _BARE_ZERO_RE.sub("0px", value)
    return value


def _set_css_declaration(
    entries: list[tuple[str, str, str]],
    property_name: str,
    value: Any,
    priority: str = "",
) -> list[tuple[str, str, str]]:
    value = "" if value is None else str(value)
    value = _normalize_zero_lengths(property_name, value)
    priority = "important" if str(priority or "").lower() == "important" else ""
    updated = False
    next_entries: list[tuple[str, str, str]] = []
    for name, current_value, current_priority in entries:
        if name == property_name:
            if not updated:
                next_entries.append((property_name, value, priority))
                updated = True
            continue
        next_entries.append((name, current_value, current_priority))
    if not updated:
        next_entries.append((property_name, value, priority))
    return next_entries


def _serialize_css_declarations(entries: list[tuple[str, str, str]], compact: bool = False) -> str:
    separator = ":" if compact else ": "
    joiner = "" if compact else " "
    declarations = [
        (f"{name}{separator}{value}{joiner}!important;" if priority == "important" else f"{name}{separator}{value};")
        for name, value, priority in entries
    ]
    return "".join(declarations) if compact else " ".join(declarations)


def _split_top_level_commas(text: str) -> list[str]:
    values: list[str] = []
    start = 0
    quote = None
    escaped = False
    depth = 0
    for index, char in enumerate(text or ""):
        if quote is not None:
            if escaped:
                escaped = False
            elif char == "\\":
                escaped = True
            elif char == quote:
                quote = None
            continue
        if char in ("'", '"'):
            quote = char
            continue
        if char in "([":
            depth += 1
            continue
        if char in ")]" and depth > 0:
            depth -= 1
            continue
        if char == "," and depth == 0:
            values.append(text[start:index].strip())
            start = index + 1
    tail = (text or "")[start:].strip()
    if tail:
        values.append(tail)
    return values


def _find_matching_paren(css_text: str, open_index: int) -> int:
    quote = None
    escaped = False
    depth = 0
    for index in range(open_index, len(css_text)):
        char = css_text[index]
        if quote is not None:
            if escaped:
                escaped = False
            elif char == "\\":
                escaped = True
            elif char == quote:
                quote = None
            continue
        if char in ("'", '"'):
            quote = char
            continue
        if char == "(":
            depth += 1
            continue
        if char == ")":
            depth -= 1
            if depth == 0:
                return index
    return -1


def _split_top_level_word(text: str, word: str) -> tuple[str, str] | None:
    quote = None
    escaped = False
    depth = 0
    lower_word = word.lower()
    lower_text = (text or "").lower()
    for index, char in enumerate(text or ""):
        if quote is not None:
            if escaped:
                escaped = False
            elif char == "\\":
                escaped = True
            elif char == quote:
                quote = None
            continue
        if char in ("'", '"'):
            quote = char
            continue
        if char in "([":
            depth += 1
            continue
        if char in ")]" and depth > 0:
            depth -= 1
            continue
        if depth != 0 or lower_text[index : index + len(lower_word)] != lower_word:
            continue
        before = text[index - 1] if index > 0 else " "
        after_index = index + len(lower_word)
        after = text[after_index] if after_index < len(text) else " "
        if before.isspace() and after.isspace():
            return text[:index], text[after_index:]
    return None


[docs] class StyleSheet: """Base stylesheet object exposed through document-level style collections. Concrete CSS sheets build on this interface with parsed rules and mutable declaration content. """ def __init__(self) -> None: # whether the sheet is prevented from applying (spec default: enabled) self._disabled: bool = False self.href: str | None = None self.parentStyleSheet: StyleSheet | None = None self.ownerNode = None self.title: str | None = None self.type: str = "text/css" self.media: MediaList = MediaList() @property def disabled(self) -> bool: """Whether this sheet is prevented from contributing to the cascade (spec default: enabled). A disabled sheet's rules are excluded in ``_build_rule_index``; toggling this also bumps the same stylesheet epoch ``insertRule``/``deleteRule``/``replace(Sync)`` already bump, so it invalidates the rule-index and computed-style caches exactly like any other rule change -- otherwise an element already queried once would keep serving its pre-toggle computed style forever.""" return self._disabled @disabled.setter def disabled(self, value: bool) -> None: value = bool(value) if value != self._disabled: self._disabled = value _cssom.bump_stylesheet_epoch() else: self._disabled = value
# @property # def href(self): # """Returns a DOMString representing the location of the stylesheet.""" # return self.href # @property # def media(self): # """Returns a MediaList representing the intended destination medium for style information.""" # raise NotImplementedError # @property # def ownerNode(self): # """Returns a Node associating this style sheet with the current document.""" # raise NotImplementedError # @property # def parentStyleSheet(self): # """Returns a StyleSheet including this one, if any; returns null if there aren't any.""" # raise NotImplementedError # @property # def title(self): # """Returns a DOMString representing the advisory title of the current style sheet.""" # raise NotImplementedError # @property # def type(self): # """Returns a DOMString representing the style sheet language for this style sheet.""" # raise NotImplementedError
[docs] class StyleSheetList(list): """List-like collection returned by ``Document.styleSheets``. It behaves like an ordered DOM collection while still being convenient to iterate over as a Python list. """ def _populate_stylesheets_from_document(self, doc) -> None: """parse the document to find all the stylesheets and add them to the list.""" sheets = doc.querySelectorAll('link[rel="stylesheet"]') for sheet in sheets: ss = CSSStyleSheet() ss._constructed = False ss.href = sheet.href ss.ownerNode = sheet self.append(ss) styles = doc.querySelectorAll("style") for style in styles: ss = CSSStyleSheet() ss._constructed = False ss.href = doc.URL ss.ownerNode = style ss.replaceSync(style.textContent or "") self.append(ss) @property def length(self) -> int: """Returns the number of CSSStyleSheet objects in the collection.""" return len(self)
[docs] def item(self, index: int) -> CSSStyleSheet | None: """Returns the CSSStyleSheet object at the index passed in, or null if no item exists for that index.""" try: return self[index] except IndexError: return None
[docs] class CSSRule: """Base class for CSSOM rules stored inside a stylesheet.""" UNKNOWN_RULE: int = 0 STYLE_RULE: int = 1 CHARSET_RULE: int = 2 IMPORT_RULE: int = 3 MEDIA_RULE: int = 4 FONT_FACE_RULE: int = 5 PAGE_RULE: int = 6 NAMESPACE_RULE: int = 7 KEYFRAMES_RULE: int = 8 KEYFRAME_RULE: int = 9 COUNTER_STYLE_RULE: int = 10 SUPPORTS_RULE: int = 11 FONT_FEATURE_VALUES_RULE: int = 12 VIEWPORT_RULE: int = 13 SUPPORTS_CONDITION_RULE: int = 14 DOCUMENT_RULE: int = 15 COLOR_PROFILE_RULE: int = 16 LAYER_BLOCK_RULE: int = 17 LAYER_STATEMENT_RULE: int = 18 CONTAINER_RULE: int = 19 SCOPE_RULE: int = 20 PROPERTY_RULE: int = 21 NESTED_DECLARATIONS_RULE: int = 22 WHEN_RULE: int = 23 ELSE_RULE: int = 24 # GROUPING_RULES = [FONT_FACE_RULE, MEDIA_RULE, NAMESPACE_RULE, KEYFRAMES_RULE, COUNTER_STYLE_RULE, SUPPORTS_RULE, SUPPORTS_CONDITION_RULE] def __init__(self): # self._type = None self.parentRule: "CSSRule | None" = None self.parentStyleSheet: "CSSStyleSheet | None" = None self.type: int | None = None self._cssText: str | None = None @property def cssText(self): """Represents the textual representation of the rule, e.g. "h1,h2 { font-size: 16pt }" or "@import 'url'". Per the CSSOM spec this is read-only; the specialized rule interfaces compute it from their own properties. The generic ``CSSRule`` fallback reads the raw text captured in ``_cssText`` at parse time. """ return self._cssText or "" def __str__(self): return self.cssText
# @property # def parentRule(self): # """Returns the containing rule, otherwise null. E.g. if this rule is a style rule inside an @media block, # the parent rule would be that CSSMediaRule.""" # raise NotImplementedError # def parentStyleSheet(self): # """Returns the CSSStyleSheet object for the style sheet that contains this rule""" # raise NotImplementedError # def type(self): # """Returns one of the Type constants to determine which type of rule is represented.""" # raise NotImplementedError
[docs] class CSSImportRule(CSSRule): """The CSSImportRule interface represents an @import rule in a CSS style sheet.""" def __init__(self): super().__init__() self.href: str | None = None self.media: "MediaList | None" = None self.layerName: str | None = None self.supportsText: str = "" self.styleSheet: "CSSStyleSheet | None" = None self.type = CSSRule.IMPORT_RULE @property def cssText(self): layer = "" if self.layerName is not None: layer = " layer" if self.layerName == "" else f" layer({self.layerName})" supports = f" supports({self.supportsText})" if self.supportsText else "" media = f" {self.media.mediaText}" if self.media and self.media.mediaText else "" return f"@import {self.href}{layer}{supports}{media};"
# @property # def href(self): # """Returns a DOMString representing the URL of the imported style sheet.""" # raise NotImplementedError # @property # def media(self): # """Returns a MediaList representing the intended destination medium for style information.""" # raise NotImplementedError # @property # def styleSheet(self): # """Returns the CSSStyleSheet object representing the style sheet referenced by this rule.""" # raise NotImplementedError
[docs] class CSSStyleRule(CSSRule): """The CSSStyleRule interface represents a single style rule in a CSS style sheet.""" def __init__(self): super().__init__() self.selectorText: str | None = None self.style: "CSSStyleDeclaration | None" = None self.cssRules: "CSSRuleList" = CSSRuleList() self.type = CSSRule.STYLE_RULE # @property # def selectorText(self): # """Returns the textual representation of the rule selector.""" # raise NotImplementedError # @property # def style(self): # """Returns the CSSStyleDeclaration object representing the declaration block of this rule.""" # raise NotImplementedError @property def cssText(self): """Returns the textual representation of the rule.""" body = self.style.cssText if self.style is not None else "" children = " ".join(str(rule) for rule in self.cssRules) if children: body = f"{body} {children}".strip() return f"{self.selectorText} {{{body}}}"
# @cssText.setter # def cssText(self, value): # """Sets the textual representation of the rule.""" # raise NotImplementedError
[docs] class CSSFontFaceRule(CSSRule): """The CSSFontFaceRule interface represents a @font-face rule in a CSS style sheet.""" def __init__(self): super().__init__() self.style: "CSSStyleDeclaration | None" = None self.type = CSSRule.FONT_FACE_RULE @property def cssText(self): return f"@font-face {{{self.style.cssText if self.style is not None else ''}}}"
# @property # def style(self): # """Returns the CSSStyleDeclaration object representing the declaration block of this rule.""" # raise NotImplementedError
[docs] class CSSPageRule(CSSRule): """The CSSPageRule interface represents a @page rule in a CSS style sheet.""" def __init__(self): super().__init__() self.selectorText: str | None = None self.style: "CSSStyleDeclaration | None" = None self.type = CSSRule.PAGE_RULE @property def cssText(self): selector = f" {self.selectorText}" if self.selectorText else "" return f"@page{selector} {{{self.style.cssText if self.style is not None else ''}}}"
# @property # def selectorText(self): # """Returns the textual representation of the page selector for the rule.""" # raise NotImplementedError # @property # def style(self): # """Returns the CSSStyleDeclaration object representing the declaration block of this rule.""" # raise NotImplementedError
[docs] class CSSNamespaceRule(CSSRule): """The CSSNamespaceRule interface represents an @namespace rule in a CSS style sheet.""" def __init__(self): super().__init__() self.namespaceURI: str | None = None self.prefix: str | None = None self.type = CSSRule.NAMESPACE_RULE @property def cssText(self): prefix = f" {self.prefix}" if self.prefix else "" return f"@namespace{prefix} {self.namespaceURI};"
# @property # def namespaceURI(self): # """Returns the namespace URI for this rule.""" # raise NotImplementedError # @property # def prefix(self): # """Returns the namespace prefix for this rule.""" # raise NotImplementedError
[docs] class CSSKeyframesRule(CSSRule): """The CSSKeyframesRule interface represents a @keyframes at-rule.""" def __init__(self): super().__init__() self.cssRules: "CSSRuleList" = CSSRuleList() self.name: str | None = None self.type = CSSRule.KEYFRAMES_RULE @property def cssText(self): return f"@keyframes {self.name} {{{' '.join(str(rule) for rule in self.cssRules)}}}"
# @property # def cssRules(self): # """Returns the CSSRuleList object representing the CSS rules in the media rule.""" # raise NotImplementedError # @property # def name(self): # """Returns the name of the keyframes. This is a DOMString containing the unescaped keyframes name.""" # raise NotImplementedError
[docs] class CSSKeyframeRule(CSSRule): """The CSSKeyframeRule interface represents a single @keyframes at-rule.""" def __init__(self): super().__init__() self.cssRules: "CSSRuleList" = CSSRuleList() self.keyText: str | None = None self.style: "CSSStyleDeclaration | None" = None self.type = CSSRule.KEYFRAME_RULE @property def cssText(self): return f"{self.keyText} {{{self.style.cssText if self.style is not None else ''}}}"
# @property # def cssRules(self): # """Returns the CSSRuleList object representing the CSS rules in the media rule.""" # raise NotImplementedError # @property # def keyText(self): # """Returns the text of the keyframe rule.""" # raise NotImplementedError # @property # def style(self): # """Returns the CSSStyleDeclaration object representing the declaration block of this rule.""" # raise NotImplementedError
[docs] class CSSCounterStyleRule(CSSRule): """The CSSCounterStyleRule interface represents a @counter-style rule.""" def __init__(self): super().__init__() self.name: str | None = None self.system: str | None = None self.style: "CSSStyleDeclaration | None" = None self.type = CSSRule.COUNTER_STYLE_RULE @property def cssText(self): return f"@counter-style {self.name} {{{self.style.cssText if self.style is not None else ''}}}"
# @property # def name(self): # """Returns the name of the counter style.""" # raise NotImplementedError # @property # def system(self): # """Returns the system of the counter style.""" # raise NotImplementedError
[docs] class CSSDocumentRule(CSSRule): """The CSSDocumentRule interface represents an @document rule.""" def __init__(self): super().__init__() self.cssRules: "CSSRuleList" = CSSRuleList() self.type = CSSRule.DOCUMENT_RULE @property def cssText(self): return f"@document {{{' '.join(str(rule) for rule in self.cssRules or [])}}}"
# @property # def cssRules(self): # """Returns the CSSRuleList object representing the CSS rules in the media rule.""" # raise NotImplementedError
[docs] class CSSColorProfileRule(CSSRule): """The CSSColorProfileRule interface represents an @color-profile rule.""" def __init__(self): super().__init__() self.colorProfile: str | None = None self.style: "CSSStyleDeclaration | None" = None self.type = CSSRule.COLOR_PROFILE_RULE @property def cssText(self): return f"@color-profile {self.colorProfile} {{{self.style.cssText if self.style is not None else ''}}}"
# @property # def colorProfile(self): # """Returns the textual representation of the rule selector.""" # raise NotImplementedError
[docs] class CSSFontFeatureValuesRule(CSSRule): """The CSSFontFeatureValuesRule interface represents a @font-feature-values rule.""" def __init__(self): super().__init__() self.cssRules: "CSSRuleList" = CSSRuleList() self.type = CSSRule.FONT_FEATURE_VALUES_RULE @property def cssText(self): return f"@font-feature-values {{{' '.join(str(rule) for rule in self.cssRules)}}}"
# @property # def cssRules(self): # """Returns the CSSRuleList object representing the CSS rules in the media rule.""" # raise NotImplementedError
[docs] class CSSGroupingRule(CSSRule): """The CSSGroupingRule interface represents a @grouping rule.""" def __init__(self): super().__init__() self.cssRules: "CSSRuleList" = CSSRuleList() # self.type = CSSRule.GROUPING_RULE @property def cssText(self): return " ".join(str(rule) for rule in self.cssRules)
[docs] def deleteRule(self, index: int): """Deletes the rule at the specified index from this grouping rule.""" from domonic.dom import DOMException if index < 0 or index >= len(self.cssRules): raise DOMException(DOMException.INDEX_SIZE_ERR, "Index is out of range.") del self.cssRules[index] _cssom.bump_stylesheet_epoch()
[docs] def insertRule(self, rule: str, index: int | None = None): """Inserts a child rule into this grouping rule.""" from domonic.dom import DOMException rules = _parse_rule_list(self.parentStyleSheet, _strip_css_comments(rule), self) if len(rules) == 0: raise DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Invalid CSS rule.") if len(rules) > 1: raise DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Only one rule is allowed.") if index is None: index = len(self.cssRules) if index < 0 or index > len(self.cssRules): raise DOMException(DOMException.INDEX_SIZE_ERR, "Index is out of range.") self.cssRules.insert(index, rules[0]) _cssom.bump_stylesheet_epoch() return index
# @property # def cssRules(self): # """Returns the CSSRuleList object representing the CSS rules in the media rule.""" # raise NotImplementedError
[docs] class CSSConditionRule(CSSGroupingRule): """The CSSConditionRule interface represents a condition rule.""" def __init__(self): super().__init__() self.conditionText: str | None = None self.cssRules: "CSSRuleList" = CSSRuleList()
# self.type = CSSRule.CONDITIONAL_RULE # @property # def conditionText(self): # """Returns the textual representation of the condition sub-rule.""" # raise NotImplementedError # @property # def cssRules(self): # """Returns the CSSRuleList object representing the CSS rules in the media rule.""" # raise NotImplementedError
[docs] class CSSSupportsRule(CSSConditionRule): """The CSSSupportsRule interface represents a @supports at-rule.""" def __init__(self): super().__init__() self.conditionText: str | None = None self.cssRules: "CSSRuleList" = CSSRuleList() self.type = CSSRule.SUPPORTS_RULE @property def cssText(self): return f"@supports {self.conditionText} {{{' '.join(str(rule) for rule in self.cssRules)}}}"
[docs] class CSSSupportsConditionRule(CSSGroupingRule): """The CSSSupportsConditionRule interface represents a @supports-condition rule.""" def __init__(self): super().__init__() self.name: str = "" self.type = CSSRule.SUPPORTS_CONDITION_RULE @property def cssText(self): return f"@supports-condition {self.name} {{{' '.join(str(rule) for rule in self.cssRules)}}}"
[docs] class CSSWhenRule(CSSConditionRule): """Represents a CSS Conditional Level 5 @when rule.""" def __init__(self): super().__init__() self.conditionText: str = "" self.type = CSSRule.WHEN_RULE @property def cssText(self): return f"@when {self.conditionText} {{{' '.join(str(rule) for rule in self.cssRules)}}}"
[docs] class CSSElseRule(CSSConditionRule): """Represents a CSS Conditional Level 5 @else rule.""" def __init__(self): super().__init__() self.conditionText: str = "" self.type = CSSRule.ELSE_RULE @property def cssText(self): condition = f" {self.conditionText}" if self.conditionText else "" return f"@else{condition} {{{' '.join(str(rule) for rule in self.cssRules)}}}"
[docs] class CSSMediaRule(CSSConditionRule): """The CSSMediaRule interface represents a @media rule in a CSS style sheet.""" def __init__(self): super().__init__() self.media: "MediaList" = MediaList() self.cssRules: "CSSRuleList" = CSSRuleList() self.type = CSSRule.MEDIA_RULE # @property # def cssRules(self): # """Returns a CSSRuleList containing the CSS rules in the media rule.""" # raise NotImplementedError # @property # def media(self): # """Returns a MediaList representing the intended destination medium for style information.""" # raise NotImplementedError @property def cssText(self): return f"@media {self.media.mediaText} {{{' '.join(str(rule) for rule in self.cssRules)}}}"
[docs] class CSSContainerRule(CSSConditionRule): """The CSSContainerRule interface represents a @container rule.""" def __init__(self): super().__init__() self.containerName: str = "" self.containerQuery: str = "" self.conditions: list[dict[str, str]] = [] self.type = CSSRule.CONTAINER_RULE @property def cssText(self): condition = self.conditionText or _serialize_container_conditions(self.conditions) prelude = f" {condition}" if condition else "" return f"@container{prelude} {{{' '.join(str(rule) for rule in self.cssRules)}}}"
[docs] class CSSScopeRule(CSSGroupingRule): """The CSSScopeRule interface represents a @scope rule.""" def __init__(self): super().__init__() self.start: str | None = None self.end: str | None = None self.type = CSSRule.SCOPE_RULE @property def cssText(self): start = f" ({self.start})" if self.start else "" end = f" to ({self.end})" if self.end else "" return f"@scope{start}{end} {{{' '.join(str(rule) for rule in self.cssRules)}}}"
[docs] class CSSLayerBlockRule(CSSGroupingRule): """The CSSLayerBlockRule interface represents a @layer block rule.""" def __init__(self): super().__init__() self.name: str = "" self.type = CSSRule.LAYER_BLOCK_RULE @property def cssText(self): name = f" {self.name}" if self.name else "" return f"@layer{name} {{{' '.join(str(rule) for rule in self.cssRules)}}}"
[docs] class CSSLayerStatementRule(CSSRule): """The CSSLayerStatementRule interface represents a statement-style @layer rule.""" def __init__(self): super().__init__() self.nameList: list[str] = [] self.type = CSSRule.LAYER_STATEMENT_RULE @property def cssText(self): return f"@layer {', '.join(self.nameList)};"
[docs] class CSSPropertyRule(CSSRule): """The CSSPropertyRule interface represents a Houdini ``@property`` rule.""" def __init__(self): super().__init__() self.name: str = "" self.style: "CSSStyleDeclaration | None" = None self.type = CSSRule.PROPERTY_RULE def _descriptor(self, name: str, default: str = "") -> str: if self.style is None: return default value = self.style.getPropertyValue(name) return value if value else default @property def syntax(self) -> str: return self._descriptor("syntax", '"*"') @property def inherits(self) -> bool: return self._descriptor("inherits").strip().lower() == "true" @property def initialValue(self) -> str | None: return self._descriptor("initial-value") or None def _register(self) -> None: """Feed this rule into the shared registered-property table so the cascade honours its inheritance flag and initial value.""" if not self.name.startswith("--"): return syntax = self.syntax.strip().strip("\"'") or "*" _cssom.register_property( self.name, syntax=syntax, inherits=self.inherits, initial_value=self.initialValue, ) @property def cssText(self): return f"@property {self.name} {{{self.style.cssText if self.style is not None else ''}}}"
[docs] class CSSNestedDeclarations(CSSRule): """The CSSNestedDeclarations interface represents declarations mixed with nested rules.""" def __init__(self): super().__init__() self.style: "CSSStyleDeclaration | None" = None self.type = CSSRule.NESTED_DECLARATIONS_RULE @property def cssText(self): return self.style.cssText if self.style is not None else ""
[docs] class MediaList(list): """The MediaList interface represents a list of media. It is used in the @media at-rule.""" @property def length(self) -> int: """Returns the number of media in the list.""" return len(self) @property def mediaText(self) -> str: """Returns a string containing the text of the media query.""" return ", ".join(self) @mediaText.setter def mediaText(self, value: Any) -> None: self[:] = [part.strip() for part in str(value or "").split(",") if part.strip()]
[docs] def item(self, index: int) -> str | None: """Returns the media at the given index in the MediaList.""" try: return self[index] except IndexError: return None
[docs] def appendMedium(self, newMedium: str) -> None: """Appends a new medium to the end of the list.""" self.append(newMedium)
[docs] def deleteMedium(self, oldMedium: str) -> None: """Removes medium in the media list. If the medium is not found nothing happens.""" if oldMedium in self: self.remove(oldMedium)
[docs] class CSSRuleList(list): """A CSSRuleList represents an ordered collection of read-only CSSRule objects. While the CSSRuleList object is read-only, and cannot be directly modified, it is considered a live object, as the content can change over time. """ @property def length(self) -> int: """Returns an integer representing the number of CSSRule objects in the collection.""" return len(self)
[docs] def item(self, index: int) -> "CSSRule | None": """Gets a single CSSRule.""" try: return self[index] except IndexError: return None
#: an XHTML ``<style>`` element's text wrapped in exactly one CDATA section #: (``<![CDATA[ ... ]]>``) -- valid, common XHTML practice for keeping a #: stylesheet well-formed as XML. domonic's HTML parser has no CDATA-section #: mode, so the markers survive as literal text in the element's #: ``textContent`` instead of being stripped as markup; ordinary CSS never #: both starts with ``<![CDATA[`` and ends with ``]]>``, so stripping it here #: is safe. _CDATA_WRAPPER_RE = re.compile(r"^\s*<!\[CDATA\[(.*)\]\]>\s*$", re.S) def _strip_cdata_wrapper(text: str) -> str: match = _CDATA_WRAPPER_RE.match(text) return match.group(1) if match else text
[docs] class CSSStyleSheet(StyleSheet): """Creates a new CSSStyleSheet object.""" def __init__(self, options: "dict[str, Any] | None" = None) -> None: super().__init__() self.rules: list[CSSRule] = [] self.cssRules: CSSRuleList = CSSRuleList() self.ownerRule: CSSRule | None = None # constructed sheets (``new CSSStyleSheet(...)``) accept an options bag # and are the only sheets ``replace`` / ``replaceSync`` may target self._constructed = True if isinstance(options, dict): if options.get("media") is not None: media = options["media"] self.media = media if isinstance(media, MediaList) else MediaList(str(media).split(",")) self.disabled = bool(options.get("disabled", False)) if options.get("baseURL") is not None: self.href = str(options["baseURL"]) # @property # def cssRules(): # -> 'CSSStyleRuleList': # """Returns a live CSSRuleList which maintains an up-to-date list of the CSSRule objects # that comprise the stylesheet.""" # # return CSSStyleRuleList() # raise NotImplementedError # @property # def ownerRule(self): # """If this stylesheet is imported into the document using an @import rule, # the ownerRule property returns the corresponding CSSImportRule; otherwise, this property's value is null.""" # raise NotImplementedError
[docs] def deleteRule(self, index: int): """Deletes the rule at the specified index into the stylesheet's rule list.""" from domonic.dom import DOMException if index < 0 or index >= len(self.cssRules): raise DOMException(DOMException.INDEX_SIZE_ERR, "Index is out of range.") del self.cssRules[index] self.rules = self.cssRules _cssom.bump_stylesheet_epoch()
[docs] def insertRule(self, rule: str, index: int | None = None): """Inserts a new rule at the specified position in the stylesheet, given the textual representation of the rule.""" from domonic.dom import DOMException rules = CSSParser.parseFromString(self, rule) if len(rules) == 0: raise DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Invalid CSS rule.") if len(rules) > 1: raise DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Only one rule is allowed.") if index is None: index = len(self.cssRules) if index < 0 or index > len(self.cssRules): raise DOMException(DOMException.INDEX_SIZE_ERR, "Index is out of range.") self.cssRules.insert(index, rules[0]) self.rules = self.cssRules _cssom.bump_stylesheet_epoch() return index
[docs] def replace(self, text: str): """Asynchronously replaces the content of the stylesheet and returns a Promise that resolves with the updated CSSStyleSheet.""" self.replaceSync(text) from domonic.javascript import Promise return Promise(lambda resolve, reject: resolve(self))
[docs] def replaceSync(self, text: str): """Synchronously replaces the content of the stylesheet.""" self.cssRules = CSSParser.parseFromString(self, _strip_cdata_wrapper(text or "")) self.rules = self.cssRules _cssom.bump_stylesheet_epoch()
# @property # def rules(self): # """The rules property is functionally identical to the standard cssRules property # it returns a live CSSRuleList which maintains an up-to-date list of all of the rules in the style sheet. # """ # raise NotImplementedError # Legacy methods
[docs] def addRule(self, selectorText: str, style: str, index: int | None = None): """Adds a new rule to the stylesheet given the selector to which the style applies and the style block to apply to the matching elements. This differs from insertRule(), which takes the textual representation of the entire rule as a single string. """ return self.insertRule(f"{selectorText} {{{style}}}", index)
[docs] def removeRule(self, index: int): """Functionally identical to deleteRule(); removes the rule at the specified index from the stylesheet's rule list.""" return self.deleteRule(index)
def __str__(self): # converts the rules to css code return "".join([str(rule) for rule in self.rules])
[docs] class Style: """JavaScript-style accessors for CSS declarations.""" def __init__(self, parent_node=None): self._members_checked = False self._parent_node = parent_node # so I can update a tags returned style attributes if a style gets set self._members_checked = True # NOTE - this ALWAYS needs to be last or all props will render def __setattr__(self, name: str, value: Any) -> None: descriptor = getattr(type(self), name, None) if isinstance(descriptor, property) and descriptor.fset is not None: super().__setattr__(name, value) return super().__setattr__(name, value) if not self._should_sync_style_name(name): return self._set_parent_style_property(name, "none" if value is None else value) def _should_sync_style_name(self, name: str) -> bool: if name in _CSS_STYLE_INTERNAL_NAMES or name.startswith("_"): return False if getattr(self, "_suspend_style_sync", False): return False if not getattr(self, "_members_checked", False): return False return getattr(self, "_parent_node", None) is not None def _set_parent_style_property(self, name: str, value: Any, priority: str = "") -> None: if self._parent_node is None: return property_name = _css_property_name(name) entries = _parse_css_declarations(self._parent_node.getAttribute("style") or "") # Assigning "" (or None) to a single property removes its declaration, # matching ``el.style.color = ""`` in a browser -- rather than emitting a # dangling ``color: ;``. removing = value is None or str(value) == "" if removing: entries = [entry for entry in entries if entry[0] != property_name] else: entries = _set_css_declaration(entries, property_name, value, priority) # Normalised "prop: value; " form, the same as ``setProperty`` and a # browser's -- assigning through ``el.style`` rewrites the attribute. css_text = _serialize_css_declarations(entries) object.__setattr__(self._parent_node, "_syncing_style_attr", True) try: self._parent_node.setAttribute("style", css_text) finally: object.__setattr__(self._parent_node, "_syncing_style_attr", False) if hasattr(self, "_css_text"): object.__setattr__(self, "_css_text", css_text) if hasattr(self, "_declared_properties") and not property_name.startswith("--"): declared_properties = set(getattr(self, "_declared_properties", set())) attribute_name = _css_attribute_name(property_name) if removing: declared_properties.discard(attribute_name) else: declared_properties.add(attribute_name) object.__setattr__(self, "_declared_properties", declared_properties)
[docs] @staticmethod def style_set_decorator(func: Callable[..., Any]) -> Callable[..., Any]: from functools import wraps @wraps(func) def style_wrapper(self, *args, **kwargs): value = args[0] if value is None: value = "none" elif _css_property_name(func.__name__) in _ZERO_LENGTH_PROPERTIES: normalized = _normalize_zero_lengths(_css_property_name(func.__name__), str(value)) if normalized != str(value): value = normalized args = (value,) + tuple(args[1:]) func(self, value, *args, **kwargs) # to only render to the Node what gets set. allows init to run first # note - this was a counter prior to 0.8.3 and is now a bool as counter stops custom props on extended classes if not self._members_checked: return if getattr(self, "_suspend_style_sync", False): return # A shorthand assigned through the IDL (``el.style.margin = ...``) # must expand to its longhands, exactly like ``setProperty`` -- so # route it there when available (CSSStyleDeclaration). kebab = _css_property_name(func.__name__) if _cssom.is_shorthand(kebab) and hasattr(self, "setProperty") and str(value) not in ("", "none"): self.setProperty(kebab, value) return self._set_parent_style_property(func.__name__, value) return style_wrapper
[docs] @staticmethod def style_get_decorator(func: Callable[..., Any]) -> Callable[..., Any]: from functools import wraps @wraps(func) def style_wrapper(value=None, *args, **kwargs): self = value kebab = _css_property_name(func.__name__) try: result = func(value, *args, **kwargs) except AttributeError: # The per-property private attribute was never set on this # instance. Two reasons this happens by design: # * ``Style.__init__`` no longer assigns all ~380 defaults # one-by-one (it was ~430 us per ``Style()``); an unset # property resolves lazily here instead. # * ``ComputedStyleDeclaration`` skips ``Style.__init__`` # entirely and serves every read through the cascade. # Prefer a real declared / computed value from the CSSOM read # surface (so ``getComputedStyle(el).color`` matches # ``.getPropertyValue("color")``); otherwise fall back to the # property's initial value, exactly what ``__init__`` used to # pre-seed (``fontSize`` -> ``"medium"``, most -> ``"none"``). if hasattr(self, "getPropertyValue"): got = self.getPropertyValue(kebab) if got: return got return _STYLE_INITIAL_VALUES.get(func.__name__, "none") # a shorthand (``el.style.margin``) reflects the declaration block: # ``getPropertyValue`` returns an explicitly-set shorthand or # reconstructs one from longhands set individually if _cssom.is_shorthand(kebab) and hasattr(self, "getPropertyValue"): rebuilt = self.getPropertyValue(kebab) if rebuilt: return rebuilt if result is None: result = "none" return result return style_wrapper
@property @style_get_decorator def alignContent(self): return self.__alignContent @alignContent.setter @style_set_decorator def alignContent(self, value="stretch", *args, **kwargs): self.__alignContent = value @property @style_get_decorator def alignItems(self): return self.__alignItems @alignItems.setter @style_set_decorator def alignItems(self, value=None, *args, **kwargs): self.__alignItems = value @property @style_get_decorator def alignSelf(self): return self.__alignSelf @alignSelf.setter @style_set_decorator def alignSelf(self, value=None, *args, **kwargs): self.__alignSelf = value @property @style_get_decorator def animation(self): return self.__animation @animation.setter @style_set_decorator def animation(self, value=None, *args, **kwargs): self.__animation = value @property @style_get_decorator def animationDelay(self): return self.__animationDelay @animationDelay.setter @style_set_decorator def animationDelay(self, value=None, *args, **kwargs): self.__animationDelay = value @property @style_get_decorator def animationDirection(self): return self.__animationDirection @animationDirection.setter @style_set_decorator def animationDirection(self, value=None, *args, **kwargs): self.__animationDirection = value @property @style_get_decorator def animationDuration(self): return self.__animationDuration @animationDuration.setter @style_set_decorator def animationDuration(self, value=None, *args, **kwargs): self.__animationDuration = value @property @style_get_decorator def animationFillMode(self): return self.__animationFillMode @animationFillMode.setter @style_set_decorator def animationFillMode(self, value=None, *args, **kwargs): self.__animationFillMode = value @property @style_get_decorator def animationIterationCount(self): return self.__animationIterationCount @animationIterationCount.setter @style_set_decorator def animationIterationCount(self, value=None, *args, **kwargs): self.__animationIterationCount = value @property @style_get_decorator def animationName(self): return self.__animationName @animationName.setter @style_set_decorator def animationName(self, value=None, *args, **kwargs): self.__animationName = value @property @style_get_decorator def animationTimingFunction(self): return self.__animationTimingFunction @animationTimingFunction.setter @style_set_decorator def animationTimingFunction(self, value=None, *args, **kwargs): self.__animationTimingFunction = value @property @style_get_decorator def animationPlayState(self): return self.__animationPlayState @animationPlayState.setter @style_set_decorator def animationPlayState(self, value=None, *args, **kwargs): self.__animationPlayState = value @property @style_get_decorator def background(self): return self.__background @background.setter @style_set_decorator def background(self, value=None, *args, **kwargs): self.__background = value @property @style_get_decorator def backgroundAttachment(self): return self.__backgroundAttachment @backgroundAttachment.setter @style_set_decorator def backgroundAttachment(self, value=None, *args, **kwargs): self.__backgroundAttachment = value @property @style_get_decorator def backgroundColor(self): return self.__backgroundColor @backgroundColor.setter @style_set_decorator def backgroundColor(self, value=None, *args, **kwargs): self.__backgroundColor = value @property @style_get_decorator def backgroundImage(self): return self.__backgroundImage @backgroundImage.setter @style_set_decorator def backgroundImage(self, value=None, *args, **kwargs): self.__backgroundImage = value @property @style_get_decorator def backgroundPosition(self): return self.__backgroundPosition @backgroundPosition.setter @style_set_decorator def backgroundPosition(self, value=None, *args, **kwargs): self.__backgroundPosition = value @property @style_get_decorator def backgroundRepeat(self): return self.__backgroundRepeat @backgroundRepeat.setter @style_set_decorator def backgroundRepeat(self, value=None, *args, **kwargs): self.__backgroundRepeat = value @property @style_get_decorator def backgroundClip(self): return self.__backgroundClip @backgroundClip.setter @style_set_decorator def backgroundClip(self, value=None, *args, **kwargs): self.__backgroundClip = value @property @style_get_decorator def backgroundOrigin(self): return self.__backgroundOrigin @backgroundOrigin.setter @style_set_decorator def backgroundOrigin(self, value=None, *args, **kwargs): self.__backgroundOrigin = value @property @style_get_decorator def backgroundSize(self): return self.__backgroundSize @backgroundSize.setter @style_set_decorator def backgroundSize(self, value=None, *args, **kwargs): self.__backgroundSize = value @property @style_get_decorator def backfaceVisibility(self): return self.__backfaceVisibility @backfaceVisibility.setter @style_set_decorator def backfaceVisibility(self, value=None, *args, **kwargs): self.__backfaceVisibility = value @property @style_get_decorator def border(self): return self.__border @border.setter @style_set_decorator def border(self, value=None, *args, **kwargs): self.__border = value @property @style_get_decorator def borderBottom(self): return self.__borderBottom @borderBottom.setter @style_set_decorator def borderBottom(self, value=None, *args, **kwargs): self.__borderBottom = value @property @style_get_decorator def borderBottomColor(self): return self.__borderBottomColor @borderBottomColor.setter @style_set_decorator def borderBottomColor(self, value=None, *args, **kwargs): self.__borderBottomColor = value @property @style_get_decorator def borderBottomLeftRadius(self): return self.__borderBottomLeftRadius @borderBottomLeftRadius.setter @style_set_decorator def borderBottomLeftRadius(self, value=None, *args, **kwargs): self.__borderBottomLeftRadius = value @property @style_get_decorator def borderBottomRightRadius(self): return self.__borderBottomRightRadius @borderBottomRightRadius.setter @style_set_decorator def borderBottomRightRadius(self, value=None, *args, **kwargs): self.__borderBottomRightRadius = value @property @style_get_decorator def borderBottomStyle(self): return self.__borderBottomStyle @borderBottomStyle.setter @style_set_decorator def borderBottomStyle(self, value=None, *args, **kwargs): self.__borderBottomStyle = value @property @style_get_decorator def borderBottomWidth(self): return self.__borderBottomWidth @borderBottomWidth.setter @style_set_decorator def borderBottomWidth(self, value=None, *args, **kwargs): self.__borderBottomWidth = value @property @style_get_decorator def borderCollapse(self): return self.__borderCollapse @borderCollapse.setter @style_set_decorator def borderCollapse(self, value=None, *args, **kwargs): self.__borderCollapse = value @property @style_get_decorator def borderColor(self): return self.__borderColor @borderColor.setter @style_set_decorator def borderColor(self, value=None, *args, **kwargs): self.__borderColor = value @property @style_get_decorator def borderImage(self): return self.__borderImage @borderImage.setter @style_set_decorator def borderImage(self, value=None, *args, **kwargs): self.__borderImage = value @property @style_get_decorator def borderImageOutset(self): return self.__borderImageOutset @borderImageOutset.setter @style_set_decorator def borderImageOutset(self, value=None, *args, **kwargs): self.__borderImageOutset = value @property @style_get_decorator def borderImageRepeat(self): return self.__borderImageRepeat @borderImageRepeat.setter @style_set_decorator def borderImageRepeat(self, value=None, *args, **kwargs): self.__borderImageRepeat = value @property @style_get_decorator def borderImageSlice(self): return self.__borderImageSlice @borderImageSlice.setter @style_set_decorator def borderImageSlice(self, value=None, *args, **kwargs): self.__borderImageSlice = value @property @style_get_decorator def borderImageSource(self): return self.__borderImageSource @borderImageSource.setter @style_set_decorator def borderImageSource(self, value=None, *args, **kwargs): self.__borderImageSource = value @property @style_get_decorator def borderImageWidth(self): return self.__borderImageWidth @borderImageWidth.setter @style_set_decorator def borderImageWidth(self, value=None, *args, **kwargs): self.__borderImageWidth = value @property @style_get_decorator def borderLeft(self): return self.__borderLeft @borderLeft.setter @style_set_decorator def borderLeft(self, value=None, *args, **kwargs): self.__borderLeft = value @property @style_get_decorator def borderLeftColor(self): return self.__borderLeftColor @borderLeftColor.setter @style_set_decorator def borderLeftColor(self, value=None, *args, **kwargs): self.__borderLeftColor = value @property @style_get_decorator def borderLeftStyle(self): return self.__borderLeftStyle @borderLeftStyle.setter @style_set_decorator def borderLeftStyle(self, value=None, *args, **kwargs): self.__borderLeftStyle = value @property @style_get_decorator def borderLeftWidth(self): return self.__borderLeftWidth @borderLeftWidth.setter @style_set_decorator def borderLeftWidth(self, value=None, *args, **kwargs): self.__borderLeftWidth = value @property @style_get_decorator def borderRadius(self): return self.__borderRadius @borderRadius.setter @style_set_decorator def borderRadius(self, value=None, *args, **kwargs): self.__borderRadius = value @property @style_get_decorator def borderRight(self): return self.__borderRight @borderRight.setter @style_set_decorator def borderRight(self, value=None, *args, **kwargs): self.__borderRight = value @property @style_get_decorator def borderRightColor(self): return self.__borderRightColor @borderRightColor.setter @style_set_decorator def borderRightColor(self, value=None, *args, **kwargs): self.__borderRightColor = value @property @style_get_decorator def borderRightStyle(self): return self.__borderRightStyle @borderRightStyle.setter @style_set_decorator def borderRightStyle(self, value=None, *args, **kwargs): self.__borderRightStyle = value @property @style_get_decorator def borderRightWidth(self): return self.__borderRightWidth @borderRightWidth.setter @style_set_decorator def borderRightWidth(self, value=None, *args, **kwargs): self.__borderRightWidth = value @property @style_get_decorator def borderSpacing(self): return self.__borderSpacing @borderSpacing.setter @style_set_decorator def borderSpacing(self, value=None, *args, **kwargs): self.__borderSpacing = value @property @style_get_decorator def borderStyle(self): return self.__borderStyle @borderStyle.setter @style_set_decorator def borderStyle(self, value=None, *args, **kwargs): self.__borderStyle = value @property @style_get_decorator def borderTop(self): return self.__borderTop @borderTop.setter @style_set_decorator def borderTop(self, value=None, *args, **kwargs): self.__borderTop = value @property @style_get_decorator def borderTopColor(self): return self.__borderTopColor @borderTopColor.setter @style_set_decorator def borderTopColor(self, value=None, *args, **kwargs): self.__borderTopColor = value @property @style_get_decorator def borderTopLeftRadius(self): return self.__borderTopLeftRadius @borderTopLeftRadius.setter @style_set_decorator def borderTopLeftRadius(self, value=None, *args, **kwargs): self.__borderTopLeftRadius = value @property @style_get_decorator def borderTopRightRadius(self): return self.__borderTopRightRadius @borderTopRightRadius.setter @style_set_decorator def borderTopRightRadius(self, value=None, *args, **kwargs): self.__borderTopRightRadius = value @property @style_get_decorator def borderTopStyle(self): return self.__borderTopStyle @borderTopStyle.setter @style_set_decorator def borderTopStyle(self, value=None, *args, **kwargs): self.__borderTopStyle = value @property @style_get_decorator def borderTopWidth(self): return self.__borderTopWidth @borderTopWidth.setter @style_set_decorator def borderTopWidth(self, value=None, *args, **kwargs): self.__borderTopWidth = value @property @style_get_decorator def borderWidth(self): return self.__borderWidth @borderWidth.setter @style_set_decorator def borderWidth(self, value=None, *args, **kwargs): self.__borderWidth = value @property @style_get_decorator def bottom(self): return self.__bottom @bottom.setter @style_set_decorator def bottom(self, value=None, *args, **kwargs): self.__bottom = value @property @style_get_decorator def boxDecorationBreak(self): return self.__boxDecorationBreak @boxDecorationBreak.setter @style_set_decorator def boxDecorationBreak(self, value=None, *args, **kwargs): self.__boxDecorationBreak = value @property @style_get_decorator def boxShadow(self): return self.__boxShadow @boxShadow.setter @style_set_decorator def boxShadow(self, value=None, *args, **kwargs): self.__boxShadow = value @property @style_get_decorator def boxSizing(self): return self.__boxSizing @boxSizing.setter @style_set_decorator def boxSizing(self, value=None, *args, **kwargs): self.__boxSizing = value @property @style_get_decorator def captionSide(self): return self.__captionSide @captionSide.setter @style_set_decorator def captionSide(self, value=None, *args, **kwargs): self.__captionSide = value @property @style_get_decorator def clear(self): return self.__clear @clear.setter @style_set_decorator def clear(self, value=None, *args, **kwargs): self.__clear = value @property @style_get_decorator def clip(self): return self.__clip @clip.setter @style_set_decorator def clip(self, value=None, *args, **kwargs): self.__clip = value @property @style_get_decorator def color(self): return self.__color @color.setter @style_set_decorator def color(self, value=None, *args, **kwargs): self.__color = value @property @style_get_decorator def columnCount(self): return self.__columnCount @columnCount.setter @style_set_decorator def columnCount(self, value=None, *args, **kwargs): self.__columnCount = value @property @style_get_decorator def columnFill(self): return self.__columnFill @columnFill.setter @style_set_decorator def columnFill(self, value=None, *args, **kwargs): self.__columnFill = value @property @style_get_decorator def columnGap(self): return self.__columnGap @columnGap.setter @style_set_decorator def columnGap(self, value=None, *args, **kwargs): self.__columnGap = value @property @style_get_decorator def columnRule(self): return self.__columnRule @columnRule.setter @style_set_decorator def columnRule(self, value=None, *args, **kwargs): self.__columnRule = value @property @style_get_decorator def columnRuleColor(self): return self.__columnRuleColor @columnRuleColor.setter @style_set_decorator def columnRuleColor(self, value=None, *args, **kwargs): self.__columnRuleColor = value @property @style_get_decorator def columnRuleStyle(self): return self.__columnRuleStyle @columnRuleStyle.setter @style_set_decorator def columnRuleStyle(self, value=None, *args, **kwargs): self.__columnRuleStyle = value @property @style_get_decorator def columnRuleWidth(self): return self.__columnRuleWidth @columnRuleWidth.setter @style_set_decorator def columnRuleWidth(self, value=None, *args, **kwargs): self.__columnRuleWidth = value @property @style_get_decorator def columns(self): return self.__columns @columns.setter @style_set_decorator def columns(self, value=None, *args, **kwargs): self.__columns = value @property @style_get_decorator def columnSpan(self): return self.__columnSpan @columnSpan.setter @style_set_decorator def columnSpan(self, value=None, *args, **kwargs): self.__columnSpan = value @property @style_get_decorator def columnWidth(self): return self.__columnWidth @columnWidth.setter @style_set_decorator def columnWidth(self, value=None, *args, **kwargs): self.__columnWidth = value @property @style_get_decorator def content(self): return self.__content @content.setter @style_set_decorator def content(self, value=None, *args, **kwargs): self.__content = value @property @style_get_decorator def counterIncrement(self): return self.__counterIncrement @counterIncrement.setter @style_set_decorator def counterIncrement(self, value=None, *args, **kwargs): self.__counterIncrement = value @property @style_get_decorator def counterReset(self): return self.__counterReset @counterReset.setter @style_set_decorator def counterReset(self, value=None, *args, **kwargs): self.__counterReset = value @property @style_get_decorator def cursor(self): return self.__cursor @cursor.setter @style_set_decorator def cursor(self, value=None, *args, **kwargs): self.__cursor = value @property @style_get_decorator def direction(self): return self.__direction @direction.setter @style_set_decorator def direction(self, value=None, *args, **kwargs): self.__direction = value @property @style_get_decorator def display(self): return self.__display @display.setter @style_set_decorator def display(self, value=None, *args, **kwargs): self.__display = value @property @style_get_decorator def emptyCells(self): return self.__emptyCells @emptyCells.setter @style_set_decorator def emptyCells(self, value=None, *args, **kwargs): self.__emptyCells = value @property @style_get_decorator def filter(self): return self.__filter @filter.setter @style_set_decorator def filter(self, value=None, *args, **kwargs): self.__filter = value @property @style_get_decorator def flex(self): return self.__flex @flex.setter @style_set_decorator def flex(self, value=None, *args, **kwargs): self.__flex = value @property @style_get_decorator def flexBasis(self): return self.__flexBasis @flexBasis.setter @style_set_decorator def flexBasis(self, value=None, *args, **kwargs): self.__flexBasis = value @property @style_get_decorator def flexDirection(self): return self.__flexDirection @flexDirection.setter @style_set_decorator def flexDirection(self, value=None, *args, **kwargs): self.__flexDirection = value @property @style_get_decorator def flexFlow(self): return self.__flexFlow @flexFlow.setter @style_set_decorator def flexFlow(self, value=None, *args, **kwargs): self.__flexFlow = value @property @style_get_decorator def flexGrow(self): return self.__flexGrow @flexGrow.setter @style_set_decorator def flexGrow(self, value=None, *args, **kwargs): self.__flexGrow = value @property @style_get_decorator def flexShrink(self): return self.__flexShrink @flexShrink.setter @style_set_decorator def flexShrink(self, value=None, *args, **kwargs): self.__flexShrink = value @property @style_get_decorator def flexWrap(self): return self.__flexWrap @flexWrap.setter @style_set_decorator def flexWrap(self, value=None, *args, **kwargs): self.__flexWrap = value @property @style_get_decorator def float(self): return self.__float @float.setter @style_set_decorator def float(self, value=None, *args, **kwargs): self.__float = value self.__cssFloat = value @property @style_get_decorator def cssFloat(self): return self.__cssFloat @cssFloat.setter @style_set_decorator def cssFloat(self, value=None, *args, **kwargs): self.__cssFloat = value self.__float = value @property @style_get_decorator def font(self): return self.__font @font.setter @style_set_decorator def font(self, value=None, *args, **kwargs): self.__font = value @property @style_get_decorator def fontFamily(self): return self.__fontFamily @fontFamily.setter @style_set_decorator def fontFamily(self, value=None, *args, **kwargs): self.__fontFamily = value @property @style_get_decorator def fontSize(self): return self.__fontSize @fontSize.setter @style_set_decorator def fontSize(self, value=None, *args, **kwargs): self.__fontSize = value @property @style_get_decorator def fontStyle(self): return self.__fontStyle @fontStyle.setter @style_set_decorator def fontStyle(self, value=None, *args, **kwargs): self.__fontStyle = value @property @style_get_decorator def fontVariant(self): return self.__fontVariant @fontVariant.setter @style_set_decorator def fontVariant(self, value=None, *args, **kwargs): self.__fontVariant = value @property @style_get_decorator def fontWeight(self): return self.__fontWeight @fontWeight.setter @style_set_decorator def fontWeight(self, value=None, *args, **kwargs): self.__fontWeight = value @property @style_get_decorator def fontSizeAdjust(self): return self.__fontSizeAdjust @fontSizeAdjust.setter @style_set_decorator def fontSizeAdjust(self, value=None, *args, **kwargs): self.__fontSizeAdjust = value @property @style_get_decorator def fontStretch(self): return self.__fontStretch @fontStretch.setter @style_set_decorator def fontStretch(self, value=None, *args, **kwargs): self.__fontStretch = value @property @style_get_decorator def hangingPunctuation(self): return self.__hangingPunctuation @hangingPunctuation.setter @style_set_decorator def hangingPunctuation(self, value=None, *args, **kwargs): self.__hangingPunctuation = value @property @style_get_decorator def height(self): return self.__height @height.setter @style_set_decorator def height(self, value=None, *args, **kwargs): self.__height = value @property @style_get_decorator def hyphens(self): return self.__hyphens @hyphens.setter @style_set_decorator def hyphens(self, value=None, *args, **kwargs): self.__hyphens = value @property @style_get_decorator def icon(self): return self.__icon @icon.setter @style_set_decorator def icon(self, value=None, *args, **kwargs): self.__icon = value @property @style_get_decorator def imageOrientation(self): return self.__imageOrientation @imageOrientation.setter @style_set_decorator def imageOrientation(self, value=None, *args, **kwargs): self.__imageOrientation = value @property @style_get_decorator def isolation(self): return self.__isolation @isolation.setter @style_set_decorator def isolation(self, value=None, *args, **kwargs): self.__isolation = value @property @style_get_decorator def justifyContent(self): return self.__justifyContent @justifyContent.setter @style_set_decorator def justifyContent(self, value=None, *args, **kwargs): self.__justifyContent = value @property @style_get_decorator def left(self): return self.__left @left.setter @style_set_decorator def left(self, value=None, *args, **kwargs): self.__left = value @property @style_get_decorator def letterSpacing(self): return self.__letterSpacing @letterSpacing.setter @style_set_decorator def letterSpacing(self, value=None, *args, **kwargs): self.__letterSpacing = value @property @style_get_decorator def lineHeight(self): return self.__lineHeight @lineHeight.setter @style_set_decorator def lineHeight(self, value=None, *args, **kwargs): self.__lineHeight = value @property @style_get_decorator def listStyle(self): return self.__listStyle @listStyle.setter @style_set_decorator def listStyle(self, value=None, *args, **kwargs): self.__listStyle = value @property @style_get_decorator def listStyleImage(self): return self.__listStyleImage @listStyleImage.setter @style_set_decorator def listStyleImage(self, value=None, *args, **kwargs): self.__listStyleImage = value @property @style_get_decorator def listStylePosition(self): return self.__listStylePosition @listStylePosition.setter @style_set_decorator def listStylePosition(self, value=None, *args, **kwargs): self.__listStylePosition = value @property @style_get_decorator def listStyleType(self): return self.__listStyleType @listStyleType.setter @style_set_decorator def listStyleType(self, value=None, *args, **kwargs): self.__listStyleType = value @property @style_get_decorator def margin(self): return self.__margin @margin.setter @style_set_decorator def margin(self, value=None, *args, **kwargs): self.__margin = value @property @style_get_decorator def marginBottom(self): return self.__marginBottom @marginBottom.setter @style_set_decorator def marginBottom(self, value=None, *args, **kwargs): self.__marginBottom = value @property @style_get_decorator def marginLeft(self): return self.__marginLeft @marginLeft.setter @style_set_decorator def marginLeft(self, value=None, *args, **kwargs): self.__marginLeft = value @property @style_get_decorator def marginRight(self): return self.__marginRight @marginRight.setter @style_set_decorator def marginRight(self, value=None, *args, **kwargs): self.__marginRight = value @property @style_get_decorator def marginTop(self): return self.__marginTop @marginTop.setter @style_set_decorator def marginTop(self, value=None, *args, **kwargs): self.__marginTop = value @property @style_get_decorator def maxHeight(self): return self.__maxHeight @maxHeight.setter @style_set_decorator def maxHeight(self, value=None, *args, **kwargs): self.__maxHeight = value @property @style_get_decorator def maxWidth(self): return self.__maxWidth @maxWidth.setter @style_set_decorator def maxWidth(self, value=None, *args, **kwargs): self.__maxWidth = value @property @style_get_decorator def minHeight(self): return self.__minHeight @minHeight.setter @style_set_decorator def minHeight(self, value=None, *args, **kwargs): self.__minHeight = value @property @style_get_decorator def minWidth(self): return self.__minWidth @minWidth.setter @style_set_decorator def minWidth(self, value=None, *args, **kwargs): self.__minWidth = value @property @style_get_decorator def navDown(self): return self.__navDown @navDown.setter @style_set_decorator def navDown(self, value=None, *args, **kwargs): self.__navDown = value @property @style_get_decorator def navIndex(self): return self.__navIndex @navIndex.setter @style_set_decorator def navIndex(self, value=None, *args, **kwargs): self.__navIndex = value @property @style_get_decorator def navLeft(self): return self.__navLeft @navLeft.setter @style_set_decorator def navLeft(self, value=None, *args, **kwargs): self.__navLeft = value @property @style_get_decorator def navRight(self): return self.__navRight @navRight.setter @style_set_decorator def navRight(self, value=None, *args, **kwargs): self.__navRight = value @property @style_get_decorator def navUp(self): return self.__navUp @navUp.setter @style_set_decorator def navUp(self, value=None, *args, **kwargs): self.__navUp = value @property @style_get_decorator def objectFit(self): return self.__objectFit @objectFit.setter @style_set_decorator def objectFit(self, value=None, *args, **kwargs): self.__objectFit = value @property @style_get_decorator def objectPosition(self): return self.__objectPosition @objectPosition.setter @style_set_decorator def objectPosition(self, value=None, *args, **kwargs): self.__objectPosition = value @property @style_get_decorator def opacity(self): return self.__opacity @opacity.setter @style_set_decorator def opacity(self, value=None, *args, **kwargs): self.__opacity = value @property @style_get_decorator def order(self): return self.__order @order.setter @style_set_decorator def order(self, value=None, *args, **kwargs): self.__order = value @property @style_get_decorator def orphans(self): return self.__orphans @orphans.setter @style_set_decorator def orphans(self, value=None, *args, **kwargs): self.__orphans = value @property @style_get_decorator def outline(self): return self.__outline @outline.setter @style_set_decorator def outline(self, value=None, *args, **kwargs): self.__outline = value @property @style_get_decorator def outlineColor(self): return self.__outlineColor @outlineColor.setter @style_set_decorator def outlineColor(self, value=None, *args, **kwargs): self.__outlineColor = value @property @style_get_decorator def outlineOffset(self): return self.__outlineOffset @outlineOffset.setter @style_set_decorator def outlineOffset(self, value=None, *args, **kwargs): self.__outlineOffset = value @property @style_get_decorator def outlineStyle(self): return self.__outlineStyle @outlineStyle.setter @style_set_decorator def outlineStyle(self, value=None, *args, **kwargs): self.__outlineStyle = value @property @style_get_decorator def outlineWidth(self): return self.__outlineWidth @outlineWidth.setter @style_set_decorator def outlineWidth(self, value=None, *args, **kwargs): self.__outlineWidth = value @property @style_get_decorator def overflow(self): return self.__overflow @overflow.setter @style_set_decorator def overflow(self, value=None, *args, **kwargs): self.__overflow = value @property @style_get_decorator def overflowX(self): return self.__overflowX @overflowX.setter @style_set_decorator def overflowX(self, value=None, *args, **kwargs): self.__overflowX = value @property @style_get_decorator def overflowY(self): return self.__overflowY @overflowY.setter @style_set_decorator def overflowY(self, value=None, *args, **kwargs): self.__overflowY = value @property @style_get_decorator def padding(self): return self.__padding @padding.setter @style_set_decorator def padding(self, value=None, *args, **kwargs): self.__padding = value @property @style_get_decorator def paddingBottom(self): return self.__paddingBottom @paddingBottom.setter @style_set_decorator def paddingBottom(self, value=None, *args, **kwargs): self.__paddingBottom = value @property @style_get_decorator def paddingLeft(self): return self.__paddingLeft @paddingLeft.setter @style_set_decorator def paddingLeft(self, value=None, *args, **kwargs): self.__paddingLeft = value @property @style_get_decorator def paddingRight(self): return self.__paddingRight @paddingRight.setter @style_set_decorator def paddingRight(self, value=None, *args, **kwargs): self.__paddingRight = value @property @style_get_decorator def paddingTop(self): return self.__paddingTop @paddingTop.setter @style_set_decorator def paddingTop(self, value=None, *args, **kwargs): self.__paddingTop = value @property @style_get_decorator def pageBreakAfter(self): return self.__pageBreakAfter @pageBreakAfter.setter @style_set_decorator def pageBreakAfter(self, value=None, *args, **kwargs): self.__pageBreakAfter = value @property @style_get_decorator def pageBreakBefore(self): return self.__pageBreakBefore @pageBreakBefore.setter @style_set_decorator def pageBreakBefore(self, value=None, *args, **kwargs): self.__pageBreakBefore = value @property @style_get_decorator def pageBreakInside(self): return self.__pageBreakInside @pageBreakInside.setter @style_set_decorator def pageBreakInside(self, value=None, *args, **kwargs): self.__pageBreakInside = value @property @style_get_decorator def perspective(self): return self.__perspective @perspective.setter @style_set_decorator def perspective(self, value=None, *args, **kwargs): self.__perspective = value @property @style_get_decorator def perspectiveOrigin(self): return self.__perspectiveOrigin @perspectiveOrigin.setter @style_set_decorator def perspectiveOrigin(self, value=None, *args, **kwargs): self.__perspectiveOrigin = value @property @style_get_decorator def position(self): return self.__position @position.setter @style_set_decorator def position(self, value=None, *args, **kwargs): self.__position = value @property @style_get_decorator def quotes(self): return self.__quotes @quotes.setter @style_set_decorator def quotes(self, value=None, *args, **kwargs): self.__quotes = value @property @style_get_decorator def resize(self): return self.__resize @resize.setter @style_set_decorator def resize(self, value=None, *args, **kwargs): self.__resize = value @property @style_get_decorator def right(self): return self.__right @right.setter @style_set_decorator def right(self, value=None, *args, **kwargs): self.__right = value @property @style_get_decorator def tableLayout(self): return self.__tableLayout @tableLayout.setter @style_set_decorator def tableLayout(self, value=None, *args, **kwargs): self.__tableLayout = value @property @style_get_decorator def tabSize(self): return self.__tabSize @tabSize.setter @style_set_decorator def tabSize(self, value=None, *args, **kwargs): self.__tabSize = value @property @style_get_decorator def textAlign(self): return self.__textAlign @textAlign.setter @style_set_decorator def textAlign(self, value=None, *args, **kwargs): self.__textAlign = value @property @style_get_decorator def textAlignLast(self): return self.__textAlignLast @textAlignLast.setter @style_set_decorator def textAlignLast(self, value=None, *args, **kwargs): self.__textAlignLast = value @property @style_get_decorator def textDecoration(self): return self.__textDecoration @textDecoration.setter @style_set_decorator def textDecoration(self, value=None, *args, **kwargs): self.__textDecoration = value @property @style_get_decorator def textDecorationColor(self): return self.__textDecorationColor @textDecorationColor.setter @style_set_decorator def textDecorationColor(self, value=None, *args, **kwargs): self.__textDecorationColor = value @property @style_get_decorator def textDecorationLine(self): return self.__textDecorationLine @textDecorationLine.setter @style_set_decorator def textDecorationLine(self, value=None, *args, **kwargs): self.__textDecorationLine = value @property @style_get_decorator def textDecorationStyle(self): return self.__textDecorationStyle @textDecorationStyle.setter @style_set_decorator def textDecorationStyle(self, value=None, *args, **kwargs): self.__textDecorationStyle = value @property @style_get_decorator def textIndent(self): return self.__textIndent @textIndent.setter @style_set_decorator def textIndent(self, value=None, *args, **kwargs): self.__textIndent = value @property @style_get_decorator def textJustify(self): return self.__textJustify @textJustify.setter @style_set_decorator def textJustify(self, value=None, *args, **kwargs): self.__textJustify = value @property @style_get_decorator def textOverflow(self): return self.__textOverflow @textOverflow.setter @style_set_decorator def textOverflow(self, value=None, *args, **kwargs): self.__textOverflow = value @property @style_get_decorator def textShadow(self): return self.__textShadow @textShadow.setter @style_set_decorator def textShadow(self, value=None, *args, **kwargs): self.__textShadow = value @property @style_get_decorator def textTransform(self): return self.__textTransform @textTransform.setter @style_set_decorator def textTransform(self, value=None, *args, **kwargs): self.__textTransform = value @property @style_get_decorator def top(self): return self.__top @top.setter @style_set_decorator def top(self, value=None, *args, **kwargs): self.__top = value @property @style_get_decorator def transform(self): return self.__transform @transform.setter @style_set_decorator def transform(self, value=None, *args, **kwargs): self.__transform = value @property @style_get_decorator def transformOrigin(self): return self.__transformOrigin @transformOrigin.setter @style_set_decorator def transformOrigin(self, value=None, *args, **kwargs): self.__transformOrigin = value @property @style_get_decorator def transformStyle(self): return self.__transformStyle @transformStyle.setter @style_set_decorator def transformStyle(self, value=None, *args, **kwargs): self.__transformStyle = value @property @style_get_decorator def transition(self): return self.__transition @transition.setter @style_set_decorator def transition(self, value=None, *args, **kwargs): self.__transition = value @property @style_get_decorator def transitionProperty(self): return self.__transitionProperty @transitionProperty.setter @style_set_decorator def transitionProperty(self, value=None, *args, **kwargs): self.__transitionProperty = value @property @style_get_decorator def transitionDuration(self): return self.__transitionDuration @transitionDuration.setter @style_set_decorator def transitionDuration(self, value=None, *args, **kwargs): self.__transitionDuration = value @property @style_get_decorator def transitionTimingFunction(self): return self.__transitionTimingFunction @transitionTimingFunction.setter @style_set_decorator def transitionTimingFunction(self, value=None, *args, **kwargs): self.__transitionTimingFunction = value @property @style_get_decorator def transitionDelay(self): return self.__transitionDelay @transitionDelay.setter @style_set_decorator def transitionDelay(self, value=None, *args, **kwargs): self.__transitionDelay = value @property @style_get_decorator def unicodeBidi(self): return self.__unicodeBidi @unicodeBidi.setter @style_set_decorator def unicodeBidi(self, value=None, *args, **kwargs): self.__unicodeBidi = value @property @style_get_decorator def userSelect(self): return self.__userSelect @userSelect.setter @style_set_decorator def userSelect(self, value=None, *args, **kwargs): self.__userSelect = value @property @style_get_decorator def verticalAlign(self): return self.__verticalAlign @verticalAlign.setter @style_set_decorator def verticalAlign(self, value=None, *args, **kwargs): self.__verticalAlign = value @property @style_get_decorator def visibility(self): return self.__visibility @visibility.setter @style_set_decorator def visibility(self, value=None, *args, **kwargs): self.__visibility = value @property @style_get_decorator def whiteSpace(self): return self.__whiteSpace @whiteSpace.setter @style_set_decorator def whiteSpace(self, value=None, *args, **kwargs): self.__whiteSpace = value @property @style_get_decorator def width(self): return self.__width @width.setter @style_set_decorator def width(self, value=None, *args, **kwargs): self.__width = value @property @style_get_decorator def wordBreak(self): return self.__wordBreak @wordBreak.setter @style_set_decorator def wordBreak(self, value=None, *args, **kwargs): self.__wordBreak = value @property @style_get_decorator def wordSpacing(self): return self.__wordSpacing @wordSpacing.setter @style_set_decorator def wordSpacing(self, value=None, *args, **kwargs): self.__wordSpacing = value @property @style_get_decorator def wordWrap(self): return self.__wordWrap @wordWrap.setter @style_set_decorator def wordWrap(self, value=None, *args, **kwargs): self.__wordWrap = value @property @style_get_decorator def widows(self): return self.__widows @widows.setter @style_set_decorator def widows(self, value=None, *args, **kwargs): self.__widows = value @property @style_get_decorator def zIndex(self): return self.__zIndex @zIndex.setter @style_set_decorator def zIndex(self, value=None, *args, **kwargs): self.__zIndex = value # new adds all newer css3 properties @property def all(self): return self.__all @all.setter @style_set_decorator def all(self, value=None, *args, **kwargs): self.__all = value @property def alignmentBaseline(self): return self.__alignmentBaseline @alignmentBaseline.setter @style_set_decorator def alignmentBaseline(self, value=None, *args, **kwargs): self.__alignmentBaseline = value @property def appearance(self): return self.__appearance @appearance.setter @style_set_decorator def appearance(self, value=None, *args, **kwargs): self.__appearance = value @property def backdropFilter(self): return self.__backdropFilter @backdropFilter.setter @style_set_decorator def backdropFilter(self, value=None, *args, **kwargs): self.__backdropFilter = value @property def backgroundBlendMode(self): return self.__backgroundBlendMode @backgroundBlendMode.setter @style_set_decorator def backgroundBlendMode(self, value=None, *args, **kwargs): self.__backgroundBlendMode = value @property def backgroundPositionX(self): return self.__backgroundPositionX @backgroundPositionX.setter @style_set_decorator def backgroundPositionX(self, value=None, *args, **kwargs): self.__backgroundPositionX = value @property def backgroundPositionY(self): return self.__backgroundPositionY @backgroundPositionY.setter @style_set_decorator def backgroundPositionY(self, value=None, *args, **kwargs): self.__backgroundPositionY = value @property def backgroundRepeatX(self): return self.__backgroundRepeatX @backgroundRepeatX.setter @style_set_decorator def backgroundRepeatX(self, value=None, *args, **kwargs): self.__backgroundRepeatX = value @property def backgroundRepeatY(self): return self.__backgroundRepeatY @backgroundRepeatY.setter @style_set_decorator def backgroundRepeatY(self, value=None, *args, **kwargs): self.__backgroundRepeatY = value @property def baselineShift(self): return self.__baselineShift @baselineShift.setter @style_set_decorator def baselineShift(self, value=None, *args, **kwargs): self.__baselineShift = value @property def blockSize(self): return self.__blockSize @blockSize.setter @style_set_decorator def blockSize(self, value=None, *args, **kwargs): self.__blockSize = value @property def borderBlockEnd(self): return self.__borderBlockEnd @borderBlockEnd.setter @style_set_decorator def borderBlockEnd(self, value=None, *args, **kwargs): self.__borderBlockEnd = value @property def borderBlockEndColor(self): return self.__borderBlockEndColor @borderBlockEndColor.setter @style_set_decorator def borderBlockEndColor(self, value=None, *args, **kwargs): self.__borderBlockEndColor = value @property def borderBlockEndStyle(self): return self.__borderBlockEndStyle @borderBlockEndStyle.setter @style_set_decorator def borderBlockEndStyle(self, value=None, *args, **kwargs): self.__borderBlockEndStyle = value @property def borderBlockEndWidth(self): return self.__borderBlockEndWidth @borderBlockEndWidth.setter @style_set_decorator def borderBlockEndWidth(self, value=None, *args, **kwargs): self.__borderBlockEndWidth = value @property def borderBlockStart(self): return self.__borderBlockStart @borderBlockStart.setter @style_set_decorator def borderBlockStart(self, value=None, *args, **kwargs): self.__borderBlockStart = value @property def borderBlockStartColor(self): return self.__borderBlockStartColor @borderBlockStartColor.setter @style_set_decorator def borderBlockStartColor(self, value=None, *args, **kwargs): self.__borderBlockStartColor = value @property def borderBlockStartStyle(self): return self.__borderBlockStartStyle @borderBlockStartStyle.setter @style_set_decorator def borderBlockStartStyle(self, value=None, *args, **kwargs): self.__borderBlockStartStyle = value @property def borderBlockStartWidth(self): return self.__borderBlockStartWidth @borderBlockStartWidth.setter @style_set_decorator def borderBlockStartWidth(self, value=None, *args, **kwargs): self.__borderBlockStartWidth = value @property def borderInlineEnd(self): return self.__borderInlineEnd @borderInlineEnd.setter @style_set_decorator def borderInlineEnd(self, value=None, *args, **kwargs): self.__borderInlineEnd = value @property def borderInlineEndColor(self): return self.__borderInlineEndColor @borderInlineEndColor.setter @style_set_decorator def borderInlineEndColor(self, value=None, *args, **kwargs): self.__borderInlineEndColor = value @property def borderInlineEndStyle(self): return self.__borderInlineEndStyle @borderInlineEndStyle.setter @style_set_decorator def borderInlineEndStyle(self, value=None, *args, **kwargs): self.__borderInlineEndStyle = value @property def borderInlineEndWidth(self): return self.__borderInlineEndWidth @borderInlineEndWidth.setter @style_set_decorator def borderInlineEndWidth(self, value=None, *args, **kwargs): self.__borderInlineEndWidth = value @property def borderInlineStart(self): return self.__borderInlineStart @borderInlineStart.setter @style_set_decorator def borderInlineStart(self, value=None, *args, **kwargs): self.__borderInlineStart = value @property def borderInlineStartColor(self): return self.__borderInlineStartColor @borderInlineStartColor.setter @style_set_decorator def borderInlineStartColor(self, value=None, *args, **kwargs): self.__borderInlineStartColor = value @property def borderInlineStartStyle(self): return self.__borderInlineStartStyle @borderInlineStartStyle.setter @style_set_decorator def borderInlineStartStyle(self, value=None, *args, **kwargs): self.__borderInlineStartStyle = value @property def borderInlineStartWidth(self): return self.__borderInlineStartWidth @borderInlineStartWidth.setter @style_set_decorator def borderInlineStartWidth(self, value=None, *args, **kwargs): self.__borderInlineStartWidth = value @property def breakAfter(self): return self.__breakAfter @breakAfter.setter @style_set_decorator def breakAfter(self, value=None, *args, **kwargs): self.__breakAfter = value @property def breakBefore(self): return self.__breakBefore @breakBefore.setter @style_set_decorator def breakBefore(self, value=None, *args, **kwargs): self.__breakBefore = value @property def breakInside(self): return self.__breakInside @breakInside.setter @style_set_decorator def breakInside(self, value=None, *args, **kwargs): self.__breakInside = value @property def bufferedRendering(self): return self.__bufferedRendering @bufferedRendering.setter @style_set_decorator def bufferedRendering(self, value=None, *args, **kwargs): self.__bufferedRendering = value @property def caretColor(self): return self.__caretColor @caretColor.setter @style_set_decorator def caretColor(self, value=None, *args, **kwargs): self.__caretColor = value @property def clipPath(self): return self.__clipPath @clipPath.setter @style_set_decorator def clipPath(self, value=None, *args, **kwargs): self.__clipPath = value @property def clipRule(self): return self.__clipRule @clipRule.setter @style_set_decorator def clipRule(self, value=None, *args, **kwargs): self.__clipRule = value @property def colorInterpolation(self): return self.__colorInterpolation @colorInterpolation.setter @style_set_decorator def colorInterpolation(self, value=None, *args, **kwargs): self.__colorInterpolation = value @property def colorInterpolationFilters(self): return self.__colorInterpolationFilters @colorInterpolationFilters.setter @style_set_decorator def colorInterpolationFilters(self, value=None, *args, **kwargs): self.__colorInterpolationFilters = value @property def colorRendering(self): return self.__colorRendering @colorRendering.setter @style_set_decorator def colorRendering(self, value=None, *args, **kwargs): self.__colorRendering = value @property def colorScheme(self): return self.__colorScheme @colorScheme.setter @style_set_decorator def colorScheme(self, value=None, *args, **kwargs): self.__colorScheme = value @property def contain(self): return self.__contain @contain.setter @style_set_decorator def contain(self, value=None, *args, **kwargs): self.__contain = value @property def containIntrinsicSize(self): return self.__containIntrinsicSize @containIntrinsicSize.setter @style_set_decorator def containIntrinsicSize(self, value=None, *args, **kwargs): self.__containIntrinsicSize = value @property def contentVisibility(self): return self.__contentVisibility @contentVisibility.setter @style_set_decorator def contentVisibility(self, value=None, *args, **kwargs): self.__contentVisibility = value @property def counterSet(self): return self.__counterSet @counterSet.setter @style_set_decorator def counterSet(self, value=None, *args, **kwargs): self.__counterSet = value @property def cx(self): return self.__cx @cx.setter @style_set_decorator def cx(self, value=None, *args, **kwargs): self.__cx = value @property def cy(self): return self.__cy @cy.setter @style_set_decorator def cy(self, value=None, *args, **kwargs): self.__cy = value @property def dominantBaseline(self): return self.__dominantBaseline @dominantBaseline.setter @style_set_decorator def dominantBaseline(self, value=None, *args, **kwargs): self.__dominantBaseline = value @property def d(self): return self.__d @d.setter @style_set_decorator def d(self, value=None, *args, **kwargs): self.__d = value @property def fill(self): return self.__fill @fill.setter @style_set_decorator def fill(self, value=None, *args, **kwargs): self.__fill = value @property def fillOpacity(self): return self.__fillOpacity @fillOpacity.setter @style_set_decorator def fillOpacity(self, value=None, *args, **kwargs): self.__fillOpacity = value @property def fillRule(self): return self.__fillRule @fillRule.setter @style_set_decorator def fillRule(self, value=None, *args, **kwargs): self.__fillRule = value @property def fontDisplay(self): return self.__fontDisplay @fontDisplay.setter @style_set_decorator def fontDisplay(self, value=None, *args, **kwargs): self.__fontDisplay = value @property def floodColor(self): return self.__floodColor @floodColor.setter @style_set_decorator def floodColor(self, value=None, *args, **kwargs): self.__floodColor = value @property def floodOpacity(self): return self.__floodOpacity @floodOpacity.setter @style_set_decorator def floodOpacity(self, value=None, *args, **kwargs): self.__floodOpacity = value @property def fontFeatureSettings(self): return self.__fontFeatureSettings @fontFeatureSettings.setter @style_set_decorator def fontFeatureSettings(self, value=None, *args, **kwargs): self.__fontFeatureSettings = value @property def fontKerning(self): return self.__fontKerning @fontKerning.setter @style_set_decorator def fontKerning(self, value=None, *args, **kwargs): self.__fontKerning = value @property def fontOpticalSizing(self): return self.__fontOpticalSizing @fontOpticalSizing.setter @style_set_decorator def fontOpticalSizing(self, value=None, *args, **kwargs): self.__fontOpticalSizing = value @property def fontVariantCaps(self): return self.__fontVariantCaps @fontVariantCaps.setter @style_set_decorator def fontVariantCaps(self, value=None, *args, **kwargs): self.__fontVariantCaps = value @property def fontVariantEastAsian(self): return self.__fontVariantEastAsian @fontVariantEastAsian.setter @style_set_decorator def fontVariantEastAsian(self, value=None, *args, **kwargs): self.__fontVariantEastAsian = value @property def fontVariantLigatures(self): return self.__fontVariantLigatures @fontVariantLigatures.setter @style_set_decorator def fontVariantLigatures(self, value=None, *args, **kwargs): self.__fontVariantLigatures = value @property def fontVariantNumeric(self): return self.__fontVariantNumeric @fontVariantNumeric.setter @style_set_decorator def fontVariantNumeric(self, value=None, *args, **kwargs): self.__fontVariantNumeric = value @property def fontVariationSettings(self): return self.__fontVariationSettings @fontVariationSettings.setter @style_set_decorator def fontVariationSettings(self, value=None, *args, **kwargs): self.__fontVariationSettings = value @property def gap(self): return self.__gap @gap.setter @style_set_decorator def gap(self, value=None, *args, **kwargs): self.__gap = value @property def grid(self): return self.__grid @grid.setter @style_set_decorator def grid(self, value=None, *args, **kwargs): self.__grid = value @property def gridArea(self): return self.__gridArea @gridArea.setter @style_set_decorator def gridArea(self, value=None, *args, **kwargs): self.__gridArea = value @property def gridAutoColumns(self): return self.__gridAutoColumns @gridAutoColumns.setter @style_set_decorator def gridAutoColumns(self, value=None, *args, **kwargs): self.__gridAutoColumns = value @property def gridAutoFlow(self): return self.__gridAutoFlow @gridAutoFlow.setter @style_set_decorator def gridAutoFlow(self, value=None, *args, **kwargs): self.__gridAutoFlow = value @property def gridAutoRows(self): return self.__gridAutoRows @gridAutoRows.setter @style_set_decorator def gridAutoRows(self, value=None, *args, **kwargs): self.__gridAutoRows = value @property def gridColumn(self): return self.__gridColumn @gridColumn.setter @style_set_decorator def gridColumn(self, value=None, *args, **kwargs): self.__gridColumn = value @property def gridColumnEnd(self): return self.__gridColumnEnd @gridColumnEnd.setter @style_set_decorator def gridColumnEnd(self, value=None, *args, **kwargs): self.__gridColumnEnd = value @property def gridColumnGap(self): return self.__gridColumnGap @gridColumnGap.setter @style_set_decorator def gridColumnGap(self, value=None, *args, **kwargs): self.__gridColumnGap = value @property def gridColumnStart(self): return self.__gridColumnStart @gridColumnStart.setter @style_set_decorator def gridColumnStart(self, value=None, *args, **kwargs): self.__gridColumnStart = value @property def gridGap(self): return self.__gridGap @gridGap.setter @style_set_decorator def gridGap(self, value=None, *args, **kwargs): self.__gridGap = value @property def gridRow(self): return self.__gridRow @gridRow.setter @style_set_decorator def gridRow(self, value=None, *args, **kwargs): self.__gridRow = value @property def gridRowEnd(self): return self.__gridRowEnd @gridRowEnd.setter @style_set_decorator def gridRowEnd(self, value=None, *args, **kwargs): self.__gridRowEnd = value @property def gridRowGap(self): return self.__gridRowGap @gridRowGap.setter @style_set_decorator def gridRowGap(self, value=None, *args, **kwargs): self.__gridRowGap = value @property def gridRowStart(self): return self.__gridRowStart @gridRowStart.setter @style_set_decorator def gridRowStart(self, value=None, *args, **kwargs): self.__gridRowStart = value @property def gridTemplate(self): return self.__gridTemplate @gridTemplate.setter @style_set_decorator def gridTemplate(self, value=None, *args, **kwargs): self.__gridTemplate = value @property def gridTemplateAreas(self): return self.__gridTemplateAreas @gridTemplateAreas.setter @style_set_decorator def gridTemplateAreas(self, value=None, *args, **kwargs): self.__gridTemplateAreas = value @property def gridTemplateColumns(self): return self.__gridTemplateColumns @gridTemplateColumns.setter @style_set_decorator def gridTemplateColumns(self, value=None, *args, **kwargs): self.__gridTemplateColumns = value @property def gridTemplateRows(self): return self.__gridTemplateRows @gridTemplateRows.setter @style_set_decorator def gridTemplateRows(self, value=None, *args, **kwargs): self.__gridTemplateRows = value @property def imageRendering(self): return self.__imageRendering @imageRendering.setter @style_set_decorator def imageRendering(self, value=None, *args, **kwargs): self.__imageRendering = value @property def inherits(self): return self.__inherits @inherits.setter @style_set_decorator def inherits(self, value=None, *args, **kwargs): self.__inherits = value @property def initialValue(self): return self.__initialValue @initialValue.setter @style_set_decorator def initialValue(self, value=None, *args, **kwargs): self.__initialValue = value @property def inlineSize(self): return self.__inlineSize @inlineSize.setter @style_set_decorator def inlineSize(self, value=None, *args, **kwargs): self.__inlineSize = value @property def justifyItems(self): return self.__justifyItems @justifyItems.setter @style_set_decorator def justifyItems(self, value=None, *args, **kwargs): self.__justifyItems = value @property def justifySelf(self): return self.__justifySelf @justifySelf.setter @style_set_decorator def justifySelf(self, value=None, *args, **kwargs): self.__justifySelf = value @property def lightingColor(self): return self.__lightingColor @lightingColor.setter @style_set_decorator def lightingColor(self, value=None, *args, **kwargs): self.__lightingColor = value @property def lineBreak(self): return self.__lineBreak @lineBreak.setter @style_set_decorator def lineBreak(self, value=None, *args, **kwargs): self.__lineBreak = value @property def marginBlockEnd(self): return self.__marginBlockEnd @marginBlockEnd.setter @style_set_decorator def marginBlockEnd(self, value=None, *args, **kwargs): self.__marginBlockEnd = value @property def marginBlockStart(self): return self.__marginBlockStart @marginBlockStart.setter @style_set_decorator def marginBlockStart(self, value=None, *args, **kwargs): self.__marginBlockStart = value @property def marginInlineEnd(self): return self.__marginInlineEnd @marginInlineEnd.setter @style_set_decorator def marginInlineEnd(self, value=None, *args, **kwargs): self.__marginInlineEnd = value @property def marginInlineStart(self): return self.__marginInlineStart @marginInlineStart.setter @style_set_decorator def marginInlineStart(self, value=None, *args, **kwargs): self.__marginInlineStart = value @property def marker(self): return self.__marker @marker.setter @style_set_decorator def marker(self, value=None, *args, **kwargs): self.__marker = value @property def markerEnd(self): return self.__markerEnd @markerEnd.setter @style_set_decorator def markerEnd(self, value=None, *args, **kwargs): self.__markerEnd = value @property def markerMid(self): return self.__markerMid @markerMid.setter @style_set_decorator def markerMid(self, value=None, *args, **kwargs): self.__markerMid = value @property def markerStart(self): return self.__markerStart @markerStart.setter @style_set_decorator def markerStart(self, value=None, *args, **kwargs): self.__markerStart = value @property def mask(self): return self.__mask @mask.setter @style_set_decorator def mask(self, value=None, *args, **kwargs): self.__mask = value @property def maskType(self): return self.__maskType @maskType.setter @style_set_decorator def maskType(self, value=None, *args, **kwargs): self.__maskType = value @property def maxBlockSize(self): return self.__maxBlockSize @maxBlockSize.setter @style_set_decorator def maxBlockSize(self, value=None, *args, **kwargs): self.__maxBlockSize = value @property def maxInlineSize(self): return self.__maxInlineSize @maxInlineSize.setter @style_set_decorator def maxInlineSize(self, value=None, *args, **kwargs): self.__maxInlineSize = value @property def maxZoom(self): return self.__maxZoom @maxZoom.setter @style_set_decorator def maxZoom(self, value=None, *args, **kwargs): self.__maxZoom = value @property def minBlockSize(self): return self.__minBlockSize @minBlockSize.setter @style_set_decorator def minBlockSize(self, value=None, *args, **kwargs): self.__minBlockSize = value @property def minInlineSize(self): return self.__minInlineSize @minInlineSize.setter @style_set_decorator def minInlineSize(self, value=None, *args, **kwargs): self.__minInlineSize = value @property def minZoom(self): return self.__minZoom @minZoom.setter @style_set_decorator def minZoom(self, value=None, *args, **kwargs): self.__minZoom = value @property def mixBlendMode(self): return self.__mixBlendMode @mixBlendMode.setter @style_set_decorator def mixBlendMode(self, value=None, *args, **kwargs): self.__mixBlendMode = value # objectFit / objectPosition are defined once above with the standard # get/set decorators; the duplicate definitions that lived here were removed. @property def offset(self): return self.__offset @offset.setter @style_set_decorator def offset(self, value=None, *args, **kwargs): self.__offset = value @property def offsetDistance(self): return self.__offsetDistance @offsetDistance.setter @style_set_decorator def offsetDistance(self, value=None, *args, **kwargs): self.__offsetDistance = value @property def offsetPath(self): return self.__offsetPath @offsetPath.setter @style_set_decorator def offsetPath(self, value=None, *args, **kwargs): self.__offsetPath = value @property def offsetRotate(self): return self.__offsetRotate @offsetRotate.setter @style_set_decorator def offsetRotate(self, value=None, *args, **kwargs): self.__offsetRotate = value @property def orientation(self): return self.__orientation @orientation.setter @style_set_decorator def orientation(self, value=None, *args, **kwargs): self.__orientation = value @property def overflowAnchor(self): return self.__overflowAnchor @overflowAnchor.setter @style_set_decorator def overflowAnchor(self, value=None, *args, **kwargs): self.__overflowAnchor = value @property def overflowWrap(self): return self.__overflowWrap @overflowWrap.setter @style_set_decorator def overflowWrap(self, value=None, *args, **kwargs): self.__overflowWrap = value @property def overscrollBehavior(self): return self.__overscrollBehavior @overscrollBehavior.setter @style_set_decorator def overscrollBehavior(self, value=None, *args, **kwargs): self.__overscrollBehavior = value @property def overscrollBehaviorBlock(self): return self.__overscrollBehaviorBlock @overscrollBehaviorBlock.setter @style_set_decorator def overscrollBehaviorBlock(self, value=None, *args, **kwargs): self.__overscrollBehaviorBlock = value @property def overscrollBehaviorInline(self): return self.__overscrollBehaviorInline @overscrollBehaviorInline.setter @style_set_decorator def overscrollBehaviorInline(self, value=None, *args, **kwargs): self.__overscrollBehaviorInline = value @property def overscrollBehaviorX(self): return self.__overscrollBehaviorX @overscrollBehaviorX.setter @style_set_decorator def overscrollBehaviorX(self, value=None, *args, **kwargs): self.__overscrollBehaviorX = value @property def overscrollBehaviorY(self): return self.__overscrollBehaviorY @overscrollBehaviorY.setter @style_set_decorator def overscrollBehaviorY(self, value=None, *args, **kwargs): self.__overscrollBehaviorY = value @property def paddingBlockEnd(self): return self.__paddingBlockEnd @paddingBlockEnd.setter @style_set_decorator def paddingBlockEnd(self, value=None, *args, **kwargs): self.__paddingBlockEnd = value @property def paddingBlockStart(self): return self.__paddingBlockStart @paddingBlockStart.setter @style_set_decorator def paddingBlockStart(self, value=None, *args, **kwargs): self.__paddingBlockStart = value @property def paddingInlineEnd(self): return self.__paddingInlineEnd @paddingInlineEnd.setter @style_set_decorator def paddingInlineEnd(self, value=None, *args, **kwargs): self.__paddingInlineEnd = value @property def paddingInlineStart(self): return self.__paddingInlineStart @paddingInlineStart.setter @style_set_decorator def paddingInlineStart(self, value=None, *args, **kwargs): self.__paddingInlineStart = value @property def page(self): return self.__page @page.setter @style_set_decorator def page(self, value=None, *args, **kwargs): self.__page = value @property def pageOrientation(self): return self.__pageOrientation @pageOrientation.setter @style_set_decorator def pageOrientation(self, value=None, *args, **kwargs): self.__pageOrientation = value @property def paintOrder(self): return self.__paintOrder @paintOrder.setter @style_set_decorator def paintOrder(self, value=None, *args, **kwargs): self.__paintOrder = value @property def placeContent(self): return self.__placeContent @placeContent.setter @style_set_decorator def placeContent(self, value=None, *args, **kwargs): self.__placeContent = value @property def placeItems(self): return self.__placeItems @placeItems.setter @style_set_decorator def placeItems(self, value=None, *args, **kwargs): self.__placeItems = value @property def placeSelf(self): return self.__placeSelf @placeSelf.setter @style_set_decorator def placeSelf(self, value=None, *args, **kwargs): self.__placeSelf = value @property def pointerEvents(self): return self.__pointerEvents @pointerEvents.setter @style_set_decorator def pointerEvents(self, value=None, *args, **kwargs): self.__pointerEvents = value @property def r(self): return self.__r @r.setter @style_set_decorator def r(self, value=None, *args, **kwargs): self.__r = value @property def rowGap(self): return self.__rowGap @rowGap.setter @style_set_decorator def rowGap(self, value=None, *args, **kwargs): self.__rowGap = value @property def rubyPosition(self): return self.__rubyPosition @rubyPosition.setter @style_set_decorator def rubyPosition(self, value=None, *args, **kwargs): self.__rubyPosition = value @property def rx(self): return self.__rx @rx.setter @style_set_decorator def rx(self, value=None, *args, **kwargs): self.__rx = value @property def ry(self): return self.__ry @ry.setter @style_set_decorator def ry(self, value=None, *args, **kwargs): self.__ry = value @property def scrollBehavior(self): return self.__scrollBehavior @scrollBehavior.setter @style_set_decorator def scrollBehavior(self, value=None, *args, **kwargs): self.__scrollBehavior = value @property def scrollMargin(self): return self.__scrollMargin @scrollMargin.setter @style_set_decorator def scrollMargin(self, value=None, *args, **kwargs): self.__scrollMargin = value @property def scrollMarginBlock(self): return self.__scrollMarginBlock @scrollMarginBlock.setter @style_set_decorator def scrollMarginBlock(self, value=None, *args, **kwargs): self.__scrollMarginBlock = value @property def scrollMarginBlockEnd(self): return self.__scrollMarginBlockEnd @scrollMarginBlockEnd.setter @style_set_decorator def scrollMarginBlockEnd(self, value=None, *args, **kwargs): self.__scrollMarginBlockEnd = value @property def scrollMarginBlockStart(self): return self.__scrollMarginBlockStart @scrollMarginBlockStart.setter @style_set_decorator def scrollMarginBlockStart(self, value=None, *args, **kwargs): self.__scrollMarginBlockStart = value @property def scrollMarginBottom(self): return self.__scrollMarginBottom @scrollMarginBottom.setter @style_set_decorator def scrollMarginBottom(self, value=None, *args, **kwargs): self.__scrollMarginBottom = value @property def scrollMarginInline(self): return self.__scrollMarginInline @scrollMarginInline.setter @style_set_decorator def scrollMarginInline(self, value=None, *args, **kwargs): self.__scrollMarginInline = value @property def scrollMarginInlineEnd(self): return self.__scrollMarginInlineEnd @scrollMarginInlineEnd.setter @style_set_decorator def scrollMarginInlineEnd(self, value=None, *args, **kwargs): self.__scrollMarginInlineEnd = value @property def scrollMarginInlineStart(self): return self.__scrollMarginInlineStart @scrollMarginInlineStart.setter @style_set_decorator def scrollMarginInlineStart(self, value=None, *args, **kwargs): self.__scrollMarginInlineStart = value @property def scrollMarginLeft(self): return self.__scrollMarginLeft @scrollMarginLeft.setter @style_set_decorator def scrollMarginLeft(self, value=None, *args, **kwargs): self.__scrollMarginLeft = value @property def scrollMarginRight(self): return self.__scrollMarginRight @scrollMarginRight.setter @style_set_decorator def scrollMarginRight(self, value=None, *args, **kwargs): self.__scrollMarginRight = value @property def scrollMarginTop(self): return self.__scrollMarginTop @scrollMarginTop.setter @style_set_decorator def scrollMarginTop(self, value=None, *args, **kwargs): self.__scrollMarginTop = value @property def scrollPadding(self): return self.__scrollPadding @scrollPadding.setter @style_set_decorator def scrollPadding(self, value=None, *args, **kwargs): self.__scrollPadding = value @property def scrollPaddingBlock(self): return self.__scrollPaddingBlock @scrollPaddingBlock.setter @style_set_decorator def scrollPaddingBlock(self, value=None, *args, **kwargs): self.__scrollPaddingBlock = value @property def scrollPaddingBlockEnd(self): return self.__scrollPaddingBlockEnd @scrollPaddingBlockEnd.setter @style_set_decorator def scrollPaddingBlockEnd(self, value=None, *args, **kwargs): self.__scrollPaddingBlockEnd = value @property def scrollPaddingBlockStart(self): return self.__scrollPaddingBlockStart @scrollPaddingBlockStart.setter @style_set_decorator def scrollPaddingBlockStart(self, value=None, *args, **kwargs): self.__scrollPaddingBlockStart = value @property def scrollPaddingBottom(self): return self.__scrollPaddingBottom @scrollPaddingBottom.setter @style_set_decorator def scrollPaddingBottom(self, value=None, *args, **kwargs): self.__scrollPaddingBottom = value @property def scrollPaddingInline(self): return self.__scrollPaddingInline @scrollPaddingInline.setter @style_set_decorator def scrollPaddingInline(self, value=None, *args, **kwargs): self.__scrollPaddingInline = value @property def scrollPaddingInlineEnd(self): return self.__scrollPaddingInlineEnd @scrollPaddingInlineEnd.setter @style_set_decorator def scrollPaddingInlineEnd(self, value=None, *args, **kwargs): self.__scrollPaddingInlineEnd = value @property def scrollPaddingInlineStart(self): return self.__scrollPaddingInlineStart @scrollPaddingInlineStart.setter @style_set_decorator def scrollPaddingInlineStart(self, value=None, *args, **kwargs): self.__scrollPaddingInlineStart = value @property def scrollPaddingLeft(self): return self.__scrollPaddingLeft @scrollPaddingLeft.setter @style_set_decorator def scrollPaddingLeft(self, value=None, *args, **kwargs): self.__scrollPaddingLeft = value @property def scrollPaddingRight(self): return self.__scrollPaddingRight @scrollPaddingRight.setter @style_set_decorator def scrollPaddingRight(self, value=None, *args, **kwargs): self.__scrollPaddingRight = value @property def scrollPaddingTop(self): return self.__scrollPaddingTop @scrollPaddingTop.setter @style_set_decorator def scrollPaddingTop(self, value=None, *args, **kwargs): self.__scrollPaddingTop = value @property def scrollSnapAlign(self): return self.__scrollSnapAlign @scrollSnapAlign.setter @style_set_decorator def scrollSnapAlign(self, value=None, *args, **kwargs): self.__scrollSnapAlign = value @property def scrollSnapStop(self): return self.__scrollSnapStop @scrollSnapStop.setter @style_set_decorator def scrollSnapStop(self, value=None, *args, **kwargs): self.__scrollSnapStop = value @property def scrollSnapType(self): return self.__scrollSnapType @scrollSnapType.setter @style_set_decorator def scrollSnapType(self, value=None, *args, **kwargs): self.__scrollSnapType = value @property def shapeImageThreshold(self): return self.__shapeImageThreshold @shapeImageThreshold.setter @style_set_decorator def shapeImageThreshold(self, value=None, *args, **kwargs): self.__shapeImageThreshold = value @property def shapeMargin(self): return self.__shapeMargin @shapeMargin.setter @style_set_decorator def shapeMargin(self, value=None, *args, **kwargs): self.__shapeMargin = value @property def shapeOutside(self): return self.__shapeOutside @shapeOutside.setter @style_set_decorator def shapeOutside(self, value=None, *args, **kwargs): self.__shapeOutside = value @property def shapeRendering(self): return self.__shapeRendering @shapeRendering.setter @style_set_decorator def shapeRendering(self, value=None, *args, **kwargs): self.__shapeRendering = value @property def size(self): return self.__size @size.setter @style_set_decorator def size(self, value=None, *args, **kwargs): self.__size = value @property def speak(self): return self.__speak @speak.setter @style_set_decorator def speak(self, value=None, *args, **kwargs): self.__speak = value @property def src(self): return self.__src @src.setter @style_set_decorator def src(self, value=None, *args, **kwargs): self.__src = value @property def stopColor(self): return self.__stopColor @stopColor.setter @style_set_decorator def stopColor(self, value=None, *args, **kwargs): self.__stopColor = value @property def stopOpacity(self): return self.__stopOpacity @stopOpacity.setter @style_set_decorator def stopOpacity(self, value=None, *args, **kwargs): self.__stopOpacity = value @property def stroke(self): return self.__stroke @stroke.setter @style_set_decorator def stroke(self, value=None, *args, **kwargs): self.__stroke = value @property def strokeDasharray(self): return self.__strokeDasharray @strokeDasharray.setter @style_set_decorator def strokeDasharray(self, value=None, *args, **kwargs): self.__strokeDasharray = value @property def strokeDashoffset(self): return self.__strokeDashoffset @strokeDashoffset.setter @style_set_decorator def strokeDashoffset(self, value=None, *args, **kwargs): self.__strokeDashoffset = value @property def strokeLinecap(self): return self.__strokeLinecap @strokeLinecap.setter @style_set_decorator def strokeLinecap(self, value=None, *args, **kwargs): self.__strokeLinecap = value @property def strokeLinejoin(self): return self.__strokeLinejoin @strokeLinejoin.setter @style_set_decorator def strokeLinejoin(self, value=None, *args, **kwargs): self.__strokeLinejoin = value @property def strokeMiterlimit(self): return self.__strokeMiterlimit @strokeMiterlimit.setter @style_set_decorator def strokeMiterlimit(self, value=None, *args, **kwargs): self.__strokeMiterlimit = value @property def strokeOpacity(self): return self.__strokeOpacity @strokeOpacity.setter @style_set_decorator def strokeOpacity(self, value=None, *args, **kwargs): self.__strokeOpacity = value @property def strokeWidth(self): return self.__strokeWidth @strokeWidth.setter @style_set_decorator def strokeWidth(self, value=None, *args, **kwargs): self.__strokeWidth = value @property def syntax(self): return self.__syntax @syntax.setter @style_set_decorator def syntax(self, value=None, *args, **kwargs): self.__syntax = value @property def textAnchor(self): return self.__textAnchor @textAnchor.setter @style_set_decorator def textAnchor(self, value=None, *args, **kwargs): self.__textAnchor = value @property def textCombineUpright(self): return self.__textCombineUpright @textCombineUpright.setter @style_set_decorator def textCombineUpright(self, value=None, *args, **kwargs): self.__textCombineUpright = value @property def textDecorationSkipInk(self): return self.__textDecorationSkipInk @textDecorationSkipInk.setter @style_set_decorator def textDecorationSkipInk(self, value=None, *args, **kwargs): self.__textDecorationSkipInk = value @property def textOrientation(self): return self.__textOrientation @textOrientation.setter @style_set_decorator def textOrientation(self, value=None, *args, **kwargs): self.__textOrientation = value @property def textRendering(self): return self.__textRendering @textRendering.setter @style_set_decorator def textRendering(self, value=None, *args, **kwargs): self.__textRendering = value @property def textSizeAdjust(self): return self.__textSizeAdjust @textSizeAdjust.setter @style_set_decorator def textSizeAdjust(self, value=None, *args, **kwargs): self.__textSizeAdjust = value @property def textUnderlinePosition(self): return self.__textUnderlinePosition @textUnderlinePosition.setter @style_set_decorator def textUnderlinePosition(self, value=None, *args, **kwargs): self.__textUnderlinePosition = value @property def touchAction(self): return self.__touchAction @touchAction.setter @style_set_decorator def touchAction(self, value=None, *args, **kwargs): self.__touchAction = value @property def transformBox(self): return self.__transformBox @transformBox.setter @style_set_decorator def transformBox(self, value=None, *args, **kwargs): self.__transformBox = value @property def unicodeRange(self): return self.__unicodeRange @unicodeRange.setter @style_set_decorator def unicodeRange(self, value=None, *args, **kwargs): self.__unicodeRange = value @property def userZoom(self): return self.__userZoom @userZoom.setter @style_set_decorator def userZoom(self, value=None, *args, **kwargs): self.__userZoom = value @property def vectorEffect(self): return self.__vectorEffect @vectorEffect.setter @style_set_decorator def vectorEffect(self, value=None, *args, **kwargs): self.__vectorEffect = value @property def willChange(self): return self.__willChange @willChange.setter @style_set_decorator def willChange(self, value=None, *args, **kwargs): self.__willChange = value @property def writingMode(self): return self.__writingMode @writingMode.setter @style_set_decorator def writingMode(self, value=None, *args, **kwargs): self.__writingMode = value @property def x(self): return self.__x @x.setter @style_set_decorator def x(self, value=None, *args, **kwargs): self.__x = value @property def y(self): return self.__y @y.setter @style_set_decorator def y(self, value=None, *args, **kwargs): self.__y = value @property @style_get_decorator def zoom(self): return self.__zoom @zoom.setter @style_set_decorator def zoom(self, value=None, *args, **kwargs): self.__zoom = value
# @property # @style_get_decorator # def zoomAndPan(self): # return self.__zoomAndPan # @zoomAndPan.setter # @style_set_decorator # def zoomAndPan(self, value=None, *args, **kwargs): # self.__zoomAndPan = value # @property # @style_get_decorator # def zoomAndResize(self): # return self.__zoomAndResize # @zoomAndResize.setter # @style_set_decorator # def zoomAndResize(self, value=None, *args, **kwargs): # self.__zoomAndResize = value # @property # @style_get_decorator # def zoomType(self): # return self.__zoomType # @zoomType.setter # @style_set_decorator # def zoomType(self, value=None, *args, **kwargs): # self.__zoomType = value # Modifies an existing CSS property or creates a new CSS property in the declaration block. """ # def setProperty(self, property, value): # self[property] = value
[docs] class CSSStyleDeclaration(Style): """The CSSStyleDeclaration interface represents an object that is a CSS declaration block, and exposes style information and various style-related methods and properties. A CSSStyleDeclaration object can be exposed using three different APIs: Via HTMLElement.style, which deals with the inline styles of a single element (e.g., <div style="...">). Via the CSSStyleSheet API. For example, document.styleSheets[0].cssRules[0].style returns a CSSStyleDeclaration object on the first CSS rule in the document's first stylesheet. Via Window.getComputedStyle(), which exposes the CSSStyleDeclaration object as a read-only interface. """ def __init__(self, parentNode=None, *args, **kwargs): # super(Style).__init__(*args, **kwargs) self._css_text = "" self._declared_properties = set() self._suspend_style_sync = False super().__init__(parentNode, *args, **kwargs) # self.__parentNode = parentNode # self.__cssText = None self.parentRule = None self.cssText = ( parentNode.getAttribute("style") if parentNode is not None and parentNode.getAttribute("style") else "" ) @staticmethod def _to_kebab(name: str) -> str: return _css_property_name(name) if name is not None else "" @staticmethod def _to_camel(name: str) -> str: return _css_attribute_name(name) if name else name def _current_css_text(self) -> str: if getattr(self, "cssText", ""): return self.cssText or "" if self._parent_node is not None: return self._parent_node.getAttribute("style") or "" return "" def _property_entries(self): return _parse_css_declarations(self._current_css_text()) def _expanded_entries(self): """The declaration block the way a browser *enumerates* it: every set shorthand contributes its longhand sub-properties (in canonical order), not a single entry. This is what ``length`` / ``item`` / iteration see -- ``margin: 1px`` is four properties. Serialisation still collapses them back (see ``_collapse_box_shorthands`` / ``cssText``). Shorthands ``_cssom.expand_shorthand`` cannot split losslessly (``background``, ``transition``, ``animation``, ``grid-template``, ``grid-area``) stay as the single shorthand entry. """ order: list[str] = [] seen: dict[str, tuple[str, str]] = {} for name, value, priority in self._property_entries(): expanded = _cssom.expand_shorthand(name, value) if _cssom.is_shorthand(name) else None pairs = expanded if expanded else [(name, value)] for long_name, long_value in pairs: if long_name not in seen: order.append(long_name) seen[long_name] = (long_value, priority) return [(n, seen[n][0], seen[n][1]) for n in order] def _sync_css_text(self, entries): css_text = _serialize_css_declarations(entries) self.cssText = css_text if self._parent_node is not None: object.__setattr__(self._parent_node, "_syncing_style_attr", True) try: self._parent_node.setAttribute("style", css_text) finally: object.__setattr__(self._parent_node, "_syncing_style_attr", False) @property def cssText(self): """Textual representation of the declaration block.""" return getattr(self, "_css_text", "") @cssText.setter def cssText(self, value): raw = "" if value is None else str(value).strip() parsed_entries = [ (name, _normalize_zero_lengths(name, val), priority) for name, val, priority in _parse_css_declarations(raw) ] entries = self._collapse_box_shorthands(parsed_entries) # CSSOM: the getter always returns the *serialised* declaration block, # regardless of the input text -- so normalise here. text = _serialize_css_declarations(entries) next_declared_properties = {self._to_camel(name) for name, _, _ in entries if not name.startswith("--")} previous_declared_properties = set(getattr(self, "_declared_properties", set())) object.__setattr__(self, "_css_text", text) if ( getattr(self, "_parent_node", None) is not None and getattr(self, "_members_checked", False) and not getattr(self._parent_node, "_syncing_style_attr", False) ): self._parent_node.setAttribute("style", text) object.__setattr__(self, "_suspend_style_sync", True) try: for attribute_name in previous_declared_properties - next_declared_properties: try: setattr(self, attribute_name, "") except Exception: object.__setattr__(self, attribute_name, "") for name, property_value, _ in entries: if name.startswith("--"): continue # keep both the shorthand and its longhand camelCase accessors current pairs = [(name, property_value)] if _cssom.is_shorthand(name): pairs += _cssom.expand_shorthand(name, property_value) or [] for pair_name, pair_value in pairs: attribute_name = self._to_camel(pair_name) try: setattr(self, attribute_name, pair_value) except Exception: object.__setattr__(self, attribute_name, pair_value) object.__setattr__(self, "_declared_properties", next_declared_properties) finally: object.__setattr__(self, "_suspend_style_sync", False) @property def length(self): """The number of properties in the declaration block. A set shorthand counts as its longhand sub-properties, the way a browser reports it (``el.style.margin = "1px"`` -> ``length`` 4).""" return len(self._expanded_entries()) # @property # def parentRule(self): # """The containing CSSRule.""" # return self.__parentRule # @parentRule.setter # def parentRule(self, value=None, *args, **kwargs): # self.__parentRule = value # @property # def cssFloat(self): # """ Special alias for the float CSS property. """ # raise NotImplementedError
[docs] def getPropertyPriority(self, propertyName): """Returns the optional priority, "important".""" target = self._to_kebab(propertyName) for name, _, priority in self._property_entries(): if name == target: return priority # a longhand inherits the priority of a set shorthand for name, _, priority in self._expanded_entries(): if name == target: return priority return ""
[docs] def getPropertyValue(self, propertyName: str) -> str: """Returns the value of the property with the specified name. Shorthand / longhand are resolved against each other: asking for a longhand pulls its value out of a set shorthand, and asking for a shorthand reconstructs it from its longhands when possible. """ target = self._to_kebab(propertyName) entries = self._property_entries() for name, value, _ in reversed(entries): if name == target: return value for shorthand in _cssom.LONGHAND_TO_SHORTHANDS.get(target, ()): for name, value, _ in reversed(entries): if name == shorthand: expanded = _cssom.expand_shorthand(shorthand, value) if expanded: for long_name, long_value in expanded: if long_name == target: return long_value if _cssom.is_shorthand(target): built = _cssom.build_shorthand(target, self.getPropertyValue) if built: return built attribute_name = self._to_camel(propertyName) if attribute_name not in getattr(self, "_declared_properties", set()): return "" value = getattr(self, attribute_name, "") return "" if value is None else value
[docs] def item(self, index: int) -> str: """Returns a CSS property name by its index, or the empty string if the index is out-of-bounds. An alternative to accessing nodeList[i] (which instead returns undefined when i is out-of-bounds). This is mostly useful for non-JavaScript DOM implementations. """ entries = self._expanded_entries() if index < 0 or index >= len(entries): return "" return entries[index][0]
def __getitem__(self, index): if isinstance(index, int): return self.item(index) return self.getPropertyValue(index) def __setitem__(self, propertyName, value): self.setProperty(propertyName, value) def __contains__(self, propertyName): return self.getPropertyValue(propertyName) != "" def __iter__(self): for index in range(self.length): yield self.item(index)
[docs] def removeProperty(self, propertyName): """Removes a property from the CSS declaration block.""" current = self.getPropertyValue(propertyName) target = self._to_kebab(propertyName) entries = self._property_entries() if not any(e[0] == target for e in entries): # removing a longhand covered by a stored shorthand: expand the # shorthand first so the other longhands survive, like a browser for shorthand in _cssom.LONGHAND_TO_SHORTHANDS.get(target, ()): shorthand_entry = next((e for e in entries if e[0] == shorthand), None) if shorthand_entry is None: continue expanded = _cssom.expand_shorthand(shorthand, shorthand_entry[1]) if not expanded: continue idx = entries.index(shorthand_entry) prio = shorthand_entry[2] entries = entries[:idx] + [(ln, lv, prio) for ln, lv in expanded] + entries[idx + 1 :] entries = [entry for entry in entries if entry[0] != target] entries = self._collapse_shorthands(entries) self._sync_css_text(entries) try: object.__setattr__(self, "_suspend_style_sync", True) if not target.startswith("--"): setattr(self, self._to_camel(propertyName), "") except Exception: return current finally: object.__setattr__(self, "_suspend_style_sync", False) return current
# Modifies an existing CSS property or creates a new CSS property in the declaration block. """
[docs] def setProperty(self, property, value, priority=None): target = self._to_kebab(property) value = "" if value is None else str(value) priority = "" if priority is None else str(priority).strip().lower() if priority not in ("", "important") and value: return if _IMPORTANT_RE.search(value): value = _IMPORTANT_RE.sub("", value).strip() priority = "important" if value == "": self.removeProperty(target) return value = _normalize_zero_lengths(target, value) entries = self._property_entries() if _cssom.is_shorthand(target): covered = set(_cssom.longhands_for(target)) entries = [e for e in entries if e[0] not in covered and e[0] != target] entries.append((target, value, priority)) else: for shorthand in _cssom.LONGHAND_TO_SHORTHANDS.get(target, ()): shorthand_entry = next((e for e in entries if e[0] == shorthand), None) if shorthand_entry is None: continue expanded = _cssom.expand_shorthand(shorthand, shorthand_entry[1]) if not expanded: continue idx = entries.index(shorthand_entry) prio = shorthand_entry[2] entries = entries[:idx] + [(ln, lv, prio) for ln, lv in expanded] + entries[idx + 1 :] entries = _set_css_declaration(entries, target, value, priority) entries = self._collapse_box_shorthands(entries) self._sync_css_text(entries) if target.startswith("--"): return object.__setattr__(self, "_suspend_style_sync", True) try: setattr(self, self._to_camel(property), value) if _cssom.is_shorthand(target): for long_name, long_value in _cssom.expand_shorthand(target, value) or (): setattr(self, self._to_camel(long_name), long_value) finally: object.__setattr__(self, "_suspend_style_sync", False)
#: shorthands re-formed from a complete longhand set when serialising the #: declaration block, the way a browser normalises ``cssText``. Ordered #: widest-first so ``border`` claims all 12 longhands before ``border-width`` #: / ``border-top`` get a chance at the leftovers. ``font`` / ``list-style`` #: / ``text-decoration`` / ``background`` / ``transition`` / ``animation`` / #: ``grid-*`` are deliberately absent -- browsers only collapse those when #: every longhand (including ones domonic does not model) is at its initial #: value, so re-forming them here would emit shorthands a browser would not. _COLLAPSE_ORDER = ( "border", "border-width", "border-style", "border-color", "border-top", "border-right", "border-bottom", "border-left", "border-radius", "margin", "padding", "inset", "outline", "column-rule", "overflow", "gap", "place-content", "place-items", "place-self", "columns", "flex-flow", "flex", ) @classmethod def _collapse_shorthands(cls, entries): """Replace each complete, priority-consistent set of longhands with its shorthand, matching how a browser serialises a declaration block.""" entries = list(entries) for shorthand in cls._COLLAPSE_ORDER: longs = _cssom.longhands_for(shorthand) by_name = {name: (value, priority) for name, value, priority in entries} if not all(ln in by_name for ln in longs): continue priorities = {by_name[ln][1] for ln in longs} if len(priorities) != 1: continue collapsed = _cssom.build_shorthand(shorthand, lambda ln: by_name.get(ln, ("", ""))[0]) if not collapsed: continue priority = priorities.pop() first_idx = min(i for i, e in enumerate(entries) if e[0] in longs) entries = [e for e in entries if e[0] not in longs] entries.insert(first_idx, (shorthand, collapsed, priority)) # widen the border-width / border-style / border-color trio (each a # single, all-sides-equal value) into `border`, the way a browser does trio = ("border-width", "border-style", "border-color") by_name = {name: (value, priority) for name, value, priority in entries} if all(t in by_name for t in trio): prios = {by_name[t][1] for t in trio} vals = [by_name[t][0] for t in trio] if len(prios) == 1 and all(v and " " not in v for v in vals): merged = " ".join(v for v in vals if v not in ("medium", "currentcolor")) or vals[1] first_idx = min(i for i, e in enumerate(entries) if e[0] in trio) entries = [e for e in entries if e[0] not in trio] entries.insert(first_idx, ("border", merged, prios.pop())) return entries # back-compat alias -- this used to only handle the box + radius shorthands _collapse_box_shorthands = _collapse_shorthands
[docs] def getPropertyCSSValue(self, propertyName: str): """Only supported via getComputedStyle in Firefox. Returns the property value as a CSSPrimitiveValue or null for shorthand properties.""" value = self.getPropertyValue(propertyName) return value if value != "" else None
_SPECIFICITY_ID = re.compile(r"#[-\w]+") _SPECIFICITY_CLASS = re.compile(r"\.[-\w]+|\[[^\]]+\]|(?<!:):[-\w]+") _SPECIFICITY_TYPE = re.compile(r"(?:^|[\s>+~|])(-?[_a-zA-Z][-\w]*|\*)") _SPECIFICITY_PSEUDO_EL = re.compile(r"::[-\w]+|:(?:before|after|first-line|first-letter)\b") # Functional pseudo-classes whose specificity is that of their most specific # argument (:where() contributes nothing and is handled separately). _SPECIFICITY_MATCHES_ANY = ("is", "matches", "not", "has") def _split_functional_pseudo(selector: str): """Yield ``(name, args, start, end)`` for each top-level ``:name(...)`` in *selector*, where ``args`` is the raw content between the parentheses.""" i = 0 length = len(selector) while i < length: if selector[i] != ":": i += 1 continue match = re.match(r":{1,2}([-\w]+)\(", selector[i:]) if not match: i += 1 continue name = match.group(1).lower() open_paren = i + match.end() - 1 depth = 0 j = open_paren while j < length: char = selector[j] if char == "(": depth += 1 elif char == ")": depth -= 1 if depth == 0: break j += 1 yield name, selector[open_paren + 1 : j], i, j + 1 i = j + 1 def _selector_specificity(selector: str) -> tuple[int, int, int]: """CSS Selectors 4 specificity as an ``(#id, .class, type)`` triple. Handles the functional pseudo-classes: ``:where()`` adds nothing, ``:is()`` / ``:not()`` / ``:has()`` take the specificity of their most specific argument, and ``:nth-child(... of S)`` adds the specificity of ``S`` on top of the pseudo-class itself. """ sel = selector.strip() if not sel: return (0, 0, 0) ids = classes = types = 0 replacements: list[tuple[int, int]] = [] for name, args, start, end in _split_functional_pseudo(sel): if name == "where": replacements.append((start, end)) continue if name in _SPECIFICITY_MATCHES_ANY: best = (0, 0, 0) for part in _split_top_level_commas(args): part = part.strip() if part: best = max(best, _selector_specificity(part)) ids += best[0] classes += best[1] types += best[2] replacements.append((start, end)) continue if name in ("nth-child", "nth-last-child") and " of " in f" {args} ": _, _, sub = args.partition(" of ") add = (0, 0, 0) for part in _split_top_level_commas(sub): part = part.strip() if part: add = max(add, _selector_specificity(part)) ids += add[0] classes += add[1] types += add[2] # leave the ":nth-child(" ... ")" in place so it still counts once # as a pseudo-class below, but blank the argument list replacements.append((start + len(name) + 2, end - 1)) for start, end in sorted(replacements, reverse=True): sel = sel[:start] + sel[end:] pseudo_els = len(_SPECIFICITY_PSEUDO_EL.findall(sel)) # remove ::pseudo-elements before the class scan so their ':' is not counted scan = _SPECIFICITY_PSEUDO_EL.sub(" ", sel) ids += len(_SPECIFICITY_ID.findall(scan)) classes += len(_SPECIFICITY_CLASS.findall(scan)) types += len([t for t in _SPECIFICITY_TYPE.findall(scan) if t != "*"]) return ids, classes, types + pseudo_els _RIGHTMOST_KEY = re.compile(r"([#.]?[-\w]+|\*)(?:\([^)]*\))?\s*$") def _rule_bucket_key(selector: str) -> str: """The bucket a selector belongs in: ``#id``, ``.class``, ``tag`` from its rightmost compound, or ``*`` when that cannot be determined cheaply.""" sel = selector.strip().rstrip() # drop a trailing pseudo-element / pseudo-class for bucketing purposes; a # selector that is *only* a pseudo (``:root``, ``:hover``, ``:not(...)``) # has no cheap type/id/class key, so it goes in the "*" bucket stripped = re.sub(r"::?[-\w]+(?:\([^)]*\))?\s*$", "", sel).strip() if not stripped: return "*" match = _RIGHTMOST_KEY.search(stripped) if not match: return "*" token = match.group(1) if token.startswith(("#", ".")): return token if token == "*": return "*" return token.lower() def _layer_sort_key(layer: int, important: bool) -> tuple[int, int]: """Cascade-layer ordering term. Unlayered normal declarations beat layered ones and later layers beat earlier; for ``!important`` both are reversed (earlier layer wins, unlayered important is weakest).""" if important: return (0, 0) if layer == 0 else (1, -layer) return (1, 0) if layer == 0 else (0, layer) def _has_paren_internal_whitespace(value: str) -> bool: """Whether *value* has whitespace nested inside a function call's parens -- e.g. the space after ``var(--missing,`` in ``var(--missing, 4px)``, as opposed to ``var(--gap)`` (no comma/space inside at all) or plain top-level whitespace like ``1px solid red``.""" depth = 0 for char in value: if char == "(": depth += 1 elif char == ")": depth = max(depth - 1, 0) elif char.isspace() and depth > 0: return True return False def _expand_shorthands_for_cascade(entries: tuple) -> tuple: """A cascade-only, fully-longhand view of one rule's declarations. ``_collect_author_declarations`` picks a single winning declaration per *property name* across all matching rules -- but a shorthand (``border``) and one of its own longhands (``border-top``) are different names, so without this they never actually compete for a longhand they both cover (``border-top-style``); whichever one happens to be expanded first in ``_resolve()`` wins regardless of which was really later in the rule. Expanding every shorthand to physical longhands here -- before that per-name competition ever runs -- lets it decide correctly, the same way it already does for two declarations of the same longhand. This is separate from ``CSSStyleRule.style``'s own declarations (``entries``, as authored) so ``cssText``/``getPropertyValue``/ ``setProperty`` keep reporting the shorthand exactly as written -- only the copy used for property-vs-property cascade matching changes.""" result: list[tuple[str, str, str]] = [] index_by_name: dict[str, int] = {} for name, value, priority in entries: pairs = None # the box/border/font/background splitters below tokenize on bare # whitespace -- safe for an unresolved var() reference with no # fallback (``var(--gap)``, one token, splits the same as any other # single value), but not for one with a fallback (``var(--missing, # 4px)``): the space after its comma isn't a real token boundary, # and naively splitting there would scatter one value's fallback # across unrelated longhands. Left as the single shorthand entry in # that case, exactly as authored, its var() substituted later. if _cssom.is_shorthand(name) and not _has_paren_internal_whitespace(value): pairs = _cssom.expand_shorthand(name, value) for long_name, long_value in pairs if pairs is not None else ((name, value),): existing = index_by_name.get(long_name) if existing is None: index_by_name[long_name] = len(result) result.append((long_name, long_value, priority)) else: result[existing] = (long_name, long_value, priority) return tuple(result) _PSEUDO_ELEMENT_SUFFIX_RE = re.compile( r"::?(before|after|marker|placeholder|first-line|" r"first-letter|selection|backdrop|file-selector-button)\s*$" ) def _build_rule_index(sheet_list, viewport, *, features=None, media_type: str = "screen"): """``{bucket_key: [(selector, specificity, order, layer, entries)]}`` for fast element-vs-rule matching in getComputedStyle, plus the set of pseudo-element names (``"before"``, ``"after"``, ...) any selector in these sheets actually targets -- lets ``getComputedStyle(el, "::before")`` reject a page with no such rule at all without scanning a single bucket (most pages never style a pseudo-element, and a caller like a renderer asking "does this element have generated content?" pays for the whole cascade to find out otherwise). *features*/*media_type* are the window's ``mediaFeatures``/``mediaType`` (see ``_condition_rule_matches``), threaded through so a stylesheet's own ``@media`` blocks are filtered out consistently with what ``window.matchMedia()`` reports for the same window.""" index: dict[str, list] = {} pseudo_names: set[str] = set() order = 0 layers: dict[int, int] = {} # id(CSSLayerBlockRule) -> layer number for sheet in sheet_list: # per CSSOM, a disabled sheet contributes no rules to the cascade at # all -- `StyleSheet.disabled`'s setter bumps the stylesheet epoch # this index is keyed on, so toggling it also invalidates every # cached rule index/computed style that was built while it applied. if getattr(sheet, "disabled", False): continue for rule, layer in _iter_style_rules( getattr(sheet, "cssRules", None), viewport=viewport, layers=layers, features=features, media_type=media_type, ): entries = _expand_shorthands_for_cascade(rule.style._property_entries()) if not entries: continue for selector in str(rule.selectorText or "").split(","): selector = selector.strip() if not selector: continue order += 1 index.setdefault(_rule_bucket_key(selector), []).append( (selector, _selector_specificity(selector), order, layer, entries) ) pe = _PSEUDO_ELEMENT_SUFFIX_RE.search(selector) if pe: pseudo_names.add(pe.group(1)) return index, pseudo_names def _iter_style_rules(rules, *, viewport, layers=None, layer=0, features=None, media_type: str = "screen"): """Yield ``(CSSStyleRule, layer_number)`` pairs, descending into ``@media`` blocks whose condition currently matches and tracking ``@layer`` nesting (layer 0 == unlayered; each ``@layer`` block gets the next number in document order).""" if layers is None: layers = {} for rule in rules or (): # `@layer a, b;` pre-declares layer order regardless of block order if isinstance(rule, CSSLayerStatementRule): for layer_name in rule.nameList: layers.setdefault(layer_name.strip(), len(layers) + 1) continue inner = getattr(rule, "cssRules", None) if isinstance(rule, CSSStyleRule): yield rule, layer continue if not inner: continue if isinstance(rule, CSSLayerBlockRule): layer_name = (rule.name or "").strip() or f"\x00anon{id(rule)}" child_layer = layers.setdefault(layer_name, len(layers) + 1) yield from _iter_style_rules( inner, viewport=viewport, layers=layers, layer=child_layer, features=features, media_type=media_type ) continue if _condition_rule_matches(rule, viewport, features=features, media_type=media_type): yield from _iter_style_rules( inner, viewport=viewport, layers=layers, layer=layer, features=features, media_type=media_type ) def _condition_rule_matches(rule, viewport, *, features=None, media_type: str = "screen") -> bool: """Whether a conditional group rule (``@media``, ``@supports``, ...) currently applies. ``@media`` is evaluated through its parsed ``MediaList`` against the real viewport, the same matcher ``window.matchMedia()`` uses -- ``neither ``conditionText`` (a plain string) nor ``MediaList`` (a plain list) ever had a ``.matches`` attribute, so the previous ``hasattr(condition, "matches")`` check was always false and every conditional block was treated as always matching. ``@supports`` is evaluated through ``CSS.supports()``, the same ``not``/``and``/``or``/ nested-parens evaluator ``CSS.supports()`` itself exposes -- it was already correct, just never wired in here. Other condition rules (``@when``, ``@else``) have no evaluator here yet and keep the previous always-match behaviour. *features* and *media_type* mirror what ``window.matchMedia()`` uses for the same element's window (``window.mediaFeatures`` / ``window.mediaType``) -- passed through so a stylesheet's own ``@media (prefers-color-scheme: dark)`` or ``@media print`` agrees with what ``matchMedia()`` would report for the same window, instead of always falling back to the class defaults regardless of what the window actually has configured.""" if isinstance(rule, CSSMediaRule): media_text = rule.media.mediaText if rule.media else "" if not media_text: return True from domonic.window import MediaQueryList width, height = viewport return MediaQueryList._evaluate( media_text, width=width or 0, height=height or 0, features=features, media_type=media_type ) if isinstance(rule, CSSSupportsRule): return _supports_condition(rule.conditionText or "") return True def _computed_style_cache_key(element) -> tuple: """Everything that can make a previously resolved *cascade* stale: the DOM epoch (any attribute/structure change anywhere), the stylesheet epoch (any rule change anywhere), and the viewport (a resize can flip which ``@media`` rules apply). Deliberately whole-document/whole-process granularity, not per-element -- a conservative but correct starting point (see the domonic-css-performance notes for the narrower alternative). The layout epoch is deliberately *not* part of this key. Selector matching, inheritance, and shorthand resolution never consult a ``LayoutBox`` -- only a handful of used-value conversions do (``auto`` box fields and ``%`` lengths, in ``_to_used_length``/``_percent_base_px``) -- so a relayout would otherwise invalidate the entire cascade for every element just to re-derive the few properties that actually depend on geometry. Those are cached and invalidated separately, per property, in ``ComputedStyleDeclaration.getPropertyValue``.""" document = getattr(element, "ownerDocument", None) or getattr(element, "rootNode", None) window = getattr(document, "defaultView", None) if document is not None else None viewport = (getattr(window, "innerWidth", None), getattr(window, "innerHeight", None)) return (_cssom.dom_style_epoch(), _cssom.stylesheet_epoch(), viewport)
[docs] class ComputedStyleDeclaration(CSSStyleDeclaration): """Read-only ``CSSStyleDeclaration`` returned by ``window.getComputedStyle``. It resolves each property through a light cascade: matching author rules (by specificity, then source order, with ``!important`` on top), then the inline ``style`` attribute, then inherited values from the parent, then the property's initial value. """ # populated in __init__ via self.__dict__ (Style.__init__ is skipped) _resolved: Any _element: Any _pseudo: Any _chain_cache: dict def __init__(self, element, pseudo=None, _chain_cache=None): # Repeated getComputedStyle() calls on an unchanged element (the # common case for a layout engine, or anything polling geometry every # frame) would otherwise redo the whole cascade -- selector matching, # shorthand expansion, inheritance walk up to the root -- from # scratch every time. Cache the resolved state on the element itself, # keyed by everything that can make it stale; a hit just copies the # previous state across instead of recomputing it. This also makes # resolving an ancestor's style cheap the second time any sibling # queries it, since _parent_computed()/_root_font_size_px() below # construct ancestor instances through this same __init__. cache_key = _computed_style_cache_key(element) element_dict = getattr(element, "__dict__", None) store = element_dict.setdefault("_computed_style_cache", {}) if element_dict is not None else None cached = store.get(pseudo) if store is not None else None if cached is not None and cached[0] == cache_key: self.__dict__.update(cached[1]) return # Deliberately skip ``Style.__init__`` (its ~380 attribute assignments): # this class overrides the whole read surface, so all it needs is the # handful of internal fields the CSSOM helpers look for. state = self.__dict__ state["_css_text"] = "" state["_declared_properties"] = set() state["_members_checked"] = True state["_parent_node"] = None state["_suspend_style_sync"] = True state["parentRule"] = None state["_element"] = element state["_pseudo"] = pseudo # shared across a single ancestor walk so getComputedStyle on a deep # tree stays linear rather than O(depth^2) state["_chain_cache"] = {} if _chain_cache is None else _chain_cache state["_resolved"] = self._resolve() if store is not None: store[pseudo] = (cache_key, state) # -- cascade --------------------------------------------------------- def _collect_author_declarations(self): element = self._element document = getattr(element, "ownerDocument", None) or getattr(element, "rootNode", None) sheets = None for attr in ("styleSheets", "stylesheets"): sheets = getattr(document, attr, None) if document is not None else None if sheets: break try: sheet_list = list(sheets) if sheets is not None else [] except TypeError: sheet_list = [] # constructable stylesheets adopted on the document or the element's # shadow root also feed the cascade for holder in (document, getattr(element, "getRootNode", lambda: None)()): adopted = getattr(holder, "adoptedStyleSheets", None) if adopted: try: sheet_list = sheet_list + list(adopted) except TypeError: # nosec B110 - adoptedStyleSheets not iterable: skip it pass window = getattr(document, "defaultView", None) if document else None viewport = ( getattr(window, "innerWidth", None), getattr(window, "innerHeight", None), ) # Same media context ``window.matchMedia()`` would evaluate against, # so a stylesheet's own ``@media (prefers-color-scheme: dark)`` / # ``@media print`` agrees with what the window reports -- previously # this always fell back to the class defaults here regardless of # ``window.mediaFeatures``/``window.mediaType``. media_features = getattr(window, "mediaFeatures", None) media_type = getattr(window, "mediaType", None) or "screen" # The parsed index is keyed on everything that could make it stale: the # stylesheet epoch (bumped by insertRule/deleteRule/replace(Sync) on # any stylesheet), the viewport (a resize can change which @media # rules apply), the media features/type (changing either can too), # and the identity of the sheets actually in play (so reassigning # document.adoptedStyleSheets, or adding a <style> element, is picked # up without a dedicated hook for either). rule_cache_key = ( _cssom.stylesheet_epoch(), viewport, tuple(id(sheet) for sheet in sheet_list), tuple(sorted(media_features.items())) if media_features else None, media_type, ) cache = self._chain_cache entry = cache.get("__rule_index__") if entry is None and document is not None: entry = getattr(document, "_cssom_rule_index", None) if entry is None or entry[0] != rule_cache_key: entry = ( rule_cache_key, *_build_rule_index(sheet_list, viewport, features=media_features, media_type=media_type), ) if document is not None: try: document._cssom_rule_index = entry except Exception: # nosec B110 - index cache is best-effort; cache["__rule_index__"] still set below pass cache["__rule_index__"] = entry index = entry[1] want_pseudo = "" if self._pseudo: want_pseudo = str(self._pseudo).strip().lower().lstrip(":") cascade: dict[str, tuple[tuple, str, bool]] = {} # An empty index (no stylesheets at all) and "no rule in these sheets # targets this pseudo-element" both mean the bucket scan below has # nothing to find -- skip it, but (for a real, non-pseudo element) # still fall through to the presentational-hint step afterward: a # page with zero author stylesheets is exactly the case a legacy # ``bgcolor``-style hint most needs to still apply in. if index and (not want_pseudo or want_pseudo in entry[2]): # only look at rules whose rightmost compound could match this element tag_name = (element.tagName or "").lower() local_name = tag_name.rpartition(":")[2] or tag_name buckets = ["*", tag_name] if local_name == tag_name else ["*", tag_name, local_name] el_id = element.getAttribute("id") if el_id: buckets.append(f"#{el_id}") for cls in str(element.getAttribute("class") or "").split(): buckets.append(f".{cls}") for bucket in buckets: for selector, spec, order, layer, entries in index.get(bucket, ()): pe = _PSEUDO_ELEMENT_SUFFIX_RE.search(selector) sel_pseudo = pe.group(1) if pe else "" if sel_pseudo != want_pseudo: continue base_selector = selector[: pe.start()].strip() if pe else selector if not base_selector: base_selector = "*" try: # ``_matchElement`` only understands a small self-contained # subset of pseudo-classes (see ``_STRUCTURAL_PSEUDO_ # CLASSES``) and no combinators at all; anything else # (``:nth-of-type()``, ``:hover``, ``:is()``, a combinator # chain, ...) makes it return ``False`` without that # meaning "genuinely doesn't match" -- ``_parse_simple_ # selector``'s tokenizer isn't a strict validator (a # combinator selector still "parses", just into a bogus # single compound, rather than failing outright), so a # parse-failure check here cannot reliably tell the two # apart. Always falling through to the heavier bs4-backed # matcher on a ``False`` -- same as ``Element.matches()`` # does -- is the only way that stays correct for every # selector shape. ok = element._matchElement(element, base_selector) or ( element._matches_selector_chain(base_selector) is True ) except Exception: ok = False if not ok: continue for name, value, priority in entries: important = priority == "important" key = (important, _layer_sort_key(layer, important), spec, order) if name not in cascade or key > cascade[name][0]: cascade[name] = (key, value, important) result = {name: (value, important) for name, (_, value, important) in cascade.items()} resolver = _PRESENTATIONAL_HINT_RESOLVER # A pseudo-element has no HTML attributes of its own to carry a # presentational hint about -- only a real element does. if resolver is not None and not want_pseudo: # A presentational hint (a legacy HTML attribute a browser treats # as implicit style -- ``bgcolor``, an ``<img>``'s ``width``/ # ``height``, ...) is the weakest declaration in the real CSS # cascade: weaker than any author rule for the same property # regardless of specificity or source order, and weaker than an # inline ``style=""`` too (merged in afterwards, in ``_resolve``). # domonic has no opinion on which HTML attributes map to which # properties -- that mapping is rendering policy for whatever is # built on top of this -- so this only owns the one thing only # the cascade can get right: filling a hint in *exclusively* for # a property no real author rule declared at all, so it's already # too late for it to compete with, or override, any real # declaration found above, and a later CSSOM write still simply # overrides it downstream the normal way, with no special-casing # needed anywhere else in the cascade. try: hints = resolver(element) or {} except Exception: hints = {} for name, value in hints.items(): if value is not None and name not in result: result[name] = (str(value), False) return result def _resolve(self): element = self._element # 1 + 2: author rules then the inline style attribute. Per the cascade, # normal inline beats normal author but loses to important author; # important inline beats everything here. Author declarations already # arrive fully expanded to physical longhands (``_build_rule_index`` # expands every shorthand before its own per-name cascade competition # runs), so only the inline attribute -- not indexed the same way -- # still needs its own shorthands expanded here. Merging author-then- # inline and expanding together (rather than as two separate passes) # means a later inline shorthand/longhand correctly overrides an # earlier author one for a longhand they both cover, the same way a # later declaration always beats an earlier one for the same property. author = self._collect_author_declarations() important_author = {name for name, (_, imp) in author.items() if imp} merged: list[tuple[str, str, str]] = [ (name, value, "important" if imp else "") for name, (value, imp) in author.items() ] inline = getattr(element, "getAttribute", lambda *_: "")("style") or "" for name, value, priority in _parse_css_declarations(inline): if priority != "important": covering = _cssom.LONGHAND_TO_SHORTHANDS.get(name, ()) if name in important_author or any(shorthand in important_author for shorthand in covering): continue merged.append((name, value, priority)) resolved: dict[str, str] = { name: value for name, value, _ in _expand_shorthands_for_cascade(tuple(merged)) } # 3: inherited values from the parent's computed style. A pseudo- # element has no DOM node of its own -- ``element`` is still the real # element it's attached to -- so per CSS Pseudo-Elements ("the parent # of a generated box is the element to whose box tree the # pseudo-element is attached"), a ::before/::after inherits from # *that* element's own computed style, not its DOM parent's (which # would skip right over any inherited property, e.g. font-family, # that the element's own rules set). cache = self._chain_cache parent_computed = None if self._pseudo: parent_computed = cache.get(id(element)) if parent_computed is None: parent_computed = ComputedStyleDeclaration(element, None, _chain_cache=cache) cache[id(element)] = parent_computed else: parent = getattr(element, "parentNode", None) if parent is not None and _is_inheritance_source(parent): parent_computed = cache.get(id(parent)) if parent_computed is None: parent_computed = ComputedStyleDeclaration(parent, None, _chain_cache=cache) cache[id(parent)] = parent_computed return _ResolvedView(resolved, parent_computed) # -- read-only CSSOM surface -------------------------------------- def _custom_property(self, name: str) -> str: """Computed value of a custom property (they inherit by default).""" return self._resolved.get(name) or ""
[docs] def getPropertyValue(self, propertyName: str) -> str: target = self._to_kebab(propertyName) # This instance is immutable once resolved (it's a read-only snapshot # -- see the class docstring), so the *cascaded* value for a given # longhand never changes across repeated reads. Memoize it: a caller # that asks for the same property more than once (or a shorthand that # rebuilds itself from the same longhands `getPropertyValue` recurses # into below) does the unit/colour/keyword conversion once, not every # time. Lives in ``self.__dict__`` alongside ``_resolved`` and # ``_font_size_px_cache``, so it rides along whenever this instance is # reused from the per-element computed-style cache too. # # A small minority of properties are *also* layout-dependent: an # ``auto`` box field or a ``%`` length reads the element's (or its # parent's) attached ``LayoutBox`` (see ``_to_used_length`` / # ``_percent_base_px``), so their used value can go stale on a # relayout even though nothing about the cascade changed. Rather than # invalidate the whole cache on every layout epoch bump (which would # throw away selector-matching and inheritance work for properties a # relayout never touches), each entry that is layout-dependent also # carries the layout epoch it was computed under, and is recomputed # only when that epoch has moved; every other entry is cached for the # lifetime of this cascade, same as before. cache = self.__dict__.get("_property_value_cache") if cache is None: cache = {} self.__dict__["_property_value_cache"] = cache else: cached = cache.get(target) if cached is not None: cached_layout_epoch, cached_value = cached if cached_layout_epoch is None or cached_layout_epoch == _cssom.layout_epoch(): return cached_value value = self._compute_property_value(target) cache[target] = (self._layout_epoch_of(target) if value is not None else None, value) return value
def _layout_epoch_of(self, target: str) -> "int | None": """The layout epoch to stamp a just-computed property value with, or ``None`` if it could not have consulted a ``LayoutBox`` and is safe to cache regardless of future relayouts. Mirrors exactly the two conditions under which ``_to_used_length``/``_percent_base_px`` ever look at a box: an ``auto`` value on one of the handful of properties a box reports (``width``/``height``/``margin-*``), or any length containing a literal ``%``.""" if target not in _USED_LENGTH_PROPERTIES: return None raw = (self._resolved.get(target) or "").strip() if not raw: return None if "%" in raw: return _cssom.layout_epoch() if target in _AUTO_BOX_FIELDS and raw.lower() == "auto": return _cssom.layout_epoch() return None def _compute_property_value(self, target: str) -> str: # A border's width computes to 0 when its style is none/hidden -- # the width is never actually rendered, so a browser doesn't report # whatever length happens to be sitting in border-width (per spec, # and matching the default: border-style's initial value is "none", # so an element with no border styling at all reports 0px here, not # "medium"). style_prop = _BORDER_WIDTH_TO_STYLE.get(target) if style_prop is not None: border_style = (self._resolved.get(style_prop) or "").strip().lower() if border_style in ("none", "hidden"): return "0px" # a minimal UA-stylesheet default: these tags are never content, so # a real browser's UA stylesheet gives them display:none outright -- # it's why a <script> tag's source text never shows up as a # paragraph on the page. Weakest possible priority, exactly like a # real UA stylesheet: only applies when nothing (author rule, # inline style) ever declared `display` at all, so any actual # cascade value for it is completely unaffected. if ( target == "display" and not self._pseudo and "display" not in self._resolved._declared and (getattr(self._element, "tagName", "") or "").lower() in _UA_DISPLAY_NONE_TAGS ): return "none" # a shorthand is rebuilt from its (already used-value-resolved) longhands # -- but only once its own raw value has no unresolved var() left in # it: a fallback's internal comma/space isn't safe to split into # per-longhand pieces (see _has_paren_internal_whitespace), so this # shorthand's own longhands were never actually given a real, # correctly-substituted value to rebuild from -- each one falls back # to its bare initial value instead, which "rebuilds" into a # confidently wrong result rather than the real one below. raw_target_value = self._resolved.get(target) or "" if ( _cssom.is_shorthand(target) and "var(" not in raw_target_value.lower() and _cssom.expand_shorthand(target, raw_target_value) is not None ): built = _cssom.build_shorthand(target, self.getPropertyValue) if built: return built value = self._resolved.get(target) # min-width/min-height's initial (and only auto) value never actually # behaves as "no minimum" in a browser -- Chrome (and every other # engine) reports it as 0px rather than the literal keyword. if target in _MIN_SIZE_PROPERTIES and (value or "auto").strip().lower() == "auto": return "0px" if value: if not target.startswith("--") and "var(" in value: value = _expand_var_references(value, self._custom_property).strip() if target in _USED_LENGTH_PROPERTIES: return self._to_used_length(target, value) if target in _COLOR_PROPERTIES or target.endswith("color"): return self._to_used_color(target, value) if target == "transform": composed = _transform_to_matrix( value, em_px=self._font_size_px(), rem_px=self._root_font_size_px(), ) return composed if composed is not None else value if target == "display": return self._computed_display(value) mapped = _COMPUTED_KEYWORD_MAP.get(target) if mapped is not None: return mapped.get(value.strip().lower(), value) return value if _cssom.is_shorthand(target): return _cssom.build_shorthand(target, self.getPropertyValue) return value # -- used-value resolution (font-relative + absolute length units) ---- def _to_used_color(self, target: str, value: str) -> str: # currentColor (anywhere but `color` itself) -> the computed color if target != "color" and "currentcolor" in value.lower(): current = self.getPropertyValue("color") if current and current.lower() != "currentcolor": # ``current`` is page-controlled CSS text. Passing it as a # replacement string would make ``re.sub`` interpret any # backslash-number sequence as a group reference; a callback # returns it literally regardless of its contents. value = re.sub(r"(?i)\bcurrentcolor\b", lambda _match: current, value) normalized = _cssom.normalize_color(value) return normalized if normalized is not None else value def _font_size_px(self) -> float: cached = self.__dict__.get("_font_size_px_cache") if cached is not None: return cached parent = self._parent_computed() parent_px = parent._font_size_px() if parent is not None else 16.0 raw = str(self._resolved.get("font-size") or "medium").strip() # Every other property's used-value path expands a `var()` reference # before parsing the result as a length (see the generic dispatch in # `_compute_property_value`) -- this one has to do it too, and # explicitly, rather than relying on a caller having already done it: # `_font_size_px` is not just reached through `getPropertyValue`, it # is also the em/rem base every *other* used-length resolution goes # through (`_to_used_length`, `_eval_calc_to_px`, line-height's # unitless multiplier, ...), so leaving `var(--x)` unexpanded here # silently fails to parse as a length and falls back to the # *inherited* size instead -- wrong for the common case of a # design-token stylesheet (`font-size: var(--font-size-small, # 0.875rem)`), not just a synthetic one. if "var(" in raw.lower(): raw = _expand_var_references(raw, self._custom_property).strip() if "calc(" in raw.lower(): px = _eval_calc_to_px(raw, em_px=parent_px, rem_px=self._root_font_size_px()) else: px = _length_string_to_px( raw, em_px=parent_px, rem_px=self._root_font_size_px(), percent_px=parent_px, ) if px is None: px = _ABSOLUTE_FONT_SIZE_KEYWORDS.get(raw.lower(), parent_px) px = self._apply_monospace_font_size_quirk(px, parent) self.__dict__["_font_size_px_cache"] = px return px def _apply_monospace_font_size_quirk(self, px: float, parent: "ComputedStyleDeclaration | None") -> float: """The "monospace font-size quirk" every real browser applies (Blink: ``FontBuilder::CheckForGenericFamilyChange``): an element whose ``font-family`` resolves to exactly the single generic ``monospace`` keyword gets its font-size scaled by 13/16 relative to what it would otherwise be (the reverse, 16/13, on leaving monospace) -- the reason a bare monospace element renders visibly smaller than surrounding text, and why ``font-family: monospace, monospace`` is the well-known trick to opt back out of it. It only fires at the exact point monospace-ness *changes* between an element and its parent (a descendant that inherits an already-monospace, already-scaled ancestor is not scaled again), and only when this element's own ``font-size`` was not itself specified as an absolute length -- an explicit ``font-size: 16px`` on the monospace element itself always means exactly ``16px``, never a scaled value. Confirmed against ``wpt/css/CSS2/tables/table-anonymous-objects- 059.xht``. This is squarely a cascade-level default, not a rendering one -- the 13/16 ratio is a fixed constant, not something derived from real glyph metrics, so it needs no font data domonic doesn't have.""" own_declared = str(self._resolved._declared.get("font-size") or "").strip() if own_declared and "var(" in own_declared.lower(): own_declared = _expand_var_references(own_declared, self._custom_property).strip() own_match = _LENGTH_TOKEN_RE.match(own_declared) if own_declared else None if own_match and own_match.group(2).lower() in _ABSOLUTE_LENGTH_UNITS: return px # an explicit absolute length always opts out own_monospace = _is_exactly_generic_monospace(self.getPropertyValue("font-family")) parent_monospace = _is_exactly_generic_monospace(parent.getPropertyValue("font-family")) if parent else False if own_monospace == parent_monospace: return px return px * _MONOSPACE_FONT_SIZE_SCALE if own_monospace else px / _MONOSPACE_FONT_SIZE_SCALE def _root_font_size_px(self) -> float: cache = self._chain_cache if "__root_font_px__" in cache: return cache["__root_font_px__"] node = self._element root = node while True: parent = getattr(node, "parentNode", None) if parent is None or not _is_inheritance_source(parent): root = node break node = parent px = 16.0 if root is not self._element: rc = ComputedStyleDeclaration(root, None, _chain_cache=cache) px = rc._font_size_px() cache["__root_font_px__"] = px return px def _parent_computed(self): parent = getattr(self._element, "parentNode", None) if parent is None or not _is_inheritance_source(parent): return None cache = self._chain_cache got = cache.get(id(parent)) if got is None: got = ComputedStyleDeclaration(parent, None, _chain_cache=cache) cache[id(parent)] = got return got def _to_used_length(self, target: str, value: str) -> str: if target == "font-size": return _px_str(self._font_size_px()) # border-width's keywords (thin/medium/thick) resolve to a fixed px # length; a "none"/"hidden" border-style already short-circuits to # "0px" in _compute_property_value before this is reached, so a # keyword seen here always belongs to a style that actually draws. if target in _BORDER_WIDTH_TO_STYLE: keyword_px = _BORDER_WIDTH_KEYWORD_PX.get(value.strip().lower()) if keyword_px is not None: return _px_str(keyword_px) # Once a layout engine has attached geometry to this element, its own # box holds the real used value for whichever of these was "auto" -- # not a guess. (A percentage is handled separately below, against # the *parent's* box -- resolving it doesn't need this element's own # geometry, only its containing block's.) auto_field = _AUTO_BOX_FIELDS.get(target) if auto_field is not None and value.strip().lower() == "auto": box = _element_layout_box(self._element) if box is not None: if target in ("width", "height"): used = self._content_size_from_box(box, vertical=(target == "height")) else: used = getattr(box, auto_field, None) if used is not None: return _px_str(used) font_px = self._font_size_px() if "calc(" in value.lower(): evaluated = _eval_calc_to_px(value, em_px=font_px, rem_px=self._root_font_size_px()) if evaluated is not None: # see the ``target == "line-height"`` branch below for why a # negative result here becomes ``normal`` instead of a real # negative used length -- ``calc()`` is the one other path # that can produce a negative line-height. if target == "line-height" and evaluated < 0: return "normal" return _px_str(evaluated) return value if target == "line-height": low = value.strip().lower() if low in ("normal", "inherit", "initial", "unset", ""): return value match = _LENGTH_TOKEN_RE.match(value.strip()) # CSS 2.1 10.8.1: line-height "does not allow negative values" -- # a negative multiplier, percentage, or length makes the whole # declaration invalid, so a real browser discards it and the used # value falls back to ``normal`` instead of a real negative # length (which would otherwise drive a negative line-box strut # height downstream). Domonic has no general cascade-level # "declaration validity" concept that would let a different, # lower-priority but valid declaration win instead when one # exists -- narrower than the full spec behaviour, but correct # for the common case where line-height has only the one, # invalid declaration. Checked before the per-form branches below # so it covers all three forms -- 1.5, 150%, and 20px alike -- # with one check instead of three. if match and float(match.group(1)) < 0: return "normal" if match and match.group(2) == "": # unitless multiplier return _px_str(float(match.group(1)) * font_px) if match and match.group(2) == "%": # % of the computed font-size return _px_str(float(match.group(1)) / 100.0 * font_px) percent_px = self._percent_base_px(target) if "%" in value else None parts = [] changed = False for token in value.split(): px = _length_string_to_px( token, em_px=font_px, rem_px=self._root_font_size_px(), percent_px=percent_px, ) if px is None: parts.append(token) else: changed = True parts.append(_px_str(px)) return " ".join(parts) if changed else value def _content_size_from_box(self, box: Any, vertical: bool) -> "float | None": """The width/height a ``LayoutBox`` contributes to getComputedStyle. ``box.content_width``/``content_height`` is used directly when the layout engine set it explicitly. Otherwise, fall back to the concrete box dimensions the engine handed back so consumers can round-trip real layout geometry without also calculating content dimensions.""" explicit = box.content_height if vertical else box.content_width if explicit is not None: return explicit return box.height if vertical else box.width #: two-value ``display`` syntax -> the legacy single keyword a browser #: reports (CSS Display 3 "computed value" column) _DISPLAY_TWO_VALUE = { "block flow": "block", "inline flow": "inline", "block flow-root": "flow-root", "inline flow-root": "inline-block", "block flex": "flex", "inline flex": "inline-flex", "block grid": "grid", "inline grid": "inline-grid", "block table": "table", "inline table": "inline-table", "block list-item": "list-item", "flow list-item": "list-item", "block flow list-item": "list-item", } _DISPLAY_BLOCKIFY = { "inline": "block", "inline-block": "block", "run-in": "block", "inline-table": "table", "inline-flex": "flex", "inline-grid": "grid", "-webkit-inline-box": "-webkit-box", } def _computed_display(self, value: str) -> str: display = value.strip().lower() display = self._DISPLAY_TWO_VALUE.get(display, display) if display in ("none", "contents"): return display float_val = str(self._resolved.get("float") or "none").strip().lower() position = str(self._resolved.get("position") or "static").strip().lower() if float_val != "none" or position in ("absolute", "fixed"): return self._DISPLAY_BLOCKIFY.get(display, display) return display def _percent_base_px(self, target: str) -> "float | None": """The px a ``%`` resolves against for *target*: the used inline size of the containing block. Once a layout engine has attached a box to the parent, its actual content size is used -- exact, and correct for any percentage-eligible property, not just the ones ``_to_used_length`` special-cases for ``auto``. Failing that, this falls back to an approximation found without full layout: the nearest ancestor with an explicit px ``width`` (or ``height`` for the vertical properties), else the viewport for a root-level element.""" vertical = target in ("height", "min-height", "max-height", "top", "bottom") prop = "height" if vertical else "width" node = getattr(self._element, "parentNode", None) parent_box = _element_layout_box(node) if parent_box is not None: parent_computed = self._parent_computed() if parent_computed is not None: used = parent_computed._content_size_from_box(parent_box, vertical) if used is not None: return used cache = self._chain_cache while node is not None and _is_inheritance_source(node): declared = _parse_css_declarations(getattr(node, "getAttribute", lambda *_: "")("style") or "") for name, val, _ in declared: if name == prop: px = _length_string_to_px( val.strip(), em_px=16.0, rem_px=self._root_font_size_px(), percent_px=None, ) if px is not None: return px node = getattr(node, "parentNode", None) # root-level: fall back to the viewport inline size window = cache.get("__window__") if window is None: document = getattr(self._element, "ownerDocument", None) window = getattr(document, "defaultView", None) if document else None cache["__window__"] = window if window is not None: return float(getattr(window, "innerHeight" if vertical else "innerWidth", 0) or 0) or None return None def _property_entries(self): # getComputedStyle enumerates longhands (and custom properties) only, # never the shorthand entries the cascade also parked in _resolved, # and reports used values return [ (name, self.getPropertyValue(name), "") for name in self._resolved.names() if name.startswith("--") or not _cssom.is_shorthand(name) ] def _expanded_entries(self): # a computed style is already a flat set of resolved longhands return self._property_entries() @property def cssText(self): if not getattr(self, "_resolved", None): return getattr(self, "_css_text", "") return _serialize_css_declarations(self._property_entries()) @cssText.setter def cssText(self, value): if getattr(self, "_resolved", None) is None: object.__setattr__(self, "_css_text", "" if value is None else str(value)) return raise Exception("NoModificationAllowedError: getComputedStyle is read-only")
[docs] def setProperty(self, *_a, **_k): raise Exception("NoModificationAllowedError: getComputedStyle is read-only")
[docs] def removeProperty(self, *_a, **_k): raise Exception("NoModificationAllowedError: getComputedStyle is read-only")
#: properties whose computed value getComputedStyle reports as a used <length> #: in px. ``%`` / ``auto`` / ``calc()`` / keywords are left untouched (they need #: layout); only font-relative and absolute units are resolved. #: a border-*-width longhand -> the border-*-style longhand that gates it #: (its computed value is 0 whenever that style is "none"/"hidden") _BORDER_WIDTH_TO_STYLE = { "border-top-width": "border-top-style", "border-right-width": "border-right-style", "border-bottom-width": "border-bottom-style", "border-left-width": "border-left-style", } #: border-width's three keywords, CSS 2.1 8.5.3 -- the spec leaves their #: exact widths up to the UA beyond "thin < medium < thick"; 1px/3px/5px is #: the convention every browser (Chrome included) uses. _BORDER_WIDTH_KEYWORD_PX = {"thin": 1.0, "medium": 3.0, "thick": 5.0} #: tags a real browser's UA stylesheet gives display:none outright, since #: nothing about them is content. _UA_DISPLAY_NONE_TAGS = frozenset({"head", "title", "script", "style", "meta", "link", "noscript", "template"}) #: min-width/min-height's initial value is "auto", but no engine treats an #: unresolved "auto" as the reported computed value -- see _compute_property_value _MIN_SIZE_PROPERTIES = frozenset({"min-width", "min-height"}) def _is_inheritance_source(node) -> bool: """Whether *node* is a real ancestor to inherit computed style from. Ordinarily that means ``nodeType == ELEMENT_NODE``. But domonic's ``html`` tag doubles as the ``Document`` (the last one created becomes the active document -- see the dom guide), so the actual root ``<html>`` element reports ``nodeType == DOCUMENT_NODE`` like any other ``Document``. Without this, nothing set on ``<html>`` itself (an attribute selector, a class, a tag rule) ever reaches ``<body>`` or anything below it via inheritance -- a real ``Document`` is excluded since it has no ``tagName`` of its own.""" node_type = getattr(node, "nodeType", None) if node_type == 1: # Node.ELEMENT_NODE return True return node_type == 9 and bool(getattr(node, "tagName", "")) # Node.DOCUMENT_NODE #: an "auto" longhand -> the LayoutBox field holding its real used value, #: once a layout engine has attached one (see _to_used_length) _AUTO_BOX_FIELDS = { "width": "content_width", "height": "content_height", "margin-top": "margin_top", "margin-right": "margin_right", "margin-bottom": "margin_bottom", "margin-left": "margin_left", } def _element_layout_box(element): """The ``domonic.layout.LayoutBox`` attached to *element*, or ``None`` if no layout engine has supplied one (or *element* is ``None``, e.g. a root element's parent). A lazy import: ``domonic.layout`` imports this module for the cascade, so importing it back at module scope here would be circular.""" if element is None: return None try: from domonic.layout import get_layout_box except ImportError: # pragma: no cover - domonic.layout always ships with domonic return None return get_layout_box(element) _USED_LENGTH_PROPERTIES = frozenset( { "font-size", "line-height", "letter-spacing", "word-spacing", "text-indent", "margin-top", "margin-right", "margin-bottom", "margin-left", "padding-top", "padding-right", "padding-bottom", "padding-left", "border-top-width", "border-right-width", "border-bottom-width", "border-left-width", "outline-width", "column-rule-width", "column-width", "column-gap", "row-gap", "border-spacing", "width", "height", "min-width", "min-height", "max-width", "max-height", "top", "right", "bottom", "left", "border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius", "border-bottom-left-radius", } ) #: enumerated computed values a browser reports in a canonical form _COMPUTED_KEYWORD_MAP: dict[str, dict[str, str]] = { "font-weight": {"normal": "400", "bold": "700"}, } #: properties whose computed value getComputedStyle reports as ``rgb()`` / #: ``rgba()`` (also everything matching ``*color`` -- see getPropertyValue) _COLOR_PROPERTIES = frozenset( { "color", "background-color", "border-top-color", "border-right-color", "border-bottom-color", "border-left-color", "outline-color", "column-rule-color", "text-decoration-color", "caret-color", "text-emphasis-color", "fill", "stroke", "stop-color", "flood-color", "lighting-color", } ) _ABSOLUTE_FONT_SIZE_KEYWORDS = { "xx-small": 9.6, "x-small": 12.0, "small": 13.333, "medium": 16.0, "large": 18.667, "x-large": 24.0, "xx-large": 32.0, "xxx-large": 48.0, } #: the fixed 13/16 ratio behind the "monospace font-size quirk" -- see #: ``ComputedStyleDeclaration._apply_monospace_font_size_quirk``. _MONOSPACE_FONT_SIZE_SCALE = 13.0 / 16.0 def _is_exactly_generic_monospace(font_family: "str | None") -> bool: """Whether a resolved ``font-family`` is the single generic keyword ``monospace`` and nothing else -- ``monospace, Arial`` or a quoted family name does not count, only the exact generic keyword alone (the well-known ``font-family: monospace, monospace`` trick relies on the second entry making this ``False`` again).""" return (font_family or "").strip().strip("'\"").lower() == "monospace" #: absolute length unit -> px (CSS reference pixel: 1in == 96px) _ABSOLUTE_LENGTH_UNITS = { "px": 1.0, "in": 96.0, "cm": 96.0 / 2.54, "mm": 96.0 / 25.4, "q": 96.0 / 25.4 / 4, "pt": 96.0 / 72.0, "pc": 16.0, } _LENGTH_TOKEN_RE = re.compile(r"^([+-]?(?:\d+\.?\d*|\.\d+))([a-z%]*)$", re.I) def _px_str(px: float) -> str: rounded = round(px, 4) if rounded == int(rounded): return f"{int(rounded)}px" return f"{rounded}px" _PX_VALUE_RE = re.compile(r"^(-?\d+\.?\d*)px$") def _px_value(value: "str | None") -> "float | None": """The number out of an already-resolved ``"Npx"`` string, or ``None`` if *value* isn't one (used-value resolution failed, or it's still a keyword/ percentage) -- lets ``_content_size_from_box`` bail out to the ordinary fallback instead of raising.""" if not value: return None match = _PX_VALUE_RE.match(value.strip()) return float(match.group(1)) if match else None def _angle_to_deg(token: str) -> "float | None": match = re.match(r"^([+-]?(?:\d+\.?\d*|\.\d+))(deg|rad|grad|turn)?$", token.strip(), re.I) if not match: return None number = float(match.group(1)) unit = (match.group(2) or "deg").lower() return { "deg": number, "rad": math.degrees(number), "grad": number * 0.9, "turn": number * 360.0, }[unit] def _transform_to_matrix(value: str, *, em_px: float, rem_px: float) -> "str | None": """Compose a CSS ``transform`` list into ``matrix(...)`` / ``matrix3d(...)`` the way ``getComputedStyle`` reports it, or ``None`` to keep it verbatim (a percentage length, or an unsupported function).""" text = value.strip() if text.lower() in ("", "none"): return "none" functions = re.findall(r"([a-zA-Z0-9]+)\s*\(([^)]*)\)", text) if not functions: return None from domonic.dom import DOMMatrix result = DOMMatrix() def length(tok: str) -> "float | None": return _length_string_to_px(tok, em_px=em_px, rem_px=rem_px, percent_px=None) # apply left-to-right: DOMMatrix.multiplySelf post-multiplies (self . other, # spec order), so composing in written order -- I . f1 . f2 . ... -- gives # the same matrix CSS does (the rightmost function applied to the point # first). for name, raw_args in functions: fn = name.lower() args = [a.strip() for a in raw_args.split(",") if a.strip()] try: if fn in ("translate", "translatex", "translatey", "translate3d"): xs = args + ["0", "0", "0"] tx = length(xs[0]) if fn != "translatey" else 0.0 ty = ( length(xs[0]) if fn == "translatey" else length(xs[1]) if fn in ("translate", "translate3d") else 0.0 ) if tx is None or ty is None: return None result.translateSelf(tx, ty) elif fn == "translatez": if length(args[0]) is None: return None elif fn in ("scale", "scalex", "scaley", "scale3d"): if fn == "scalex": sx, sy = float(args[0]), 1.0 elif fn == "scaley": sx, sy = 1.0, float(args[0]) else: sx = float(args[0]) sy = float(args[1]) if len(args) > 1 else sx result.scaleSelf(sx, sy) elif fn in ("rotate", "rotatez"): deg = _angle_to_deg(args[0]) if deg is None: return None result.rotateSelf(deg) elif fn in ("rotatex", "rotatey"): deg = _angle_to_deg(args[0]) if deg is None: return None if fn == "rotatex": result.rotateSelf(deg, 0, 0) else: result.rotateSelf(0, deg, 0) elif fn == "rotate3d": x, y, z = float(args[0]), float(args[1]), float(args[2]) deg = _angle_to_deg(args[3]) if deg is None: return None result.rotateSelf(x * deg, y * deg, z * deg) elif fn in ("skew", "skewx", "skewy"): ax = _angle_to_deg(args[0]) if fn != "skewy" else 0.0 ay = ( _angle_to_deg(args[0]) if fn == "skewy" else _angle_to_deg(args[1]) if fn == "skew" and len(args) > 1 else 0.0 ) if ax is None or ay is None: return None if ay: result.skewYSelf(ay) if ax: result.skewXSelf(ax) elif fn in ("matrix", "matrix3d"): result.multiplySelf(DOMMatrix(*[float(a) for a in args])) elif fn == "perspective": if length(args[0]) is None: return None else: return None except (ValueError, IndexError): return None return result.toString() _CH_EX_RESOLVER: "Callable[[str, float, float], float | None] | None" = None
[docs] def set_ch_ex_resolver(resolver: "Callable[[str, float, float], float | None] | None") -> None: """Register a callback used to resolve ``ch``/``ex`` length units instead of the CSS-spec 0.5em approximation ``getComputedStyle`` falls back to by default. Domonic itself has no glyph metrics to measure a real character advance or x-height against, but a renderer built on top of it (which does have a font) can supply one. Called as ``resolver(unit, number, em_px) -> float | None`` -- *unit* is ``"ch"`` or ``"ex"``, *number* the numeric part of the token (``2`` for ``"2ch"``), *em_px* the element's computed font-size in px -- only once the parser has already identified the token as one of these two units, so every other length (the overwhelming majority) never pays for the call. Returning ``None`` falls back to the default approximation for that one conversion. Pass ``None`` to remove a previously registered resolver.""" global _CH_EX_RESOLVER _CH_EX_RESOLVER = resolver
_PRESENTATIONAL_HINT_RESOLVER: "Callable[[Any], dict[str, Any] | None] | None" = None
[docs] def set_presentational_hint_resolver(resolver: "Callable[[Any], dict[str, Any] | None] | None") -> None: """Register a callback that supplies presentational hints -- the implicit style a legacy HTML attribute carries (``bgcolor``, an ``<img>``'s ``width``/``height``, ...) -- for ``getComputedStyle``'s cascade to fold in at the one priority a real browser gives them: weaker than *any* author stylesheet rule for the same property, and weaker than an inline ``style=""``, but still stronger than a property's plain initial value. Domonic has no opinion on which HTML attributes should become which CSS properties -- that mapping is rendering policy for whatever is built on top of it -- so this only owns the cascade-priority slot only the cascade itself can get right. Called as ``resolver(element) -> {property_name: value} | None`` once per cascade resolution, kebab-case longhand property names (``"background- color"``, not ``backgroundColor``). A returned entry is used only for a property no real author rule declared at all; any real declaration -- of any specificity, source order, or a later CSSOM write -- overrides it normally, with no special-casing needed anywhere else in the cascade. "Once per cascade resolution" means the same thing it already means for an author stylesheet: consulted fresh on a cache miss (the first read, or after a DOM/stylesheet mutation invalidates the cached cascade), not re-consulted just because whatever *resolver* itself reads changed on its own with nothing else moving -- so a host that computes hints from an element's own attributes should set them before that element's style is first read, the same ordering it would already need for a real ``style=""`` attribute set via ``setAttribute``. Pass ``None`` to remove a previously registered resolver.""" global _PRESENTATIONAL_HINT_RESOLVER _PRESENTATIONAL_HINT_RESOLVER = resolver
def _length_string_to_px(token: str, *, em_px: float, rem_px: float, percent_px: "float | None") -> "float | None": """Convert a single length token to px, or ``None`` if it is not a plain length this resolver handles (``auto``, ``calc(...)``, a bare keyword, or a ``%`` with no base).""" stripped = token.strip() # Fast path for the tokens a stylesheet overwhelmingly actually uses -- # a bare integer ``px`` length (``12px``, ``0px``) or a bare ``0`` -- so # the common case skips the regex match, the ``.lower()`` call, and the # unit-table lookup below. Anything else (a decimal, a different unit, a # keyword) falls through to the general path unchanged. if stripped == "0": return 0.0 if stripped.endswith("px"): head = stripped[:-2] if head and (head.isdigit() or (head[0] in "+-" and head[1:].isdigit())): return float(head) match = _LENGTH_TOKEN_RE.match(stripped) if not match: return None number, unit = float(match.group(1)), match.group(2).lower() if unit in ("",) and number == 0: return 0.0 if unit == "%": return None if percent_px is None else number / 100.0 * percent_px if unit in ("em", "rem"): return number * (em_px if unit == "em" else rem_px) if unit in ("ch", "ex"): # domonic has no real glyph metrics ("0" advance / x-height) of its # own to measure against, so it defaults to the value the CSS spec # itself mandates for exactly this case: 0.5em for both units. A # host that does have real font metrics (a renderer built on top of # domonic) can register a resolver to use them instead -- called # only once the unit is already known to be ``ch``/``ex``, so a # plain ``px``/``em`` length never pays for the dispatch. if _CH_EX_RESOLVER is not None: resolved = _CH_EX_RESOLVER(unit, number, em_px) if resolved is not None: return resolved return number * 0.5 * em_px if unit == "": return None # a unitless non-zero number is not a length return None if unit not in _ABSOLUTE_LENGTH_UNITS else number * _ABSOLUTE_LENGTH_UNITS[unit] _CALC_LENGTH_TERM_RE = re.compile(r"(?<![\w.])([+-]?(?:\d+\.?\d*|\.\d+))(px|em|rem|ch|ex|in|cm|mm|q|pt|pc)\b", re.I) def _eval_calc_to_px(expr: str, *, em_px: float, rem_px: float) -> "float | None": """Evaluate a ``calc(...)`` body to px, or ``None`` if it mixes in a percentage / viewport / other unit that needs layout.""" body = expr.strip() if body.lower().startswith("calc(") and body.endswith(")"): body = body[5:-1] # any unit we cannot resolve to px -> bail (keep the calc() verbatim) if re.search(r"(?<![\w.])[+-]?(?:\d+\.?\d*|\.\d+)" r"(%|vw|vh|vmin|vmax|fr|svh|lvh|dvh)\b", body, re.I): return None if "calc(" in body.lower(): return None # nested calc -- keep it simple def _sub(match: "re.Match[str]") -> str: px = _length_string_to_px(match.group(0), em_px=em_px, rem_px=rem_px, percent_px=None) return repr(px) if px is not None else match.group(0) numeric = _CALC_LENGTH_TERM_RE.sub(_sub, body) if re.search(r"[a-z]", numeric, re.I): # an unresolved unit slipped through return None if not re.fullmatch(r"[-+*/()\d.eE\s]+", numeric): return None try: node: Any = ast.parse(numeric, "<calc>", "eval") except SyntaxError: return None def _ev(n: Any) -> float: if isinstance(n, ast.Expression): return _ev(n.body) if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)): return float(n.value) if isinstance(n, ast.UnaryOp) and isinstance(n.op, (ast.UAdd, ast.USub)): v = _ev(n.operand) return v if isinstance(n.op, ast.UAdd) else -v if isinstance(n, ast.BinOp) and isinstance(n.op, (ast.Add, ast.Sub, ast.Mult, ast.Div)): a, b = _ev(n.left), _ev(n.right) if isinstance(n.op, ast.Add): return a + b if isinstance(n.op, ast.Sub): return a - b if isinstance(n.op, ast.Mult): return a * b return a / b if b else 0.0 raise ValueError("unsupported calc expression") try: return _ev(node) except (ValueError, ZeroDivisionError): return None def _expand_var_references(value: str, resolve, _depth: int = 0) -> str: """Substitute ``var(--name[, fallback])`` references in a computed value. ``resolve(name)`` returns the computed value of custom property ``name`` (``""`` when unset). Follows the spec fallback rule and is paren-aware so nested ``var()`` in the fallback works; bails out on deep recursion (a custom-property cycle). """ if _depth > 20 or "var(" not in value: return value out: list[str] = [] i = 0 length = len(value) while i < length: if value.startswith("var(", i): depth = 0 j = i + 3 while j < length: if value[j] == "(": depth += 1 elif value[j] == ")": depth -= 1 if depth == 0: break j += 1 inner = value[i + 4 : j] comma = _find_next_top_level_char(inner, 0, ",") if comma == -1: name, fallback = inner.strip(), None else: name = inner[:comma].strip() fallback = inner[comma + 1 :].strip() substituted = resolve(name) if name.startswith("--") else "" if substituted and substituted.strip(): out.append(_expand_var_references(substituted, resolve, _depth + 1)) elif fallback is not None: out.append(_expand_var_references(fallback, resolve, _depth + 1)) # else: unresolved with no fallback -> contributes nothing i = j + 1 continue out.append(value[i]) i += 1 return "".join(out) class _ResolvedView: """Lazy property resolver: explicit value -> inherited -> initial.""" def __init__(self, declared: dict[str, str], parent_computed): self._declared = declared self._parent = parent_computed def names(self): return list(self._declared) def _inherited(self, name: str) -> str: if self._parent is not None: value = self._parent.getPropertyValue(name) if value: return value return _cssom.initial_value(name) def get(self, name: str) -> str: if name in self._declared: value = self._declared[name] low = value.strip().lower() # CSS-wide keywords as explicit values if low == "inherit": return self._inherited(name) if low == "initial": return _cssom.initial_value(name) if low in ("unset", "revert", "revert-layer"): return self._inherited(name) if _cssom.inherits(name) else _cssom.initial_value(name) return value if _cssom.inherits(name) and self._parent is not None: inherited = self._parent.getPropertyValue(name) if inherited: return inherited return _cssom.initial_value(name) def _known_css_property_names() -> set[str]: global _KNOWN_CSS_PROPERTIES if _KNOWN_CSS_PROPERTIES is None: style = Style() names = set(_ADDITIONAL_CSS_PROPERTY_NAMES) for name in style.__dict__: if name.startswith("_") or name in _CSS_STYLE_INTERNAL_NAMES: continue names.add(_css_property_name(name)) for name, descriptor in Style.__dict__.items(): if isinstance(descriptor, property): names.add(_css_property_name(name)) _KNOWN_CSS_PROPERTIES = names return _KNOWN_CSS_PROPERTIES def _has_balanced_css_delimiters(text: str) -> bool: quote = None escaped = False stack: list[str] = [] pairs = {")": "(", "]": "[", "}": "{"} for char in text or "": if quote is not None: if escaped: escaped = False elif char == "\\": escaped = True elif char == quote: quote = None continue if char in ("'", '"'): quote = char continue if char in "([{": stack.append(char) continue if char in ")]}": if not stack or stack[-1] != pairs[char]: return False stack.pop() return quote is None and not stack def _supports_property_value(property_name: str, value: str) -> bool: if property_name != property_name.strip() or not property_name: return False if value is None or not str(value).strip() or _IMPORTANT_RE.search(str(value)): return False if not _has_balanced_css_delimiters(str(value)): return False normalized = _css_property_name(property_name) if normalized.startswith("--"): return len(normalized) > 2 return normalized in _known_css_property_names() def _supports_condition(condition_text: str) -> bool: text = (condition_text or "").strip() if not text: return False split = _split_top_level_word(text, "or") if split: return _supports_condition(split[0]) or _supports_condition(split[1]) split = _split_top_level_word(text, "and") if split: return _supports_condition(split[0]) and _supports_condition(split[1]) if text.lower().startswith("not "): return not _supports_condition(text[4:].strip()) text = _strip_wrapping_parens(text) lower = text.lower() for function_name in ( "selector", "font-tech", "font-format", "named-feature", "env", ): function_match = _consume_css_function(text, function_name) if function_match and function_match[0]: return True at_rule_match = _consume_css_function(text, "at-rule") if at_rule_match: return at_rule_match[0].strip().lower() in { "@container", "@counter-style", "@font-face", "@font-feature-values", "@import", "@keyframes", "@layer", "@media", "@namespace", "@page", "@property", "@scope", "@supports", } if lower.startswith("selector("): return False entries = _parse_css_declarations(text) return len(entries) == 1 and _supports_property_value(entries[0][0], entries[0][1])
[docs] class CSSStyleValue: """Base of the CSS Typed OM value hierarchy.""" def __str__(self) -> str: # pragma: no cover - overridden return ""
[docs] def toString(self) -> str: return str(self)
[docs] @staticmethod def parse(property_name: str, css_text: str) -> "CSSStyleValue": return _parse_css_style_value(str(css_text))
[docs] @staticmethod def parseAll(property_name: str, css_text: str) -> "list[CSSStyleValue]": return [_parse_css_style_value(part) for part in _split_top_level_commas(str(css_text))] or [ _parse_css_style_value(str(css_text)) ]
[docs] class CSSKeywordValue(CSSStyleValue): def __init__(self, value: Any) -> None: self.value = str(value) def __str__(self) -> str: return self.value def __eq__(self, other: object) -> bool: if isinstance(other, CSSKeywordValue): return self.value == other.value return NotImplemented def __hash__(self) -> int: return hash(self.value)
[docs] class CSSUnitValue(CSSStyleValue): """A numeric value with a unit -- ``CSS.px(10)`` / ``CSSUnitValue(10, "px")``.""" def __init__(self, value: float, unit: str) -> None: self.value = float(value) # CSSOM unit names: "number", "percent", or the CSS unit token lowercased self.unit = {"": "number", "%": "percent"}.get(unit, unit.lower()) @property def _token(self) -> str: return {"number": "", "percent": "%"}.get(self.unit, self.unit) def __str__(self) -> str: number = int(self.value) if self.value == int(self.value) else self.value return f"{number}{self._token}"
[docs] def to(self, unit: str) -> "CSSUnitValue": target = {"": "number", "%": "percent"}.get(unit, unit.lower()) if target == self.unit: return CSSUnitValue(self.value, unit) factor = _CSS_UNIT_CANONICAL.get(self.unit) target_factor = _CSS_UNIT_CANONICAL.get(target) if factor is None or target_factor is None: raise TypeError(f"cannot convert {self.unit!r} to {target!r}") return CSSUnitValue(self.value * factor / target_factor, unit)
def _combine(self, other: Any, op: Callable[[float, float], float]) -> "CSSUnitValue": if isinstance(other, (int, float)): return CSSUnitValue(op(self.value, float(other)), self._token) if isinstance(other, CSSUnitValue) and other.unit == self.unit: return CSSUnitValue(op(self.value, other.value), self._token) raise TypeError("CSSUnitValue operands must share a unit") def __add__(self, other: Any) -> "CSSUnitValue": return self._combine(other, lambda a, b: a + b) def __sub__(self, other: Any) -> "CSSUnitValue": return self._combine(other, lambda a, b: a - b) def __mul__(self, other: Any) -> "CSSUnitValue": return self._combine(other, lambda a, b: a * b) def __truediv__(self, other: Any) -> "CSSUnitValue": return self._combine(other, lambda a, b: a / b) def __eq__(self, other: object) -> bool: if isinstance(other, CSSUnitValue): return self.value == other.value and self.unit == other.unit return NotImplemented def __hash__(self) -> int: return hash((self.value, self.unit))
#: canonical-unit factors for CSSUnitValue.to() within a compatible group _CSS_UNIT_CANONICAL = { "px": 1.0, "in": 96.0, "cm": 96.0 / 2.54, "mm": 96.0 / 25.4, "q": 96.0 / 25.4 / 4, "pt": 96.0 / 72.0, "pc": 16.0, "deg": 1.0, "grad": 0.9, "rad": 180.0 / math.pi, "turn": 360.0, "s": 1.0, "ms": 0.001, "hz": 1.0, "khz": 1000.0, "dppx": 1.0, "dpi": 1.0 / 96.0, "dpcm": 2.54 / 96.0, } _CSS_TYPED_OM_UNIT_RE = re.compile(r"^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)\s*([a-z%]*)\s*$", re.I) def _parse_css_style_value(css_text: str) -> CSSStyleValue: text = str(css_text).strip() match = _CSS_TYPED_OM_UNIT_RE.match(text) if match: return CSSUnitValue(float(match.group(1)), match.group(2)) return CSSKeywordValue(text)
[docs] class StylePropertyMap: """Minimal ``StylePropertyMap`` / ``StylePropertyMapReadOnly``. Backed by a live ``CSSStyleDeclaration``; ``get`` returns a Typed OM value, ``getAll`` a list, and (when writable) ``set`` / ``append`` / ``delete`` / ``clear`` proxy to the declaration. """ def __init__(self, declaration, *, read_only: bool = False) -> None: self._decl = declaration self._read_only = read_only def _guard(self) -> None: if self._read_only: raise Exception("NoModificationAllowedError: this map is read-only")
[docs] def get(self, property_name: str): value = self._decl.getPropertyValue(property_name) return _parse_css_style_value(value) if value else None
[docs] def getAll(self, property_name: str) -> list: value = self._decl.getPropertyValue(property_name) if not value: return [] return [_parse_css_style_value(part) for part in _split_top_level_commas(value)] or [ _parse_css_style_value(value) ]
[docs] def has(self, property_name: str) -> bool: return self._decl.getPropertyValue(property_name) != ""
[docs] def set(self, property_name: str, value: Any) -> None: self._guard() self._decl.setProperty(property_name, str(value))
[docs] def append(self, property_name: str, value: Any) -> None: self._guard() existing = self._decl.getPropertyValue(property_name) combined = f"{existing}, {value}" if existing else str(value) self._decl.setProperty(property_name, combined)
[docs] def delete(self, property_name: str) -> None: self._guard() self._decl.removeProperty(property_name)
[docs] def clear(self) -> None: self._guard() for name in list(self._decl): self._decl.removeProperty(name)
def __iter__(self): for name in self._decl: yield name, self.get(name) @property def size(self) -> int: return self._decl.length
[docs] class CSS: """Namespace for CSS utility functions.""" # -- CSS Typed OM numeric factories ----------------------------------- number = staticmethod(lambda v=0: CSSUnitValue(v, "")) percent = staticmethod(lambda v=0: CSSUnitValue(v, "%")) px = staticmethod(lambda v=0: CSSUnitValue(v, "px")) em = staticmethod(lambda v=0: CSSUnitValue(v, "em")) rem = staticmethod(lambda v=0: CSSUnitValue(v, "rem")) ex = staticmethod(lambda v=0: CSSUnitValue(v, "ex")) ch = staticmethod(lambda v=0: CSSUnitValue(v, "ch")) vw = staticmethod(lambda v=0: CSSUnitValue(v, "vw")) vh = staticmethod(lambda v=0: CSSUnitValue(v, "vh")) vmin = staticmethod(lambda v=0: CSSUnitValue(v, "vmin")) vmax = staticmethod(lambda v=0: CSSUnitValue(v, "vmax")) cm = staticmethod(lambda v=0: CSSUnitValue(v, "cm")) mm = staticmethod(lambda v=0: CSSUnitValue(v, "mm")) Q = staticmethod(lambda v=0: CSSUnitValue(v, "Q")) pt = staticmethod(lambda v=0: CSSUnitValue(v, "pt")) pc = staticmethod(lambda v=0: CSSUnitValue(v, "pc")) deg = staticmethod(lambda v=0: CSSUnitValue(v, "deg")) grad = staticmethod(lambda v=0: CSSUnitValue(v, "grad")) rad = staticmethod(lambda v=0: CSSUnitValue(v, "rad")) turn = staticmethod(lambda v=0: CSSUnitValue(v, "turn")) s = staticmethod(lambda v=0: CSSUnitValue(v, "s")) ms = staticmethod(lambda v=0: CSSUnitValue(v, "ms")) fr = staticmethod(lambda v=0: CSSUnitValue(v, "fr")) dpi = staticmethod(lambda v=0: CSSUnitValue(v, "dpi")) dpcm = staticmethod(lambda v=0: CSSUnitValue(v, "dpcm")) dppx = staticmethod(lambda v=0: CSSUnitValue(v, "dppx"))
[docs] @staticmethod def escape(ident: Any) -> str: """Serialize a string for safe use as a CSS identifier.""" ident = "" if ident is None else str(ident) result: list[str] = [] length = len(ident) for index, char in enumerate(ident): code = ord(char) if code == 0: result.append("\ufffd") elif 0x0001 <= code <= 0x001F or code == 0x007F: result.append(f"\\{code:x} ") elif index == 0 and char.isdigit(): result.append(f"\\{code:x} ") elif index == 1 and char.isdigit() and ident[0] == "-": result.append(f"\\{code:x} ") elif index == 0 and char == "-" and length == 1: result.append("\\-") elif code >= 0x0080 or char in "-_" or char.isalnum(): result.append(char) else: result.append(f"\\{char}") return "".join(result)
[docs] @staticmethod def supports(property: str, value: str | None = None) -> bool: """Best-effort syntax-level equivalent of ``CSS.supports()``.""" if value is None: return _supports_condition(str(property)) return _supports_property_value(str(property), str(value))
[docs] @staticmethod def registerProperty(definition) -> None: """Register a custom property (CSS Properties and Values API). ``definition`` is a mapping with ``name`` (a ``--`` custom property), ``syntax`` (default ``"*"``), ``inherits`` (bool, required by the spec), and an optional ``initialValue``. Once registered, the property's inheritance and initial value are honoured by ``window.getComputedStyle``. """ if not isinstance(definition, dict): definition = { key: getattr(definition, key) for key in ("name", "syntax", "inherits", "initialValue") if hasattr(definition, key) } name = str(definition.get("name", "")) if not name.startswith("--") or len(name) < 3: raise SyntaxError(f"registerProperty: invalid custom property name {name!r}") if _cssom.registered_property(name) is not None: raise Exception(f"InvalidModificationError: {name} has already been registered") syntax = str(definition.get("syntax", "*")).strip().strip("\"'") or "*" initial_value = definition.get("initialValue") if initial_value is not None: initial_value = str(initial_value) if syntax != "*" and initial_value is None: raise SyntaxError("registerProperty: initialValue is required when syntax is not '*'") _cssom.register_property( name, syntax=syntax, inherits=bool(definition.get("inherits", False)), initial_value=initial_value, )
def _normalize_declaration_text(css_text: str) -> str: text = " ".join(line.strip() for line in (css_text or "").strip().splitlines() if line.strip()) text = re.sub(r"\s+", " ", text) entries = _parse_css_declarations(text) if not entries: return text.strip() serialized = _serialize_css_declarations(entries) if len(entries) == 1 and not text.rstrip().endswith(";"): return serialized[:-1] return serialized def _skip_css_whitespace(css_text: str, index: int) -> int: while index < len(css_text) and css_text[index].isspace(): index += 1 return index def _find_next_rule_boundary(css_text: str, start: int) -> tuple[int, str]: quote = None escaped = False depth = 0 for index in range(start, len(css_text)): char = css_text[index] if quote is not None: if escaped: escaped = False elif char == "\\": escaped = True elif char == quote: quote = None continue if char in ("'", '"'): quote = char continue if char in "([": depth += 1 continue if char in ")]" and depth > 0: depth -= 1 continue if depth == 0 and char in "{;": return index, char return -1, "" def _find_next_top_level_char(css_text: str, start: int, target: str) -> int: quote = None escaped = False depth = 0 for index in range(start, len(css_text)): char = css_text[index] if quote is not None: if escaped: escaped = False elif char == "\\": escaped = True elif char == quote: quote = None continue if char in ("'", '"'): quote = char continue if char in "([": depth += 1 continue if char in ")]" and depth > 0: depth -= 1 continue if depth == 0 and char == target: return index return -1 def _find_matching_brace(css_text: str, open_index: int) -> int: quote = None escaped = False depth = 0 for index in range(open_index, len(css_text)): char = css_text[index] if quote is not None: if escaped: escaped = False elif char == "\\": escaped = True elif char == quote: quote = None continue if char in ("'", '"'): quote = char continue if char == "{": depth += 1 continue if char == "}": depth -= 1 if depth == 0: return index return -1 def _style_from_declarations(css_text: str, parentRule: CSSRule) -> CSSStyleDeclaration: style = CSSStyleDeclaration() style.cssText = _normalize_declaration_text(css_text) style.parentRule = parentRule return style def _extract_style_block_parts(block: str) -> tuple[str, list[tuple[str, str, str]]]: declarations: list[str] = [] nested_parts: list[tuple[str, str, str]] = [] cursor = 0 seen_nested_rule = False while True: open_index = _find_next_top_level_char(block, cursor, "{") if open_index == -1: tail = block[cursor:] if seen_nested_rule: if _parse_css_declarations(tail): nested_parts.append(("declarations", "", tail)) else: declarations.append(tail) break segment = block[cursor:open_index] split_at = segment.rfind(";") if split_at == -1: declarations_text = "" nested_prelude = segment.strip() else: declarations_text = segment[: split_at + 1] nested_prelude = segment[split_at + 1 :].strip() if seen_nested_rule: if _parse_css_declarations(declarations_text): nested_parts.append(("declarations", "", declarations_text)) else: declarations.append(declarations_text) close_index = _find_matching_brace(block, open_index) if close_index == -1: if seen_nested_rule: if _parse_css_declarations(block[open_index:]): nested_parts.append(("declarations", "", block[open_index:])) else: declarations.append(block[open_index:]) break nested_parts.append(("rule", nested_prelude, block[open_index + 1 : close_index])) seen_nested_rule = True cursor = close_index + 1 return "".join(declarations), nested_parts def _parse_single_container_condition(value: str) -> dict[str, str]: value = (value or "").strip() if not value: return {"name": "", "query": ""} lower = value.lower() query_starters = ("(", "style(", "scroll-state(", "not ", "not(", "and ", "or ") if lower.startswith(query_starters): return {"name": "", "query": value} parts = value.split(None, 1) if len(parts) == 1: return {"name": "", "query": parts[0]} return {"name": parts[0], "query": parts[1].strip()} def _parse_container_conditions(prelude: str) -> list[dict[str, str]]: value = prelude.replace("@container", "", 1).strip() if not value: return [] return [_parse_single_container_condition(item) for item in _split_top_level_commas(value)] def _serialize_container_conditions(conditions: list[dict[str, str]]) -> str: parts: list[str] = [] for condition in conditions or []: name = condition.get("name", "") query = condition.get("query", "") parts.append(f"{name} {query}".strip()) return ", ".join(part for part in parts if part) def _parse_container_prelude(prelude: str) -> tuple[str, str, list[dict[str, str]]]: conditions = _parse_container_conditions(prelude) if len(conditions) == 1: condition = conditions[0] return condition["name"], condition["query"], conditions return "", "", conditions def _strip_wrapping_parens(value: str) -> str: value = (value or "").strip() if value.startswith("("): close_index = _find_matching_paren(value, 0) if close_index == len(value) - 1: return value[1:-1].strip() return value def _parse_scope_prelude(prelude: str) -> tuple[str | None, str | None]: value = prelude.replace("@scope", "", 1).strip() if not value: return None, None split = _split_top_level_word(value, "to") if split: start, end = split return _strip_wrapping_parens(start), _strip_wrapping_parens(end) return _strip_wrapping_parens(value), None def _consume_css_function(text: str, function_name: str) -> tuple[str, str] | None: text = (text or "").lstrip() prefix = f"{function_name}(" if not text.lower().startswith(prefix): return None open_index = len(function_name) close_index = _find_matching_paren(text, open_index) if close_index == -1: return None return text[open_index + 1 : close_index].strip(), text[close_index + 1 :].strip() def _parse_import_tail(text: str) -> tuple[str | None, str, str]: layer_name: str | None = None supports_text = "" media_text = (text or "").strip() layer_match = _consume_css_function(media_text, "layer") if layer_match: layer_name, media_text = layer_match elif media_text.lower() == "layer" or media_text.lower().startswith("layer "): layer_name = "" media_text = media_text[5:].strip() supports_match = _consume_css_function(media_text, "supports") if supports_match: supports_text, media_text = supports_match return layer_name, supports_text, media_text.strip() def _parse_statement_at_rule( prelude: str, parentStyleSheet: "CSSStyleSheet | None", parentRule: CSSRule | None, ) -> CSSRule | None: # rule is rebound to a different CSSRule subclass per branch. rule: Any lower = prelude.lower() if not lower.startswith("@") and parentRule is not None and _parse_css_declarations(prelude): rule = CSSNestedDeclarations() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.style = _style_from_declarations(prelude, rule) return rule if lower.startswith("@import"): rule = CSSImportRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rest = prelude[len("@import") :].strip() href_match = re.match(r"(url\([^)]+\)|\"[^\"]+\"|'[^']+')\s*(.*)$", rest) if href_match: rule.href = href_match.group(1) layer_name, supports_text, media_text = _parse_import_tail(href_match.group(2).strip()) rule.layerName = layer_name rule.supportsText = supports_text rule.media = MediaList([item for item in _split_top_level_commas(media_text) if item]) else: rule.href = rest rule.media = MediaList() return rule if lower.startswith("@namespace"): rule = CSSNamespaceRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rest = prelude[len("@namespace") :].strip() parts = rest.split(None, 1) if len(parts) == 2: rule.prefix = parts[0] rule.namespaceURI = parts[1] else: rule.namespaceURI = rest return rule if lower.startswith("@layer"): rule = CSSLayerStatementRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.nameList = [name for name in _split_top_level_commas(prelude[len("@layer") :].strip()) if name] return rule rule = CSSRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.type = CSSRule.CHARSET_RULE if lower.startswith("@charset") else CSSRule.UNKNOWN_RULE rule._cssText = f"{prelude};" return rule def _parse_keyframe_rules( parentStyleSheet: "CSSStyleSheet | None", parentRule: CSSKeyframesRule, block: str ) -> CSSRuleList: rules = CSSRuleList() index = 0 while index < len(block): index = _skip_css_whitespace(block, index) boundary, kind = _find_next_rule_boundary(block, index) if boundary == -1 or kind != "{": break key_text = block[index:boundary].strip() close_index = _find_matching_brace(block, boundary) if close_index == -1: break rule = CSSKeyframeRule() rule.keyText = key_text rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.style = _style_from_declarations(block[boundary + 1 : close_index], rule) rules.append(rule) index = close_index + 1 return rules def _parse_style_rule( parentStyleSheet: "CSSStyleSheet | None", parentRule: CSSRule | None, selectorText: str, block: str, ) -> CSSStyleRule: rule = CSSStyleRule() rule.selectorText = selectorText.strip() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule declarations, nested_rules = _extract_style_block_parts(block) rule.style = _style_from_declarations(declarations, rule) for nested_type, nested_prelude, nested_block in nested_rules: if nested_type == "declarations": nested = CSSNestedDeclarations() nested.parentStyleSheet = parentStyleSheet nested.parentRule = rule nested.style = _style_from_declarations(nested_block, nested) if nested.style.length: rule.cssRules.append(nested) continue if not nested_prelude: continue rule.cssRules.append(_parse_block_rule(parentStyleSheet, rule, nested_prelude, nested_block)) return rule def _parse_block_rule( parentStyleSheet: "CSSStyleSheet | None", parentRule: CSSRule | None, prelude: str, block: str, ) -> CSSRule: # rule is rebound to a different CSSRule subclass per branch. rule: Any lower = prelude.lower() if lower.startswith("@media"): rule = CSSMediaRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule for medium in _split_top_level_commas(prelude[len("@media") :].strip()): if medium: rule.media.appendMedium(medium) rule.conditionText = rule.media.mediaText rule.cssRules = _parse_rule_list(parentStyleSheet, block, rule) return rule if lower.startswith("@supports-condition"): rule = CSSSupportsConditionRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.name = prelude[len("@supports-condition") :].strip() rule.cssRules = _parse_rule_list(parentStyleSheet, block, rule) return rule if lower.startswith("@supports"): rule = CSSSupportsRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.conditionText = prelude[len("@supports") :].strip() rule.cssRules = _parse_rule_list(parentStyleSheet, block, rule) return rule if lower.startswith("@when"): rule = CSSWhenRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.conditionText = prelude[len("@when") :].strip() rule.cssRules = _parse_rule_list(parentStyleSheet, block, rule) return rule if lower.startswith("@else"): rule = CSSElseRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.conditionText = prelude[len("@else") :].strip() rule.cssRules = _parse_rule_list(parentStyleSheet, block, rule) return rule if lower.startswith("@container"): rule = CSSContainerRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.containerName, rule.containerQuery, rule.conditions = _parse_container_prelude(prelude) rule.conditionText = _serialize_container_conditions(rule.conditions) rule.cssRules = _parse_rule_list(parentStyleSheet, block, rule) return rule if lower.startswith("@scope"): rule = CSSScopeRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.start, rule.end = _parse_scope_prelude(prelude) rule.cssRules = _parse_rule_list(parentStyleSheet, block, rule) return rule if lower.startswith("@layer"): rule = CSSLayerBlockRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.name = prelude[len("@layer") :].strip() rule.cssRules = _parse_rule_list(parentStyleSheet, block, rule) return rule if lower.startswith("@keyframes") or lower.startswith("@-webkit-keyframes"): rule = CSSKeyframesRule() rule.name = prelude.split(None, 1)[1].strip() if len(prelude.split(None, 1)) > 1 else "" rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.cssRules = _parse_keyframe_rules(parentStyleSheet, rule, block) return rule if lower.startswith("@font-face"): rule = CSSFontFaceRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.style = _style_from_declarations(block, rule) return rule if lower.startswith("@page"): rule = CSSPageRule() rule.selectorText = prelude[len("@page") :].strip() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.style = _style_from_declarations(block, rule) return rule if lower.startswith("@property"): rule = CSSPropertyRule() rule.name = prelude[len("@property") :].strip() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.style = _style_from_declarations(block, rule) rule._register() return rule if lower.startswith("@counter-style"): rule = CSSCounterStyleRule() rule.name = prelude[len("@counter-style") :].strip() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.style = _style_from_declarations(block, rule) rule.system = rule.style.getPropertyValue("system") return rule if lower.startswith("@color-profile"): rule = CSSColorProfileRule() rule.colorProfile = prelude[len("@color-profile") :].strip() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.style = _style_from_declarations(block, rule) return rule if lower.startswith("@font-feature-values"): rule = CSSFontFeatureValuesRule() rule.parentStyleSheet = parentStyleSheet rule.parentRule = parentRule rule.cssRules = _parse_rule_list(parentStyleSheet, block, rule) return rule return _parse_style_rule(parentStyleSheet, parentRule, prelude, block) def _parse_rule_list( parentStyleSheet: "CSSStyleSheet | None", cssText: str, parentRule: CSSRule | None = None, ) -> CSSRuleList: rules = CSSRuleList() css = cssText or "" index = 0 while index < len(css): index = _skip_css_whitespace(css, index) if index >= len(css): break boundary, kind = _find_next_rule_boundary(css, index) if boundary == -1: break prelude = css[index:boundary].strip() if not prelude: index = boundary + 1 continue if kind == ";": rule = _parse_statement_at_rule(prelude, parentStyleSheet, parentRule) if rule is not None: rules.append(rule) index = boundary + 1 continue close_index = _find_matching_brace(css, boundary) if close_index == -1: break block = css[boundary + 1 : close_index] rules.append(_parse_block_rule(parentStyleSheet, parentRule, prelude, block)) index = close_index + 1 return rules
[docs] class CSSParser:
[docs] @staticmethod def parseFromString(parentStyleSheet: CSSStyleSheet, cssText: str): return _parse_rule_list(parentStyleSheet, _strip_css_comments(cssText))