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 operator
17import re
18import sys
19from typing import TYPE_CHECKING, Literal, Protocol, TypeAlias, Union, cast, overload
21if TYPE_CHECKING:
22 from collections.abc import Iterable, Iterator, Sequence
24 # typing.Self requires Python 3.11
25 from typing_extensions import Self
28def ascii_lower(string: str) -> str:
29 """Lower-case, but only in the ASCII range."""
30 return string.encode("utf8").lower().decode("utf8")
33class SelectorError(Exception):
34 """Common parent for :class:`SelectorSyntaxError` and
35 :class:`ExpressionError`.
37 You can just use ``except SelectorError:`` when calling
38 :meth:`~GenericTranslator.css_to_xpath` and handle both exceptions types.
40 """
43class SelectorSyntaxError(SelectorError, SyntaxError):
44 """Parsing a selector that does not match the grammar."""
47#### Parsed objects
49Tree: TypeAlias = Union[
50 "Element",
51 "Hash",
52 "Class",
53 "Function",
54 "Pseudo",
55 "Attrib",
56 "Negation",
57 "Relation",
58 "Matching",
59 "SpecificityAdjustment",
60 "CombinedSelector",
61]
62PseudoElement: TypeAlias = Union["FunctionalPseudoElement", str]
65class Selector:
66 """
67 Represents a parsed selector.
69 :meth:`~GenericTranslator.selector_to_xpath` accepts this object,
70 but ignores :attr:`pseudo_element`. It is the user’s responsibility
71 to account for pseudo-elements and reject selectors with unknown
72 or unsupported pseudo-elements.
74 """
76 def __init__(self, tree: Tree, pseudo_element: PseudoElement | None = None) -> None:
77 self.parsed_tree = tree
78 if pseudo_element is not None and not isinstance(
79 pseudo_element, FunctionalPseudoElement
80 ):
81 pseudo_element = ascii_lower(pseudo_element)
82 #: A :class:`FunctionalPseudoElement`,
83 #: or the identifier for the pseudo-element as a string,
84 # or ``None``.
85 #:
86 #: +-------------------------+----------------+--------------------------------+
87 #: | | Selector | Pseudo-element |
88 #: +=========================+================+================================+
89 #: | CSS3 syntax | ``a::before`` | ``'before'`` |
90 #: +-------------------------+----------------+--------------------------------+
91 #: | Older syntax | ``a:before`` | ``'before'`` |
92 #: +-------------------------+----------------+--------------------------------+
93 #: | From the Lists3_ draft, | ``li::marker`` | ``'marker'`` |
94 #: | not in Selectors3 | | |
95 #: +-------------------------+----------------+--------------------------------+
96 #: | Invalid pseudo-class | ``li:marker`` | ``None`` |
97 #: +-------------------------+----------------+--------------------------------+
98 #: | Functional | ``a::foo(2)`` | ``FunctionalPseudoElement(…)`` |
99 #: +-------------------------+----------------+--------------------------------+
100 #:
101 #: .. _Lists3: http://www.w3.org/TR/2011/WD-css3-lists-20110524/#marker-pseudoelement
102 self.pseudo_element = pseudo_element
104 def __repr__(self) -> str:
105 if isinstance(self.pseudo_element, FunctionalPseudoElement):
106 pseudo_element = repr(self.pseudo_element)
107 elif self.pseudo_element:
108 pseudo_element = f"::{self.pseudo_element}"
109 else:
110 pseudo_element = ""
111 return f"{self.__class__.__name__}[{self.parsed_tree!r}{pseudo_element}]"
113 def canonical(self) -> str:
114 """Return a CSS representation for this selector (a string)"""
115 if isinstance(self.pseudo_element, FunctionalPseudoElement):
116 pseudo_element = f"::{self.pseudo_element.canonical()}"
117 elif self.pseudo_element:
118 pseudo_element = f"::{_serialize_ident(self.pseudo_element)}"
119 else:
120 pseudo_element = ""
121 res = f"{self.parsed_tree.canonical()}{pseudo_element}"
122 # Strip a redundant universal selector from e.g. "*.foo" (but not
123 # from e.g. "* > foo").
124 if len(res) > 1 and res[0] == "*" and res[1] in "#.[:":
125 res = res[1:]
126 return res
128 def specificity(self) -> tuple[int, int, int]:
129 """Return the specificity_ of this selector as a tuple of 3 integers.
131 .. _specificity: http://www.w3.org/TR/selectors/#specificity
133 """
134 a, b, c = self.parsed_tree.specificity()
135 if self.pseudo_element:
136 c += 1
137 return a, b, c
140class Class:
141 """
142 Represents selector.class_name
143 """
145 def __init__(self, selector: Tree, class_name: str) -> None:
146 self.selector = selector
147 self.class_name = class_name
149 def __repr__(self) -> str:
150 return f"{self.__class__.__name__}[{self.selector!r}.{self.class_name}]"
152 def canonical(self) -> str:
153 return f"{self.selector.canonical()}.{_serialize_ident(self.class_name)}"
155 def specificity(self) -> tuple[int, int, int]:
156 a, b, c = self.selector.specificity()
157 b += 1
158 return a, b, c
161class FunctionalPseudoElement:
162 """
163 Represents selector::name(arguments)
165 .. attribute:: name
167 The name (identifier) of the pseudo-element, as a string.
169 .. attribute:: arguments
171 The arguments of the pseudo-element, as a list of tokens.
173 **Note:** tokens are not part of the public API,
174 and may change between cssselect versions.
175 Use at your own risks.
177 """
179 def __init__(self, name: str, arguments: Sequence[Token]):
180 self.name = ascii_lower(name)
181 self.arguments = arguments
183 def __repr__(self) -> str:
184 token_values = [token.value for token in self.arguments]
185 return f"{self.__class__.__name__}[::{self.name}({token_values!r})]"
187 def argument_types(self) -> list[str]:
188 return [token.type for token in self.arguments]
190 def canonical(self) -> str:
191 args = "".join(token.css() for token in self.arguments)
192 return f"{_serialize_ident(self.name)}({args})"
195class Function:
196 """
197 Represents selector:name(expr)
198 """
200 def __init__(self, selector: Tree, name: str, arguments: Sequence[Token]) -> None:
201 self.selector = selector
202 self.name = ascii_lower(name)
203 self.arguments = arguments
205 def __repr__(self) -> str:
206 token_values = [token.value for token in self.arguments]
207 return f"{self.__class__.__name__}[{self.selector!r}:{self.name}({token_values!r})]"
209 def argument_types(self) -> list[str]:
210 return [token.type for token in self.arguments]
212 def canonical(self) -> str:
213 args = "".join(token.css() for token in self.arguments)
214 return f"{self.selector.canonical()}:{_serialize_ident(self.name)}({args})"
216 def specificity(self) -> tuple[int, int, int]:
217 a, b, c = self.selector.specificity()
218 b += 1
219 return a, b, c
222class Pseudo:
223 """
224 Represents selector:ident
225 """
227 def __init__(self, selector: Tree, ident: str) -> None:
228 self.selector = selector
229 self.ident = ascii_lower(ident)
231 def __repr__(self) -> str:
232 return f"{self.__class__.__name__}[{self.selector!r}:{self.ident}]"
234 def canonical(self) -> str:
235 return f"{self.selector.canonical()}:{_serialize_ident(self.ident)}"
237 def specificity(self) -> tuple[int, int, int]:
238 a, b, c = self.selector.specificity()
239 b += 1
240 return a, b, c
243class Negation:
244 """
245 Represents selector:not(subselector)
246 """
248 def __init__(self, selector: Tree, subselector: Tree) -> None:
249 self.selector = selector
250 self.subselector = subselector
252 def __repr__(self) -> str:
253 return f"{self.__class__.__name__}[{self.selector!r}:not({self.subselector!r})]"
255 def canonical(self) -> str:
256 subsel = self.subselector.canonical()
257 # Strip a redundant universal selector from e.g. "*.foo" (but not
258 # from e.g. "* > foo").
259 if len(subsel) > 1 and subsel[0] == "*" and subsel[1] in "#.[:":
260 subsel = subsel[1:]
261 return f"{self.selector.canonical()}:not({subsel})"
263 def specificity(self) -> tuple[int, int, int]:
264 a1, b1, c1 = self.selector.specificity()
265 a2, b2, c2 = self.subselector.specificity()
266 return a1 + a2, b1 + b2, c1 + c2
269class Relation:
270 """
271 Represents selector:has(subselector)
272 """
274 def __init__(self, selector: Tree, combinator: Token, subselector: Selector):
275 self.selector = selector
276 self.combinator = combinator
277 self.subselector = subselector
279 def _combinator_prefix(self) -> str:
280 # The descendant combinator is implicit in :has() arguments.
281 if self.combinator.value == " ":
282 return ""
283 return f"{self.combinator.value} "
285 def __repr__(self) -> str:
286 return (
287 f"{self.__class__.__name__}[{self.selector!r}"
288 f":has({self._combinator_prefix()}{self.subselector!r})]"
289 )
291 def canonical(self) -> str:
292 subsel = self.subselector.canonical()
293 if len(subsel) > 1:
294 subsel = subsel.lstrip("*")
295 return f"{self.selector.canonical()}:has({self._combinator_prefix()}{subsel})"
297 def specificity(self) -> tuple[int, int, int]:
298 a1, b1, c1 = self.selector.specificity()
299 a2, b2, c2 = self.subselector.specificity()
300 return a1 + a2, b1 + b2, c1 + c2
303class Matching:
304 """
305 Represents selector:is(selector_list)
306 """
308 def __init__(self, selector: Tree, selector_list: Iterable[Tree]):
309 self.selector = selector
310 self.selector_list = selector_list
312 def __repr__(self) -> str:
313 args_str = ", ".join(repr(s) for s in self.selector_list)
314 return f"{self.__class__.__name__}[{self.selector!r}:is({args_str})]"
316 def canonical(self) -> str:
317 selector_arguments = []
318 for s in self.selector_list:
319 selarg = s.canonical()
320 if len(selarg) > 1:
321 selarg = selarg.lstrip("*")
322 selector_arguments.append(selarg)
323 args_str = ", ".join(selector_arguments)
324 return f"{self.selector.canonical()}:is({args_str})"
326 def specificity(self) -> tuple[int, int, int]:
327 a1, b1, c1 = self.selector.specificity()
328 a2, b2, c2 = max(x.specificity() for x in self.selector_list)
329 return a1 + a2, b1 + b2, c1 + c2
332class SpecificityAdjustment:
333 """
334 Represents selector:where(selector_list)
335 Same as selector:is(selector_list), but its specificity is always 0
336 """
338 def __init__(self, selector: Tree, selector_list: list[Tree]):
339 self.selector = selector
340 self.selector_list = selector_list
342 def __repr__(self) -> str:
343 args_str = ", ".join(repr(s) for s in self.selector_list)
344 return f"{self.__class__.__name__}[{self.selector!r}:where({args_str})]"
346 def canonical(self) -> str:
347 selector_arguments = []
348 for s in self.selector_list:
349 selarg = s.canonical()
350 if len(selarg) > 1:
351 selarg = selarg.lstrip("*")
352 selector_arguments.append(selarg)
353 args_str = ", ".join(selector_arguments)
354 return f"{self.selector.canonical()}:where({args_str})"
356 def specificity(self) -> tuple[int, int, int]:
357 # :where() itself contributes no specificity, but the compound
358 # selector it applies to does.
359 return self.selector.specificity()
362class Attrib:
363 """
364 Represents selector[namespace|attrib operator value]
365 """
367 @overload
368 def __init__(
369 self,
370 selector: Tree,
371 namespace: str | None,
372 attrib: str,
373 operator: Literal["exists"],
374 value: None,
375 ) -> None: ...
377 @overload
378 def __init__(
379 self,
380 selector: Tree,
381 namespace: str | None,
382 attrib: str,
383 operator: str,
384 value: Token,
385 ) -> None: ...
387 def __init__(
388 self,
389 selector: Tree,
390 namespace: str | None,
391 attrib: str,
392 operator: str,
393 value: Token | None,
394 ) -> None:
395 self.selector = selector
396 self.namespace = namespace
397 self.attrib = attrib
398 self.operator = operator
399 self.value = value
401 def __repr__(self) -> str:
402 attrib = f"{self.namespace}|{self.attrib}" if self.namespace else self.attrib
403 if self.operator == "exists":
404 return f"{self.__class__.__name__}[{self.selector!r}[{attrib}]]"
405 assert self.value is not None
406 return f"{self.__class__.__name__}[{self.selector!r}[{attrib} {self.operator} {self.value.value!r}]]"
408 def canonical(self) -> str:
409 attrib = _serialize_ident(self.attrib)
410 if self.namespace:
411 attrib = f"{_serialize_ident(self.namespace)}|{attrib}"
413 if self.operator == "exists":
414 op = attrib
415 else:
416 assert self.value is not None
417 op = f"{attrib}{self.operator}{self.value.css()}"
419 return f"{self.selector.canonical()}[{op}]"
421 def specificity(self) -> tuple[int, int, int]:
422 a, b, c = self.selector.specificity()
423 b += 1
424 return a, b, c
427class Element:
428 """
429 Represents namespace|element
431 `None` is for the universal selector '*'
433 """
435 def __init__(
436 self, namespace: str | None = None, element: str | None = None
437 ) -> None:
438 self.namespace = namespace
439 self.element = element
441 def __repr__(self) -> str:
442 return f"{self.__class__.__name__}[{self.canonical()}]"
444 def canonical(self) -> str:
445 element = _serialize_ident(self.element) if self.element else "*"
446 if self.namespace:
447 element = f"{_serialize_ident(self.namespace)}|{element}"
448 return element
450 def specificity(self) -> tuple[int, int, int]:
451 if self.element:
452 return 0, 0, 1
453 return 0, 0, 0
456class Hash:
457 """
458 Represents selector#id
459 """
461 def __init__(self, selector: Tree, id: str) -> None: # noqa: A002
462 self.selector = selector
463 self.id = id
465 def __repr__(self) -> str:
466 return f"{self.__class__.__name__}[{self.selector!r}#{self.id}]"
468 def canonical(self) -> str:
469 return f"{self.selector.canonical()}#{_serialize_ident(self.id)}"
471 def specificity(self) -> tuple[int, int, int]:
472 a, b, c = self.selector.specificity()
473 a += 1
474 return a, b, c
477class CombinedSelector:
478 def __init__(self, selector: Tree, combinator: str, subselector: Tree) -> None:
479 assert selector is not None
480 self.selector = selector
481 self.combinator = combinator
482 self.subselector = subselector
484 def __repr__(self) -> str:
485 comb = "<followed>" if self.combinator == " " else self.combinator
486 return (
487 f"{self.__class__.__name__}[{self.selector!r} {comb} {self.subselector!r}]"
488 )
490 def canonical(self) -> str:
491 subsel = self.subselector.canonical()
492 if len(subsel) > 1:
493 subsel = subsel.lstrip("*")
494 combinator = " " if self.combinator == " " else f" {self.combinator} "
495 return f"{self.selector.canonical()}{combinator}{subsel}"
497 def specificity(self) -> tuple[int, int, int]:
498 a1, b1, c1 = self.selector.specificity()
499 a2, b2, c2 = self.subselector.specificity()
500 return a1 + a2, b1 + b2, c1 + c2
503#### Parser
505# foo
506_el_re = re.compile(r"^[ \t\r\n\f]*([a-zA-Z]+)[ \t\r\n\f]*$")
508# foo#bar or #bar
509_id_re = re.compile(r"^[ \t\r\n\f]*([a-zA-Z]*)#([a-zA-Z0-9_-]+)[ \t\r\n\f]*$")
511# foo.bar or .bar
512_class_re = re.compile(
513 r"^[ \t\r\n\f]*([a-zA-Z]*)\.([a-zA-Z][a-zA-Z0-9_-]*)[ \t\r\n\f]*$"
514)
517def parse(css: str) -> list[Selector]:
518 """Parse a CSS *group of selectors*.
520 If you don't care about pseudo-elements or selector specificity,
521 you can skip this and use :meth:`~GenericTranslator.css_to_xpath`.
523 :param css:
524 A *group of selectors* as a string.
525 :raises:
526 :class:`SelectorSyntaxError` on invalid selectors.
527 :returns:
528 A list of parsed :class:`Selector` objects, one for each
529 selector in the comma-separated group.
531 """
532 # Fast path for simple cases
533 match = _el_re.match(css)
534 if match:
535 return [Selector(Element(element=match.group(1)))]
536 match = _id_re.match(css)
537 if match is not None:
538 return [Selector(Hash(Element(element=match.group(1) or None), match.group(2)))]
539 match = _class_re.match(css)
540 if match is not None:
541 return [
542 Selector(Class(Element(element=match.group(1) or None), match.group(2)))
543 ]
545 stream = TokenStream(tokenize(css))
546 stream.source = css
547 return list(parse_selector_group(stream))
550# except SelectorSyntaxError:
551# e = sys.exc_info()[1]
552# message = "%s at %s -> %r" % (
553# e, stream.used, stream.peek())
554# e.msg = message
555# e.args = tuple([message])
556# raise
559def parse_selector_group(stream: TokenStream) -> Iterator[Selector]:
560 stream.skip_whitespace()
561 while 1:
562 yield Selector(*parse_selector(stream))
563 if stream.peek() == ("DELIM", ","):
564 stream.next()
565 stream.skip_whitespace()
566 else:
567 break
570def parse_selector(stream: TokenStream) -> tuple[Tree, PseudoElement | None]:
571 result, pseudo_element = parse_simple_selector(stream)
572 while 1:
573 stream.skip_whitespace()
574 peek = stream.peek()
575 if peek in (("EOF", None), ("DELIM", ",")):
576 break
577 if pseudo_element:
578 raise SelectorSyntaxError(
579 f"Got pseudo-element ::{pseudo_element} not at the end of a selector"
580 )
581 if peek.is_delim("+", ">", "~"):
582 # A combinator
583 combinator = cast("str", stream.next().value)
584 stream.skip_whitespace()
585 else:
586 # By exclusion, the last parse_simple_selector() ended
587 # at peek == ' '
588 combinator = " "
589 next_selector, pseudo_element = parse_simple_selector(stream)
590 result = CombinedSelector(result, combinator, next_selector)
591 return result, pseudo_element
594def parse_simple_selector(
595 stream: TokenStream,
596 inside_negation: bool = False,
597 inside_selector_list: bool = False,
598) -> tuple[Tree, PseudoElement | None]:
599 stream.skip_whitespace()
600 selector_start = len(stream.used)
601 peek = stream.peek()
602 if peek.type == "IDENT" or peek == ("DELIM", "*"):
603 if peek.type == "IDENT":
604 namespace = stream.next().value
605 else:
606 stream.next()
607 namespace = None
608 if stream.peek() == ("DELIM", "|"):
609 stream.next()
610 element = stream.next_ident_or_star()
611 else:
612 element = namespace
613 namespace = None
614 else:
615 element = namespace = None
616 result: Tree = Element(namespace, element)
617 pseudo_element: PseudoElement | None = None
618 while 1:
619 peek = stream.peek()
620 if (
621 peek.type in ("S", "EOF")
622 or peek.is_delim(",", "+", ">", "~")
623 or (inside_negation and peek == ("DELIM", ")"))
624 ):
625 break
626 if pseudo_element:
627 raise SelectorSyntaxError(
628 f"Got pseudo-element ::{pseudo_element} not at the end of a selector"
629 )
630 if peek.type == "HASH":
631 result = Hash(result, cast("str", stream.next().value))
632 elif peek == ("DELIM", "."):
633 stream.next()
634 result = Class(result, stream.next_ident())
635 elif peek == ("DELIM", "|"):
636 # The explicit "no namespace" syntax, e.g. |div: only valid at
637 # the very start of a simple selector.
638 if len(stream.used) != selector_start:
639 raise SelectorSyntaxError(f"Expected selector, got {peek}")
640 stream.next()
641 result = Element(None, stream.next_ident_or_star())
642 elif peek == ("DELIM", "["):
643 stream.next()
644 result = parse_attrib(result, stream)
645 elif peek == ("DELIM", ":"):
646 stream.next()
647 if stream.peek() == ("DELIM", ":"):
648 stream.next()
649 pseudo_element = stream.next_ident()
650 if stream.peek() == ("DELIM", "("):
651 stream.next()
652 pseudo_element = FunctionalPseudoElement(
653 pseudo_element, parse_arguments(stream)
654 )
655 continue
656 ident = stream.next_ident()
657 if ident.lower() in ("first-line", "first-letter", "before", "after"):
658 # Special case: CSS 2.1 pseudo-elements can have a single ':'
659 # Any new pseudo-element must have two.
660 pseudo_element = str(ident)
661 continue
662 if stream.peek() != ("DELIM", "("):
663 result = Pseudo(result, ident)
664 if result.ident == "scope":
665 # :scope is only supported at the start of a selector,
666 # i.e. never in :is()/:where()/:matches() arguments
667 # (where a preceding comma separates arguments, not
668 # selectors), and otherwise only when the tokens
669 # preceding its compound selector are the start of the
670 # input or a comma.
671 preceding = stream.used[:selector_start]
672 while preceding and preceding[-1].type == "S":
673 preceding = preceding[:-1]
674 if inside_selector_list or (
675 preceding and not preceding[-1].is_delim(",")
676 ):
677 raise SelectorSyntaxError(
678 'Got pseudo-class ":scope" not at the start of a selector'
679 )
680 continue
681 stream.next()
682 stream.skip_whitespace()
683 if ident.lower() == "not":
684 if inside_selector_list:
685 raise SelectorSyntaxError(
686 ":not() is not supported inside :is(), :where() and :matches()"
687 )
688 if inside_negation:
689 raise SelectorSyntaxError("Got nested :not()")
690 argument, argument_pseudo_element = parse_simple_selector(
691 stream, inside_negation=True
692 )
693 while 1:
694 # Whitespace before the closing parenthesis is not a
695 # descendant combinator.
696 stream.skip_whitespace()
697 peek = stream.peek()
698 if argument_pseudo_element:
699 raise SelectorSyntaxError(
700 f"Got pseudo-element ::{argument_pseudo_element} inside :not() at {peek.pos}"
701 )
702 if peek == ("DELIM", ")"):
703 stream.next()
704 break
705 if peek.is_delim("+", ">", "~"):
706 argument_combinator = cast("str", stream.next().value)
707 stream.skip_whitespace()
708 elif peek.type == "EOF" or peek.is_delim(","):
709 # A selector list is not supported in :not().
710 raise SelectorSyntaxError(f"Expected ')', got {peek}")
711 else:
712 argument_combinator = " "
713 next_selector, argument_pseudo_element = parse_simple_selector(
714 stream, inside_negation=True
715 )
716 argument = CombinedSelector(
717 argument, argument_combinator, next_selector
718 )
719 result = Negation(result, argument)
720 elif ident.lower() == "has":
721 combinator, arguments = parse_relative_selector(stream)
722 result = Relation(result, combinator, arguments)
724 elif ident.lower() in ("matches", "is"):
725 selectors = parse_simple_selector_arguments(stream)
726 result = Matching(result, selectors)
727 elif ident.lower() == "where":
728 selectors = parse_simple_selector_arguments(stream)
729 result = SpecificityAdjustment(result, selectors)
730 else:
731 result = Function(result, ident, parse_arguments(stream))
732 else:
733 raise SelectorSyntaxError(f"Expected selector, got {peek}")
734 if len(stream.used) == selector_start:
735 raise SelectorSyntaxError(f"Expected selector, got {stream.peek()}")
736 return result, pseudo_element
739def parse_arguments(stream: TokenStream) -> list[Token]: # noqa: RET503
740 arguments: list[Token] = []
741 while 1:
742 stream.skip_whitespace()
743 next_ = stream.next()
744 if next_.type in ("IDENT", "STRING", "NUMBER") or next_ in [
745 ("DELIM", "+"),
746 ("DELIM", "-"),
747 ]:
748 arguments.append(next_)
749 elif next_ == ("DELIM", ")"):
750 return arguments
751 else:
752 raise SelectorSyntaxError(f"Expected an argument, got {next_}")
755def parse_relative_selector(stream: TokenStream) -> tuple[Token, Selector]:
756 stream.skip_whitespace()
757 subselector_tokens: list[Token] = []
758 next_ = stream.next()
760 if next_ in [("DELIM", "+"), ("DELIM", ">"), ("DELIM", "~")]:
761 combinator = next_
762 stream.skip_whitespace()
763 next_ = stream.next()
764 else:
765 combinator = Token("DELIM", " ", pos=0)
767 seen_whitespace = False
768 while 1:
769 if next_.type == "S":
770 # Whitespace is valid before the closing parenthesis; anywhere
771 # else it would be a descendant combinator, which is not
772 # supported in :has() arguments.
773 seen_whitespace = True
774 elif next_.type == "IDENT" or next_ in [("DELIM", "."), ("DELIM", "*")]:
775 if seen_whitespace:
776 raise SelectorSyntaxError(f"Expected an argument, got {next_}")
777 subselector_tokens.append(next_)
778 elif next_ == ("DELIM", ")"):
779 break
780 else:
781 raise SelectorSyntaxError(f"Expected an argument, got {next_}")
782 next_ = stream.next()
784 # Reparse the collected tokens instead of their concatenated source
785 # text, so that escaped identifiers are preserved.
786 subselector_tokens.append(EOFToken(next_.pos))
787 result, _ = parse_simple_selector(TokenStream(subselector_tokens))
788 return combinator, Selector(result)
791def parse_simple_selector_arguments(stream: TokenStream) -> list[Tree]:
792 arguments = []
793 while 1:
794 result, pseudo_element = parse_simple_selector(
795 stream, inside_negation=True, inside_selector_list=True
796 )
797 if pseudo_element:
798 raise SelectorSyntaxError(
799 f"Got pseudo-element ::{pseudo_element} inside function"
800 )
801 stream.skip_whitespace()
802 next_ = stream.next()
803 if next_ == ("DELIM", ","):
804 stream.skip_whitespace()
805 arguments.append(result)
806 elif next_ == ("DELIM", ")"):
807 arguments.append(result)
808 break
809 else:
810 raise SelectorSyntaxError(f"Expected an argument, got {next_}")
811 return arguments
814def parse_attrib(selector: Tree, stream: TokenStream) -> Attrib:
815 stream.skip_whitespace()
816 attrib = stream.next_ident_or_star()
817 if attrib is None and stream.peek() != ("DELIM", "|"):
818 raise SelectorSyntaxError(f"Expected '|', got {stream.peek()}")
819 namespace: str | None
820 op: str | None
821 if stream.peek() == ("DELIM", "|"):
822 stream.next()
823 if stream.peek() == ("DELIM", "="):
824 namespace = None
825 stream.next()
826 op = "|="
827 else:
828 namespace = attrib
829 attrib = stream.next_ident()
830 op = None
831 else:
832 namespace = op = None
833 if op is None:
834 stream.skip_whitespace()
835 next_ = stream.next()
836 if next_ == ("DELIM", "]"):
837 return Attrib(selector, namespace, cast("str", attrib), "exists", None)
838 if next_ == ("DELIM", "="):
839 op = "="
840 elif next_.is_delim("^", "$", "*", "~", "|", "!") and (
841 stream.peek() == ("DELIM", "=")
842 ):
843 op = cast("str", next_.value) + "="
844 stream.next()
845 else:
846 raise SelectorSyntaxError(f"Operator expected, got {next_}")
847 stream.skip_whitespace()
848 value = stream.next()
849 if value.type not in ("IDENT", "STRING"):
850 raise SelectorSyntaxError(f"Expected string or ident, got {value}")
851 stream.skip_whitespace()
852 next_ = stream.next()
853 if next_ != ("DELIM", "]"):
854 raise SelectorSyntaxError(f"Expected ']', got {next_}")
855 return Attrib(selector, namespace, cast("str", attrib), op, value)
858def parse_series(tokens: Iterable[Token]) -> tuple[int, int]:
859 """Parses the arguments for :nth-child() and friends."""
860 for token in tokens:
861 if token.type == "STRING":
862 raise ValueError("String tokens not allowed in series.")
863 # The An+B microsyntax is ASCII-case-insensitive: 2N+1, EVEN, Odd...
864 s = ascii_lower("".join(cast("str", token.value) for token in tokens).strip())
865 if s == "odd":
866 return 2, 1
867 if s == "even":
868 return 2, 0
869 if s == "n":
870 return 1, 0
871 if "n" not in s:
872 # Just b
873 return 0, int(s)
874 a, b = s.split("n", 1)
875 a_as_int: int
876 if not a:
877 a_as_int = 1
878 elif a in {"-", "+"}:
879 a_as_int = int(a + "1")
880 else:
881 a_as_int = int(a)
882 b_as_int = int(b) if b else 0
883 return a_as_int, b_as_int
886#### Token objects
889class Token(tuple[str, str | None]): # noqa: SLOT001
890 @overload
891 def __new__(
892 cls,
893 type_: Literal["IDENT", "HASH", "STRING", "S", "DELIM", "NUMBER"],
894 value: str,
895 pos: int,
896 ) -> Self: ...
898 @overload
899 def __new__(cls, type_: Literal["EOF"], value: None, pos: int) -> Self: ...
901 def __new__(cls, type_: str, value: str | None, pos: int) -> Self:
902 obj = tuple.__new__(cls, (type_, value))
903 obj.pos = pos
904 return obj
906 def __repr__(self) -> str:
907 return f"<{self.type} '{self.value}' at {self.pos}>"
909 def is_delim(self, *values: str) -> bool:
910 return self.type == "DELIM" and self.value in values
912 pos: int
914 @property
915 def type(self) -> str:
916 return self[0]
918 @property
919 def value(self) -> str | None:
920 return self[1]
922 def css(self) -> str:
923 if self.type == "STRING":
924 # Escape as CSS (repr() would use Python escapes, which mean
925 # something else in CSS, e.g. '\n' is just the letter 'n').
926 escaped = cast("str", self.value).replace("\\", "\\\\").replace("'", "\\'")
927 escaped = _sub_string_control_char(_replace_string_control_char, escaped)
928 return f"'{escaped}'"
929 if self.type == "IDENT":
930 return _serialize_ident(cast("str", self.value))
931 return cast("str", self.value)
934class EOFToken(Token):
935 def __new__(cls, pos: int) -> Self:
936 return Token.__new__(cls, "EOF", None, pos)
938 def __repr__(self) -> str:
939 return f"<{self.type} at {self.pos}>"
942#### Tokenizer
945class TokenMacros:
946 unicode_escape = r"\\([0-9a-f]{1,6})(?:\r\n|[ \n\r\t\f])?"
947 escape = unicode_escape + r"|\\[^\n\r\f0-9a-f]"
948 string_escape = r"\\(?:\n|\r\n|\r|\f)|" + escape
949 nonascii = r"[^\0-\177]"
950 nmchar = f"[_a-z0-9-]|{escape}|{nonascii}"
951 nmstart = f"[_a-z]|{escape}|{nonascii}"
954class MatchFunc(Protocol):
955 def __call__(
956 self, string: str, pos: int = ..., endpos: int = ...
957 ) -> re.Match[str] | None: ...
960def _compile(pattern: str) -> MatchFunc:
961 return re.compile(pattern % vars(TokenMacros), re.IGNORECASE).match
964_match_whitespace = _compile(r"[ \t\r\n\f]+")
965_match_number = _compile(r"[+-]?(?:[0-9]*\.[0-9]+|[0-9]+)")
966_match_hash = _compile("#(?:%(nmchar)s)+")
967_match_ident = _compile("-?(?:%(nmstart)s)(?:%(nmchar)s)*")
968_match_string_by_quote = {
969 "'": _compile(r"([^\n\r\f\\']|%(string_escape)s)*"),
970 '"': _compile(r'([^\n\r\f\\"]|%(string_escape)s)*'),
971}
973_sub_simple_escape = re.compile(r"\\(.)").sub
974_sub_unicode_escape = re.compile(TokenMacros.unicode_escape, re.IGNORECASE).sub
975_sub_newline_escape = re.compile(r"\\(?:\n|\r\n|\r|\f)").sub
976_sub_string_control_char = re.compile(r"[\x00-\x1f\x7f]").sub
978# Same as r'\1', but faster on CPython
979_replace_simple = operator.methodcaller("group", 1)
982def _replace_unicode(match: re.Match[str]) -> str:
983 codepoint = int(match.group(1), 16)
984 if codepoint > sys.maxunicode or 0xD800 <= codepoint <= 0xDFFF:
985 codepoint = 0xFFFD
986 return chr(codepoint)
989def _replace_string_control_char(match: re.Match[str]) -> str:
990 # The trailing space ends the escape sequence, in case the next
991 # character is a hexadecimal digit.
992 return f"\\{ord(match.group()):x} "
995def unescape_ident(value: str) -> str:
996 value = _sub_unicode_escape(_replace_unicode, value)
997 return _sub_simple_escape(_replace_simple, value)
1000def _serialize_ident(value: str) -> str:
1001 """Serialize a string as a CSS identifier, escaping special characters.
1003 Implements the CSSOM "serialize an identifier" algorithm:
1004 https://drafts.csswg.org/cssom/#serialize-an-identifier
1005 """
1006 result = []
1007 for i, char in enumerate(value):
1008 code = ord(char)
1009 serialized = char
1010 if code == 0:
1011 serialized = "\N{REPLACEMENT CHARACTER}"
1012 elif code <= 0x1F or code == 0x7F:
1013 serialized = f"\\{code:x} "
1014 elif "0" <= char <= "9":
1015 if i == 0 or (i == 1 and value[0] == "-"):
1016 # An identifier cannot start with a digit
1017 # (or a '-' followed by a digit).
1018 serialized = f"\\{code:x} "
1019 elif char == "-":
1020 if len(value) == 1 or (i == 0 and value[1] == "-"):
1021 # CSSOM leaves a leading "--" unescaped (such identifiers
1022 # are valid since CSS Syntax 3), but the tokenizer only
1023 # implements the CSS 2.1 identifier grammar and would not
1024 # be able to parse the result, so escape the first "-".
1025 serialized = "\\-"
1026 elif not (
1027 code >= 0x80 or char == "_" or "a" <= char <= "z" or "A" <= char <= "Z"
1028 ):
1029 serialized = f"\\{char}"
1030 result.append(serialized)
1031 return "".join(result)
1034def tokenize(s: str) -> Iterator[Token]:
1035 pos = 0
1036 len_s = len(s)
1037 while pos < len_s:
1038 match = _match_whitespace(s, pos=pos)
1039 if match:
1040 yield Token("S", " ", pos)
1041 pos = match.end()
1042 continue
1044 match = _match_ident(s, pos=pos)
1045 if match:
1046 value = unescape_ident(match.group())
1047 yield Token("IDENT", value, pos)
1048 pos = match.end()
1049 continue
1051 match = _match_hash(s, pos=pos)
1052 if match:
1053 value = unescape_ident(match.group()[1:])
1054 yield Token("HASH", value, pos)
1055 pos = match.end()
1056 continue
1058 quote = s[pos]
1059 if quote in _match_string_by_quote:
1060 match = _match_string_by_quote[quote](s, pos=pos + 1)
1061 assert match, "Should have found at least an empty match"
1062 end_pos = match.end()
1063 if end_pos == len_s:
1064 raise SelectorSyntaxError(f"Unclosed string at {pos}")
1065 if s[end_pos] != quote:
1066 raise SelectorSyntaxError(f"Invalid string at {pos}")
1067 value = _sub_simple_escape(
1068 _replace_simple,
1069 _sub_unicode_escape(
1070 _replace_unicode, _sub_newline_escape("", match.group())
1071 ),
1072 )
1073 yield Token("STRING", value, pos)
1074 pos = end_pos + 1
1075 continue
1077 match = _match_number(s, pos=pos)
1078 if match:
1079 value = match.group()
1080 yield Token("NUMBER", value, pos)
1081 pos = match.end()
1082 continue
1084 pos2 = pos + 2
1085 if s[pos:pos2] == "/*":
1086 pos = s.find("*/", pos2)
1087 if pos == -1:
1088 pos = len_s
1089 else:
1090 pos += 2
1091 continue
1093 yield Token("DELIM", s[pos], pos)
1094 pos += 1
1096 assert pos == len_s
1097 yield EOFToken(pos)
1100class TokenStream:
1101 def __init__(self, tokens: Iterable[Token], source: str | None = None) -> None:
1102 self.used: list[Token] = []
1103 self.tokens = iter(tokens)
1104 self.source = source
1105 self.peeked: Token | None = None
1106 self._peeking = False
1107 self.next_token = self.tokens.__next__
1109 def next(self) -> Token:
1110 if self._peeking:
1111 self._peeking = False
1112 assert self.peeked is not None
1113 self.used.append(self.peeked)
1114 return self.peeked
1115 next_ = self.next_token()
1116 self.used.append(next_)
1117 return next_
1119 def peek(self) -> Token:
1120 if not self._peeking:
1121 self.peeked = self.next_token()
1122 self._peeking = True
1123 assert self.peeked is not None
1124 return self.peeked
1126 def next_ident(self) -> str:
1127 next_ = self.next()
1128 if next_.type != "IDENT":
1129 raise SelectorSyntaxError(f"Expected ident, got {next_}")
1130 return cast("str", next_.value)
1132 def next_ident_or_star(self) -> str | None:
1133 next_ = self.next()
1134 if next_.type == "IDENT":
1135 return next_.value
1136 if next_ == ("DELIM", "*"):
1137 return None
1138 raise SelectorSyntaxError(f"Expected ident or '*', got {next_}")
1140 def skip_whitespace(self) -> None:
1141 # A comment between two whitespace runs yields two consecutive
1142 # whitespace tokens, so a single check is not enough.
1143 while self.peek().type == "S":
1144 self.next()