initial commit
This commit is contained in:
3
deps/inkex/css/__init__.py
vendored
Normal file
3
deps/inkex/css/__init__.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
"""CSS Processing module"""
|
||||
|
||||
from .compiler import CSSCompiler
|
||||
483
deps/inkex/css/compiler.py
vendored
Normal file
483
deps/inkex/css/compiler.py
vendored
Normal file
@@ -0,0 +1,483 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright (C) 2023 - 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.
|
||||
#
|
||||
|
||||
"""CSS evaluation logic, forked from cssselect2 (rewritten without eval, targeted to
|
||||
our data structure). CSS selectors are compiled into boolean evaluator functions.
|
||||
All HTML-specific code has been removed, and we don't duplicate the tree data structure
|
||||
but work on the normal tree."""
|
||||
|
||||
import re
|
||||
from lxml import etree
|
||||
from typing import Union, List
|
||||
from tinycss2.nth import parse_nth
|
||||
|
||||
from . import parser
|
||||
from .parser import SelectorError
|
||||
|
||||
# http://dev.w3.org/csswg/selectors/#whitespace
|
||||
split_whitespace = re.compile("[^ \t\r\n\f]+").findall
|
||||
|
||||
|
||||
def ascii_lower(string): # from webencodings
|
||||
r"""Transform (only) ASCII letters to lower case: A-Z is mapped to a-z."""
|
||||
return string.encode("utf8").lower().decode("utf8")
|
||||
|
||||
|
||||
# pylint: disable=protected-access,comparison-with-callable,invalid-name,bad-super-call
|
||||
# pylint: disable=unnecessary-lambda-assignment
|
||||
|
||||
|
||||
## Iterators without comments.
|
||||
def iterancestors(element):
|
||||
"""Iterate over ancestors but ignore comments."""
|
||||
for e in element.iterancestors():
|
||||
if isinstance(e, etree._Comment):
|
||||
continue
|
||||
yield e
|
||||
|
||||
|
||||
def iterdescendants(element):
|
||||
"""Iterate over descendants but ignore comments"""
|
||||
for e in element.iterdescendants():
|
||||
if isinstance(e, etree._Comment):
|
||||
continue
|
||||
yield e
|
||||
|
||||
|
||||
def itersiblings(element, preceding=False):
|
||||
"""Iterate over descendants but ignore comments"""
|
||||
for e in element.itersiblings(preceding=preceding):
|
||||
if isinstance(e, etree._Comment):
|
||||
continue
|
||||
yield e
|
||||
|
||||
|
||||
def iterchildren(element):
|
||||
"""Iterate over children but ignore comments"""
|
||||
for e in element.iterchildren():
|
||||
if isinstance(e, etree._Comment):
|
||||
continue
|
||||
yield e
|
||||
|
||||
|
||||
def getprevious(element):
|
||||
"""Get the previous non-comment element"""
|
||||
for e in itersiblings(element, preceding=True):
|
||||
return e
|
||||
return None
|
||||
|
||||
|
||||
def getnext(element):
|
||||
"""Get the next non-comment element"""
|
||||
for e in itersiblings(element, preceding=False):
|
||||
return e
|
||||
return None
|
||||
|
||||
|
||||
def FALSE(_el):
|
||||
"""Always returns 0"""
|
||||
return 0
|
||||
|
||||
|
||||
def TRUE(_el):
|
||||
"""Always returns 1"""
|
||||
return 1
|
||||
|
||||
|
||||
class BooleanCompiler:
|
||||
def __init__(self) -> None:
|
||||
self._func_map = {
|
||||
parser.CombinedSelector: self._compile_combined,
|
||||
parser.CompoundSelector: self._compile_compound,
|
||||
parser.NegationSelector: self._compile_negation,
|
||||
parser.RelationalSelector: self._compile_relational,
|
||||
parser.MatchesAnySelector: self._compile_any,
|
||||
parser.SpecificityAdjustmentSelector: self._compile_any,
|
||||
parser.LocalNameSelector: self._compile_local_name,
|
||||
parser.NamespaceSelector: self._compile_namespace,
|
||||
parser.ClassSelector: self._compile_class,
|
||||
parser.IDSelector: self._compile_id,
|
||||
parser.AttributeSelector: self._compile_attribute,
|
||||
parser.PseudoClassSelector: self._compile_pseudoclass,
|
||||
parser.FunctionalPseudoClassSelector: self._compile_functional_pseudoclass,
|
||||
}
|
||||
|
||||
def _compile_combined(self, selector: parser.CombinedSelector):
|
||||
left_inside = self.compile_node(selector.left)
|
||||
if left_inside == FALSE:
|
||||
return FALSE # 0 and x == 0
|
||||
if left_inside == TRUE:
|
||||
# 1 and x == x, but the element matching 1 still needs to exist.
|
||||
if selector.combinator in (" ", ">"):
|
||||
left = lambda el: el.getparent() is not None
|
||||
|
||||
elif selector.combinator in ("~", "+"):
|
||||
left = lambda el: getprevious(el) is not None
|
||||
|
||||
else:
|
||||
raise SelectorError("Unknown combinator", selector.combinator)
|
||||
elif selector.combinator == " ":
|
||||
left = lambda el: any((left_inside(e)) for e in el.ancestors())
|
||||
|
||||
elif selector.combinator == ">":
|
||||
left = lambda el: el.getparent() is not None and left_inside(el.getparent())
|
||||
|
||||
elif selector.combinator == "+":
|
||||
left = lambda el: getprevious(el) is not None and left_inside(
|
||||
getprevious(el)
|
||||
)
|
||||
|
||||
elif selector.combinator == "~":
|
||||
left = lambda el: any(
|
||||
(left_inside(e)) for e in itersiblings(el, preceding=True)
|
||||
)
|
||||
|
||||
else:
|
||||
raise SelectorError("Unknown combinator", selector.combinator)
|
||||
|
||||
right = self.compile_node(selector.right)
|
||||
if right == FALSE:
|
||||
return FALSE # 0 and x == 0
|
||||
if right == TRUE:
|
||||
return left # 1 and x == x
|
||||
# Evaluate combinators right to left
|
||||
return lambda el: right(el) and left(el)
|
||||
|
||||
def _compile_compound(self, selector: parser.CompoundSelector):
|
||||
sub_expressions = [
|
||||
expr
|
||||
for expr in map(self.compile_node, selector.simple_selectors)
|
||||
if expr != TRUE
|
||||
]
|
||||
if len(sub_expressions) == 1:
|
||||
return sub_expressions[0]
|
||||
if FALSE in sub_expressions:
|
||||
return FALSE
|
||||
if sub_expressions:
|
||||
return lambda e: all(expr(e) for expr in sub_expressions)
|
||||
return TRUE # all([]) == True
|
||||
|
||||
def _compile_negation(self, selector: parser.NegationSelector):
|
||||
sub_expressions = [
|
||||
expr
|
||||
for expr in [
|
||||
self.compile_node(selector.parsed_tree)
|
||||
for selector in selector.selector_list
|
||||
]
|
||||
if expr != TRUE
|
||||
]
|
||||
if not sub_expressions:
|
||||
return FALSE
|
||||
return lambda el: not any(expr(el) for expr in sub_expressions)
|
||||
|
||||
@staticmethod
|
||||
def _get_subexpr(expression, relative_selector):
|
||||
"""Helper function for RelationalSelector"""
|
||||
if relative_selector.combinator == " ":
|
||||
return lambda el: any(expression(e) for e in iterdescendants(el))
|
||||
if relative_selector.combinator == ">":
|
||||
return lambda el: any(expression(e) for e in iterchildren(el))
|
||||
if relative_selector.combinator == "+":
|
||||
return lambda el: expression(next(itersiblings(el)))
|
||||
if relative_selector.combinator == "~":
|
||||
return lambda el: any(expression(e) for e in itersiblings(el))
|
||||
raise SelectorError(
|
||||
f"Unknown relational selector '{relative_selector.combinator}'"
|
||||
)
|
||||
|
||||
def _compile_relational(self, selector: parser.RelationalSelector):
|
||||
sub_expr = []
|
||||
|
||||
for relative_selector in selector.selector_list:
|
||||
expression = self.compile_node(relative_selector.selector.parsed_tree)
|
||||
if expression == FALSE:
|
||||
continue
|
||||
sub_expr.append(self._get_subexpr(expression, relative_selector))
|
||||
return lambda el: any(expr(el) for expr in sub_expr)
|
||||
|
||||
def _compile_any(
|
||||
self,
|
||||
selector: Union[
|
||||
parser.MatchesAnySelector, parser.SpecificityAdjustmentSelector
|
||||
],
|
||||
):
|
||||
sub_expressions = [
|
||||
expr
|
||||
for expr in [
|
||||
self.compile_node(selector.parsed_tree)
|
||||
for selector in selector.selector_list
|
||||
]
|
||||
if expr != FALSE
|
||||
]
|
||||
if not sub_expressions:
|
||||
return FALSE
|
||||
return lambda el: any(expr(el) for expr in sub_expressions)
|
||||
|
||||
def _compile_local_name(self, selector: parser.LocalNameSelector):
|
||||
return lambda el: el.TAG == selector.local_name
|
||||
|
||||
def _compile_namespace(self, selector: parser.NamespaceSelector):
|
||||
return lambda el: el.NAMESPACE == selector.namespace
|
||||
|
||||
def _compile_class(self, selector: parser.ClassSelector):
|
||||
return lambda el: selector.class_name in el.classes
|
||||
|
||||
def _compile_id(self, selector: parser.IDSelector):
|
||||
return lambda el: super(etree.ElementBase, el).get("id", None) == selector.ident # type: ignore
|
||||
|
||||
def _compile_attribute(self, selector: parser.AttributeSelector):
|
||||
if selector.namespace is not None:
|
||||
if selector.namespace:
|
||||
key_func = lambda el: (
|
||||
f"{{{selector.namespace}}}{selector.name}"
|
||||
if el.NAMESPACE != selector.namespace
|
||||
else selector.name
|
||||
)
|
||||
|
||||
else:
|
||||
key_func = lambda el: selector.name
|
||||
|
||||
value = selector.value
|
||||
if selector.case_sensitive is False:
|
||||
value = value.lower()
|
||||
|
||||
attribute_value = (
|
||||
lambda el: super(etree.ElementBase, el)
|
||||
.get(key_func(el), "") # type: ignore
|
||||
.lower()
|
||||
)
|
||||
|
||||
else:
|
||||
attribute_value = lambda el: super(etree.ElementBase, el).get( # type: ignore
|
||||
key_func(el), ""
|
||||
)
|
||||
|
||||
if selector.operator is None:
|
||||
return lambda el: key_func(el) in el.attrib
|
||||
if selector.operator == "=":
|
||||
return lambda el: (
|
||||
key_func(el) in el.attrib and attribute_value(el) == value
|
||||
)
|
||||
if selector.operator == "~=":
|
||||
return (
|
||||
FALSE
|
||||
if len(value.split()) != 1 or value.strip() != value
|
||||
else lambda el: value in split_whitespace(attribute_value(el))
|
||||
)
|
||||
if selector.operator == "|=":
|
||||
return lambda el: (
|
||||
key_func(el) in el.attrib
|
||||
and (
|
||||
attribute_value(el) == value
|
||||
or attribute_value(el).startswith(value + "-")
|
||||
)
|
||||
)
|
||||
if selector.operator == "^=":
|
||||
if value:
|
||||
return lambda el: attribute_value(el).startswith(value)
|
||||
return FALSE
|
||||
if selector.operator == "$=":
|
||||
return (
|
||||
(lambda el: attribute_value(el).endswith(value)) if value else FALSE
|
||||
)
|
||||
if selector.operator == "*=":
|
||||
return (lambda el: value in attribute_value(el)) if value else FALSE
|
||||
raise SelectorError("Unknown attribute operator", selector.operator)
|
||||
# In any namespace
|
||||
raise NotImplementedError # TODO
|
||||
|
||||
def _compile_pseudoclass(self, selector: parser.PseudoClassSelector):
|
||||
if selector.name in ("link", "any-link", "local-link"):
|
||||
|
||||
def ancestors_or_self(el):
|
||||
yield el
|
||||
yield from iterancestors(el)
|
||||
|
||||
return lambda el: any(
|
||||
e.TAG == "a" and super(etree.ElementBase, e).get("href", "") != "" # type: ignore
|
||||
for e in ancestors_or_self(el)
|
||||
)
|
||||
if selector.name in (
|
||||
"visited",
|
||||
"hover",
|
||||
"active",
|
||||
"focus",
|
||||
"focus-within",
|
||||
"focus-visible",
|
||||
"target",
|
||||
"target-within",
|
||||
"current",
|
||||
"past",
|
||||
"future",
|
||||
"playing",
|
||||
"paused",
|
||||
"seeking",
|
||||
"buffering",
|
||||
"stalled",
|
||||
"muted",
|
||||
"volume-locked",
|
||||
"user-valid",
|
||||
"user-invalid",
|
||||
):
|
||||
# Not applicable in a static context: never match.
|
||||
return FALSE
|
||||
if selector.name in ("enabled", "disabled", "checked"):
|
||||
# Not applicable to SVG
|
||||
return FALSE
|
||||
if selector.name in ("root", "scope"):
|
||||
return lambda el: el.getparent() is None
|
||||
if selector.name == "first-child":
|
||||
return lambda el: getprevious(el) is None
|
||||
if selector.name == "last-child":
|
||||
return lambda el: getnext(el) is None
|
||||
if selector.name == "first-of-type":
|
||||
return lambda el: all(
|
||||
s.tag != el.tag for s in itersiblings(el, preceding=True)
|
||||
)
|
||||
if selector.name == "last-of-type":
|
||||
return lambda el: all(s.tag != el.tag for s in itersiblings(el))
|
||||
if selector.name == "only-child":
|
||||
return lambda el: getnext(el) is None and getprevious(el) is None
|
||||
if selector.name == "only-of-type":
|
||||
return lambda el: all(s.tag != el.tag for s in itersiblings(el)) and all(
|
||||
s.tag != el.tag for s in itersiblings(el, preceding=True)
|
||||
)
|
||||
if selector.name == "empty":
|
||||
return lambda el: not list(el) and el.text is None
|
||||
raise SelectorError("Unknown pseudo-class", selector.name)
|
||||
|
||||
def _compile_lang(self, selector: parser.FunctionalPseudoClassSelector):
|
||||
langs = []
|
||||
tokens = [
|
||||
token
|
||||
for token in selector.arguments
|
||||
if token.type not in ("whitespace", "comment")
|
||||
]
|
||||
while tokens:
|
||||
token = tokens.pop(0)
|
||||
if token.type == "ident":
|
||||
langs.append(token.lower_value)
|
||||
elif token.type == "string":
|
||||
langs.append(ascii_lower(token.value))
|
||||
else:
|
||||
raise SelectorError("Invalid arguments for :lang()")
|
||||
if tokens:
|
||||
token = tokens.pop(0)
|
||||
if token.type != "ident" and token.value != ",":
|
||||
raise SelectorError("Invalid arguments for :lang()")
|
||||
|
||||
def haslang(el, lang):
|
||||
print(
|
||||
el.get("lang"),
|
||||
lang,
|
||||
el.get("lang", "") == lang or el.get("lang", "").startswith(lang + "-"),
|
||||
)
|
||||
return el.get("lang", "").lower() == lang or el.get(
|
||||
"lang", ""
|
||||
).lower().startswith(lang + "-")
|
||||
|
||||
return lambda el: any(
|
||||
haslang(el, lang) or any(haslang(el2, lang) for el2 in iterancestors(el))
|
||||
for lang in langs
|
||||
)
|
||||
|
||||
def _compile_functional_pseudoclass(
|
||||
self, selector: parser.FunctionalPseudoClassSelector
|
||||
):
|
||||
if selector.name == "lang":
|
||||
return self._compile_lang(selector)
|
||||
nth: List[str] = []
|
||||
selector_list: List[str] = []
|
||||
current_list = nth
|
||||
for argument in selector.arguments:
|
||||
if argument.type == "ident" and argument.value == "of":
|
||||
if current_list is nth:
|
||||
current_list = selector_list
|
||||
continue
|
||||
current_list.append(argument)
|
||||
|
||||
if selector_list:
|
||||
compiled = tuple(
|
||||
self.compile_node(selector.parsed_tree)
|
||||
for selector in parser.parse(selector_list)
|
||||
)
|
||||
test = lambda el: all(expr(el) for expr in compiled)
|
||||
|
||||
else:
|
||||
test = TRUE
|
||||
|
||||
if selector.name == "nth-child":
|
||||
count = lambda el: sum(
|
||||
1 for e in itersiblings(el, preceding=True) if test(e)
|
||||
)
|
||||
|
||||
elif selector.name == "nth-last-child":
|
||||
count = lambda el: sum(1 for e in itersiblings(el) if test(e))
|
||||
elif selector.name == "nth-of-type":
|
||||
count = lambda el: sum(
|
||||
1
|
||||
for s in (e for e in itersiblings(el, preceding=True) if test(e))
|
||||
if s.tag == el.tag
|
||||
)
|
||||
|
||||
elif selector.name == "nth-last-of-type":
|
||||
count = lambda el: sum(
|
||||
1 for s in (e for e in itersiblings(el) if test(e)) if s.tag == el.tag
|
||||
)
|
||||
|
||||
else:
|
||||
raise SelectorError("Unknown pseudo-class", selector.name)
|
||||
|
||||
count_func = lambda el: count(el) if test(el) else float("nan")
|
||||
|
||||
result = parse_nth(nth)
|
||||
if result is None:
|
||||
raise SelectorError(f"Invalid arguments for :{selector.name}()")
|
||||
a, b = result
|
||||
# x is the number of siblings before/after the element
|
||||
# Matches if a positive or zero integer n exists so that:
|
||||
# x = a*n + b-1
|
||||
# x = a*n + B
|
||||
B = b - 1
|
||||
if a == 0:
|
||||
# x = B
|
||||
return lambda el: count_func(el) == B
|
||||
|
||||
# n = (x - B) / a
|
||||
def evaluator(el):
|
||||
n, r = divmod(count_func(el) - B, a)
|
||||
return r == 0 and n >= 0
|
||||
|
||||
return evaluator
|
||||
|
||||
def compile_node(self, selector):
|
||||
"""Return a boolean expression, as a callable.
|
||||
|
||||
When evaluated in a context where the `el` variable is an
|
||||
:class:`cssselect2.tree.Element` object, tells whether the element is a
|
||||
subject of `selector`.
|
||||
|
||||
"""
|
||||
try:
|
||||
return self._func_map[selector.__class__](selector)
|
||||
except KeyError as e:
|
||||
raise TypeError(type(selector), selector) from e
|
||||
|
||||
|
||||
CSSCompiler = BooleanCompiler()
|
||||
548
deps/inkex/css/parser.py
vendored
Normal file
548
deps/inkex/css/parser.py
vendored
Normal file
@@ -0,0 +1,548 @@
|
||||
# Forked from cssselect2, 1.2.1, BSD License
|
||||
|
||||
"""Parse CSS declarations."""
|
||||
|
||||
from tinycss2 import parse_component_value_list
|
||||
|
||||
__all__ = ["parse"]
|
||||
|
||||
SUPPORTED_PSEUDO_ELEMENTS = {
|
||||
# As per CSS Pseudo-Elements Module Level 4
|
||||
"first-line",
|
||||
"first-letter",
|
||||
"prefix",
|
||||
"postfix",
|
||||
"selection",
|
||||
"target-text",
|
||||
"spelling-error",
|
||||
"grammar-error",
|
||||
"before",
|
||||
"after",
|
||||
"marker",
|
||||
"placeholder",
|
||||
"file-selector-button",
|
||||
# As per CSS Generated Content for Paged Media Module
|
||||
"footnote-call",
|
||||
"footnote-marker",
|
||||
# As per CSS Scoping Module Level 1
|
||||
"content",
|
||||
"shadow",
|
||||
}
|
||||
|
||||
|
||||
def parse(input, namespaces=None, forgiving=False, relative=False):
|
||||
"""Yield tinycss2 selectors found in given ``input``.
|
||||
|
||||
:param input:
|
||||
A string, or an iterable of tinycss2 component values.
|
||||
|
||||
"""
|
||||
if isinstance(input, str):
|
||||
input = parse_component_value_list(input)
|
||||
tokens = TokenStream(input)
|
||||
namespaces = namespaces or {}
|
||||
try:
|
||||
yield parse_selector(tokens, namespaces, relative)
|
||||
except SelectorError as exception:
|
||||
if forgiving:
|
||||
return
|
||||
raise exception
|
||||
while 1:
|
||||
next = tokens.next()
|
||||
if next is None:
|
||||
return
|
||||
elif next == ",":
|
||||
try:
|
||||
yield parse_selector(tokens, namespaces, relative)
|
||||
except SelectorError as exception:
|
||||
if not forgiving:
|
||||
raise exception
|
||||
else:
|
||||
if not forgiving:
|
||||
raise SelectorError(next, f"unexpected {next.type} token.")
|
||||
|
||||
|
||||
def parse_selector(tokens, namespaces, relative=False):
|
||||
tokens.skip_whitespace_and_comment()
|
||||
if relative:
|
||||
peek = tokens.peek()
|
||||
if peek in (">", "+", "~"):
|
||||
initial_combinator = peek.value
|
||||
tokens.next()
|
||||
else:
|
||||
initial_combinator = " "
|
||||
tokens.skip_whitespace_and_comment()
|
||||
result, pseudo_element = parse_compound_selector(tokens, namespaces)
|
||||
while 1:
|
||||
has_whitespace = tokens.skip_whitespace()
|
||||
while tokens.skip_comment():
|
||||
has_whitespace = tokens.skip_whitespace() or has_whitespace
|
||||
selector = Selector(result, pseudo_element)
|
||||
if relative:
|
||||
selector = RelativeSelector(initial_combinator, selector)
|
||||
if pseudo_element is not None:
|
||||
return selector
|
||||
peek = tokens.peek()
|
||||
if peek is None or peek == ",":
|
||||
return selector
|
||||
elif peek in (">", "+", "~"):
|
||||
combinator = peek.value
|
||||
tokens.next()
|
||||
elif has_whitespace:
|
||||
combinator = " "
|
||||
else:
|
||||
return selector
|
||||
compound, pseudo_element = parse_compound_selector(tokens, namespaces)
|
||||
result = CombinedSelector(result, combinator, compound)
|
||||
|
||||
|
||||
def parse_compound_selector(tokens, namespaces):
|
||||
type_selectors = parse_type_selector(tokens, namespaces)
|
||||
simple_selectors = type_selectors if type_selectors is not None else []
|
||||
while 1:
|
||||
simple_selector, pseudo_element = parse_simple_selector(tokens, namespaces)
|
||||
if pseudo_element is not None or simple_selector is None:
|
||||
break
|
||||
simple_selectors.append(simple_selector)
|
||||
|
||||
if simple_selectors or (type_selectors, pseudo_element) != (None, None):
|
||||
return CompoundSelector(simple_selectors), pseudo_element
|
||||
|
||||
peek = tokens.peek()
|
||||
peek_type = peek.type if peek else "EOF"
|
||||
raise SelectorError(peek, f"expected a compound selector, got {peek_type}")
|
||||
|
||||
|
||||
def parse_type_selector(tokens, namespaces):
|
||||
tokens.skip_whitespace()
|
||||
qualified_name = parse_qualified_name(tokens, namespaces)
|
||||
if qualified_name is None:
|
||||
return None
|
||||
|
||||
simple_selectors = []
|
||||
namespace, local_name = qualified_name
|
||||
if local_name is not None:
|
||||
simple_selectors.append(LocalNameSelector(local_name))
|
||||
if namespace is not None:
|
||||
simple_selectors.append(NamespaceSelector(namespace))
|
||||
return simple_selectors
|
||||
|
||||
|
||||
def parse_simple_selector(tokens, namespaces):
|
||||
peek = tokens.peek()
|
||||
if peek is None:
|
||||
return None, None
|
||||
if peek.type == "hash" and peek.is_identifier:
|
||||
tokens.next()
|
||||
return IDSelector(peek.value), None
|
||||
elif peek == ".":
|
||||
tokens.next()
|
||||
next = tokens.next()
|
||||
if next is None or next.type != "ident":
|
||||
raise SelectorError(next, f"Expected a class name, got {next}")
|
||||
return ClassSelector(next.value), None
|
||||
elif peek.type == "[] block":
|
||||
tokens.next()
|
||||
attr = parse_attribute_selector(TokenStream(peek.content), namespaces)
|
||||
return attr, None
|
||||
elif peek == ":":
|
||||
tokens.next()
|
||||
next = tokens.next()
|
||||
if next == ":":
|
||||
next = tokens.next()
|
||||
if next is None or next.type != "ident":
|
||||
raise SelectorError(next, f"Expected a pseudo-element name, got {next}")
|
||||
value = next.lower_value
|
||||
if value not in SUPPORTED_PSEUDO_ELEMENTS:
|
||||
raise SelectorError(
|
||||
next, f"Expected a supported pseudo-element, got {value}"
|
||||
)
|
||||
return None, value
|
||||
elif next is not None and next.type == "ident":
|
||||
name = next.lower_value
|
||||
if name in ("before", "after", "first-line", "first-letter"):
|
||||
return None, name
|
||||
else:
|
||||
return PseudoClassSelector(name), None
|
||||
elif next is not None and next.type == "function":
|
||||
name = next.lower_name
|
||||
if name in ("is", "where", "not", "has"):
|
||||
return parse_logical_combination(next, namespaces, name), None
|
||||
else:
|
||||
return (FunctionalPseudoClassSelector(name, next.arguments), None)
|
||||
else:
|
||||
raise SelectorError(next, f"unexpected {next} token.")
|
||||
else:
|
||||
return None, None
|
||||
|
||||
|
||||
def parse_logical_combination(matches_any_token, namespaces, name):
|
||||
forgiving = True
|
||||
relative = False
|
||||
if name == "is":
|
||||
selector_class = MatchesAnySelector
|
||||
elif name == "where":
|
||||
selector_class = SpecificityAdjustmentSelector
|
||||
elif name == "not":
|
||||
forgiving = False
|
||||
selector_class = NegationSelector
|
||||
elif name == "has":
|
||||
relative = True
|
||||
selector_class = RelationalSelector
|
||||
|
||||
selectors = [
|
||||
selector
|
||||
for selector in parse(
|
||||
matches_any_token.arguments, namespaces, forgiving, relative
|
||||
)
|
||||
if selector.pseudo_element is None
|
||||
]
|
||||
return selector_class(selectors)
|
||||
|
||||
|
||||
def parse_attribute_selector(tokens, namespaces):
|
||||
tokens.skip_whitespace()
|
||||
qualified_name = parse_qualified_name(tokens, namespaces, is_attribute=True)
|
||||
if qualified_name is None:
|
||||
next = tokens.next()
|
||||
raise SelectorError(next, f"expected attribute name, got {next}")
|
||||
namespace, local_name = qualified_name
|
||||
|
||||
tokens.skip_whitespace()
|
||||
peek = tokens.peek()
|
||||
if peek is None:
|
||||
operator = None
|
||||
value = None
|
||||
elif peek in ("=", "~=", "|=", "^=", "$=", "*="):
|
||||
operator = peek.value
|
||||
tokens.next()
|
||||
tokens.skip_whitespace()
|
||||
next = tokens.next()
|
||||
if next is None or next.type not in ("ident", "string"):
|
||||
next_type = "None" if next is None else next.type
|
||||
raise SelectorError(next, f"expected attribute value, got {next_type}")
|
||||
value = next.value
|
||||
else:
|
||||
raise SelectorError(peek, f"expected attribute selector operator, got {peek}")
|
||||
|
||||
tokens.skip_whitespace()
|
||||
next = tokens.next()
|
||||
case_sensitive = None
|
||||
if next is not None:
|
||||
if next.type == "ident" and next.value.lower() == "i":
|
||||
case_sensitive = False
|
||||
elif next.type == "ident" and next.value.lower() == "s":
|
||||
case_sensitive = True
|
||||
else:
|
||||
raise SelectorError(next, f"expected ], got {next.type}")
|
||||
return AttributeSelector(namespace, local_name, operator, value, case_sensitive)
|
||||
|
||||
|
||||
def parse_qualified_name(tokens, namespaces, is_attribute=False):
|
||||
"""Return ``(namespace, local)`` for given tokens.
|
||||
|
||||
Can also return ``None`` for a wildcard.
|
||||
|
||||
The empty string for ``namespace`` means "no namespace".
|
||||
|
||||
"""
|
||||
peek = tokens.peek()
|
||||
if peek is None:
|
||||
return None
|
||||
if peek.type == "ident":
|
||||
first_ident = tokens.next()
|
||||
peek = tokens.peek()
|
||||
if peek != "|":
|
||||
namespace = "" if is_attribute else namespaces.get(None, None)
|
||||
return namespace, (first_ident.value, first_ident.lower_value)
|
||||
tokens.next()
|
||||
namespace = namespaces.get(first_ident.value)
|
||||
if namespace is None:
|
||||
raise SelectorError(
|
||||
first_ident, f"undefined namespace prefix: {first_ident.value}"
|
||||
)
|
||||
elif peek == "*":
|
||||
next = tokens.next()
|
||||
peek = tokens.peek()
|
||||
if peek != "|":
|
||||
if is_attribute:
|
||||
raise SelectorError(next, f"expected local name, got {next.type}")
|
||||
return namespaces.get(None, None), None
|
||||
tokens.next()
|
||||
namespace = None
|
||||
elif peek == "|":
|
||||
tokens.next()
|
||||
namespace = ""
|
||||
else:
|
||||
return None
|
||||
|
||||
# If we get here, we just consumed '|' and set ``namespace``
|
||||
next = tokens.next()
|
||||
if next.type == "ident":
|
||||
return namespace, (next.value, next.lower_value)
|
||||
elif next == "*" and not is_attribute:
|
||||
return namespace, None
|
||||
else:
|
||||
raise SelectorError(next, f"expected local name, got {next.type}")
|
||||
|
||||
|
||||
class SelectorError(ValueError):
|
||||
"""A specialized ``ValueError`` for invalid selectors."""
|
||||
|
||||
|
||||
class TokenStream:
|
||||
def __init__(self, tokens):
|
||||
self.tokens = iter(tokens)
|
||||
self.peeked = [] # In reversed order
|
||||
|
||||
def next(self):
|
||||
if self.peeked:
|
||||
return self.peeked.pop()
|
||||
else:
|
||||
return next(self.tokens, None)
|
||||
|
||||
def peek(self):
|
||||
if not self.peeked:
|
||||
self.peeked.append(next(self.tokens, None))
|
||||
return self.peeked[-1]
|
||||
|
||||
def skip(self, skip_types):
|
||||
found = False
|
||||
while 1:
|
||||
peek = self.peek()
|
||||
if peek is None or peek.type not in skip_types:
|
||||
break
|
||||
self.next()
|
||||
found = True
|
||||
return found
|
||||
|
||||
def skip_whitespace(self):
|
||||
return self.skip(["whitespace"])
|
||||
|
||||
def skip_comment(self):
|
||||
return self.skip(["comment"])
|
||||
|
||||
def skip_whitespace_and_comment(self):
|
||||
return self.skip(["comment", "whitespace"])
|
||||
|
||||
|
||||
class Selector:
|
||||
def __init__(self, tree, pseudo_element=None):
|
||||
self.parsed_tree = tree
|
||||
self.pseudo_element = pseudo_element
|
||||
if pseudo_element is None:
|
||||
#: Tuple of 3 integers: http://www.w3.org/TR/selectors/#specificity
|
||||
self.specificity = tree.specificity
|
||||
else:
|
||||
a, b, c = tree.specificity
|
||||
self.specificity = a, b, c + 1
|
||||
|
||||
def __repr__(self):
|
||||
pseudo = f"::{self.pseudo_element}" if self.pseudo_element else ""
|
||||
return f"{self.parsed_tree!r}{pseudo}"
|
||||
|
||||
|
||||
class RelativeSelector:
|
||||
def __init__(self, combinator, selector):
|
||||
self.combinator = combinator
|
||||
self.selector = selector
|
||||
|
||||
@property
|
||||
def specificity(self):
|
||||
return self.selector.specificity
|
||||
|
||||
@property
|
||||
def pseudo_element(self):
|
||||
return self.selector.pseudo_element
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"{self.selector!r}"
|
||||
if self.combinator == " "
|
||||
else f"{self.combinator} {self.selector!r}"
|
||||
)
|
||||
|
||||
|
||||
class CombinedSelector:
|
||||
def __init__(self, left, combinator, right):
|
||||
#: Combined or compound selector
|
||||
self.left = left
|
||||
# One of `` `` (a single space), ``>``, ``+`` or ``~``.
|
||||
self.combinator = combinator
|
||||
#: compound selector
|
||||
self.right = right
|
||||
|
||||
@property
|
||||
def specificity(self):
|
||||
a1, b1, c1 = self.left.specificity
|
||||
a2, b2, c2 = self.right.specificity
|
||||
return a1 + a2, b1 + b2, c1 + c2
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.left!r}{self.combinator}{self.right!r}"
|
||||
|
||||
|
||||
class CompoundSelector:
|
||||
def __init__(self, simple_selectors):
|
||||
self.simple_selectors = simple_selectors
|
||||
|
||||
@property
|
||||
def specificity(self):
|
||||
if self.simple_selectors:
|
||||
# zip(*foo) turns [(a1, b1, c1), (a2, b2, c2), ...]
|
||||
# into [(a1, a2, ...), (b1, b2, ...), (c1, c2, ...)]
|
||||
return tuple(
|
||||
map(sum, zip(*(sel.specificity for sel in self.simple_selectors)))
|
||||
)
|
||||
else:
|
||||
return 0, 0, 0
|
||||
|
||||
def __repr__(self):
|
||||
return "".join(map(repr, self.simple_selectors))
|
||||
|
||||
|
||||
class LocalNameSelector:
|
||||
specificity = 0, 0, 1
|
||||
|
||||
def __init__(self, local_name):
|
||||
self.local_name, self.lower_local_name = local_name
|
||||
|
||||
def __repr__(self):
|
||||
return self.local_name
|
||||
|
||||
|
||||
class NamespaceSelector:
|
||||
specificity = 0, 0, 0
|
||||
|
||||
def __init__(self, namespace):
|
||||
#: The namespace URL as a string,
|
||||
#: or the empty string for elements not in any namespace.
|
||||
self.namespace = namespace
|
||||
|
||||
def __repr__(self):
|
||||
if self.namespace == "":
|
||||
return "|"
|
||||
else:
|
||||
return f"{{{self.namespace}}}|"
|
||||
|
||||
|
||||
class IDSelector:
|
||||
specificity = 1, 0, 0
|
||||
|
||||
def __init__(self, ident):
|
||||
self.ident = ident
|
||||
|
||||
def __repr__(self):
|
||||
return f"#{self.ident}"
|
||||
|
||||
|
||||
class ClassSelector:
|
||||
specificity = 0, 1, 0
|
||||
|
||||
def __init__(self, class_name):
|
||||
self.class_name = class_name
|
||||
|
||||
def __repr__(self):
|
||||
return f".{self.class_name}"
|
||||
|
||||
|
||||
class AttributeSelector:
|
||||
specificity = 0, 1, 0
|
||||
|
||||
def __init__(self, namespace, name, operator, value, case_sensitive):
|
||||
self.namespace = namespace
|
||||
self.name, self.lower_name = name
|
||||
#: A string like ``=`` or ``~=``, or None for ``[attr]`` selectors
|
||||
self.operator = operator
|
||||
#: A string, or None for ``[attr]`` selectors
|
||||
self.value = value
|
||||
#: ``True`` if case-sensitive, ``False`` if case-insensitive, ``None``
|
||||
#: if depends on the document language
|
||||
self.case_sensitive = case_sensitive
|
||||
|
||||
def __repr__(self):
|
||||
namespace = "*|" if self.namespace is None else f"{{{self.namespace}}}"
|
||||
case_sensitive = (
|
||||
""
|
||||
if self.case_sensitive is None
|
||||
else f" {'s' if self.case_sensitive else 'i'}"
|
||||
)
|
||||
return f"[{namespace}{self.name}{self.operator}{self.value!r}{case_sensitive}]"
|
||||
|
||||
|
||||
class PseudoClassSelector:
|
||||
specificity = 0, 1, 0
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return ":" + self.name
|
||||
|
||||
|
||||
class FunctionalPseudoClassSelector:
|
||||
specificity = 0, 1, 0
|
||||
|
||||
def __init__(self, name, arguments):
|
||||
self.name = name
|
||||
self.arguments = arguments
|
||||
|
||||
def __repr__(self):
|
||||
return f":{self.name}{tuple(self.arguments)!r}"
|
||||
|
||||
|
||||
class NegationSelector:
|
||||
def __init__(self, selector_list):
|
||||
self.selector_list = selector_list
|
||||
|
||||
@property
|
||||
def specificity(self):
|
||||
if self.selector_list:
|
||||
return max(selector.specificity for selector in self.selector_list)
|
||||
else:
|
||||
return (0, 0, 0)
|
||||
|
||||
def __repr__(self):
|
||||
return f":not({', '.join(repr(sel) for sel in self.selector_list)})"
|
||||
|
||||
|
||||
class RelationalSelector:
|
||||
def __init__(self, selector_list):
|
||||
self.selector_list = selector_list
|
||||
|
||||
@property
|
||||
def specificity(self):
|
||||
if self.selector_list:
|
||||
return max(selector.specificity for selector in self.selector_list)
|
||||
else:
|
||||
return (0, 0, 0)
|
||||
|
||||
def __repr__(self):
|
||||
return f":has({', '.join(repr(sel) for sel in self.selector_list)})"
|
||||
|
||||
|
||||
class MatchesAnySelector:
|
||||
def __init__(self, selector_list):
|
||||
self.selector_list = selector_list
|
||||
|
||||
@property
|
||||
def specificity(self):
|
||||
if self.selector_list:
|
||||
return max(selector.specificity for selector in self.selector_list)
|
||||
else:
|
||||
return (0, 0, 0)
|
||||
|
||||
def __repr__(self):
|
||||
return f":is({', '.join(repr(sel) for sel in self.selector_list)})"
|
||||
|
||||
|
||||
class SpecificityAdjustmentSelector:
|
||||
def __init__(self, selector_list):
|
||||
self.selector_list = selector_list
|
||||
|
||||
@property
|
||||
def specificity(self):
|
||||
return (0, 0, 0)
|
||||
|
||||
def __repr__(self):
|
||||
return f":where({', '.join(repr(sel) for sel in self.selector_list)})"
|
||||
Reference in New Issue
Block a user