757 lines
26 KiB
Python
757 lines
26 KiB
Python
# coding=utf-8
|
|
#
|
|
# Copyright (C) 2005 Aaron Spike, aaron@ekips.org
|
|
# 2019-2020 Martin Owens
|
|
# 2021 Jonathan Neuhauser, jonathan.neuhauser@outlook.com
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
"""
|
|
Functions for handling styles and embedded css
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from typing import Optional, Generator, TYPE_CHECKING, Tuple
|
|
from lxml import etree
|
|
import tinycss2
|
|
import tinycss2.ast
|
|
|
|
from .colors import Color
|
|
from .properties import (
|
|
_get_tokens_from_value,
|
|
_is_inherit,
|
|
all_properties,
|
|
shorthand_from_value,
|
|
shorthand_properties,
|
|
TokenList,
|
|
_strip_whitespace_nodes,
|
|
)
|
|
from .css import CSSCompiler, parser
|
|
|
|
from .utils import FragmentError, NotifyList, NotifyOrderedDict
|
|
from .elements._utils import NSS
|
|
from .elements import _base
|
|
|
|
if TYPE_CHECKING:
|
|
from .elements._base import BaseElement
|
|
|
|
|
|
class Classes(NotifyList):
|
|
"""A list of classes applied to an element (used in css and js)"""
|
|
|
|
def __init__(self, classes=None, callback=None, element=None):
|
|
if isinstance(classes, str):
|
|
classes = classes.split()
|
|
super().__init__(classes or (), callback=callback)
|
|
|
|
def __str__(self):
|
|
return " ".join(self)
|
|
|
|
|
|
@dataclass
|
|
class StyleValue:
|
|
"""Encapsulates a single parsed style value + its importance state"""
|
|
|
|
value: TokenList
|
|
important: bool = False
|
|
|
|
def __str__(self):
|
|
return tinycss2.serialize(self.value) + ("!important" if self.important else "")
|
|
|
|
def __eq__(self, other):
|
|
return (
|
|
tinycss2.serialize(self.value) == tinycss2.serialize(other.value)
|
|
and self.important == other.important
|
|
)
|
|
|
|
def is_inherit(self):
|
|
"""Checks if the value is "inherit" """
|
|
return _is_inherit(self.value)
|
|
|
|
|
|
class Style(NotifyOrderedDict):
|
|
"""A list of style directives
|
|
|
|
.. versionchanged:: 1.2
|
|
The Style API now allows for access to parsed / processed styles via the
|
|
:func:`call` method.
|
|
|
|
.. automethod:: __call__
|
|
.. automethod:: __getitem__
|
|
.. automethod:: __setitem__
|
|
"""
|
|
|
|
color_props = ("stroke", "fill", "stop-color", "flood-color", "lighting-color")
|
|
opacity_props = ("stroke-opacity", "fill-opacity", "opacity", "stop-opacity")
|
|
unit_props = "stroke-width"
|
|
"""Dictionary of attributes with units.
|
|
|
|
..versionadded:: 1.2
|
|
"""
|
|
associated_props = {
|
|
"fill": "fill-opacity",
|
|
"stroke": "stroke-opacity",
|
|
"stop-color": "stop-opacity",
|
|
}
|
|
"""Dictionary of association between color and opacity attributes.
|
|
|
|
.. versionadded:: 1.2
|
|
"""
|
|
|
|
def __init__(self, style=None, callback=None, element=None, **kw):
|
|
self.callback = None
|
|
self.element = element
|
|
|
|
# if style is passed as kwargs, replace underscores by dashes
|
|
style = style or [(k.replace("_", "-"), v) for k, v in kw.items()]
|
|
|
|
self.update(style)
|
|
|
|
# Should accept dict, Style, parsed string, list etc.
|
|
super().__init__(callback=callback)
|
|
|
|
def _add(self, key: str, value: StyleValue):
|
|
# Works with both regular dictionaries and Styles
|
|
if key in shorthand_properties:
|
|
chg = shorthand_properties[key].converter.get_shorthand_changes(value.value) # type: ignore
|
|
for k, v in chg.items():
|
|
self._add(k, StyleValue(v, value.important))
|
|
else:
|
|
if key not in self or (
|
|
not self.get_store(key).important or value.important
|
|
):
|
|
# Only overwrite if importance of existing value is higher
|
|
super().__setitem__(key, value)
|
|
|
|
def _get_val(self, key: str, value):
|
|
if key in all_properties and not isinstance(value, str):
|
|
return StyleValue(
|
|
all_properties[key].converter.convert_back(value, self.element)
|
|
)
|
|
return StyleValue(_get_tokens_from_value(value))
|
|
|
|
def _attr_callback(self, key):
|
|
def inner(value):
|
|
self[key] = value
|
|
|
|
return inner
|
|
|
|
def _parse_str(self, style: str) -> Generator[Tuple[str, StyleValue], None, None]:
|
|
"""Create a dictionary from the value of a CSS rule (such as an inline style or
|
|
from an embedded style sheet), including its !important state, in a tokenized
|
|
form. Whitespace tokens from the start and end of the value are stripped.
|
|
|
|
Args:
|
|
style: the content of a CSS rule to parse. Can also be a List of
|
|
ComponentValues
|
|
|
|
Yields:
|
|
Tuple[str, class:`~inkex.style.StyleValue`]: the parsed attribute
|
|
"""
|
|
result = tinycss2.parse_declaration_list(
|
|
style, skip_comments=True, skip_whitespace=True
|
|
)
|
|
for declaration in result:
|
|
if isinstance(declaration, tinycss2.ast.Declaration):
|
|
yield (
|
|
declaration.name,
|
|
StyleValue(
|
|
_strip_whitespace_nodes(declaration.value),
|
|
declaration.important,
|
|
),
|
|
)
|
|
|
|
@staticmethod
|
|
def parse_str(style: str, element=None):
|
|
"""Parse a style passed as string"""
|
|
return Style(style, element=element)
|
|
|
|
def __str__(self):
|
|
"""Format an inline style attribute from a dictionary"""
|
|
return self.to_str()
|
|
|
|
def to_str(self, sep=";"):
|
|
"""Convert to string using a custom delimiter"""
|
|
return sep.join([f"{key}:{value}" for key, value in self.items()])
|
|
|
|
def __add__(self, other):
|
|
"""Add two styles together to get a third, composing them"""
|
|
ret = self.copy()
|
|
ret.update(Style(other))
|
|
return ret
|
|
|
|
def __iadd__(self, other):
|
|
"""Add style to this style, the same as ``style.update(dict)``"""
|
|
self.update(other)
|
|
return self
|
|
|
|
def __sub__(self, other):
|
|
"""Remove keys and return copy"""
|
|
ret = self.copy()
|
|
ret.__isub__(other)
|
|
return ret
|
|
|
|
def __isub__(self, other):
|
|
"""Remove keys from this style, list of keys or other style dictionary"""
|
|
for key in other:
|
|
self.pop(key, None)
|
|
return self
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def copy(self):
|
|
"""Create a copy of the style.
|
|
|
|
.. versionadded:: 1.2"""
|
|
ret = Style({}, element=self.element)
|
|
for key, value in super().items():
|
|
ret[key] = value
|
|
return ret
|
|
|
|
def update(self, other):
|
|
"""Update, while respecting ``!important`` declarations, and simplifying
|
|
shorthands"""
|
|
if isinstance(other, Style):
|
|
for k, v in super(NotifyOrderedDict, other).items():
|
|
self._add(k, v)
|
|
# Order raw dictionaries so tests can be made reliable
|
|
elif isinstance(other, dict):
|
|
for k, v in sorted(other.items()):
|
|
self._add(k, self._get_val(k, v))
|
|
|
|
elif isinstance(other, list) and all(isinstance(i, tuple) for i in other):
|
|
for k, v in other:
|
|
self._add(k, self._get_val(k, v))
|
|
|
|
elif isinstance(other, str) or (isinstance(other, list)):
|
|
for k, v in self._parse_str(other):
|
|
self._add(k, v)
|
|
|
|
def add_inherited(self, parent):
|
|
"""Creates a new Style containing all parent styles with importance "!important"
|
|
and current styles with importance "!important"
|
|
|
|
.. versionadded:: 1.2
|
|
|
|
Args:
|
|
parent: the parent style that will be merged into this one (will not be
|
|
altered)
|
|
|
|
Returns:
|
|
Style: the merged Style object
|
|
"""
|
|
ret = self.copy()
|
|
|
|
if not (isinstance(parent, Style)):
|
|
return ret
|
|
|
|
for item in parent.keys():
|
|
apply = False
|
|
if item in all_properties and all_properties[item].inherited:
|
|
# only set parent value if value is not set or parent importance is
|
|
# higher
|
|
if item not in ret or (
|
|
not self.get_importance(item) and parent.get_importance(item)
|
|
):
|
|
apply = True
|
|
if item in ret and ret.get_store(item).is_inherit():
|
|
apply = True
|
|
if apply:
|
|
super(NotifyOrderedDict, ret).__setitem__(item, parent.get_store(item))
|
|
return ret
|
|
|
|
def __setitem__(self, key, value):
|
|
"""Sets a style value.
|
|
|
|
.. versionchanged:: 1.2
|
|
``value`` can now also be non-string objects such as a Gradient.
|
|
|
|
Args:
|
|
key (str): the attribute name
|
|
value (Any):
|
|
|
|
- a :class:`StyleValue`
|
|
- a TokenList (tokenized CSS value),
|
|
- a string with the value
|
|
- any other object. The converter associated with the provided key will
|
|
attempt to create a string out of the passed value.
|
|
Raises:
|
|
ValueError: when passing something else than string, StyleValue or TokenList
|
|
and key is not a known style attribute
|
|
Error: Other exceptions may be raised when converting non-string objects."""
|
|
|
|
if isinstance(value, StyleValue):
|
|
super().__setitem__(key, value)
|
|
return
|
|
if isinstance(value, str):
|
|
value = value.strip()
|
|
tokenized = _get_tokens_from_value(value)
|
|
|
|
if key in all_properties:
|
|
all_properties[key].converter.raise_invalid_value(
|
|
tokenized, self.element
|
|
)
|
|
value = tokenized
|
|
elif (
|
|
isinstance(value, list)
|
|
and len(value) > 0
|
|
and all(isinstance(i, tinycss2.ast.Node) for i in value)
|
|
):
|
|
pass
|
|
elif key in all_properties:
|
|
value = all_properties[key].converter.convert_back(value, self.element)
|
|
else:
|
|
raise TypeError()
|
|
# Convert value to StyleValue
|
|
super().__setitem__(key, StyleValue(value, False))
|
|
|
|
def __getitem__(self, key):
|
|
"""Returns the unparsed value of the element (minus a possible ``!important``)
|
|
|
|
.. versionchanged:: 1.2
|
|
``!important`` is removed from the value.
|
|
"""
|
|
return tinycss2.serialize(super().__getitem__(key).value)
|
|
|
|
def get(self, key, default=None):
|
|
try:
|
|
return self[key]
|
|
except KeyError:
|
|
return default
|
|
|
|
def get_store(self, key):
|
|
"""Gets the :class:`~inkex.properties.BaseStyleValue` of this key, since the
|
|
other interfaces - :func:`__getitem__` and :func:`__call__` - return the
|
|
original and parsed value, respectively.
|
|
|
|
.. versionadded:: 1.2
|
|
|
|
Args:
|
|
key (str): the attribute name
|
|
|
|
Returns:
|
|
BaseStyleValue: the BaseStyleValue struct of this attribute
|
|
"""
|
|
return super().__getitem__(key)
|
|
|
|
def __call__(self, key: str, element: Optional[BaseElement] = None, default=None):
|
|
"""Return the parsed value of a style. Optionally, an element can be passed
|
|
that will be used to find gradient definitions etc.
|
|
|
|
.. versionadded:: 1.2"""
|
|
tmp = super().get(key, None)
|
|
v: None | TokenList = None if tmp is None else tmp.value
|
|
if (v is None and (key in all_properties or default is not None)) or (
|
|
v is not None and _is_inherit(v)
|
|
): # if the value is still inherit here, return the default
|
|
v = (
|
|
_get_tokens_from_value(default)
|
|
if default is not None
|
|
else (
|
|
all_properties[key].default_value if key in all_properties else None
|
|
)
|
|
)
|
|
if v is None:
|
|
return v
|
|
if v is not None:
|
|
if key in shorthand_properties:
|
|
return tinycss2.serialize(v)
|
|
if key in all_properties:
|
|
result = all_properties[key].converter.convert(
|
|
v, element or self.element
|
|
)
|
|
else:
|
|
result = tinycss2.serialize(v)
|
|
if isinstance(result, list) and not isinstance(result, Color):
|
|
result = NotifyList(result, callback=self._attr_callback(key))
|
|
return result
|
|
raise KeyError("Unknown attribute")
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, Style):
|
|
other = Style(other)
|
|
if self.keys() != other.keys():
|
|
return False
|
|
for arg in set(self) | set(other):
|
|
if self.get_store(arg) != other.get_store(arg):
|
|
return False
|
|
return True
|
|
|
|
def items(self):
|
|
"""The styles's items as string
|
|
|
|
.. versionadded:: 1.2"""
|
|
for key, value in super().items():
|
|
yield key, tinycss2.serialize(value.value)
|
|
|
|
def get_importance(self, key, default=False):
|
|
"""Returns whether the declaration with ``key`` is marked as ``!important``
|
|
|
|
.. versionadded:: 1.2"""
|
|
if key in self:
|
|
return self.get_store(key).important
|
|
return default
|
|
|
|
def set_importance(self, key, importance):
|
|
"""Sets the ``!important`` state of a declaration with key ``key``
|
|
|
|
.. versionadded:: 1.2"""
|
|
if key in self:
|
|
super().__getitem__(key).important = importance
|
|
else:
|
|
raise KeyError()
|
|
self._callback()
|
|
|
|
def get_color(self, name="fill"):
|
|
"""Get the color AND opacity as one Color object"""
|
|
color = Color(self.get(name, "none"))
|
|
color.alpha = float(self.get(name + "-opacity", 1.0))
|
|
return color
|
|
|
|
def set_color(self, color, name="fill"):
|
|
"""Sets the given color AND opacity as rgba to the fill or stroke style
|
|
properties."""
|
|
color = Color(color)
|
|
if color.alpha is not None and name in Style.associated_props:
|
|
self[Style.associated_props[name]] = color.alpha
|
|
color.alpha = None
|
|
self[name] = color
|
|
|
|
def update_urls(self, old_id, new_id):
|
|
"""Find urls in this style and replace them with the new id"""
|
|
for _, elem in super().items():
|
|
for token in elem.value:
|
|
if (
|
|
isinstance(token, tinycss2.ast.URLToken)
|
|
and token.value == f"#{old_id}"
|
|
):
|
|
token.value = f"#{new_id}"
|
|
token.representation = f"url(#{new_id})"
|
|
self._callback()
|
|
|
|
def interpolate(self, other, fraction):
|
|
# type: (Style, Style, float) -> Style
|
|
"""Interpolate all properties.
|
|
|
|
.. versionadded:: 1.1"""
|
|
from .tween import StyleInterpolator
|
|
from inkex.elements import PathElement
|
|
|
|
if self.element is None:
|
|
self.element = PathElement(style=str(self))
|
|
if other.element is None:
|
|
other.element = PathElement(style=str(other))
|
|
return StyleInterpolator(self.element, other.element).interpolate(fraction)
|
|
|
|
@classmethod
|
|
def cascaded_style(cls, element: BaseElement):
|
|
"""Returns the cascaded style of an element (all rules that apply the element
|
|
itself), based on the stylesheets, the presentation attributes and the inline
|
|
style using the respective specificity of the style
|
|
|
|
see https://www.w3.org/TR/CSS22/cascade.html#cascading-order
|
|
|
|
.. versionadded:: 1.2
|
|
|
|
Args:
|
|
element (BaseElement): the element that the cascaded style will be
|
|
computed for
|
|
|
|
Returns:
|
|
Style: the cascaded style
|
|
"""
|
|
try:
|
|
styles = list(element.root.stylesheets.lookup_specificity(element))
|
|
except FragmentError:
|
|
styles = []
|
|
|
|
# presentation attributes have specificity 0,
|
|
# see https://www.w3.org/TR/SVG/styling.html#PresentationAttributes
|
|
styles.append([element.presentation_style(), (0, 0, 0)])
|
|
|
|
# would be (1, 0, 0, 0), but then we'd have to extend every entry
|
|
styles.append([element.style, (float("inf"), 0, 0)])
|
|
|
|
# sort styles by specificity (ascending, so when overwriting it's correct)
|
|
styles = sorted(styles, key=lambda item: item[1])
|
|
|
|
result = styles[0][0].copy()
|
|
for style, _ in styles[1:]:
|
|
result.update(style)
|
|
result.element = element
|
|
return result
|
|
|
|
@classmethod
|
|
def specified_style(cls, element):
|
|
"""Returns the specified style of an element, i.e. the cascaded style +
|
|
inheritance, see https://www.w3.org/TR/CSS22/cascade.html#specified-value
|
|
|
|
.. versionadded:: 1.2
|
|
|
|
Args:
|
|
element (BaseElement): the element that the specified style will be computed
|
|
for
|
|
|
|
Returns:
|
|
Style: the specified style
|
|
"""
|
|
|
|
# We currently dont treat the case where parent=absolute value and
|
|
# element=relative value, i.e. specified = relative * absolute.
|
|
cascaded = Style.cascaded_style(element)
|
|
|
|
parent = element.getparent()
|
|
|
|
if parent is not None and isinstance(parent, _base.BaseElement):
|
|
cascaded = Style.add_inherited(cascaded, parent.specified_style())
|
|
cascaded.element = element
|
|
return cascaded # doesn't have a parent
|
|
|
|
@classmethod
|
|
def _get_cascade(cls, attribute: str, element: BaseElement) -> Optional[TokenList]:
|
|
if attribute in shorthand_from_value:
|
|
|
|
def relevant(style):
|
|
return attribute in style or shorthand_from_value[attribute] in style
|
|
|
|
else:
|
|
|
|
def relevant(style):
|
|
return attribute in style
|
|
|
|
try:
|
|
values = []
|
|
for sheet in element.root.stylesheets:
|
|
for style in sheet:
|
|
if relevant(style):
|
|
value = style.get_store(attribute)
|
|
values += [
|
|
(value, spec) for spec in style.get_specificities(element)
|
|
]
|
|
except FragmentError:
|
|
values = []
|
|
|
|
# presentation attributes have specificity 0,
|
|
# see https://www.w3.org/TR/SVG/styling.html#PresentationAttributes
|
|
# they also cannot be shorthands and are always important=False
|
|
if attribute in element.attrib:
|
|
values.append(
|
|
(
|
|
StyleValue(
|
|
_get_tokens_from_value(element.attrib[attribute]),
|
|
False,
|
|
),
|
|
(0, 0, 0),
|
|
)
|
|
)
|
|
|
|
if relevant(element.style):
|
|
values.append((element.style.get_store(attribute), (float("inf"), 0, 0)))
|
|
if len(values) == 0:
|
|
return None
|
|
# Sort according to importance, then specificity
|
|
values.sort(key=lambda item: (item[0].important, item[1]))
|
|
return values[-1][0].value
|
|
|
|
@classmethod
|
|
def _get_style(cls, attribute: str, element: BaseElement):
|
|
"""Specified style for :param:`attribute`"""
|
|
# The resolution order is:
|
|
# - cascade -> then resolve the value, except if the value is "inherit"
|
|
# - parent's computed value
|
|
# - initial (default) value -> then resolve
|
|
|
|
result = None
|
|
current = element
|
|
inherited = (
|
|
all_properties[attribute].inherited
|
|
if attribute in all_properties
|
|
else False
|
|
)
|
|
while True:
|
|
result = cls._get_cascade(attribute, current)
|
|
if result is not None and not _is_inherit(result):
|
|
break
|
|
current = current.getparent()
|
|
if current is None or (not inherited and not _is_inherit(result)):
|
|
break
|
|
# Compute value based on current
|
|
if result is None or _is_inherit(result): # Fallback to default value
|
|
if attribute in all_properties:
|
|
result = all_properties[attribute].default_value
|
|
else:
|
|
return None
|
|
return (
|
|
all_properties[attribute].converter.convert(result, current)
|
|
if attribute in all_properties
|
|
else tinycss2.serialize(result)
|
|
)
|
|
|
|
|
|
class StyleSheets(list):
|
|
"""
|
|
Special mechanism which contains all the stylesheets for an svg document
|
|
while also caching lookups for specific elements.
|
|
|
|
This caching is needed because data can't be attached to elements as they are
|
|
re-created on the fly by lxml so lookups have to be centralised.
|
|
"""
|
|
|
|
def lookup(self, element):
|
|
"""
|
|
Find all styles for this element.
|
|
"""
|
|
for sheet in self:
|
|
for style in sheet.lookup(element):
|
|
yield style
|
|
|
|
def lookup_specificity(self, element):
|
|
"""
|
|
Find all styles for this element and return the specificity of the match.
|
|
|
|
.. versionadded:: 1.2
|
|
"""
|
|
for sheet in self:
|
|
for style in sheet.lookup_specificity(element):
|
|
yield style
|
|
|
|
|
|
class StyleSheet(list):
|
|
"""
|
|
A style sheet, usually the CDATA contents of a style tag, but also
|
|
a css file used with a css. Will yield multiple Style() classes.
|
|
"""
|
|
|
|
def __init__(self, content=None, callback=None):
|
|
super().__init__()
|
|
self.callback = None
|
|
# Remove comments
|
|
if content is None:
|
|
parsed = []
|
|
else:
|
|
parsed = tinycss2.parse_stylesheet(
|
|
content, skip_comments=True, skip_whitespace=True
|
|
)
|
|
# Parse rules
|
|
for block in parsed:
|
|
if isinstance(block, tinycss2.ast.QualifiedRule):
|
|
self.append(block)
|
|
self.callback = callback
|
|
|
|
def __str__(self):
|
|
return "\n" + "\n".join([str(style) for style in self]) + "\n"
|
|
|
|
def _callback(self, style=None): # pylint: disable=unused-argument
|
|
if self.callback is not None:
|
|
self.callback(self)
|
|
|
|
def add(self, rule, style):
|
|
"""Append a rule and style combo to this stylesheet"""
|
|
self.append(
|
|
ConditionalStyle(rules=rule, style=str(style), callback=self._callback)
|
|
)
|
|
|
|
def append(self, other: str | tinycss2.ast.QualifiedRule):
|
|
"""Make sure callback is called when updating"""
|
|
if isinstance(other, str):
|
|
other = tinycss2.parse_one_rule(other)
|
|
if isinstance(other, tinycss2.ast.QualifiedRule):
|
|
other = ConditionalStyle(
|
|
other.prelude, other.content, callback=self._callback
|
|
)
|
|
super().append(other)
|
|
self._callback()
|
|
|
|
def lookup(self, element):
|
|
"""Lookup the element against all the styles in this sheet"""
|
|
for style in self:
|
|
if any(style.checks(element)):
|
|
yield style
|
|
|
|
def lookup_specificity(self, element):
|
|
"""Lookup the element_id against all the styles in this sheet
|
|
and return the specificity of the match
|
|
|
|
Args:
|
|
element: the element of the element that styles are being queried for
|
|
|
|
Yields:
|
|
Tuple[ConditionalStyle, Tuple[int, int, int]]: all matched styles and the
|
|
specificity of the match
|
|
"""
|
|
for style in self:
|
|
for specificity in style.get_specificities(element):
|
|
yield (style, specificity)
|
|
|
|
|
|
class ConditionalStyle(Style):
|
|
"""
|
|
Just like a Style object, but includes one or more
|
|
conditional rules which places this style in a stylesheet
|
|
rather than being an attribute style.
|
|
"""
|
|
|
|
def __init__(
|
|
self, rules: str | TokenList = "*", style=None, callback=None, **kwargs
|
|
):
|
|
super().__init__(style=style, callback=callback, **kwargs)
|
|
self._rules: str | TokenList = rules
|
|
self.rules = list(parser.parse(rules, namespaces=NSS))
|
|
self.checks = [
|
|
CSSCompiler.compile_node(selector.parsed_tree) for selector in self.rules
|
|
]
|
|
|
|
def matches(self, element: etree.Element):
|
|
"""Checks if an individual element matches this selector.
|
|
|
|
.. versionadded:: 1.4"""
|
|
if isinstance(element, etree._Comment):
|
|
return False
|
|
if any(check(element) for check in self.checks):
|
|
return True
|
|
return False
|
|
|
|
def all_matches(self, document: etree.Element):
|
|
"""Get all matches of this selector in document as iterator.
|
|
|
|
.. versionadded:: 1.4"""
|
|
for el in document.iter():
|
|
if self.matches(el):
|
|
yield el
|
|
|
|
def __str__(self):
|
|
"""Return this style as a css entry with class"""
|
|
content = self.to_str(";\n ")
|
|
rules = ",\n".join(str(rule) for rule in self.rules)
|
|
if content:
|
|
return f"{rules} {{\n {content};\n}}"
|
|
return f"{rules} {{}}"
|
|
|
|
def get_specificities(self, element: Optional[BaseElement] = None):
|
|
"""Gets an iterator of the specificity of all rules in this ConditionalStyle
|
|
|
|
.. versionadded:: 1.2"""
|
|
if element is not None:
|
|
for rule, check in zip(self.rules, self.checks):
|
|
if check(element):
|
|
yield rule.specificity
|
|
else:
|
|
for rule in self.rules:
|
|
yield rule.specificity
|