Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/soupsieve/css_parser.py: 26%
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"""CSS selector parser."""
2from __future__ import annotations
3import re
4from functools import lru_cache
5from . import util
6from . import css_match as cm
7from . import css_types as ct
8from .util import SelectorSyntaxError
9import warnings
10from typing import Match, Any, Callable, Iterator, Iterable, Mapping, cast
11from dataclasses import dataclass
12from collections import UserDict
13import threading
15__all__ = ('SelectorSyntaxError',)
17RE_LOCK = threading.Lock()
18SEL_LOCK = threading.RLock()
20UNICODE_REPLACEMENT_CHAR = 0xFFFD
22# Non-ASCII identifier code point ranges (inclusive): https://drafts.csswg.org/css-syntax-3/#non-ascii-ident-code-point
23NON_ASCII_IDENT_RANGES = (
24 (0x00B7, 0x00B7),
25 (0x00C0, 0x00D6),
26 (0x00D8, 0x00F6),
27 (0x00F8, 0x037D),
28 (0x037F, 0x1FFF),
29 (0x200C, 0x200D),
30 (0x203F, 0x2040),
31 (0x2070, 0x218F),
32 (0x2C00, 0x2FEF),
33 (0x3001, 0xD7FF),
34 (0xF900, 0xFDCF),
35 (0xFDF0, 0xFFFD),
36 (0x10000, 0x10FFFF)
37)
39SELECTOR_LIMIT = 8192
41# Simple pseudo classes that take no parameters
42PSEUDO_SIMPLE = {
43 ":any-link",
44 ":empty",
45 ":first-child",
46 ":first-of-type",
47 ":in-range",
48 ":open",
49 ":out-of-range",
50 ":last-child",
51 ":last-of-type",
52 ":link",
53 ":only-child",
54 ":only-of-type",
55 ":root",
56 ':checked',
57 ':default',
58 ':disabled',
59 ':enabled',
60 ':indeterminate',
61 ':optional',
62 ':placeholder-shown',
63 ':read-only',
64 ':read-write',
65 ':required',
66 ':scope',
67 ':defined',
68 ':muted'
69}
71# Supported, simple pseudo classes that match nothing in the Soup Sieve environment
72PSEUDO_SIMPLE_NO_MATCH = {
73 ':active',
74 ':autofill',
75 ':buffering',
76 ':current',
77 ':focus',
78 ':focus-visible',
79 ':focus-within',
80 ':fullscreen',
81 ':future',
82 ':host',
83 ':hover',
84 ':local-link',
85 ':past',
86 ':paused',
87 ':picture-in-picture',
88 ':playing',
89 ':popover-open',
90 ':seeking',
91 ':stalled',
92 ':target',
93 ':target-within',
94 ':user-invalid',
95 ':volume-locked',
96 ':visited'
97}
99# Attributes whose values are matched case insensitively on HTML elements in HTML documents
100# https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors
101HTML_CASE_INSENSITIVE_ATTRIBUTES = frozenset(
102 (
103 'accept', 'accept-charset', 'align', 'alink', 'axis', 'bgcolor', 'charset', 'checked', 'clear',
104 'codetype', 'color', 'compact', 'declare', 'defer', 'dir', 'direction', 'disabled', 'enctype',
105 'face', 'frame', 'hreflang', 'http-equiv', 'lang', 'language', 'link', 'media', 'method',
106 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'rel', 'rev', 'rules', 'scope',
107 'scrolling', 'selected', 'shape', 'target', 'text', 'type', 'valign', 'valuetype', 'vlink'
108 )
109)
111# Complex pseudo classes that take selector lists
112PSEUDO_COMPLEX = {
113 ':contains',
114 ':-soup-contains',
115 ':-soup-contains-own',
116 ':has',
117 ':is',
118 ':matches',
119 ':not',
120 ':where'
121}
123PSEUDO_COMPLEX_NO_MATCH = {
124 ':current',
125 ':host',
126 ':host-context'
127}
129# Complex pseudo classes that take very specific parameters and are handled special
130PSEUDO_SPECIAL = {
131 ':dir',
132 ':lang',
133 ':nth-child',
134 ':nth-last-child',
135 ':nth-last-of-type',
136 ':nth-of-type'
137}
139PSEUDO_SUPPORTED = PSEUDO_SIMPLE | PSEUDO_SIMPLE_NO_MATCH | PSEUDO_COMPLEX | PSEUDO_COMPLEX_NO_MATCH | PSEUDO_SPECIAL
141# Sub-patterns parts
142# Whitespace
143NEWLINE = r'(?>\r\n|[\n\f\r])'
144WS = fr'(?:[ \t]|{NEWLINE})'
145# Comments
146COMMENTS = r'(?:/\*(?:[^*]|\*(?!/))*+\*/)'
147# Whitespace with comments included
148WSC = fr'(?:{WS}|{COMMENTS})'
149# CSS escapes
150CSS_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}+{WS}?+|[^\r\n\f]|$))'
151CSS_STRING_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}+{WS}?+|{NEWLINE}|[^\r\n\f]|$))'
152# CSS identifier code points (browsers treat all non-ASCII code points as identifier code points)
153IDENT_START = r'[A-Za-z_\x80-\U0010ffff]'
154IDENT_CHAR = r'[-0-9A-Za-z_\x80-\U0010ffff]'
155# CSS Identifier
156IDENTIFIER = fr'''
157(?:(?:--|-?+(?:{IDENT_START}|{CSS_ESCAPES}))
158(?:{IDENT_CHAR}|{CSS_ESCAPES})*+)
159'''
160# `nth` content
161NTH = fr'[-+]?+(?:[0-9]++n?+|n)(?:(?<=n){WSC}*+[-+]{WSC}*+[0-9]++)?+'
162# Value: quoted string or identifier
163VALUE = fr'''
164(?:"(?:\\(?:{NEWLINE}|[^\r\n\f])|[^\\"\r\n\f])*+"|
165'(?:\\(?:{NEWLINE}|[^\r\n\f])|[^\\'\r\n\f])*+'|
166{IDENTIFIER})
167'''
168# Attribute value comparison. `!=` is handled special as it is non-standard.
169ATTR = fr'(?:{WSC}*+(?P<cmp>[!~^|*$]?+=){WSC}*+(?P<value>{VALUE})(?:{WSC}*+(?P<case>[is]))?+)?+'
171# Selector patterns
172# IDs (`#id`)
173PAT_ID = fr'\#{IDENTIFIER}'
174# Classes (`.class`)
175PAT_CLASS = fr'\.{IDENTIFIER}'
176# Prefix:Tag (`prefix|tag`)
177PAT_TAG = fr'(?P<tag_ns>(?:{IDENTIFIER}|\*)?+\|)?+(?P<tag_name>{IDENTIFIER}|\*)'
178# Attributes (`[attr]`, `[attr=value]`, etc.)
179PAT_ATTR = fr'\[{WSC}*+(?P<attr_ns>(?:{IDENTIFIER}|\*)?+\|(?!=))?+(?P<attr_name>{IDENTIFIER}){ATTR}{WSC}*+\]'
180# Pseudo class (`:pseudo-class`, `:pseudo-class(`)
181PAT_PSEUDO_CLASS = fr'(?P<name>:{IDENTIFIER})(?P<open>\({WSC}*+)?+'
182# Pseudo class special patterns. Matches `:pseudo-class(` for special case pseudo classes.
183PAT_PSEUDO_CLASS_SPECIAL = fr'(?P<name>:{IDENTIFIER})(?P<open>\({WSC}*+)'
184# Custom pseudo class (`:--custom-pseudo`)
185PAT_PSEUDO_CLASS_CUSTOM = fr'(?P<name>:(?=--){IDENTIFIER})'
186# Nesting ampersand selector. Matches `&`
187PAT_AMP = r'&'
188# Closing pseudo group (`)`)
189PAT_PSEUDO_CLOSE = fr'{WSC}*+\)'
190# Pseudo element (`::pseudo-element`)
191PAT_PSEUDO_ELEMENT = fr':{PAT_PSEUDO_CLASS}'
192# At rule (`@page`, etc.) (not supported)
193PAT_AT_RULE = fr'@P{IDENTIFIER}'
194# Pseudo class `nth-child` (`:nth-child(an+b [of S]?)`, `:first-child`, etc.)
195PAT_PSEUDO_NTH_CHILD = fr'''
196(?P<pseudo_nth_child>{PAT_PSEUDO_CLASS_SPECIAL}
197(?P<nth_child>{NTH}|even|odd))(?:{WSC}*+\)|(?P<of>{COMMENTS}*+{WS}{WSC}*+of{COMMENTS}*+{WS}{WSC}*+))
198'''
199# Pseudo class `nth-of-type` (`:nth-of-type(an+b)`, `:first-of-type`, etc.)
200PAT_PSEUDO_NTH_TYPE = fr'''
201(?P<pseudo_nth_type>{PAT_PSEUDO_CLASS_SPECIAL}
202(?P<nth_type>{NTH}|even|odd)){WSC}*+\)
203'''
204# Pseudo class language (`:lang("*-de", en)`)
205PAT_PSEUDO_LANG = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<values>{VALUE}(?:{WSC}*+,{WSC}*+{VALUE})*+){WSC}*+\)'
206# Pseudo class direction (`:dir(ltr)`)
207PAT_PSEUDO_DIR = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<dir>{IDENTIFIER}){WSC}*+\)'
208# Combining characters (`>`, `~`, ` `, `+`, `,`) along with any comments and whitespace around them.
209# A run of comments and whitespace is matched all at once. `ws` captures whether the run has whitespace
210# before the relation, as a run of only comments does not combine selectors.
211PAT_COMBINE = fr'''
212(?=[,+>~]|{WS}|/\*){COMMENTS}*+(?:(?P<ws>{WS}){WSC}*+)?+(?P<relation>[,+>~])?+{WSC}*+
213'''
214# Extra: Contains (`:contains(text)`)
215PAT_PSEUDO_CONTAINS = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<values>{VALUE}(?:{WSC}*+,{WSC}*+{VALUE})*+){WSC}*+\)'
217# Regular expressions
218# CSS escape pattern
219RE_CSS_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}+{WSC}?+)|(\\[^\r\n\f])|(\\$))', re.I)
220RE_CSS_STR_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}+{WS}?+)|(\\[^\r\n\f])|(\\$)|(\\{NEWLINE}))', re.I)
221# Pattern to break up `nth` specifiers
222RE_NTH = re.compile(fr'(?P<s1>[-+])?+(?P<a>[0-9]++n?+|n)(?:(?<=n){WSC}*+(?P<s2>[-+]){WSC}*+(?P<b>[0-9]++))?+', re.I)
223# Pattern to iterate multiple values.
224RE_VALUES = re.compile(fr'(?:(?P<value>{VALUE})|(?P<split>{WSC}*+,{WSC}*+))', re.X)
225# Whitespace checks
226RE_WS = re.compile(WS)
227RE_CUSTOM = re.compile(fr'^{PAT_PSEUDO_CLASS_CUSTOM}$', re.X)
228RE_PSEUDO_CLASS_SPECIAL = re.compile(PAT_PSEUDO_CLASS_SPECIAL, re.I | re.X | re.U)
229RE_PSEUDO_IGNORE = re.compile(PAT_PSEUDO_CLASS, re.X | re.I | re.U)
231QUOTED = ("'", '"')
233# Constants
234# List split token
235COMMA_COMBINATOR = ','
236# Relation token for descendant
237WS_COMBINATOR = " "
239# Parse flags
240FLG_PSEUDO = 0x01
241FLG_NOT = 0x02
242FLG_RELATIVE = 0x04
243FLG_DEFAULT = 0x08
244FLG_HTML = 0x10
245FLG_INDETERMINATE = 0x20
246FLG_OPEN = 0x40
247FLG_IN_RANGE = 0x80
248FLG_OUT_OF_RANGE = 0x100
249FLG_PLACEHOLDER_SHOWN = 0x200
250FLG_FORGIVE = 0x400
251FLG_EDITABLE = 0x800
252FLG_CHECKED = 0x1000
254# Maximum cached patterns to store
255_MAXCACHE = 500
258@lru_cache(maxsize=_MAXCACHE)
259def _cached_css_compile(
260 pattern: str,
261 namespaces: ct.Namespaces | None,
262 custom: ct.CustomSelectors | None,
263 ignore: tuple[str] | None = None,
264 flags: int = 0,
265 max_selectors: int = SELECTOR_LIMIT
266) -> cm.SoupSieve:
267 """Cached CSS compile."""
269 return cm.SoupSieve(
270 pattern,
271 CSSParser(
272 pattern,
273 custom=custom,
274 ignore=ignore,
275 flags=flags,
276 max_selectors=max_selectors
277 ).process_selectors(),
278 namespaces,
279 custom,
280 flags
281 )
284def _purge_cache() -> None:
285 """Purge the cache."""
287 _cached_css_compile.cache_clear()
290def css_unescape(content: str) -> str:
291 """
292 Unescape CSS value.
294 Strings allow for spanning the value on multiple strings by escaping a new line.
295 """
297 def replace(m: Match[str]) -> str:
298 """Replace with the appropriate substitute."""
300 if m.group(1):
301 codepoint = int(m.group(1)[1:], 16)
302 # A surrogate, or a value past Unicode, is `U+FFFD`. Zero already is.
303 if (
304 codepoint == 0
305 or codepoint > 0x10FFFF
306 or 0xD800 <= codepoint <= 0xDFFF
307 ):
308 codepoint = UNICODE_REPLACEMENT_CHAR
309 value = chr(codepoint)
310 elif m.group(2):
311 value = m.group(2)[1:]
312 elif m.group(3):
313 value = '\ufffd'
314 else:
315 value = ''
317 return value
319 quoted = content.startswith(QUOTED)
320 return (RE_CSS_STR_ESC if quoted else RE_CSS_ESC).sub(
321 replace,
322 content[1:-1] if quoted else content
323 )
326def escape(ident: str) -> str:
327 """Escape identifier."""
329 string = []
330 length = len(ident)
331 start_dash = length > 0 and ident[0] == '-'
332 if length == 1 and start_dash:
333 # Need to escape identifier that is a single `-` with no other characters
334 string.append(f'\\{ident}')
335 else:
336 for index, c in enumerate(ident):
337 codepoint = ord(c)
338 if codepoint == 0x00:
339 string.append('\ufffd')
340 elif (0x01 <= codepoint <= 0x1F) or codepoint == 0x7F:
341 string.append(f'\\{codepoint:x} ')
342 elif (index == 0 or (start_dash and index == 1)) and (0x30 <= codepoint <= 0x39):
343 string.append(f'\\{codepoint:x} ')
344 elif (
345 codepoint in (0x2D, 0x5F) or (0x30 <= codepoint <= 0x39) or
346 (0x41 <= codepoint <= 0x5A) or (0x61 <= codepoint <= 0x7A) or
347 (codepoint >= 0x80 and any(start <= codepoint <= end for start, end in NON_ASCII_IDENT_RANGES))
348 ):
349 string.append(c)
350 else:
351 string.append(f'\\{c}')
352 return ''.join(string)
355class SelectorPattern:
356 """
357 Selector pattern.
359 Patterns are shared by all parsers, so they must not hold any per-match state.
360 `match` returns the token name along with the match; a name of `None` means the
361 match is consumed without being processed further.
362 """
364 def __init__(self, name: str, pattern: str) -> None:
365 """Initialize."""
367 self.name = name
368 self.pattern = pattern
369 self._re_pattern: re.Pattern[str] | None = None
371 @property
372 def re_pattern(self) -> re.Pattern[str]:
373 """Retrieve the compiled regular expression pattern."""
375 with RE_LOCK:
376 if self._re_pattern is None:
377 self._re_pattern = re.compile(self.pattern, re.I | re.X | re.U)
378 return self._re_pattern
380 def match(self, selector: str, index: int, flags: int) -> tuple[str | None, Match[str]] | None:
381 """Match the selector."""
383 m = self.re_pattern.match(selector, index)
384 return None if m is None else (self.name, m)
387class SelectorPatternCombinator(SelectorPattern):
388 """
389 A run of comments and whitespace might be a combinator or might just be consumed.
391 Process all combinators except whitespace at the start or end, and runs of only comments.
392 """
394 def match(self, selector: str, index: int, flags: int) -> tuple[str | None, Match[str]] | None:
395 """
396 Match the selector.
398 If we match at the start or end of the string and have a non-whitespace combinator, the match is bogus.
399 If we are at the start or end and the combinator is white space, then it can be consumed with no further
400 processing. Comments are not whitespace, so a run of only comments is consumed as well.
401 """
403 m = self.re_pattern.match(selector, index)
404 # An unterminated comment matches nothing, which must not be consumed or the parser would never advance.
405 if m is None or m.end(0) == index:
406 return None
407 relation = m.group('relation')
408 if m.start(0) == 0 or m.end(0) == len(selector):
409 return None if relation else (None, m)
410 return (self.name if relation or m.group('ws') else None), m
413class SpecialPseudoPattern(SelectorPattern):
414 """Selector pattern."""
416 def __init__(self, patterns: tuple[tuple[str, tuple[str, ...], str, type[SelectorPattern]], ...]) -> None:
417 """Initialize."""
419 self.patterns = {}
420 for p in patterns:
421 name = p[0]
422 pattern = p[3](name, p[2])
423 for pseudo in p[1]:
424 self.patterns[pseudo] = pattern
426 def match(self, selector: str, index: int, flags: int) -> tuple[str | None, Match[str]] | None:
427 """Match the selector."""
429 m = RE_PSEUDO_CLASS_SPECIAL.match(selector, index)
430 if m:
431 pattern = self.patterns.get(util.lower(css_unescape(m.group('name'))))
432 if pattern:
433 return pattern.match(selector, index, flags)
434 return None
437class _Selector:
438 """
439 Intermediate selector class.
441 This stores selector data for a compound selector as we are acquiring them.
442 Once we are done collecting the data for a compound selector, we freeze
443 the data in an object that can be pickled and hashed.
444 """
446 def __init__(self, **kwargs: Any) -> None:
447 """Initialize."""
449 self.tag: ct.SelectorTag | None = kwargs.get('tag', None)
450 self.ids: list[str] = kwargs.get('ids', [])
451 self.classes: list[str] = kwargs.get('classes', [])
452 self.attributes: list[ct.SelectorAttribute] = kwargs.get('attributes', [])
453 self.nth: list[ct.SelectorNth] = kwargs.get('nth', [])
454 self.selectors: list[ct.SelectorList] = kwargs.get('selectors', [])
455 self.relations: list[_Selector] = kwargs.get('relations', [])
456 self.rel_type: str | None = kwargs.get('rel_type', None)
457 self.contains: list[ct.SelectorContains] = kwargs.get('contains', [])
458 self.lang: list[ct.SelectorLang] = kwargs.get('lang', [])
459 self.flags: int = kwargs.get('flags', 0)
460 self.no_match: bool = kwargs.get('no_match', False)
462 def _freeze(self, relation: ct.SelectorList) -> ct.Selector | ct.SelectorNull:
463 """Freeze self with an already frozen relation."""
465 if self.no_match:
466 return ct.Null
467 else:
468 return ct.Selector(
469 self.tag,
470 tuple(self.ids),
471 tuple(self.classes),
472 tuple(self.attributes),
473 tuple(self.nth),
474 tuple(self.selectors),
475 relation,
476 self.rel_type,
477 tuple(self.contains),
478 tuple(self.lang),
479 self.flags
480 )
482 def freeze(self) -> ct.Selector | ct.SelectorNull:
483 """
484 Freeze self.
486 Relations form a chain where each selector's first relation is the next link and any
487 remaining relations are carried down to follow it. As the far end of the chain must be
488 frozen first, flatten the chain and then freeze it in reverse to avoid recursion.
489 """
491 chain: list[_Selector] = []
492 pending = [self]
493 while pending:
494 sel = pending.pop()
495 chain.append(sel)
496 if sel.no_match:
497 break
498 pending.extend(reversed(sel.relations))
500 relation = ct.SelectorList()
501 for sel in reversed(chain):
502 frozen = sel._freeze(relation)
503 relation = ct.SelectorList([frozen])
504 return frozen
506 def __str__(self) -> str: # pragma: no cover
507 """String representation."""
509 return (
510 f'_Selector(tag={self.tag!r}, ids={self.ids!r}, classes={self.classes!r}, attributes={self.attributes!r}, '
511 f'nth={self.nth!r}, selectors={self.selectors!r}, relations={self.relations!r}, '
512 f'rel_type={self.rel_type!r}, contains={self.contains!r}, lang={self.lang!r}, flags={self.flags!r}, '
513 f'no_match={self.no_match!r})'
514 )
516 __repr__ = __str__
519class _SelectorContext:
520 """
521 Parsing state for a selector list.
523 Context are created for each nesting level to avoid recursion.
524 """
526 def __init__(
527 self,
528 flags: int,
529 index: int,
530 count: int,
531 on_close: Callable[[ct.SelectorList], None] | None = None
532 ) -> None:
533 """Initialize."""
535 self.sel = _Selector()
536 self.selectors: list[_Selector] = []
537 self.relations: list[_Selector] = []
538 self.has_selector = False
539 self.rel_type = ":" + WS_COMBINATOR
540 self.index = index
541 self.count = count
542 # Receives the frozen selector list when an open pseudo-class is closed.
543 self.on_close = on_close
545 # Setup various flags
546 self.is_open = bool(flags & FLG_OPEN)
547 self.is_pseudo = bool(flags & FLG_PSEUDO)
548 self.is_relative = bool(flags & FLG_RELATIVE)
549 self.is_not = bool(flags & FLG_NOT)
550 self.is_html = bool(flags & FLG_HTML)
551 self.is_default = bool(flags & FLG_DEFAULT)
552 self.is_indeterminate = bool(flags & FLG_INDETERMINATE)
553 self.is_in_range = bool(flags & FLG_IN_RANGE)
554 self.is_out_of_range = bool(flags & FLG_OUT_OF_RANGE)
555 self.is_placeholder_shown = bool(flags & FLG_PLACEHOLDER_SHOWN)
556 self.is_forgive = bool(flags & FLG_FORGIVE)
557 self.is_editable = bool(flags & FLG_EDITABLE)
558 self.is_checked = bool(flags & FLG_CHECKED)
560 # The algorithm for relative selectors require an initial selector in the selector list
561 if self.is_relative:
562 self.selectors.append(_Selector())
564 def debug(self) -> None: # pragma: no cover
565 """Print out useful debug stuff."""
567 for name in (
568 'is_pseudo', 'is_open', 'is_relative', 'is_not', 'is_html', 'is_default', 'is_indeterminate',
569 'is_in_range', 'is_out_of_range', 'is_placeholder_shown', 'is_forgive', 'is_editable', 'is_checked'
570 ):
571 if getattr(self, name):
572 print(f' {name}: True')
575@dataclass
576class CSSPattern:
577 """A CSS pattern that hasn't been processed by `CSSParser` yet."""
579 selector: str
580 flags: int
583class PseudoSelectorMap(UserDict[str, CSSPattern | ct.SelectorList]):
584 """Pseudo selector map."""
586 def __setitem__(self, key: str, value: CSSPattern | ct.SelectorList) -> None:
587 """Set item."""
589 self.data[key] = value
591 def __getitem__(self, key: str) -> ct.SelectorList:
592 """Get item."""
594 with SEL_LOCK:
595 value = self.data[key]
596 if isinstance(value, CSSPattern):
597 value = CSSParser(value.selector).process_selectors(flags=value.flags)
598 self.data[key] = value
600 return value
603# An `input` is in the Text state when its `type` is missing, `text`, or not one of
604# the other keywords, so an invalid value like `type="foo"` is a text input:
605# https://html.spec.whatwg.org/multipage/input.html#attr-input-type
606INPUT_TEXT_STATE = (
607 ':not([type=hidden], [type=search], [type=tel], [type=url], [type=email], [type=password], '
608 '[type=date], [type=month], [type=week], [type=time], [type=datetime-local], [type=number], '
609 '[type=range], [type=color], [type=checkbox], [type=radio], [type=file], [type=submit], '
610 '[type=image], [type=reset], [type=button])'
611)
613# CSS pattern for `:link` and `:any-link`
614CSS_LINK = CSSPattern('html|*:is(a, area)[href]', FLG_PSEUDO | FLG_HTML)
615# CSS pattern for `:checked`
616CSS_CHECKED = CSSPattern(
617 '''
618 html|input[type=checkbox][checked],
620 /*
621 This pattern must be at the end.
622 Special logic is applied to the last selector.
623 */
624 html|*:is(input[type=radio][checked], option)
625 ''',
626 FLG_PSEUDO | FLG_HTML | FLG_CHECKED
627)
628# CSS pattern for `:default`
629CSS_DEFAULT = CSSPattern(
630 '''
631 html|*:is(input[type=checkbox], input[type=radio])[checked],
632 html|option[selected],
634 /*
635 This pattern must be at the end.
636 Special logic is applied to the last selector.
637 */
638 html|*:is(
639 button:is([type=submit], :not([type=button], [type=reset], [commandfor], [command])),
640 input:is([type=submit], [type=image])
641 )
642 ''',
643 FLG_PSEUDO | FLG_HTML | FLG_DEFAULT
644)
645# CSS pattern for `:indeterminate`
646CSS_INDETERMINATE = CSSPattern(
647 '''
648 html|input[type="checkbox"][indeterminate],
649 html|input[type="radio"]:is(:not([name]), [name=""]):not([checked]),
650 html|progress:not([value]),
652 /*
653 This pattern must be at the end.
654 Special logic is applied to the last selector.
655 */
656 html|input[type="radio"][name]:not([name='']):not([checked])
657 ''',
658 FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE
659)
660# CSS pattern for `:disabled`
661CSS_DISABLED = CSSPattern(
662 '''
663 html|*:is(input, button, select, textarea, fieldset, optgroup, option, fieldset)[disabled],
664 html|optgroup[disabled] > html|option,
665 html|fieldset[disabled] > html|*:is(input, button, select, textarea, fieldset),
666 html|fieldset[disabled] >
667 html|*:not(legend:nth-of-type(1)) html|*:is(input, button, select, textarea, fieldset),
668 html|*:is(
669 html|select[disabled],
670 html|fieldset[disabled] > html|select,
671 html|fieldset[disabled] > html|*:not(legend:nth-of-type(1)) html|select
672 ) html|*:is(optgroup, option)
673 ''',
674 FLG_PSEUDO | FLG_HTML
675)
676# CSS pattern for `:enabled`
677CSS_ENABLED = CSSPattern(
678 '''
679 html|*:is(input, button, select, textarea, fieldset, optgroup, option, fieldset):not(:disabled)
680 ''',
681 FLG_PSEUDO | FLG_HTML
682)
683# CSS pattern for `:required`
684# The `required` attribute does not apply to `input` elements of these types.
685INPUT_REQUIRED_NOT_APPLIES = (
686 '[type=hidden], [type=range], [type=color], [type=submit], [type=image], [type=reset], [type=button]'
687)
688CSS_REQUIRED = CSSPattern(
689 f'''
690 html|input:not({INPUT_REQUIRED_NOT_APPLIES})[required],
691 html|*:is(textarea, select)[required]
692 ''',
693 FLG_PSEUDO | FLG_HTML
694)
695# CSS pattern for `:optional`
696CSS_OPTIONAL = CSSPattern(
697 f'''
698 html|*:is(input, textarea, select):not([required]),
699 html|input:is({INPUT_REQUIRED_NOT_APPLIES})
700 ''',
701 FLG_PSEUDO | FLG_HTML
702)
703# CSS pattern for `:placeholder-shown`
704CSS_PLACEHOLDER_SHOWN = CSSPattern(
705 f'''
706 html|input:is(
707 {INPUT_TEXT_STATE},
708 [type=search],
709 [type=url],
710 [type=tel],
711 [type=email],
712 [type=password],
713 [type=number]
714 )[placeholder]:not([placeholder='']):is(:not([value]), [value=""]),
715 html|textarea[placeholder]:not([placeholder=''])
716 ''',
717 FLG_PSEUDO | FLG_HTML | FLG_PLACEHOLDER_SHOWN
718)
719# CSS pattern for `:read-write` (CSS_DISABLED must be compiled first)
720CSS_READ_WRITE = CSSPattern(
721 f'''
722 html|*:is(
723 textarea,
724 input:is(
725 {INPUT_TEXT_STATE},
726 [type=search],
727 [type=url],
728 [type=tel],
729 [type=email],
730 [type=number],
731 [type=password],
732 [type=date],
733 [type=datetime-local],
734 [type=month],
735 [type=time],
736 [type=week]
737 )
738 ):not([readonly], :disabled),
739 html|*:not(input, textarea, [contenteditable="false" i])
740 ''',
741 FLG_PSEUDO | FLG_HTML | FLG_EDITABLE
742)
743# CSS pattern for `:read-only`
744CSS_READ_ONLY = CSSPattern('html|*:not(:read-write)', FLG_PSEUDO | FLG_HTML)
745# CSS pattern for `:in-range`
746CSS_IN_RANGE = CSSPattern(
747 '''
748 html|input:is(
749 [type="date"],
750 [type="month"],
751 [type="week"],
752 [type="time"],
753 [type="datetime-local"],
754 [type="number"],
755 [type="range"]
756 ):is(
757 [min],
758 [max]
759 )
760 ''',
761 FLG_PSEUDO | FLG_HTML | FLG_IN_RANGE
762)
763# CSS pattern for `:out-of-range`
764CSS_OUT_OF_RANGE = CSSPattern(
765 '''
766 html|input:is(
767 [type="date"],
768 [type="month"],
769 [type="week"],
770 [type="time"],
771 [type="datetime-local"],
772 [type="number"],
773 [type="range"]
774 ):is(
775 [min],
776 [max]
777 )
778 ''',
779 FLG_PSEUDO | FLG_HTML | FLG_OUT_OF_RANGE
780)
781# CSS pattern for :open
782CSS_OPEN = CSSPattern('html|*:is(details, dialog)[open]', FLG_PSEUDO | FLG_HTML)
783# CSS pattern for :muted
784CSS_MUTED = CSSPattern('html|*:is(video, audio)[muted]', FLG_PSEUDO | FLG_HTML)
785# CSS pattern default for `:nth-child` "of S" feature
786CSS_NTH_OF_S_DEFAULT = CSSPattern("*|*", FLG_PSEUDO)
789class CSSParser:
790 """Parse CSS selectors."""
792 CSS_TOKENS = (
793 SelectorPattern("pseudo_close", PAT_PSEUDO_CLOSE),
794 SpecialPseudoPattern(
795 (
796 (
797 "pseudo_contains",
798 (':contains', ':-soup-contains', ':-soup-contains-own'),
799 PAT_PSEUDO_CONTAINS,
800 SelectorPattern
801 ),
802 ("pseudo_nth_child", (':nth-child', ':nth-last-child'), PAT_PSEUDO_NTH_CHILD, SelectorPattern),
803 ("pseudo_nth_type", (':nth-of-type', ':nth-last-of-type'), PAT_PSEUDO_NTH_TYPE, SelectorPattern),
804 ("pseudo_lang", (':lang',), PAT_PSEUDO_LANG, SelectorPattern),
805 ("pseudo_dir", (':dir',), PAT_PSEUDO_DIR, SelectorPattern)
806 )
807 ),
808 SelectorPattern("pseudo_class_custom", PAT_PSEUDO_CLASS_CUSTOM),
809 SelectorPattern("pseudo_class", PAT_PSEUDO_CLASS),
810 SelectorPattern("pseudo_element", PAT_PSEUDO_ELEMENT),
811 SelectorPattern("amp", PAT_AMP),
812 SelectorPattern("at_rule", PAT_AT_RULE),
813 SelectorPattern("id", PAT_ID),
814 SelectorPattern("class", PAT_CLASS),
815 SelectorPattern("tag", PAT_TAG),
816 SelectorPattern("attribute", PAT_ATTR),
817 SelectorPatternCombinator("combine", PAT_COMBINE)
818 )
820 # Pseudos that expand to selectors
821 PSEUDO_SELECTORS = PseudoSelectorMap(
822 {
823 ':link': CSS_LINK,
824 ':any-link': CSS_LINK,
825 ':checked': CSS_CHECKED,
826 ':default': CSS_DEFAULT,
827 ':indeterminate': CSS_INDETERMINATE,
828 ':disabled': CSS_DISABLED,
829 ':enabled': CSS_ENABLED,
830 ':required': CSS_REQUIRED,
831 ':muted': CSS_MUTED,
832 ':open': CSS_OPEN,
833 ':optional': CSS_OPTIONAL,
834 ':read-only': CSS_READ_ONLY,
835 ':read-write': CSS_READ_WRITE,
836 ':in-range': CSS_IN_RANGE,
837 ':out-of-range': CSS_OUT_OF_RANGE,
838 ':placeholder-shown': CSS_PLACEHOLDER_SHOWN,
839 '<nth-of-s>': CSS_NTH_OF_S_DEFAULT
840 }
841 )
843 def __init__(
844 self,
845 selector: str,
846 *,
847 custom: Mapping[str, ct.SelectorList] | None = None,
848 ignore: Iterable[str] | None = None,
849 flags: int = 0,
850 max_selectors: int = SELECTOR_LIMIT
851 ) -> None:
852 """Initialize."""
854 self.pattern = selector.replace('\x00', '\ufffd')
855 self.flags = flags
856 self.debug = self.flags & util.DEBUG
857 self.nostrict = self.flags & util.NOSTRICT
858 self.custom = custom
859 self.ignore = frozenset([] if ignore is None else ignore)
860 self.count = 0
861 self.maxsel = max_selectors
863 def increment_count(self, increment: int = 1) -> None:
864 """Check the current selector count."""
866 self.count += increment
867 if self.count > self.maxsel:
868 raise ValueError(f'Selector exceeds pseudo-class nesting limit of {self.maxsel}')
870 def parse_attribute_selector(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
871 """Create attribute selector from the returned regex match."""
873 op = m.group('cmp')
874 case = util.lower(m.group('case')) if m.group('case') else None
875 ns = css_unescape(m.group('attr_ns')[:-1]) if m.group('attr_ns') else ''
876 attr = css_unescape(m.group('attr_name'))
877 html_insensitive = False
878 pattern = None
879 pattern2 = None
880 inverse = False
882 if case:
883 ignore_case = case == 'i'
884 elif util.lower(attr) in HTML_CASE_INSENSITIVE_ATTRIBUTES:
885 ignore_case = True
886 html_insensitive = True
887 else:
888 ignore_case = False
890 if op:
891 value = css_unescape(m.group('value'))
892 pattern = self.parse_attribute_pattern(op, util.lower(value) if ignore_case else value)
893 if html_insensitive:
894 # Outside HTML, the value is matched case sensitive.
895 pattern2 = self.parse_attribute_pattern(op, value)
896 inverse = op.startswith('!')
898 # Append the attribute selector
899 sel_attr = ct.SelectorAttribute(attr, ns, pattern, pattern2, inverse, ignore_case)
900 sel.attributes.append(sel_attr)
902 has_selector = True
903 return has_selector
905 @staticmethod
906 def parse_attribute_pattern(op: str, value: str) -> re.Pattern[str]:
907 """
908 Create the pattern for an attribute selector's value.
910 The pattern is applied with `search()`, so it must not need a leading `.*?` to find the value, which would make
911 the regular expression engine retry the value at every position instead of scanning for it as a literal.
913 A case insensitive pattern is compiled case sensitive for the value converted to ASCII lowercase and is matched
914 against the attribute value converted the same way. We purposely avoid using `re.I` as the regular expression
915 engine cannot then scan as a literal, making the search take quadratic time depending on the pattern.
916 """
918 if op.startswith('^'):
919 # Value start with
920 # `^=` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
921 source = r'^(?!)' if not value else fr'^{re.escape(value)}'
922 elif op.startswith('$'):
923 # Value ends with
924 # `$=` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
925 source = r'(?!)' if not value else fr'{re.escape(value)}\Z'
926 elif op.startswith('*'):
927 # Value contains
928 # `*=` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
929 source = r'(?!)' if not value else re.escape(value)
930 elif op.startswith('~'):
931 # Value contains word within space separated list
932 # `~=` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
933 value = r'(?!)' if not value or RE_WS.search(value) else re.escape(value)
934 source = fr'(?:(?<=^)|(?<=[ \t\r\n\f])){value}(?=(?:[ \t\r\n\f]|\Z))'
935 elif op.startswith('|'):
936 # Value starts with word in dash separated list
937 source = fr'^{re.escape(value)}(?:-.*)?\Z'
938 elif op.startswith('!'):
939 # Value does not matches
940 source = fr'^(?!{re.escape(value)}\Z)'
941 else:
942 # Value matches
943 source = fr'^{re.escape(value)}\Z'
944 return re.compile(source, re.DOTALL)
946 def parse_tag_pattern(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
947 """Parse tag pattern from regex match."""
949 prefix = css_unescape(m.group('tag_ns')[:-1]) if m.group('tag_ns') else None
950 tag = css_unescape(m.group('tag_name'))
951 sel.tag = ct.SelectorTag(tag, prefix)
952 has_selector = True
953 return has_selector
955 def parse_pseudo_class_custom(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
956 """
957 Parse custom pseudo class alias.
959 Compile custom selectors as we need them. When compiling a custom selector,
960 set it to `None` in the dictionary so we can avoid an infinite loop.
961 """
963 pseudo = util.lower(css_unescape(m.group('name')))
964 selector = self.custom.get(pseudo) if self.custom is not None else None
965 if selector is None:
966 raise SelectorSyntaxError(
967 f"Undefined custom selector '{pseudo}' found at position {m.end(0)}",
968 self.pattern,
969 m.end(0)
970 )
972 self.increment_count(selector.count)
973 sel.selectors.append(selector)
974 has_selector = True
975 return has_selector
977 def parse_pseudo_class(
978 self,
979 sel: _Selector,
980 m: Match[str],
981 has_selector: bool,
982 is_relative: bool
983 ) -> tuple[bool, _SelectorContext | None]:
984 """
985 Parse pseudo class.
987 If the pseudo-class opens a selector list, a new context is returned to parse its contents.
988 """
990 context = None
991 complex_pseudo = False
992 pseudo = util.lower(css_unescape(m.group('name')))
993 if m.group('open'):
994 complex_pseudo = True
995 if complex_pseudo and pseudo in PSEUDO_COMPLEX:
996 context = self.parse_pseudo_open(pseudo, m.end(0), sel.selectors.append, is_relative)
997 has_selector = True
998 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE:
999 if pseudo == ':root':
1000 sel.flags |= ct.SEL_ROOT
1001 elif pseudo == ':defined':
1002 sel.flags |= ct.SEL_DEFINED
1003 elif pseudo == ':scope':
1004 sel.flags |= ct.SEL_SCOPE
1005 elif pseudo == ':empty':
1006 sel.flags |= ct.SEL_EMPTY
1007 elif pseudo in self.PSEUDO_SELECTORS:
1008 pseudo_selector = self.PSEUDO_SELECTORS[pseudo]
1009 self.increment_count(pseudo_selector.count)
1010 sel.selectors.append(pseudo_selector)
1011 elif pseudo == ':first-child':
1012 sel.nth.append(ct.SelectorNth(0, False, 1, False, False, ct.SelectorList()))
1013 elif pseudo == ':last-child':
1014 sel.nth.append(ct.SelectorNth(0, False, 1, False, True, ct.SelectorList()))
1015 elif pseudo == ':first-of-type':
1016 sel.nth.append(ct.SelectorNth(0, False, 1, True, False, ct.SelectorList()))
1017 elif pseudo == ':last-of-type':
1018 sel.nth.append(ct.SelectorNth(0, False, 1, True, True, ct.SelectorList()))
1019 elif pseudo == ':only-child':
1020 sel.nth.extend(
1021 [
1022 ct.SelectorNth(0, False, 1, False, False, ct.SelectorList()),
1023 ct.SelectorNth(0, False, 1, False, True, ct.SelectorList())
1024 ]
1025 )
1026 elif pseudo == ':only-of-type':
1027 sel.nth.extend(
1028 [
1029 ct.SelectorNth(0, False, 1, True, False, ct.SelectorList()),
1030 ct.SelectorNth(0, False, 1, True, True, ct.SelectorList())
1031 ]
1032 )
1033 has_selector = True
1034 elif complex_pseudo and pseudo in PSEUDO_COMPLEX_NO_MATCH:
1035 context = self.parse_pseudo_open(pseudo, m.end(0))
1036 sel.no_match = True
1037 has_selector = True
1038 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE_NO_MATCH:
1039 sel.no_match = True
1040 has_selector = True
1041 elif pseudo in PSEUDO_SUPPORTED:
1042 raise SelectorSyntaxError(
1043 f"Invalid syntax for pseudo class '{pseudo}'",
1044 self.pattern,
1045 m.start(0)
1046 )
1047 else:
1048 raise SelectorSyntaxError(
1049 f"'{pseudo}' was detected as a pseudo-class and is either unsupported or invalid. "
1050 "If the syntax was not intended to be recognized as a pseudo-class, please escape the colon.",
1051 self.pattern,
1052 m.start(0)
1053 )
1055 return has_selector, context
1057 def parse_pseudo_nth(
1058 self,
1059 sel: _Selector,
1060 m: Match[str],
1061 has_selector: bool
1062 ) -> tuple[bool, _SelectorContext | None]:
1063 """
1064 Parse `nth` pseudo.
1066 If `of S` is specified, a new context is returned to parse `S`.
1067 """
1069 context = None
1070 mdict = m.groupdict()
1071 if mdict.get('pseudo_nth_child'):
1072 postfix = '_child'
1073 else:
1074 postfix = '_type'
1075 mdict['name'] = util.lower(css_unescape(mdict['name']))
1076 content = util.lower(mdict.get('nth' + postfix))
1077 if content == 'even':
1078 # 2n
1079 s1 = 2
1080 s2 = 0
1081 var = True
1082 elif content == 'odd':
1083 # 2n+1
1084 s1 = 2
1085 s2 = 1
1086 var = True
1087 else:
1088 nth_parts = cast(Match[str], RE_NTH.match(content))
1089 _s1 = '-' if nth_parts.group('s1') and nth_parts.group('s1') == '-' else ''
1090 _s2 = ''
1091 a = nth_parts.group('a')
1092 var = a.endswith('n')
1093 if a.startswith('n'):
1094 _s1 += '1'
1095 elif var:
1096 _s1 += a[:-1]
1097 else:
1098 _s2 = _s1 + a
1099 _s1 = ''
1100 if nth_parts.group('s2') and nth_parts.group('s2') == '-':
1101 _s2 = '-'
1102 if nth_parts.group('b'):
1103 _s2 += nth_parts.group('b')
1104 s1 = int(_s1, 10) if _s1 else 0
1105 s2 = int(_s2, 10) if _s2 else 0
1107 pseudo_sel = mdict['name']
1108 if postfix == '_child':
1109 last = pseudo_sel == ':nth-last-child'
1111 def append_nth(nth_sel: ct.SelectorList) -> None:
1112 """Append to this selector's `nth` attribute."""
1114 sel.nth.append(ct.SelectorNth(s1, var, s2, False, last, nth_sel))
1116 if m.group('of'):
1117 # Parse the rest of `of S`.
1118 context = self.parse_pseudo_open(pseudo_sel, m.end(0), append_nth)
1119 else:
1120 # Use default `*|*` for `of S`.
1121 nth_sel = self.PSEUDO_SELECTORS['<nth-of-s>']
1122 self.increment_count(nth_sel.count)
1123 append_nth(nth_sel)
1124 else:
1125 if pseudo_sel == ':nth-of-type':
1126 sel.nth.append(ct.SelectorNth(s1, var, s2, True, False, ct.SelectorList()))
1127 elif pseudo_sel == ':nth-last-of-type':
1128 sel.nth.append(ct.SelectorNth(s1, var, s2, True, True, ct.SelectorList()))
1129 has_selector = True
1130 return has_selector, context
1132 def parse_pseudo_open(
1133 self,
1134 name: str,
1135 index: int,
1136 on_close: Callable[[ct.SelectorList], None] | None = None,
1137 is_relative: bool = False
1138 ) -> _SelectorContext:
1139 """Create a new context for nested pseudo-class selectors."""
1141 flags = FLG_PSEUDO | FLG_OPEN
1142 if name == ':not':
1143 flags |= FLG_NOT
1144 elif name == ':has':
1145 if is_relative and not self.nostrict:
1146 raise SelectorSyntaxError(
1147 f":has() was found to be nested under :has() at position {index}",
1148 self.pattern,
1149 index
1150 )
1151 flags |= FLG_RELATIVE
1152 elif name in (':where', ':is'):
1153 flags |= FLG_FORGIVE
1155 return self.create_context(flags, index, on_close)
1157 def create_context(
1158 self,
1159 flags: int,
1160 index: int,
1161 on_close: Callable[[ct.SelectorList], None] | None = None
1162 ) -> _SelectorContext:
1163 """Create a new selector context."""
1165 context = _SelectorContext(flags, index, self.count, on_close)
1166 if self.debug: # pragma: no cover
1167 context.debug()
1168 return context
1170 def parse_has_combinator(
1171 self,
1172 sel: _Selector,
1173 m: Match[str],
1174 has_selector: bool,
1175 selectors: list[_Selector],
1176 rel_type: str,
1177 index: int
1178 ) -> tuple[bool, _Selector, str]:
1179 """Parse combinator tokens."""
1181 combinator = m.group('relation')
1182 if not combinator:
1183 combinator = WS_COMBINATOR
1184 if combinator == COMMA_COMBINATOR:
1185 if not has_selector:
1186 raise SelectorSyntaxError(
1187 f"The combinator '{combinator}' at position {index}, must have a selector before it",
1188 self.pattern,
1189 index
1190 )
1191 sel.rel_type = rel_type
1192 selectors[-1].relations.append(sel)
1193 rel_type = ":" + WS_COMBINATOR
1194 selectors.append(_Selector())
1195 else:
1196 if has_selector and self.nostrict:
1197 # End the current selector and associate the leading combinator with this selector.
1198 sel.rel_type = rel_type
1199 selectors[-1].relations.append(sel)
1200 elif has_selector:
1201 raise SelectorSyntaxError(
1202 f'Using strict rules as proposed by CSS, the complex selectors at postition {index} is not allowed',
1203 self.pattern,
1204 index
1205 )
1206 elif rel_type[1:] != WS_COMBINATOR:
1207 # It's impossible to have two whitespace combinators after each other as the patterns
1208 # will gobble up trailing whitespace. It is also impossible to have a whitespace
1209 # combinator after any other kind for the same reason. But we could have
1210 # multiple non-whitespace combinators. So if the current combinator is not a whitespace,
1211 # then we've hit the multiple combinator case, so we should fail.
1212 raise SelectorSyntaxError(
1213 f'The multiple combinators at position {index}',
1214 self.pattern,
1215 index
1216 )
1218 # Set the leading combinator for the next selector.
1219 rel_type = ':' + combinator
1221 sel = _Selector()
1222 has_selector = False
1223 return has_selector, sel, rel_type
1225 def parse_combinator(
1226 self,
1227 sel: _Selector,
1228 m: Match[str],
1229 has_selector: bool,
1230 selectors: list[_Selector],
1231 relations: list[_Selector],
1232 is_pseudo: bool,
1233 is_forgive: bool,
1234 index: int
1235 ) -> tuple[bool, _Selector]:
1236 """Parse combinator tokens."""
1238 combinator = m.group('relation')
1239 if not combinator:
1240 combinator = WS_COMBINATOR
1241 if not has_selector:
1242 if not is_forgive or combinator != COMMA_COMBINATOR:
1243 raise SelectorSyntaxError(
1244 f"The combinator '{combinator}' at position {index}, must have a selector before it",
1245 self.pattern,
1246 index
1247 )
1249 # If we are in a forgiving pseudo class, just make the selector a "no match"
1250 if combinator == COMMA_COMBINATOR:
1251 self.increment_count()
1252 del relations[:]
1253 else:
1254 if combinator == COMMA_COMBINATOR:
1255 if not sel.tag and not is_pseudo:
1256 # Implied `*`
1257 sel.tag = ct.SelectorTag('*', None)
1258 sel.relations.extend(relations)
1259 selectors.append(sel)
1260 del relations[:]
1261 else:
1262 sel.relations.extend(relations)
1263 sel.rel_type = combinator
1264 del relations[:]
1265 relations.append(sel)
1267 sel = _Selector()
1268 has_selector = False
1270 return has_selector, sel
1272 def parse_class_id(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1273 """Parse HTML classes and ids."""
1275 selector = m.group(0)
1276 if selector.startswith('.'):
1277 sel.classes.append(css_unescape(selector[1:]))
1278 else:
1279 sel.ids.append(css_unescape(selector[1:]))
1280 has_selector = True
1281 return has_selector
1283 def parse_pseudo_contains(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1284 """Parse contains."""
1286 pseudo = util.lower(css_unescape(m.group('name')))
1287 if pseudo == ":contains":
1288 warnings.warn( # noqa: B028
1289 "The pseudo class ':contains' is deprecated, ':-soup-contains' should be used moving forward.",
1290 FutureWarning
1291 )
1292 contains_own = pseudo == ":-soup-contains-own"
1293 patterns = [
1294 css_unescape(token.group('value'))
1295 for token in RE_VALUES.finditer(m.group('values'))
1296 if not token.group('split')
1297 ]
1298 sel.contains.append(ct.SelectorContains(patterns, contains_own))
1299 has_selector = True
1300 return has_selector
1302 def parse_pseudo_lang(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1303 """Parse pseudo language."""
1305 patterns = [
1306 css_unescape(token.group('value'))
1307 for token in RE_VALUES.finditer(m.group('values'))
1308 if not token.group('split')
1309 ]
1310 sel.lang.append(ct.SelectorLang(patterns))
1311 has_selector = True
1313 return has_selector
1315 def parse_pseudo_dir(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1316 """Parse pseudo direction."""
1318 value = util.lower(css_unescape(m.group('dir')))
1319 if value == 'ltr':
1320 sel.flags |= ct.SEL_DIR_LTR
1321 elif value == 'rtl':
1322 sel.flags |= ct.SEL_DIR_RTL
1323 else:
1324 # Any other identifier is valid, but does not match anything.
1325 sel.no_match = True
1326 has_selector = True
1327 return has_selector
1329 def parse_selectors(
1330 self,
1331 iselector: Iterator[tuple[str, Match[str]]],
1332 index: int = 0,
1333 flags: int = 0
1334 ) -> ct.SelectorList:
1335 """
1336 Parse selectors.
1338 Pseudo-classes that open a nested selector list push a new context onto a stack,
1339 and closing them pops back to the parent context, so nesting does not recurse.
1340 """
1342 ctx = self.create_context(flags, index)
1343 stack: list[_SelectorContext] = []
1345 for key, m in iselector:
1346 child: _SelectorContext | None = None
1348 if key not in ('combine', 'pseudo_close'):
1349 self.increment_count()
1351 # Handle parts
1352 if self.ignore:
1353 mi = RE_PSEUDO_IGNORE.match(m.group(0))
1354 if mi is not None and mi.group('name').lower() in self.ignore:
1355 raise SelectorSyntaxError(
1356 f"The selector '{mi.group('name')}' at position {m.start(0)}, has been disallowed",
1357 self.pattern,
1358 m.start(0)
1359 )
1360 if key == "at_rule":
1361 raise NotImplementedError(f"At-rules found at position {m.start(0)}")
1362 elif key == "amp":
1363 ctx.sel.flags |= ct.SEL_SCOPE
1364 ctx.has_selector = True
1365 elif key == 'pseudo_class_custom':
1366 ctx.has_selector = self.parse_pseudo_class_custom(ctx.sel, m, ctx.has_selector)
1367 elif key == 'pseudo_class':
1368 ctx.has_selector, child = self.parse_pseudo_class(ctx.sel, m, ctx.has_selector, ctx.is_relative)
1369 elif key == 'pseudo_element':
1370 raise NotImplementedError(f"Pseudo-element found at position {m.start(0)}")
1371 elif key == 'pseudo_contains':
1372 ctx.has_selector = self.parse_pseudo_contains(ctx.sel, m, ctx.has_selector)
1373 elif key in ('pseudo_nth_type', 'pseudo_nth_child'):
1374 ctx.has_selector, child = self.parse_pseudo_nth(ctx.sel, m, ctx.has_selector)
1375 elif key == 'pseudo_lang':
1376 ctx.has_selector = self.parse_pseudo_lang(ctx.sel, m, ctx.has_selector)
1377 elif key == 'pseudo_dir':
1378 ctx.has_selector = self.parse_pseudo_dir(ctx.sel, m, ctx.has_selector)
1379 elif key == 'pseudo_close':
1380 if not ctx.has_selector:
1381 if not ctx.is_forgive:
1382 raise SelectorSyntaxError(
1383 f"Expected a selector at position {m.start(0)}",
1384 self.pattern,
1385 m.start(0)
1386 )
1387 ctx.sel.no_match = True
1388 if not ctx.is_open:
1389 raise SelectorSyntaxError(
1390 f"Unmatched pseudo-class close at position {m.start(0)}",
1391 self.pattern,
1392 m.start(0)
1393 )
1395 # Hand the completed selector list to the parent and resume parsing the parent.
1396 selectors = self.finalize_selectors(ctx)
1397 if ctx.on_close is not None:
1398 ctx.on_close(selectors)
1399 ctx = stack.pop()
1400 continue
1401 elif key == 'combine':
1402 if ctx.is_relative:
1403 ctx.has_selector, ctx.sel, ctx.rel_type = self.parse_has_combinator(
1404 ctx.sel, m, ctx.has_selector, ctx.selectors, ctx.rel_type, ctx.index
1405 )
1406 else:
1407 ctx.has_selector, ctx.sel = self.parse_combinator(
1408 ctx.sel, m, ctx.has_selector, ctx.selectors, ctx.relations,
1409 ctx.is_pseudo, ctx.is_forgive, ctx.index
1410 )
1411 elif key == 'attribute':
1412 ctx.has_selector = self.parse_attribute_selector(ctx.sel, m, ctx.has_selector)
1413 elif key == 'tag':
1414 if ctx.has_selector:
1415 raise SelectorSyntaxError(
1416 f"Tag name found at position {m.start(0)} instead of at the start",
1417 self.pattern,
1418 m.start(0)
1419 )
1420 ctx.has_selector = self.parse_tag_pattern(ctx.sel, m, ctx.has_selector)
1421 elif key in ('class', 'id'):
1422 ctx.has_selector = self.parse_class_id(ctx.sel, m, ctx.has_selector)
1424 ctx.index = m.end(0)
1426 # A pseudo-class opened a nested selector list, parse it before resuming this one.
1427 if child is not None:
1428 stack.append(ctx)
1429 ctx = child
1431 # Handle selectors that are not closed
1432 if ctx.is_open:
1433 raise SelectorSyntaxError(
1434 f"Unclosed pseudo-class at position {ctx.index}",
1435 self.pattern,
1436 ctx.index
1437 )
1439 return self.finalize_selectors(ctx)
1441 def finalize_selectors(self, ctx: _SelectorContext) -> ct.SelectorList:
1442 """Complete the selector list of a context and freeze it."""
1444 sel = ctx.sel
1445 selectors = ctx.selectors
1446 relations = ctx.relations
1447 has_selector = ctx.has_selector
1449 # Cleanup completed selector piece
1450 if has_selector:
1451 if not sel.tag and not ctx.is_pseudo:
1452 # Implied `*`
1453 sel.tag = ct.SelectorTag('*', None)
1454 if ctx.is_relative:
1455 sel.rel_type = ctx.rel_type
1456 selectors[-1].relations.append(sel)
1457 else:
1458 sel.relations.extend(relations)
1459 del relations[:]
1460 selectors.append(sel)
1462 # Forgive empty slots in pseudo-classes that have lists (and are forgiving)
1463 elif ctx.is_forgive and (not selectors or not relations):
1464 # Handle normal pseudo-classes with empty slots like `:is()` etc.
1465 self.increment_count()
1466 sel.no_match = True
1467 del relations[:]
1468 selectors.append(sel)
1469 has_selector = True
1471 if not has_selector:
1472 # We will always need to finish a selector when `:has()` is used as it leads with combining.
1473 # May apply to others as well.
1474 raise SelectorSyntaxError(
1475 f'Expected a selector at position {ctx.index}',
1476 self.pattern,
1477 ctx.index
1478 )
1480 # Some patterns require additional logic, such as default. We try to make these the
1481 # last pattern, and append the appropriate flag to that selector which communicates
1482 # to the matcher what additional logic is required.
1483 if ctx.is_default:
1484 selectors[-1].flags = ct.SEL_DEFAULT
1485 if ctx.is_indeterminate:
1486 selectors[-1].flags = ct.SEL_INDETERMINATE
1487 if ctx.is_in_range:
1488 selectors[-1].flags = ct.SEL_IN_RANGE
1489 if ctx.is_out_of_range:
1490 selectors[-1].flags = ct.SEL_OUT_OF_RANGE
1491 if ctx.is_placeholder_shown:
1492 selectors[-1].flags = ct.SEL_PLACEHOLDER_SHOWN
1493 if ctx.is_editable:
1494 selectors[-1].flags = ct.SEL_EDITABLE
1495 if ctx.is_checked:
1496 selectors[-1].flags = ct.SEL_CHECKED
1498 # Return selector list
1499 return ct.SelectorList([s.freeze() for s in selectors], ctx.is_not, ctx.is_html, self.count - ctx.count)
1501 def selector_iter(self, pattern: str) -> Iterator[tuple[str, Match[str]]]:
1502 """Iterate selector tokens."""
1504 index = 0
1505 end = len(pattern) - 1
1507 if self.debug: # pragma: no cover
1508 print(f'## PARSING: {pattern!r}')
1509 while index <= end:
1510 token = None
1511 for v in self.CSS_TOKENS:
1512 token = v.match(pattern, index, self.flags)
1513 if token is not None:
1514 name, m = token
1515 if self.debug: # pragma: no cover
1516 print(f"TOKEN: '{name or 'consumed'}' --> {m.group(0)!r} at position {m.start(0)}")
1517 index = m.end(0)
1518 if name is not None:
1519 yield name, m
1520 break
1521 if token is None:
1522 c = pattern[index]
1523 # If the character represents the start of one of the known selector types,
1524 # throw an exception mentioning that the known selector type is in error;
1525 # otherwise, report the invalid character.
1526 if c == '[':
1527 msg = f"Malformed attribute selector at position {index}"
1528 elif c == '.':
1529 msg = f"Malformed class selector at position {index}"
1530 elif c == '#':
1531 msg = f"Malformed id selector at position {index}"
1532 elif c == ':':
1533 msg = f"Malformed pseudo-class selector at position {index}"
1534 else:
1535 msg = f"Invalid character {c!r} position {index}"
1536 raise SelectorSyntaxError(msg, self.pattern, index)
1537 if self.debug: # pragma: no cover
1538 print('## END PARSING')
1540 def process_selectors(self, index: int = 0, flags: int = 0) -> ct.SelectorList:
1541 """Process selectors."""
1543 return self.parse_selectors(self.selector_iter(self.pattern), index, flags)