Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/cssselect/parser.py: 74%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2cssselect.parser
3================
5Tokenizer, parser and parsed objects for CSS selectors.
8:copyright: (c) 2007-2012 Ian Bicking and contributors.
9See AUTHORS for more details.
10:license: BSD, see LICENSE for more details.
12"""
14from __future__ import annotations
16import re
17import sys
18from typing import (
19 TYPE_CHECKING,
20 Literal,
21 Protocol,
22 Self,
23 TypeAlias,
24 Union,
25 cast,
26 overload,
27)
29if TYPE_CHECKING:
30 from collections.abc import Iterable, Iterator, Sequence
33def ascii_lower(string: str) -> str:
34 """Lower-case, but only in the ASCII range."""
35 return string.encode("utf8").lower().decode("utf8")
38class SelectorError(Exception):
39 """Common parent for :class:`SelectorSyntaxError` and
40 :class:`ExpressionError`.
42 You can just use ``except SelectorError:`` when calling
43 :meth:`~GenericTranslator.css_to_xpath` and handle both exceptions types.
45 """
48class SelectorSyntaxError(SelectorError, SyntaxError):
49 """Parsing a selector that does not match the grammar."""
52#### Parsed objects
54Tree: TypeAlias = Union[
55 "Element",
56 "Hash",
57 "Class",
58 "Function",
59 "Pseudo",
60 "Attrib",
61 "Negation",
62 "Relation",
63 "Matching",
64 "SpecificityAdjustment",
65 "CombinedSelector",
66]
67PseudoElement: TypeAlias = Union["FunctionalPseudoElement", str]
70class Selector:
71 """
72 Represents a parsed selector.
74 :meth:`~GenericTranslator.selector_to_xpath` accepts this object,
75 but ignores :attr:`pseudo_element`. It is the user’s responsibility
76 to account for pseudo-elements and reject selectors with unknown
77 or unsupported pseudo-elements.
79 """
81 def __init__(self, tree: Tree, pseudo_element: PseudoElement | None = None) -> None:
82 self.parsed_tree = tree
83 if pseudo_element is not None and not isinstance(
84 pseudo_element, FunctionalPseudoElement
85 ):
86 pseudo_element = ascii_lower(pseudo_element)
87 #: A :class:`FunctionalPseudoElement`,
88 #: or the identifier for the pseudo-element as a string,
89 # or ``None``.
90 #:
91 #: +-------------------------+----------------+--------------------------------+
92 #: | | Selector | Pseudo-element |
93 #: +=========================+================+================================+
94 #: | CSS3 syntax | ``a::before`` | ``'before'`` |
95 #: +-------------------------+----------------+--------------------------------+
96 #: | Older syntax | ``a:before`` | ``'before'`` |
97 #: +-------------------------+----------------+--------------------------------+
98 #: | From the Lists3_ draft, | ``li::marker`` | ``'marker'`` |
99 #: | not in Selectors3 | | |
100 #: +-------------------------+----------------+--------------------------------+
101 #: | Invalid pseudo-class | ``li:marker`` | ``None`` |
102 #: +-------------------------+----------------+--------------------------------+
103 #: | Functional | ``a::foo(2)`` | ``FunctionalPseudoElement(…)`` |
104 #: +-------------------------+----------------+--------------------------------+
105 #:
106 #: .. _Lists3: http://www.w3.org/TR/2011/WD-css3-lists-20110524/#marker-pseudoelement
107 self.pseudo_element = pseudo_element
109 def __repr__(self) -> str:
110 if isinstance(self.pseudo_element, FunctionalPseudoElement):
111 pseudo_element = repr(self.pseudo_element)
112 elif self.pseudo_element:
113 pseudo_element = f"::{self.pseudo_element}"
114 else:
115 pseudo_element = ""
116 return f"{self.__class__.__name__}[{self.parsed_tree!r}{pseudo_element}]"
118 def canonical(self) -> str:
119 """Return a CSS representation for this selector (a string)"""
120 if isinstance(self.pseudo_element, FunctionalPseudoElement):
121 pseudo_element = f"::{self.pseudo_element.canonical()}"
122 elif self.pseudo_element:
123 pseudo_element = f"::{_serialize_ident(self.pseudo_element)}"
124 else:
125 pseudo_element = ""
126 res = f"{self.parsed_tree.canonical()}{pseudo_element}"
127 # Strip a redundant universal selector from e.g. "*.foo" (but not
128 # from e.g. "* > foo").
129 if len(res) > 1 and res[0] == "*" and res[1] in "#.[:":
130 res = res[1:]
131 return res
133 def specificity(self) -> tuple[int, int, int]:
134 """Return the specificity_ of this selector as a tuple of 3 integers.
136 .. _specificity: http://www.w3.org/TR/selectors/#specificity
138 """
139 a, b, c = self.parsed_tree.specificity()
140 if self.pseudo_element:
141 c += 1
142 return a, b, c
145class Class:
146 """
147 Represents selector.class_name
148 """
150 def __init__(self, selector: Tree, class_name: str) -> None:
151 self.selector = selector
152 self.class_name = class_name
154 def __repr__(self) -> str:
155 return f"{self.__class__.__name__}[{self.selector!r}.{self.class_name}]"
157 def canonical(self) -> str:
158 return f"{self.selector.canonical()}.{_serialize_ident(self.class_name)}"
160 def specificity(self) -> tuple[int, int, int]:
161 a, b, c = self.selector.specificity()
162 b += 1
163 return a, b, c
166class FunctionalPseudoElement:
167 """
168 Represents selector::name(arguments)
170 .. attribute:: name
172 The name (identifier) of the pseudo-element, as a string.
174 .. attribute:: arguments
176 The arguments of the pseudo-element, as a list of tokens.
178 **Note:** tokens are not part of the public API,
179 and may change between cssselect versions.
180 Use at your own risks.
182 """
184 def __init__(self, name: str, arguments: Sequence[Token]):
185 self.name = ascii_lower(name)
186 self.arguments = arguments
188 def __repr__(self) -> str:
189 token_values = [token.value for token in self.arguments]
190 return f"{self.__class__.__name__}[::{self.name}({token_values!r})]"
192 def argument_types(self) -> list[str]:
193 return [token.type for token in self.arguments]
195 def canonical(self) -> str:
196 args = "".join(token.css() for token in self.arguments)
197 return f"{_serialize_ident(self.name)}({args})"
200class Function:
201 """
202 Represents selector:name(expr)
203 """
205 def __init__(self, selector: Tree, name: str, arguments: Sequence[Token]) -> None:
206 self.selector = selector
207 self.name = ascii_lower(name)
208 self.arguments = arguments
210 def __repr__(self) -> str:
211 token_values = [token.value for token in self.arguments]
212 return f"{self.__class__.__name__}[{self.selector!r}:{self.name}({token_values!r})]"
214 def argument_types(self) -> list[str]:
215 return [token.type for token in self.arguments]
217 def canonical(self) -> str:
218 args = "".join(token.css() for token in self.arguments)
219 return f"{self.selector.canonical()}:{_serialize_ident(self.name)}({args})"
221 def specificity(self) -> tuple[int, int, int]:
222 a, b, c = self.selector.specificity()
223 b += 1
224 return a, b, c
227class Pseudo:
228 """
229 Represents selector:ident
230 """
232 def __init__(self, selector: Tree, ident: str) -> None:
233 self.selector = selector
234 self.ident = ascii_lower(ident)
236 def __repr__(self) -> str:
237 return f"{self.__class__.__name__}[{self.selector!r}:{self.ident}]"
239 def canonical(self) -> str:
240 return f"{self.selector.canonical()}:{_serialize_ident(self.ident)}"
242 def specificity(self) -> tuple[int, int, int]:
243 a, b, c = self.selector.specificity()
244 b += 1
245 return a, b, c
248class Negation:
249 """
250 Represents selector:not(subselector)
251 """
253 def __init__(self, selector: Tree, subselector: Tree) -> None:
254 self.selector = selector
255 self.subselector = subselector
257 def __repr__(self) -> str:
258 return f"{self.__class__.__name__}[{self.selector!r}:not({self.subselector!r})]"
260 def canonical(self) -> str:
261 subsel = self.subselector.canonical()
262 # Strip a redundant universal selector from e.g. "*.foo" (but not
263 # from e.g. "* > foo").
264 if len(subsel) > 1 and subsel[0] == "*" and subsel[1] in "#.[:":
265 subsel = subsel[1:]
266 return f"{self.selector.canonical()}:not({subsel})"
268 def specificity(self) -> tuple[int, int, int]:
269 a1, b1, c1 = self.selector.specificity()
270 a2, b2, c2 = self.subselector.specificity()
271 return a1 + a2, b1 + b2, c1 + c2
274def _combinator_prefix(combinator: Token) -> str:
275 # The descendant combinator is implicit in :has() arguments.
276 if combinator.value == " ":
277 return ""
278 return f"{combinator.value} "
281class Relation:
282 """
283 Represents selector:has(relative_selector_list)
285 .. attribute:: arguments
287 The relative selector list, as a list of ``(combinator, selector)``
288 pairs. *combinator* is the leading combinator token of the argument,
289 which is a space token when the argument does not spell one out.
291 """
293 def __init__(self, selector: Tree, arguments: Sequence[tuple[Token, Selector]]):
294 self.selector = selector
295 self.arguments = arguments
297 def __repr__(self) -> str:
298 args_str = ", ".join(
299 f"{_combinator_prefix(combinator)}{subselector!r}"
300 for combinator, subselector in self.arguments
301 )
302 return f"{self.__class__.__name__}[{self.selector!r}:has({args_str})]"
304 def canonical(self) -> str:
305 args_str = ", ".join(
306 f"{_combinator_prefix(combinator)}{subselector.canonical()}"
307 for combinator, subselector in self.arguments
308 )
309 return f"{self.selector.canonical()}:has({args_str})"
311 def specificity(self) -> tuple[int, int, int]:
312 a1, b1, c1 = self.selector.specificity()
313 a2, b2, c2 = max(subselector.specificity() for _, subselector in self.arguments)
314 return a1 + a2, b1 + b2, c1 + c2
317class Matching:
318 """
319 Represents selector:is(selector_list)
320 """
322 def __init__(self, selector: Tree, selector_list: Iterable[Tree]):
323 self.selector = selector
324 self.selector_list = selector_list
326 def __repr__(self) -> str:
327 args_str = ", ".join(repr(s) for s in self.selector_list)
328 return f"{self.__class__.__name__}[{self.selector!r}:is({args_str})]"
330 def canonical(self) -> str:
331 selector_arguments = []
332 for s in self.selector_list:
333 selarg = s.canonical()
334 if len(selarg) > 1 and selarg[0] == "*" and selarg[1] in "#.[:":
335 selarg = selarg[1:]
336 selector_arguments.append(selarg)
337 args_str = ", ".join(selector_arguments)
338 return f"{self.selector.canonical()}:is({args_str})"
340 def specificity(self) -> tuple[int, int, int]:
341 a1, b1, c1 = self.selector.specificity()
342 a2, b2, c2 = max(x.specificity() for x in self.selector_list)
343 return a1 + a2, b1 + b2, c1 + c2
346class SpecificityAdjustment:
347 """
348 Represents selector:where(selector_list)
349 Same as selector:is(selector_list), but its specificity is always 0
350 """
352 def __init__(self, selector: Tree, selector_list: list[Tree]):
353 self.selector = selector
354 self.selector_list = selector_list
356 def __repr__(self) -> str:
357 args_str = ", ".join(repr(s) for s in self.selector_list)
358 return f"{self.__class__.__name__}[{self.selector!r}:where({args_str})]"
360 def canonical(self) -> str:
361 selector_arguments = []
362 for s in self.selector_list:
363 selarg = s.canonical()
364 if len(selarg) > 1 and selarg[0] == "*" and selarg[1] in "#.[:":
365 selarg = selarg[1:]
366 selector_arguments.append(selarg)
367 args_str = ", ".join(selector_arguments)
368 return f"{self.selector.canonical()}:where({args_str})"
370 def specificity(self) -> tuple[int, int, int]:
371 # :where() itself contributes no specificity, but the compound
372 # selector it applies to does.
373 return self.selector.specificity()
376class Attrib:
377 """
378 Represents selector[namespace|attrib operator value flag]
380 *flag* is ``'i'`` for a case-insensitive value match, ``'s'`` for a
381 case-sensitive one, and `None` when the selector sets neither.
382 """
384 @overload
385 def __init__(
386 self,
387 selector: Tree,
388 namespace: str | None,
389 attrib: str,
390 operator: Literal["exists"],
391 value: None,
392 flag: None = None,
393 ) -> None: ...
395 @overload
396 def __init__(
397 self,
398 selector: Tree,
399 namespace: str | None,
400 attrib: str,
401 operator: str,
402 value: Token,
403 flag: str | None = None,
404 ) -> None: ...
406 def __init__(
407 self,
408 selector: Tree,
409 namespace: str | None,
410 attrib: str,
411 operator: str,
412 value: Token | None,
413 flag: str | None = None,
414 ) -> None:
415 self.selector = selector
416 self.namespace = namespace
417 self.attrib = attrib
418 self.operator = operator
419 self.value = value
420 self.flag = flag
422 def __repr__(self) -> str:
423 attrib = f"{self.namespace}|{self.attrib}" if self.namespace else self.attrib
424 if self.operator == "exists":
425 return f"{self.__class__.__name__}[{self.selector!r}[{attrib}]]"
426 assert self.value is not None
427 flag = f" {self.flag}" if self.flag else ""
428 return f"{self.__class__.__name__}[{self.selector!r}[{attrib} {self.operator} {self.value.value!r}{flag}]]"
430 def canonical(self) -> str:
431 attrib = _serialize_ident(self.attrib)
432 if self.namespace:
433 namespace = (
434 "*" if self.namespace == "*" else _serialize_ident(self.namespace)
435 )
436 attrib = f"{namespace}|{attrib}"
438 if self.operator == "exists":
439 op = attrib
440 else:
441 assert self.value is not None
442 flag = f" {self.flag}" if self.flag else ""
443 op = f"{attrib}{self.operator}{self.value.css()}{flag}"
445 return f"{self.selector.canonical()}[{op}]"
447 def specificity(self) -> tuple[int, int, int]:
448 a, b, c = self.selector.specificity()
449 b += 1
450 return a, b, c
453class Element:
454 """
455 Represents namespace|element
457 `None` is for the universal selector '*'. `namespace` is ``"*"``
458 for the explicit namespace wildcard, e.g. ``*|div``, and `None`
459 both for an unprefixed element and for the explicit ``|div``
460 no-namespace syntax. ``|*`` (any element with no namespace) has
461 `namespace` ``""``.
463 """
465 def __init__(
466 self, namespace: str | None = None, element: str | None = None
467 ) -> None:
468 self.namespace = namespace
469 self.element = element
471 def __repr__(self) -> str:
472 return f"{self.__class__.__name__}[{self.canonical()}]"
474 def canonical(self) -> str:
475 element = _serialize_ident(self.element) if self.element else "*"
476 if self.namespace is not None:
477 namespace = (
478 "*" if self.namespace == "*" else _serialize_ident(self.namespace)
479 )
480 element = f"{namespace}|{element}"
481 return element
483 def specificity(self) -> tuple[int, int, int]:
484 if self.element:
485 return 0, 0, 1
486 return 0, 0, 0
489class Hash:
490 """
491 Represents selector#id
492 """
494 def __init__(self, selector: Tree, id: str) -> None: # noqa: A002
495 self.selector = selector
496 self.id = id
498 def __repr__(self) -> str:
499 return f"{self.__class__.__name__}[{self.selector!r}#{self.id}]"
501 def canonical(self) -> str:
502 return f"{self.selector.canonical()}#{_serialize_ident(self.id)}"
504 def specificity(self) -> tuple[int, int, int]:
505 a, b, c = self.selector.specificity()
506 a += 1
507 return a, b, c
510class CombinedSelector:
511 def __init__(self, selector: Tree, combinator: str, subselector: Tree) -> None:
512 assert selector is not None
513 self.selector = selector
514 self.combinator = combinator
515 self.subselector = subselector
517 def __repr__(self) -> str:
518 comb = "<followed>" if self.combinator == " " else self.combinator
519 return (
520 f"{self.__class__.__name__}[{self.selector!r} {comb} {self.subselector!r}]"
521 )
523 def canonical(self) -> str:
524 subsel = self.subselector.canonical()
525 if len(subsel) > 1 and subsel[0] == "*" and subsel[1] in "#.[:":
526 subsel = subsel[1:]
527 combinator = " " if self.combinator == " " else f" {self.combinator} "
528 return f"{self.selector.canonical()}{combinator}{subsel}"
530 def specificity(self) -> tuple[int, int, int]:
531 a1, b1, c1 = self.selector.specificity()
532 a2, b2, c2 = self.subselector.specificity()
533 return a1 + a2, b1 + b2, c1 + c2
536#### Parser
538# foo
539_el_re = re.compile(r"^[ \t\r\n\f]*([a-zA-Z]+)[ \t\r\n\f]*$")
541# foo#bar or #bar
542_id_re = re.compile(r"^[ \t\r\n\f]*([a-zA-Z]*)#([a-zA-Z0-9_-]+)[ \t\r\n\f]*$")
544# foo.bar or .bar
545_class_re = re.compile(
546 r"^[ \t\r\n\f]*([a-zA-Z]*)\.([a-zA-Z][a-zA-Z0-9_-]*)[ \t\r\n\f]*$"
547)
550def parse(css: str) -> list[Selector]:
551 """Parse a CSS *group of selectors*.
553 If you don't care about pseudo-elements or selector specificity,
554 you can skip this and use :meth:`~GenericTranslator.css_to_xpath`.
556 :param css:
557 A *group of selectors* as a string.
558 :raises:
559 :class:`SelectorSyntaxError` on invalid selectors.
560 :returns:
561 A list of parsed :class:`Selector` objects, one for each
562 selector in the comma-separated group.
564 """
565 # Fast path for simple cases
566 match = _el_re.match(css)
567 if match:
568 return [Selector(Element(element=match.group(1)))]
569 match = _id_re.match(css)
570 if match is not None:
571 return [Selector(Hash(Element(element=match.group(1) or None), match.group(2)))]
572 match = _class_re.match(css)
573 if match is not None:
574 return [
575 Selector(Class(Element(element=match.group(1) or None), match.group(2)))
576 ]
578 stream = TokenStream(tokenize(css))
579 stream.source = css
580 return list(parse_selector_group(stream))
583# except SelectorSyntaxError:
584# e = sys.exc_info()[1]
585# message = "%s at %s -> %r" % (
586# e, stream.used, stream.peek())
587# e.msg = message
588# e.args = tuple([message])
589# raise
592def parse_selector_group(stream: TokenStream) -> Iterator[Selector]:
593 stream.skip_whitespace()
594 while 1:
595 yield Selector(*parse_selector(stream))
596 if stream.peek() == ("DELIM", ","):
597 stream.next()
598 stream.skip_whitespace()
599 else:
600 break
603def parse_selector(stream: TokenStream) -> tuple[Tree, PseudoElement | None]:
604 result, pseudo_element = parse_simple_selector(stream)
605 while 1:
606 stream.skip_whitespace()
607 peek = stream.peek()
608 if peek in (("EOF", None), ("DELIM", ",")):
609 break
610 if pseudo_element:
611 raise SelectorSyntaxError(
612 f"Got pseudo-element ::{pseudo_element} not at the end of a selector"
613 )
614 if peek.is_delim("+", ">", "~"):
615 # A combinator
616 combinator = cast("str", stream.next().value)
617 stream.skip_whitespace()
618 else:
619 # By exclusion, the last parse_simple_selector() ended
620 # at peek == ' '
621 combinator = " "
622 next_selector, pseudo_element = parse_simple_selector(stream)
623 result = CombinedSelector(result, combinator, next_selector)
624 return result, pseudo_element
627def parse_simple_selector(
628 stream: TokenStream,
629 inside_negation: bool = False,
630 inside_selector_list: bool = False,
631 inside_relative_selector: bool = False,
632) -> tuple[Tree, PseudoElement | None]:
633 stream.skip_whitespace()
634 selector_start = len(stream.used)
635 # Whether the tokens preceding this compound selector (ignoring
636 # whitespace) are the start of the input or a comma, for :scope.
637 preceding = selector_start - 1
638 while preceding >= 0 and stream.used[preceding].type == "S":
639 preceding -= 1
640 at_selector_start = preceding < 0 or stream.used[preceding].is_delim(",")
641 peek = stream.peek()
642 if peek.type == "IDENT" or peek == ("DELIM", "*"):
643 if peek.type == "IDENT":
644 namespace = stream.next().value
645 else:
646 stream.next()
647 namespace = "*"
648 if stream.peek() == ("DELIM", "|"):
649 if peek.type == "IDENT" and namespace == "*":
650 # An escaped "*" (e.g. "\*|f") is a prefix named "*",
651 # not the namespace wildcard, and no XPath prefix can be "*".
652 raise SelectorSyntaxError(f"Expected namespace prefix, got {peek}")
653 stream.next()
654 element = stream.next_ident_or_star()
655 else:
656 # An escaped "*" (e.g. "\*") is a type selector for elements
657 # named "*", not the universal selector.
658 element = namespace if peek.type == "IDENT" else None
659 namespace = None
660 else:
661 element = namespace = None
662 result: Tree = Element(namespace, element)
663 pseudo_element: PseudoElement | None = None
664 while 1:
665 peek = stream.peek()
666 if peek.type in ("S", "EOF") or peek.is_delim(",", "+", ">", "~", ")"):
667 break
668 if pseudo_element:
669 raise SelectorSyntaxError(
670 f"Got pseudo-element ::{pseudo_element} not at the end of a selector"
671 )
672 if peek.type == "HASH":
673 result = Hash(result, cast("str", stream.next().value))
674 elif peek == ("DELIM", "."):
675 stream.next()
676 result = Class(result, stream.next_ident())
677 elif peek == ("DELIM", "|"):
678 # The explicit "no namespace" syntax, e.g. |div: only valid at
679 # the very start of a simple selector.
680 if len(stream.used) != selector_start:
681 raise SelectorSyntaxError(f"Expected selector, got {peek}")
682 stream.next()
683 element = stream.next_ident_or_star()
684 # Unlike *, |* only matches elements with no namespace.
685 result = Element("" if element is None else None, element)
686 elif peek == ("DELIM", "["):
687 stream.next()
688 result = parse_attrib(result, stream)
689 elif peek == ("DELIM", ":"):
690 stream.next()
691 if stream.peek() == ("DELIM", ":"):
692 stream.next()
693 pseudo_element = stream.next_ident()
694 if stream.peek() == ("DELIM", "("):
695 stream.next()
696 pseudo_element = FunctionalPseudoElement(
697 pseudo_element, parse_arguments(stream)
698 )
699 continue
700 ident = stream.next_ident()
701 if ident.lower() in ("first-line", "first-letter", "before", "after"):
702 # Special case: CSS 2.1 pseudo-elements can have a single ':'
703 # Any new pseudo-element must have two.
704 pseudo_element = str(ident)
705 continue
706 if stream.peek() != ("DELIM", "("):
707 result = Pseudo(result, ident)
708 # :scope is only supported at the start of a selector,
709 # i.e. never in :is()/:where()/:matches() or :has()
710 # arguments (where a preceding comma separates arguments,
711 # not selectors), and otherwise only when the tokens
712 # preceding its compound selector are the start of the
713 # input or a comma.
714 if result.ident == "scope" and (
715 inside_selector_list
716 or inside_relative_selector
717 or not at_selector_start
718 ):
719 raise SelectorSyntaxError(
720 'Got pseudo-class ":scope" not at the start of a selector'
721 )
722 continue
723 stream.next()
724 stream.skip_whitespace()
725 if ident.lower() == "not":
726 if inside_selector_list:
727 raise SelectorSyntaxError(
728 ":not() is not supported inside :is(), :where() and :matches()"
729 )
730 if inside_negation:
731 raise SelectorSyntaxError("Got nested :not()")
732 argument, argument_pseudo_element = parse_simple_selector(
733 stream, inside_negation=True
734 )
735 while 1:
736 # Whitespace before the closing parenthesis is not a
737 # descendant combinator.
738 stream.skip_whitespace()
739 peek = stream.peek()
740 if argument_pseudo_element:
741 raise SelectorSyntaxError(
742 f"Got pseudo-element ::{argument_pseudo_element} inside :not() at {peek.pos}"
743 )
744 if peek == ("DELIM", ")"):
745 stream.next()
746 break
747 if peek.is_delim("+", ">", "~"):
748 argument_combinator = cast("str", stream.next().value)
749 stream.skip_whitespace()
750 elif peek.type == "EOF" or peek.is_delim(","):
751 # A selector list is not supported in :not().
752 raise SelectorSyntaxError(f"Expected ')', got {peek}")
753 else:
754 argument_combinator = " "
755 next_selector, argument_pseudo_element = parse_simple_selector(
756 stream, inside_negation=True
757 )
758 argument = CombinedSelector(
759 argument, argument_combinator, next_selector
760 )
761 result = Negation(result, argument)
762 elif ident.lower() == "has":
763 result = Relation(result, parse_relative_selector(stream))
764 elif ident.lower() in ("matches", "is"):
765 selectors = parse_simple_selector_arguments(stream)
766 result = Matching(result, selectors)
767 elif ident.lower() == "where":
768 selectors = parse_simple_selector_arguments(stream)
769 result = SpecificityAdjustment(result, selectors)
770 else:
771 result = Function(result, ident, parse_arguments(stream))
772 else:
773 raise SelectorSyntaxError(f"Expected selector, got {peek}")
774 if len(stream.used) == selector_start:
775 raise SelectorSyntaxError(f"Expected selector, got {stream.peek()}")
776 return result, pseudo_element
779def parse_arguments(stream: TokenStream) -> list[Token]: # noqa: RET503
780 arguments: list[Token] = []
781 while 1:
782 stream.skip_whitespace()
783 next_ = stream.next()
784 if next_.type in ("IDENT", "STRING", "NUMBER") or next_ in [
785 ("DELIM", "+"),
786 ("DELIM", "-"),
787 ]:
788 arguments.append(next_)
789 elif next_ == ("DELIM", ")"):
790 return arguments
791 else:
792 raise SelectorSyntaxError(f"Expected an argument, got {next_}")
795def parse_relative_selector(stream: TokenStream) -> list[tuple[Token, Selector]]:
796 """Parse the relative selector list of a :has() argument, i.e. a
797 comma-separated list of complex selectors, each optionally preceded by a
798 combinator."""
799 arguments = []
800 while 1:
801 stream.skip_whitespace()
802 peek = stream.peek()
803 if peek.is_delim("+", ">", "~"):
804 combinator = stream.next()
805 stream.skip_whitespace()
806 else:
807 combinator = Token("DELIM", " ", pos=peek.pos)
808 result, pseudo_element = parse_simple_selector(
809 stream, inside_relative_selector=True
810 )
811 while 1:
812 if pseudo_element:
813 raise SelectorSyntaxError(
814 f"Got pseudo-element ::{pseudo_element} inside function"
815 )
816 # Whitespace before a comma or the closing parenthesis is not a
817 # descendant combinator.
818 stream.skip_whitespace()
819 peek = stream.peek()
820 if peek.is_delim(",", ")"):
821 break
822 if peek.type == "EOF":
823 raise SelectorSyntaxError(f"Expected ')', got {peek}")
824 if peek.is_delim("+", ">", "~"):
825 argument_combinator = cast("str", stream.next().value)
826 stream.skip_whitespace()
827 else:
828 argument_combinator = " "
829 next_selector, pseudo_element = parse_simple_selector(stream)
830 result = CombinedSelector(result, argument_combinator, next_selector)
831 arguments.append((combinator, Selector(result)))
832 if stream.next().is_delim(")"):
833 break
834 return arguments
837def parse_simple_selector_arguments(stream: TokenStream) -> list[Tree]:
838 arguments = []
839 while 1:
840 result, pseudo_element = parse_simple_selector(
841 stream, inside_selector_list=True
842 )
843 if pseudo_element:
844 raise SelectorSyntaxError(
845 f"Got pseudo-element ::{pseudo_element} inside function"
846 )
847 stream.skip_whitespace()
848 next_ = stream.next()
849 if next_ == ("DELIM", ","):
850 stream.skip_whitespace()
851 arguments.append(result)
852 elif next_ == ("DELIM", ")"):
853 arguments.append(result)
854 break
855 else:
856 raise SelectorSyntaxError(f"Expected an argument, got {next_}")
857 return arguments
860def parse_attrib(selector: Tree, stream: TokenStream) -> Attrib:
861 stream.skip_whitespace()
862 attrib: str | None
863 namespace: str | None
864 op: str | None
865 if stream.peek() == ("DELIM", "|"):
866 # The explicit "no namespace" syntax, e.g. [|attr].
867 stream.next()
868 namespace = None
869 attrib = stream.next_ident()
870 op = None
871 else:
872 attrib = stream.next_ident_or_star()
873 if attrib is None and stream.peek() != ("DELIM", "|"):
874 raise SelectorSyntaxError(f"Expected '|', got {stream.peek()}")
875 if stream.peek() == ("DELIM", "|"):
876 stream.next()
877 if attrib is not None and stream.peek() == ("DELIM", "="):
878 namespace = None
879 stream.next()
880 op = "|="
881 else:
882 if attrib == "*":
883 # An escaped "*" (e.g. "[\*|attr]"), not the wildcard.
884 raise SelectorSyntaxError(
885 f"Expected namespace prefix, got {stream.used[-2]}"
886 )
887 namespace = "*" if attrib is None else attrib
888 attrib = stream.next_ident()
889 op = None
890 else:
891 namespace = op = None
892 if op is None:
893 stream.skip_whitespace()
894 next_ = stream.next()
895 if next_ == ("DELIM", "]"):
896 return Attrib(selector, namespace, cast("str", attrib), "exists", None)
897 if next_ == ("DELIM", "="):
898 op = "="
899 elif next_.is_delim("^", "$", "*", "~", "|", "!") and (
900 stream.peek() == ("DELIM", "=")
901 ):
902 op = cast("str", next_.value) + "="
903 stream.next()
904 else:
905 raise SelectorSyntaxError(f"Operator expected, got {next_}")
906 stream.skip_whitespace()
907 value = stream.next()
908 if value.type not in ("IDENT", "STRING"):
909 raise SelectorSyntaxError(f"Expected string or ident, got {value}")
910 stream.skip_whitespace()
911 next_ = stream.next()
912 flag = None
913 if next_.type == "IDENT":
914 flag = ascii_lower(cast("str", next_.value))
915 if flag not in ("i", "s"):
916 raise SelectorSyntaxError(f"Expected ']', got {next_}")
917 stream.skip_whitespace()
918 next_ = stream.next()
919 if next_ != ("DELIM", "]"):
920 raise SelectorSyntaxError(f"Expected ']', got {next_}")
921 return Attrib(selector, namespace, cast("str", attrib), op, value, flag)
924# An An+B integer: ASCII digits only, unlike int(), which also accepts other
925# Unicode decimal digits, underscores and surrounding whitespace.
926_match_series_integer = re.compile("[+-]?[0-9]+").fullmatch
929def _parse_series_integer(s: str) -> int:
930 if not _match_series_integer(s):
931 raise ValueError(f"Invalid integer in series: {s!r}")
932 return int(s)
935# The B of An+B with whitespace: only around a sign that is a token of its
936# own, as in "2n + 1" or "2n- 1".
937_match_series_spaced_b = re.compile(" ?[+-] ?[0-9]+").fullmatch
940def parse_series(tokens: Iterable[Token]) -> tuple[int, int]:
941 """Parses the arguments for :nth-child() and friends."""
942 parts = []
943 end = None
944 for token in tokens:
945 if token.type == "STRING":
946 raise ValueError("String tokens not allowed in series.")
947 value = cast("str", token.value)
948 if end is not None and token.pos != end:
949 # Whitespace or a comment between two tokens, which must not
950 # join them: "1 2" is not "12". An escaped identifier is longer
951 # in the source than its value, but whitespace is allowed after
952 # the identifier that ends An anyway.
953 parts.append(" ")
954 parts.append(value)
955 end = token.pos + len(value)
956 # The An+B microsyntax is ASCII-case-insensitive: 2N+1, EVEN, Odd...
957 s = ascii_lower("".join(parts).strip())
958 if s == "odd":
959 return 2, 1
960 if s == "even":
961 return 2, 0
962 if s == "n":
963 return 1, 0
964 if "n" not in s:
965 # Just b
966 return 0, _parse_series_integer(s)
967 a, b = s.split("n", 1)
968 a_as_int: int
969 if not a:
970 a_as_int = 1
971 elif a in {"-", "+"}:
972 a_as_int = int(a + "1")
973 else:
974 a_as_int = _parse_series_integer(a)
975 if " " in b:
976 if not _match_series_spaced_b(b):
977 raise ValueError(f"Invalid integer in series: {b!r}")
978 b = b.replace(" ", "")
979 b_as_int = _parse_series_integer(b) if b else 0
980 return a_as_int, b_as_int
983#### Token objects
986class Token(tuple[str, str | None]): # noqa: SLOT001
987 @overload
988 def __new__(
989 cls,
990 type_: Literal["IDENT", "HASH", "STRING", "S", "DELIM", "NUMBER"],
991 value: str,
992 pos: int,
993 ) -> Self: ...
995 @overload
996 def __new__(cls, type_: Literal["EOF"], value: None, pos: int) -> Self: ...
998 def __new__(cls, type_: str, value: str | None, pos: int) -> Self:
999 obj = tuple.__new__(cls, (type_, value))
1000 obj.pos = pos
1001 return obj
1003 def __repr__(self) -> str:
1004 return f"<{self.type} '{self.value}' at {self.pos}>"
1006 def is_delim(self, *values: str) -> bool:
1007 return self.type == "DELIM" and self.value in values
1009 pos: int
1011 @property
1012 def type(self) -> str:
1013 return self[0]
1015 @property
1016 def value(self) -> str | None:
1017 return self[1]
1019 def css(self) -> str:
1020 if self.type == "STRING":
1021 # Escape as CSS (repr() would use Python escapes, which mean
1022 # something else in CSS, e.g. '\n' is just the letter 'n').
1023 escaped = cast("str", self.value).replace("\\", "\\\\").replace("'", "\\'")
1024 escaped = _sub_string_control_char(_replace_string_control_char, escaped)
1025 return f"'{escaped}'"
1026 if self.type == "IDENT":
1027 return _serialize_ident(cast("str", self.value))
1028 return cast("str", self.value)
1031class EOFToken(Token):
1032 def __new__(cls, pos: int) -> Self:
1033 return Token.__new__(cls, "EOF", None, pos)
1035 def __repr__(self) -> str:
1036 return f"<{self.type} at {self.pos}>"
1039#### Tokenizer
1042class TokenMacros:
1043 unicode_escape = r"\\([0-9a-f]{1,6})(?:\r\n|[ \n\r\t\f])?"
1044 escape = unicode_escape + r"|\\[^\n\r\f0-9a-f]"
1045 string_escape = r"\\(?:\n|\r\n|\r|\f)|" + escape
1046 nonascii = r"[^\0-\177]"
1047 nmchar = f"[_a-z0-9-]|{escape}|{nonascii}"
1048 nmstart = f"[_a-z]|{escape}|{nonascii}"
1051class MatchFunc(Protocol):
1052 def __call__(
1053 self, string: str, pos: int = ..., endpos: int = ...
1054 ) -> re.Match[str] | None: ...
1057def _compile(pattern: str) -> MatchFunc:
1058 return re.compile(pattern % vars(TokenMacros), re.IGNORECASE).match
1061_match_whitespace = _compile(r"[ \t\r\n\f]+")
1062_match_number = _compile(r"[+-]?(?:[0-9]*\.[0-9]+|[0-9]+)")
1063_match_hash = _compile("#(?:%(nmchar)s)+")
1064_match_ident = _compile("-?(?:%(nmstart)s)(?:%(nmchar)s)*")
1065_match_string_by_quote = {
1066 "'": _compile(r"([^\n\r\f\\']|%(string_escape)s)*"),
1067 '"': _compile(r'([^\n\r\f\\"]|%(string_escape)s)*'),
1068}
1070# Each escape is decoded in a single left-to-right pass, so that the output
1071# of one escape is never read as (part of) another.
1072_sub_escape = re.compile(TokenMacros.unicode_escape + r"|\\(.)", re.IGNORECASE).sub
1073_sub_string_escape = re.compile(
1074 TokenMacros.unicode_escape + r"|\\(?:\n|\r\n|\r|\f)|\\(.)", re.IGNORECASE
1075).sub
1076_sub_string_control_char = re.compile(r"[\x00-\x1f\x7f]").sub
1078# CSS Syntax Level 3, §3.3: fold a raw U+0000 or surrogate code point to U+FFFD.
1079_sub_invalid_input_char = re.compile("[\x00\ud800-\udfff]").sub
1082def _replace_unicode(match: re.Match[str]) -> str:
1083 codepoint = int(match.group(1), 16)
1084 if codepoint == 0 or codepoint > sys.maxunicode or 0xD800 <= codepoint <= 0xDFFF:
1085 codepoint = 0xFFFD
1086 return chr(codepoint)
1089def _replace_escape(match: re.Match[str]) -> str:
1090 if match.group(1) is not None:
1091 return _replace_unicode(match)
1092 # A simple escape, or an escaped newline (only in strings).
1093 return match.group(2) or ""
1096def _replace_string_control_char(match: re.Match[str]) -> str:
1097 # The trailing space ends the escape sequence, in case the next
1098 # character is a hexadecimal digit.
1099 return f"\\{ord(match.group()):x} "
1102def unescape_ident(value: str) -> str:
1103 return _sub_escape(_replace_escape, value)
1106def _serialize_ident(value: str) -> str:
1107 """Serialize a string as a CSS identifier, escaping special characters.
1109 Implements the CSSOM "serialize an identifier" algorithm:
1110 https://drafts.csswg.org/cssom/#serialize-an-identifier
1111 """
1112 result = []
1113 for i, char in enumerate(value):
1114 code = ord(char)
1115 serialized = char
1116 if code == 0:
1117 serialized = "\N{REPLACEMENT CHARACTER}"
1118 elif code <= 0x1F or code == 0x7F:
1119 serialized = f"\\{code:x} "
1120 elif "0" <= char <= "9":
1121 if i == 0 or (i == 1 and value[0] == "-"):
1122 # An identifier cannot start with a digit
1123 # (or a '-' followed by a digit).
1124 serialized = f"\\{code:x} "
1125 elif char == "-":
1126 if len(value) == 1 or (i == 0 and value[1] == "-"):
1127 # CSSOM leaves a leading "--" unescaped (such identifiers
1128 # are valid since CSS Syntax 3), but the tokenizer only
1129 # implements the CSS 2.1 identifier grammar and would not
1130 # be able to parse the result, so escape the first "-".
1131 serialized = "\\-"
1132 elif not (
1133 code >= 0x80 or char == "_" or "a" <= char <= "z" or "A" <= char <= "Z"
1134 ):
1135 serialized = f"\\{char}"
1136 result.append(serialized)
1137 return "".join(result)
1140def tokenize(s: str) -> Iterator[Token]:
1141 # Preprocess the input stream (§3.3); the substitution is length-preserving.
1142 s = _sub_invalid_input_char("\N{REPLACEMENT CHARACTER}", s)
1143 pos = 0
1144 len_s = len(s)
1145 while pos < len_s:
1146 match = _match_whitespace(s, pos=pos)
1147 if match:
1148 yield Token("S", " ", pos)
1149 pos = match.end()
1150 continue
1152 match = _match_ident(s, pos=pos)
1153 if match:
1154 value = unescape_ident(match.group())
1155 yield Token("IDENT", value, pos)
1156 pos = match.end()
1157 continue
1159 match = _match_hash(s, pos=pos)
1160 if match:
1161 value = unescape_ident(match.group()[1:])
1162 yield Token("HASH", value, pos)
1163 pos = match.end()
1164 continue
1166 quote = s[pos]
1167 if quote in _match_string_by_quote:
1168 match = _match_string_by_quote[quote](s, pos=pos + 1)
1169 assert match, "Should have found at least an empty match"
1170 end_pos = match.end()
1171 if end_pos == len_s:
1172 raise SelectorSyntaxError(f"Unclosed string at {pos}")
1173 if s[end_pos] != quote:
1174 raise SelectorSyntaxError(f"Invalid string at {pos}")
1175 value = _sub_string_escape(_replace_escape, match.group())
1176 yield Token("STRING", value, pos)
1177 pos = end_pos + 1
1178 continue
1180 match = _match_number(s, pos=pos)
1181 if match:
1182 value = match.group()
1183 yield Token("NUMBER", value, pos)
1184 pos = match.end()
1185 continue
1187 pos2 = pos + 2
1188 if s[pos:pos2] == "/*":
1189 pos = s.find("*/", pos2)
1190 if pos == -1:
1191 pos = len_s
1192 else:
1193 pos += 2
1194 continue
1196 yield Token("DELIM", s[pos], pos)
1197 pos += 1
1199 assert pos == len_s
1200 yield EOFToken(pos)
1203class TokenStream:
1204 def __init__(self, tokens: Iterable[Token], source: str | None = None) -> None:
1205 self.used: list[Token] = []
1206 self.tokens = iter(tokens)
1207 self.source = source
1208 self.peeked: Token | None = None
1209 self._peeking = False
1210 self.next_token = self.tokens.__next__
1212 def next(self) -> Token:
1213 if self._peeking:
1214 self._peeking = False
1215 assert self.peeked is not None
1216 self.used.append(self.peeked)
1217 return self.peeked
1218 next_ = self.next_token()
1219 self.used.append(next_)
1220 return next_
1222 def peek(self) -> Token:
1223 if not self._peeking:
1224 self.peeked = self.next_token()
1225 self._peeking = True
1226 assert self.peeked is not None
1227 return self.peeked
1229 def next_ident(self) -> str:
1230 next_ = self.next()
1231 if next_.type != "IDENT":
1232 raise SelectorSyntaxError(f"Expected ident, got {next_}")
1233 return cast("str", next_.value)
1235 def next_ident_or_star(self) -> str | None:
1236 next_ = self.next()
1237 if next_.type == "IDENT":
1238 return next_.value
1239 if next_ == ("DELIM", "*"):
1240 return None
1241 raise SelectorSyntaxError(f"Expected ident or '*', got {next_}")
1243 def skip_whitespace(self) -> None:
1244 # A comment between two whitespace runs yields two consecutive
1245 # whitespace tokens, so a single check is not enough.
1246 while self.peek().type == "S":
1247 self.next()