Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/soupsieve/css_parser.py: 90%
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# Complex pseudo classes that take selector lists
100PSEUDO_COMPLEX = {
101 ':contains',
102 ':-soup-contains',
103 ':-soup-contains-own',
104 ':has',
105 ':is',
106 ':matches',
107 ':not',
108 ':where'
109}
111PSEUDO_COMPLEX_NO_MATCH = {
112 ':current',
113 ':host',
114 ':host-context'
115}
117# Complex pseudo classes that take very specific parameters and are handled special
118PSEUDO_SPECIAL = {
119 ':dir',
120 ':lang',
121 ':nth-child',
122 ':nth-last-child',
123 ':nth-last-of-type',
124 ':nth-of-type'
125}
127PSEUDO_SUPPORTED = PSEUDO_SIMPLE | PSEUDO_SIMPLE_NO_MATCH | PSEUDO_COMPLEX | PSEUDO_COMPLEX_NO_MATCH | PSEUDO_SPECIAL
129# Sub-patterns parts
130# Whitespace
131NEWLINE = r'(?>\r\n|[\n\f\r])'
132WS = fr'(?:[ \t]|{NEWLINE})'
133# Comments
134COMMENTS = r'(?:/\*(?:[^*]|\*(?!/))*+\*/)'
135# Whitespace with comments included
136WSC = fr'(?:{WS}|{COMMENTS})'
137# CSS escapes
138CSS_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}+{WS}?+|[^\r\n\f]|$))'
139CSS_STRING_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}+{WS}?+|{NEWLINE}|[^\r\n\f]|$))'
140# CSS identifier code points (browsers treat all non-ASCII code points as identifier code points)
141IDENT_START = r'[A-Za-z_\x80-\U0010ffff]'
142IDENT_CHAR = r'[-0-9A-Za-z_\x80-\U0010ffff]'
143# CSS Identifier
144IDENTIFIER = fr'''
145(?:(?:--|-?+(?:{IDENT_START}|{CSS_ESCAPES}))
146(?:{IDENT_CHAR}|{CSS_ESCAPES})*+)
147'''
148# `nth` content
149NTH = fr'[-+]?+(?:[0-9]++n?+|n)(?:(?<=n){WSC}*+[-+]{WSC}*+[0-9]++)?+'
150# Value: quoted string or identifier
151VALUE = fr'''
152(?:"(?:\\(?:{NEWLINE}|[^\r\n\f])|[^\\"\r\n\f])*+"|
153'(?:\\(?:{NEWLINE}|[^\r\n\f])|[^\\'\r\n\f])*+'|
154{IDENTIFIER})
155'''
156# Attribute value comparison. `!=` is handled special as it is non-standard.
157ATTR = fr'(?:{WSC}*+(?P<cmp>[!~^|*$]?+=){WSC}*+(?P<value>{VALUE})(?:{WSC}*+(?P<case>[is]))?+)?+'
159# Selector patterns
160# IDs (`#id`)
161PAT_ID = fr'\#{IDENTIFIER}'
162# Classes (`.class`)
163PAT_CLASS = fr'\.{IDENTIFIER}'
164# Prefix:Tag (`prefix|tag`)
165PAT_TAG = fr'(?P<tag_ns>(?:{IDENTIFIER}|\*)?+\|)?+(?P<tag_name>{IDENTIFIER}|\*)'
166# Attributes (`[attr]`, `[attr=value]`, etc.)
167PAT_ATTR = fr'\[{WSC}*+(?P<attr_ns>(?:{IDENTIFIER}|\*)?+\|(?!=))?+(?P<attr_name>{IDENTIFIER}){ATTR}{WSC}*+\]'
168# Pseudo class (`:pseudo-class`, `:pseudo-class(`)
169PAT_PSEUDO_CLASS = fr'(?P<name>:{IDENTIFIER})(?P<open>\({WSC}*+)?+'
170# Pseudo class special patterns. Matches `:pseudo-class(` for special case pseudo classes.
171PAT_PSEUDO_CLASS_SPECIAL = fr'(?P<name>:{IDENTIFIER})(?P<open>\({WSC}*+)'
172# Custom pseudo class (`:--custom-pseudo`)
173PAT_PSEUDO_CLASS_CUSTOM = fr'(?P<name>:(?=--){IDENTIFIER})'
174# Nesting ampersand selector. Matches `&`
175PAT_AMP = r'&'
176# Closing pseudo group (`)`)
177PAT_PSEUDO_CLOSE = fr'{WSC}*+\)'
178# Pseudo element (`::pseudo-element`)
179PAT_PSEUDO_ELEMENT = fr':{PAT_PSEUDO_CLASS}'
180# At rule (`@page`, etc.) (not supported)
181PAT_AT_RULE = fr'@P{IDENTIFIER}'
182# Pseudo class `nth-child` (`:nth-child(an+b [of S]?)`, `:first-child`, etc.)
183PAT_PSEUDO_NTH_CHILD = fr'''
184(?P<pseudo_nth_child>{PAT_PSEUDO_CLASS_SPECIAL}
185(?P<nth_child>{NTH}|even|odd))(?:{WSC}*+\)|(?P<of>{COMMENTS}*+{WS}{WSC}*+of{COMMENTS}*+{WS}{WSC}*+))
186'''
187# Pseudo class `nth-of-type` (`:nth-of-type(an+b)`, `:first-of-type`, etc.)
188PAT_PSEUDO_NTH_TYPE = fr'''
189(?P<pseudo_nth_type>{PAT_PSEUDO_CLASS_SPECIAL}
190(?P<nth_type>{NTH}|even|odd)){WSC}*+\)
191'''
192# Pseudo class language (`:lang("*-de", en)`)
193PAT_PSEUDO_LANG = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<values>{VALUE}(?:{WSC}*+,{WSC}*+{VALUE})*+){WSC}*+\)'
194# Pseudo class direction (`:dir(ltr)`)
195PAT_PSEUDO_DIR = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<dir>ltr|rtl){WSC}*+\)'
196# Combining characters (`>`, `~`, ` `, `+`, `,`)
197PAT_COMBINE = fr'{COMMENTS}*+(?={WS}|[,+>~]){WSC}*+(?P<relation>[,+>~])?+{WSC}*+'
198# Extra: Contains (`:contains(text)`)
199PAT_PSEUDO_CONTAINS = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<values>{VALUE}(?:{WSC}*+,{WSC}*+{VALUE})*+){WSC}*+\)'
201# Regular expressions
202# CSS escape pattern
203RE_CSS_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}+{WSC}?+)|(\\[^\r\n\f])|(\\$))', re.I)
204RE_CSS_STR_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}+{WS}?+)|(\\[^\r\n\f])|(\\$)|(\\{NEWLINE}))', re.I)
205# Pattern to break up `nth` specifiers
206RE_NTH = re.compile(fr'(?P<s1>[-+])?+(?P<a>[0-9]++n?+|n)(?:(?<=n){WSC}*+(?P<s2>[-+]){WSC}*+(?P<b>[0-9]++))?+', re.I)
207# Pattern to iterate multiple values.
208RE_VALUES = re.compile(fr'(?:(?P<value>{VALUE})|(?P<split>{WSC}*+,{WSC}*+))', re.X)
209# Whitespace checks
210RE_WS = re.compile(WS)
211RE_WS_BEGIN = re.compile(fr'^{WSC}*+')
212RE_WS_END = re.compile(fr'^(?:[ \t]|(?>\n\r|[\n\f\r])|{COMMENTS})*+')
213RE_CUSTOM = re.compile(fr'^{PAT_PSEUDO_CLASS_CUSTOM}$', re.X)
214RE_PSEUDO_CLASS_SPECIAL = re.compile(PAT_PSEUDO_CLASS_SPECIAL, re.I | re.X | re.U)
215RE_PSEUDO_IGNORE = re.compile(PAT_PSEUDO_CLASS, re.X | re.I | re.U)
217QUOTED = ("'", '"')
219# Constants
220# List split token
221COMMA_COMBINATOR = ','
222# Relation token for descendant
223WS_COMBINATOR = " "
225# Parse flags
226FLG_PSEUDO = 0x01
227FLG_NOT = 0x02
228FLG_RELATIVE = 0x04
229FLG_DEFAULT = 0x08
230FLG_HTML = 0x10
231FLG_INDETERMINATE = 0x20
232FLG_OPEN = 0x40
233FLG_IN_RANGE = 0x80
234FLG_OUT_OF_RANGE = 0x100
235FLG_PLACEHOLDER_SHOWN = 0x200
236FLG_FORGIVE = 0x400
237FLG_EDITABLE = 0x800
239# Maximum cached patterns to store
240_MAXCACHE = 500
243@lru_cache(maxsize=_MAXCACHE)
244def _cached_css_compile(
245 pattern: str,
246 namespaces: ct.Namespaces | None,
247 custom: ct.CustomSelectors | None,
248 ignore: tuple[str] | None = None,
249 flags: int = 0,
250 max_selectors: int = SELECTOR_LIMIT
251) -> cm.SoupSieve:
252 """Cached CSS compile."""
254 return cm.SoupSieve(
255 pattern,
256 CSSParser(
257 pattern,
258 custom=custom,
259 ignore=ignore,
260 flags=flags,
261 max_selectors=max_selectors
262 ).process_selectors(),
263 namespaces,
264 custom,
265 flags
266 )
269def _purge_cache() -> None:
270 """Purge the cache."""
272 _cached_css_compile.cache_clear()
275def css_unescape(content: str) -> str:
276 """
277 Unescape CSS value.
279 Strings allow for spanning the value on multiple strings by escaping a new line.
280 """
282 def replace(m: Match[str]) -> str:
283 """Replace with the appropriate substitute."""
285 if m.group(1):
286 codepoint = int(m.group(1)[1:], 16)
287 # A surrogate, or a value past Unicode, is `U+FFFD`. Zero already is.
288 if (
289 codepoint == 0
290 or codepoint > 0x10FFFF
291 or 0xD800 <= codepoint <= 0xDFFF
292 ):
293 codepoint = UNICODE_REPLACEMENT_CHAR
294 value = chr(codepoint)
295 elif m.group(2):
296 value = m.group(2)[1:]
297 elif m.group(3):
298 value = '\ufffd'
299 else:
300 value = ''
302 return value
304 quoted = content.startswith(QUOTED)
305 return (RE_CSS_STR_ESC if quoted else RE_CSS_ESC).sub(
306 replace,
307 content[1:-1] if quoted else content
308 )
311def escape(ident: str) -> str:
312 """Escape identifier."""
314 string = []
315 length = len(ident)
316 start_dash = length > 0 and ident[0] == '-'
317 if length == 1 and start_dash:
318 # Need to escape identifier that is a single `-` with no other characters
319 string.append(f'\\{ident}')
320 else:
321 for index, c in enumerate(ident):
322 codepoint = ord(c)
323 if codepoint == 0x00:
324 string.append('\ufffd')
325 elif (0x01 <= codepoint <= 0x1F) or codepoint == 0x7F:
326 string.append(f'\\{codepoint:x} ')
327 elif (index == 0 or (start_dash and index == 1)) and (0x30 <= codepoint <= 0x39):
328 string.append(f'\\{codepoint:x} ')
329 elif (
330 codepoint in (0x2D, 0x5F) or (0x30 <= codepoint <= 0x39) or
331 (0x41 <= codepoint <= 0x5A) or (0x61 <= codepoint <= 0x7A) or
332 (codepoint >= 0x80 and any(start <= codepoint <= end for start, end in NON_ASCII_IDENT_RANGES))
333 ):
334 string.append(c)
335 else:
336 string.append(f'\\{c}')
337 return ''.join(string)
340class SelectorPattern:
341 """Selector pattern."""
343 def __init__(self, name: str, pattern: str) -> None:
344 """Initialize."""
346 self.name = name
347 self.pattern = pattern
348 self._re_pattern: re.Pattern[str] | None = None
350 @property
351 def re_pattern(self) -> re.Pattern[str]:
352 """Retrieve the compiled regular expression pattern."""
354 with RE_LOCK:
355 if self._re_pattern is None:
356 self._re_pattern = re.compile(self.pattern, re.I | re.X | re.U)
357 return self._re_pattern
359 def get_name(self) -> str:
360 """Get name."""
362 return self.name
364 def match(self, selector: str, index: int, flags: int) -> Match[str] | None:
365 """Match the selector."""
367 return self.re_pattern.match(selector, index)
370class SpecialPseudoPattern(SelectorPattern):
371 """Selector pattern."""
373 def __init__(self, patterns: tuple[tuple[str, tuple[str, ...], str, type[SelectorPattern]], ...]) -> None:
374 """Initialize."""
376 self.patterns = {}
377 for p in patterns:
378 name = p[0]
379 pattern = p[3](name, p[2])
380 for pseudo in p[1]:
381 self.patterns[pseudo] = pattern
383 self.matched_name: SelectorPattern | None = None
385 def get_name(self) -> str:
386 """Get name."""
388 return '' if self.matched_name is None else self.matched_name.get_name()
390 def match(self, selector: str, index: int, flags: int) -> Match[str] | None:
391 """Match the selector."""
393 pseudo = None
394 m = RE_PSEUDO_CLASS_SPECIAL.match(selector, index)
395 if m:
396 name = util.lower(css_unescape(m.group('name')))
397 pattern = self.patterns.get(name)
398 if pattern:
399 pseudo = pattern.match(selector, index, flags)
400 if pseudo:
401 self.matched_name = pattern
403 return pseudo
406class _Selector:
407 """
408 Intermediate selector class.
410 This stores selector data for a compound selector as we are acquiring them.
411 Once we are done collecting the data for a compound selector, we freeze
412 the data in an object that can be pickled and hashed.
413 """
415 def __init__(self, **kwargs: Any) -> None:
416 """Initialize."""
418 self.tag: ct.SelectorTag | None = kwargs.get('tag', None)
419 self.ids: list[str] = kwargs.get('ids', [])
420 self.classes: list[str] = kwargs.get('classes', [])
421 self.attributes: list[ct.SelectorAttribute] = kwargs.get('attributes', [])
422 self.nth: list[ct.SelectorNth] = kwargs.get('nth', [])
423 self.selectors: list[ct.SelectorList] = kwargs.get('selectors', [])
424 self.relations: list[_Selector] = kwargs.get('relations', [])
425 self.rel_type: str | None = kwargs.get('rel_type', None)
426 self.contains: list[ct.SelectorContains] = kwargs.get('contains', [])
427 self.lang: list[ct.SelectorLang] = kwargs.get('lang', [])
428 self.flags: int = kwargs.get('flags', 0)
429 self.no_match: bool = kwargs.get('no_match', False)
431 def _freeze(self, relation: ct.SelectorList) -> ct.Selector | ct.SelectorNull:
432 """Freeze self with an already frozen relation."""
434 if self.no_match:
435 return ct.Null
436 else:
437 return ct.Selector(
438 self.tag,
439 tuple(self.ids),
440 tuple(self.classes),
441 tuple(self.attributes),
442 tuple(self.nth),
443 tuple(self.selectors),
444 relation,
445 self.rel_type,
446 tuple(self.contains),
447 tuple(self.lang),
448 self.flags
449 )
451 def freeze(self) -> ct.Selector | ct.SelectorNull:
452 """
453 Freeze self.
455 Relations form a chain where each selector's first relation is the next link and any
456 remaining relations are carried down to follow it. As the far end of the chain must be
457 frozen first, flatten the chain and then freeze it in reverse to avoid recursion.
458 """
460 chain: list[_Selector] = []
461 pending = [self]
462 while pending:
463 sel = pending.pop()
464 chain.append(sel)
465 if sel.no_match:
466 break
467 pending.extend(reversed(sel.relations))
469 relation = ct.SelectorList()
470 for sel in reversed(chain):
471 frozen = sel._freeze(relation)
472 relation = ct.SelectorList([frozen])
473 return frozen
475 def __str__(self) -> str: # pragma: no cover
476 """String representation."""
478 return (
479 f'_Selector(tag={self.tag!r}, ids={self.ids!r}, classes={self.classes!r}, attributes={self.attributes!r}, '
480 f'nth={self.nth!r}, selectors={self.selectors!r}, relations={self.relations!r}, '
481 f'rel_type={self.rel_type!r}, contains={self.contains!r}, lang={self.lang!r}, flags={self.flags!r}, '
482 f'no_match={self.no_match!r})'
483 )
485 __repr__ = __str__
488class _SelectorContext:
489 """
490 Parsing state for a selector list.
492 Context are created for each nesting level to avoid recursion.
493 """
495 def __init__(
496 self,
497 flags: int,
498 index: int,
499 count: int,
500 on_close: Callable[[ct.SelectorList], None] | None = None
501 ) -> None:
502 """Initialize."""
504 self.sel = _Selector()
505 self.selectors: list[_Selector] = []
506 self.relations: list[_Selector] = []
507 self.has_selector = False
508 self.rel_type = ":" + WS_COMBINATOR
509 self.index = index
510 self.count = count
511 # Receives the frozen selector list when an open pseudo-class is closed.
512 self.on_close = on_close
514 # Setup various flags
515 self.is_open = bool(flags & FLG_OPEN)
516 self.is_pseudo = bool(flags & FLG_PSEUDO)
517 self.is_relative = bool(flags & FLG_RELATIVE)
518 self.is_not = bool(flags & FLG_NOT)
519 self.is_html = bool(flags & FLG_HTML)
520 self.is_default = bool(flags & FLG_DEFAULT)
521 self.is_indeterminate = bool(flags & FLG_INDETERMINATE)
522 self.is_in_range = bool(flags & FLG_IN_RANGE)
523 self.is_out_of_range = bool(flags & FLG_OUT_OF_RANGE)
524 self.is_placeholder_shown = bool(flags & FLG_PLACEHOLDER_SHOWN)
525 self.is_forgive = bool(flags & FLG_FORGIVE)
526 self.is_editable = bool(flags & FLG_EDITABLE)
528 # The algorithm for relative selectors require an initial selector in the selector list
529 if self.is_relative:
530 self.selectors.append(_Selector())
532 def debug(self) -> None: # pragma: no cover
533 """Print out useful debug stuff."""
535 for name in (
536 'is_pseudo', 'is_open', 'is_relative', 'is_not', 'is_html', 'is_default', 'is_indeterminate',
537 'is_in_range', 'is_out_of_range', 'is_placeholder_shown', 'is_forgive', 'is_editable'
538 ):
539 if getattr(self, name):
540 print(f' {name}: True')
543@dataclass
544class CSSPattern:
545 """A CSS pattern that hasn't been processed by `CSSParser` yet."""
547 selector: str
548 flags: int
551class PseudoSelectorMap(UserDict[str, CSSPattern | ct.SelectorList]):
552 """Pseudo selector map."""
554 def __setitem__(self, key: str, value: CSSPattern | ct.SelectorList) -> None:
555 """Set item."""
557 self.data[key] = value
559 def __getitem__(self, key: str) -> ct.SelectorList:
560 """Get item."""
562 with SEL_LOCK:
563 value = self.data[key]
564 if isinstance(value, CSSPattern):
565 value = CSSParser(value.selector).process_selectors(flags=value.flags)
566 self.data[key] = value
568 return value
571# CSS pattern for `:link` and `:any-link`
572CSS_LINK = CSSPattern('html|*:is(a, area)[href]', FLG_PSEUDO | FLG_HTML)
573# CSS pattern for `:checked`
574CSS_CHECKED = CSSPattern(
575 '''
576 html|*:is(input[type=checkbox], input[type=radio])[checked], html|option[selected]
577 ''',
578 FLG_PSEUDO | FLG_HTML
579)
580# CSS pattern for `:default` (must compile CSS_CHECKED first)
581CSS_DEFAULT = CSSPattern(
582 '''
583 :checked,
585 /*
586 This pattern must be at the end.
587 Special logic is applied to the last selector.
588 */
589 html|form html|*:is(
590 button:is([type=submit], :not([type=button], [type=reset], [commandfor], [command])),
591 input:is([type=submit], [type=image])
592 )
593 ''',
594 FLG_PSEUDO | FLG_HTML | FLG_DEFAULT
595)
596# CSS pattern for `:indeterminate`
597CSS_INDETERMINATE = CSSPattern(
598 '''
599 html|input[type="checkbox"][indeterminate],
600 html|input[type="radio"]:is(:not([name]), [name=""]):not([checked]),
601 html|progress:not([value]),
603 /*
604 This pattern must be at the end.
605 Special logic is applied to the last selector.
606 */
607 html|input[type="radio"][name]:not([name='']):not([checked])
608 ''',
609 FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE
610)
611# CSS pattern for `:disabled`
612CSS_DISABLED = CSSPattern(
613 '''
614 html|*:is(input:not([type=hidden]), button, select, textarea, fieldset, optgroup, option, fieldset)[disabled],
615 html|optgroup[disabled] > html|option,
616 html|fieldset[disabled] > html|*:is(input:not([type=hidden]), button, select, textarea, fieldset),
617 html|fieldset[disabled] >
618 html|*:not(legend:nth-of-type(1)) html|*:is(input:not([type=hidden]), button, select, textarea, fieldset),
619 html|*:is(
620 html|select[disabled],
621 html|fieldset[disabled] > html|select,
622 html|fieldset[disabled] > html|*:not(legend:nth-of-type(1)) html|select
623 ) html|*:is(optgroup, option)
624 ''',
625 FLG_PSEUDO | FLG_HTML
626)
627# CSS pattern for `:enabled`
628CSS_ENABLED = CSSPattern(
629 '''
630 html|*:is(input:not([type=hidden]), button, select, textarea, fieldset, optgroup, option, fieldset):not(:disabled)
631 ''',
632 FLG_PSEUDO | FLG_HTML
633)
634# CSS pattern for `:required`
635CSS_REQUIRED = CSSPattern('html|*:is(input, textarea, select)[required]', FLG_PSEUDO | FLG_HTML)
636# CSS pattern for `:optional`
637CSS_OPTIONAL = CSSPattern('html|*:is(input, textarea, select):not([required])', FLG_PSEUDO | FLG_HTML)
638# CSS pattern for `:placeholder-shown`
639CSS_PLACEHOLDER_SHOWN = CSSPattern(
640 '''
641 html|input:is(
642 :not([type]),
643 [type=""],
644 [type=text],
645 [type=search],
646 [type=url],
647 [type=tel],
648 [type=email],
649 [type=password],
650 [type=number]
651 )[placeholder]:not([placeholder='']):is(:not([value]), [value=""]),
652 html|textarea[placeholder]:not([placeholder=''])
653 ''',
654 FLG_PSEUDO | FLG_HTML | FLG_PLACEHOLDER_SHOWN
655)
656# CSS pattern for `:read-write` (CSS_DISABLED must be compiled first)
657CSS_READ_WRITE = CSSPattern(
658 '''
659 html|*:is(
660 textarea,
661 input:is(
662 :not([type]),
663 [type=""],
664 [type=text],
665 [type=search],
666 [type=url],
667 [type=tel],
668 [type=email],
669 [type=number],
670 [type=password],
671 [type=date],
672 [type=datetime-local],
673 [type=month],
674 [type=time],
675 [type=week]
676 )
677 ):not([readonly], :disabled),
678 html|*:not(input, textarea, [contenteditable="false" i])
679 ''',
680 FLG_PSEUDO | FLG_HTML | FLG_EDITABLE
681)
682# CSS pattern for `:read-only`
683CSS_READ_ONLY = CSSPattern('html|*:not(:read-write)', FLG_PSEUDO | FLG_HTML)
684# CSS pattern for `:in-range`
685CSS_IN_RANGE = CSSPattern(
686 '''
687 html|input:is(
688 [type="date"],
689 [type="month"],
690 [type="week"],
691 [type="time"],
692 [type="datetime-local"],
693 [type="number"],
694 [type="range"]
695 ):is(
696 [min],
697 [max]
698 )
699 ''',
700 FLG_PSEUDO | FLG_HTML | FLG_IN_RANGE
701)
702# CSS pattern for `:out-of-range`
703CSS_OUT_OF_RANGE = CSSPattern(
704 '''
705 html|input:is(
706 [type="date"],
707 [type="month"],
708 [type="week"],
709 [type="time"],
710 [type="datetime-local"],
711 [type="number"],
712 [type="range"]
713 ):is(
714 [min],
715 [max]
716 )
717 ''',
718 FLG_PSEUDO | FLG_HTML | FLG_OUT_OF_RANGE
719)
720# CSS pattern for :open
721CSS_OPEN = CSSPattern('html|*:is(details, dialog)[open]', FLG_PSEUDO | FLG_HTML)
722# CSS pattern for :muted
723CSS_MUTED = CSSPattern('html|*:is(video, audio)[muted]', FLG_PSEUDO | FLG_HTML)
724# CSS pattern default for `:nth-child` "of S" feature
725CSS_NTH_OF_S_DEFAULT = CSSPattern("*|*", FLG_PSEUDO)
728class CSSParser:
729 """Parse CSS selectors."""
731 CSS_TOKENS = (
732 SelectorPattern("pseudo_close", PAT_PSEUDO_CLOSE),
733 SpecialPseudoPattern(
734 (
735 (
736 "pseudo_contains",
737 (':contains', ':-soup-contains', ':-soup-contains-own'),
738 PAT_PSEUDO_CONTAINS,
739 SelectorPattern
740 ),
741 ("pseudo_nth_child", (':nth-child', ':nth-last-child'), PAT_PSEUDO_NTH_CHILD, SelectorPattern),
742 ("pseudo_nth_type", (':nth-of-type', ':nth-last-of-type'), PAT_PSEUDO_NTH_TYPE, SelectorPattern),
743 ("pseudo_lang", (':lang',), PAT_PSEUDO_LANG, SelectorPattern),
744 ("pseudo_dir", (':dir',), PAT_PSEUDO_DIR, SelectorPattern)
745 )
746 ),
747 SelectorPattern("pseudo_class_custom", PAT_PSEUDO_CLASS_CUSTOM),
748 SelectorPattern("pseudo_class", PAT_PSEUDO_CLASS),
749 SelectorPattern("pseudo_element", PAT_PSEUDO_ELEMENT),
750 SelectorPattern("amp", PAT_AMP),
751 SelectorPattern("at_rule", PAT_AT_RULE),
752 SelectorPattern("id", PAT_ID),
753 SelectorPattern("class", PAT_CLASS),
754 SelectorPattern("tag", PAT_TAG),
755 SelectorPattern("attribute", PAT_ATTR),
756 SelectorPattern("combine", PAT_COMBINE)
757 )
759 # Pseudos that expand to selectors
760 PSEUDO_SELECTORS = PseudoSelectorMap(
761 {
762 ':link': CSS_LINK,
763 ':any-link': CSS_LINK,
764 ':checked': CSS_CHECKED,
765 ':default': CSS_DEFAULT,
766 ':indeterminate': CSS_INDETERMINATE,
767 ':disabled': CSS_DISABLED,
768 ':enabled': CSS_ENABLED,
769 ':required': CSS_REQUIRED,
770 ':muted': CSS_MUTED,
771 ':open': CSS_OPEN,
772 ':optional': CSS_OPTIONAL,
773 ':read-only': CSS_READ_ONLY,
774 ':read-write': CSS_READ_WRITE,
775 ':in-range': CSS_IN_RANGE,
776 ':out-of-range': CSS_OUT_OF_RANGE,
777 ':placeholder-shown': CSS_PLACEHOLDER_SHOWN,
778 '<nth-of-s>': CSS_NTH_OF_S_DEFAULT
779 }
780 )
782 def __init__(
783 self,
784 selector: str,
785 *,
786 custom: Mapping[str, ct.SelectorList] | None = None,
787 ignore: Iterable[str] | None = None,
788 flags: int = 0,
789 max_selectors: int = SELECTOR_LIMIT
790 ) -> None:
791 """Initialize."""
793 self.pattern = selector.replace('\x00', '\ufffd')
794 self.flags = flags
795 self.debug = self.flags & util.DEBUG
796 self.nostrict = self.flags & util.NOSTRICT
797 self.custom = custom
798 self.ignore = frozenset([] if ignore is None else ignore)
799 self.count = 0
800 self.maxsel = max_selectors
802 def increment_count(self, increment: int = 1) -> None:
803 """Check the current selector count."""
805 self.count += increment
806 if self.count > self.maxsel:
807 raise ValueError(f'Selector exceeds pseudo-class nesting limit of {self.maxsel}')
809 def parse_attribute_selector(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
810 """Create attribute selector from the returned regex match."""
812 op = m.group('cmp')
813 case = util.lower(m.group('case')) if m.group('case') else None
814 ns = css_unescape(m.group('attr_ns')[:-1]) if m.group('attr_ns') else ''
815 attr = css_unescape(m.group('attr_name'))
816 is_type = False
817 pattern2 = None
818 value = ''
819 inverse = False
821 if case:
822 flags = (re.I if case == 'i' else 0) | re.DOTALL
823 elif util.lower(attr) == 'type':
824 flags = re.I | re.DOTALL
825 is_type = True
826 else:
827 flags = re.DOTALL
829 if op:
830 value = css_unescape(m.group('value'))
832 if not op:
833 # Attribute name
834 pattern = None
835 elif op.startswith('^'):
836 # Value start with
837 # `^=` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
838 value = r'(?!)' if not value else re.escape(value)
839 pattern = re.compile(r'^%s.*' % value, flags)
840 elif op.startswith('$'):
841 # Value ends with
842 # `$=` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
843 value = r'(?!)' if not value else re.escape(value)
844 pattern = re.compile(r'.*?%s$' % value, flags)
845 elif op.startswith('*'):
846 # Value contains
847 # `*=` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
848 value = r'(?!)' if not value else re.escape(value)
849 pattern = re.compile(r'.*?%s.*' % value, flags)
850 elif op.startswith('~'):
851 # Value contains word within space separated list
852 # `*~` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
853 value = r'(?!)' if not value or RE_WS.search(value) else re.escape(value)
854 pattern = re.compile(r'.*?(?:(?<=^)|(?<=[ \t\r\n\f]))%s(?=(?:[ \t\r\n\f]|$)).*' % value, flags)
855 elif op.startswith('|'):
856 # Value starts with word in dash separated list
857 pattern = re.compile(r'^%s(?:-.*)?$' % re.escape(value), flags)
858 elif op.startswith('!'):
859 # Value does not matches
860 pattern = re.compile(r'^(?!%s).*$' % re.escape(value), flags)
861 inverse = True
862 else:
863 # Value matches
864 pattern = re.compile(r'^%s$' % re.escape(value), flags)
866 if is_type and pattern:
867 pattern2 = re.compile(pattern.pattern)
869 # Append the attribute selector
870 sel_attr = ct.SelectorAttribute(attr, ns, pattern, pattern2, inverse)
871 sel.attributes.append(sel_attr)
873 has_selector = True
874 return has_selector
876 def parse_tag_pattern(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
877 """Parse tag pattern from regex match."""
879 prefix = css_unescape(m.group('tag_ns')[:-1]) if m.group('tag_ns') else None
880 tag = css_unescape(m.group('tag_name'))
881 sel.tag = ct.SelectorTag(tag, prefix)
882 has_selector = True
883 return has_selector
885 def parse_pseudo_class_custom(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
886 """
887 Parse custom pseudo class alias.
889 Compile custom selectors as we need them. When compiling a custom selector,
890 set it to `None` in the dictionary so we can avoid an infinite loop.
891 """
893 pseudo = util.lower(css_unescape(m.group('name')))
894 selector = self.custom.get(pseudo) if self.custom is not None else None
895 if selector is None:
896 raise SelectorSyntaxError(
897 f"Undefined custom selector '{pseudo}' found at position {m.end(0)}",
898 self.pattern,
899 m.end(0)
900 )
902 self.increment_count(selector.count)
903 sel.selectors.append(selector)
904 has_selector = True
905 return has_selector
907 def parse_pseudo_class(
908 self,
909 sel: _Selector,
910 m: Match[str],
911 has_selector: bool,
912 is_html: bool,
913 is_relative: bool
914 ) -> tuple[bool, bool, _SelectorContext | None]:
915 """
916 Parse pseudo class.
918 If the pseudo-class opens a selector list, a new context is returned to parse its contents.
919 """
921 context = None
922 complex_pseudo = False
923 pseudo = util.lower(css_unescape(m.group('name')))
924 if m.group('open'):
925 complex_pseudo = True
926 if complex_pseudo and pseudo in PSEUDO_COMPLEX:
927 context = self.parse_pseudo_open(pseudo, m.end(0), sel.selectors.append, is_relative)
928 has_selector = True
929 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE:
930 if pseudo == ':root':
931 sel.flags |= ct.SEL_ROOT
932 elif pseudo == ':defined':
933 sel.flags |= ct.SEL_DEFINED
934 is_html = True
935 elif pseudo == ':scope':
936 sel.flags |= ct.SEL_SCOPE
937 elif pseudo == ':empty':
938 sel.flags |= ct.SEL_EMPTY
939 elif pseudo in self.PSEUDO_SELECTORS:
940 pseudo_selector = self.PSEUDO_SELECTORS[pseudo]
941 self.increment_count(pseudo_selector.count)
942 sel.selectors.append(pseudo_selector)
943 elif pseudo == ':first-child':
944 sel.nth.append(ct.SelectorNth(0, False, 1, False, False, ct.SelectorList()))
945 elif pseudo == ':last-child':
946 sel.nth.append(ct.SelectorNth(0, False, 1, False, True, ct.SelectorList()))
947 elif pseudo == ':first-of-type':
948 sel.nth.append(ct.SelectorNth(0, False, 1, True, False, ct.SelectorList()))
949 elif pseudo == ':last-of-type':
950 sel.nth.append(ct.SelectorNth(0, False, 1, True, True, ct.SelectorList()))
951 elif pseudo == ':only-child':
952 sel.nth.extend(
953 [
954 ct.SelectorNth(0, False, 1, False, False, ct.SelectorList()),
955 ct.SelectorNth(0, False, 1, False, True, ct.SelectorList())
956 ]
957 )
958 elif pseudo == ':only-of-type':
959 sel.nth.extend(
960 [
961 ct.SelectorNth(0, False, 1, True, False, ct.SelectorList()),
962 ct.SelectorNth(0, False, 1, True, True, ct.SelectorList())
963 ]
964 )
965 has_selector = True
966 elif complex_pseudo and pseudo in PSEUDO_COMPLEX_NO_MATCH:
967 context = self.parse_pseudo_open(pseudo, m.end(0))
968 sel.no_match = True
969 has_selector = True
970 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE_NO_MATCH:
971 sel.no_match = True
972 has_selector = True
973 elif pseudo in PSEUDO_SUPPORTED:
974 raise SelectorSyntaxError(
975 f"Invalid syntax for pseudo class '{pseudo}'",
976 self.pattern,
977 m.start(0)
978 )
979 else:
980 raise SelectorSyntaxError(
981 f"'{pseudo}' was detected as a pseudo-class and is either unsupported or invalid. "
982 "If the syntax was not intended to be recognized as a pseudo-class, please escape the colon.",
983 self.pattern,
984 m.start(0)
985 )
987 return has_selector, is_html, context
989 def parse_pseudo_nth(
990 self,
991 sel: _Selector,
992 m: Match[str],
993 has_selector: bool
994 ) -> tuple[bool, _SelectorContext | None]:
995 """
996 Parse `nth` pseudo.
998 If `of S` is specified, a new context is returned to parse `S`.
999 """
1001 context = None
1002 mdict = m.groupdict()
1003 if mdict.get('pseudo_nth_child'):
1004 postfix = '_child'
1005 else:
1006 postfix = '_type'
1007 mdict['name'] = util.lower(css_unescape(mdict['name']))
1008 content = util.lower(mdict.get('nth' + postfix))
1009 if content == 'even':
1010 # 2n
1011 s1 = 2
1012 s2 = 0
1013 var = True
1014 elif content == 'odd':
1015 # 2n+1
1016 s1 = 2
1017 s2 = 1
1018 var = True
1019 else:
1020 nth_parts = cast(Match[str], RE_NTH.match(content))
1021 _s1 = '-' if nth_parts.group('s1') and nth_parts.group('s1') == '-' else ''
1022 _s2 = ''
1023 a = nth_parts.group('a')
1024 var = a.endswith('n')
1025 if a.startswith('n'):
1026 _s1 += '1'
1027 elif var:
1028 _s1 += a[:-1]
1029 else:
1030 _s2 = _s1 + a
1031 _s1 = ''
1032 if nth_parts.group('s2') and nth_parts.group('s2') == '-':
1033 _s2 = '-'
1034 if nth_parts.group('b'):
1035 _s2 += nth_parts.group('b')
1036 s1 = int(_s1, 10) if _s1 else 0
1037 s2 = int(_s2, 10) if _s2 else 0
1039 pseudo_sel = mdict['name']
1040 if postfix == '_child':
1041 last = pseudo_sel == ':nth-last-child'
1043 def append_nth(nth_sel: ct.SelectorList) -> None:
1044 """Append to this selector's `nth` attribute."""
1046 sel.nth.append(ct.SelectorNth(s1, var, s2, False, last, nth_sel))
1048 if m.group('of'):
1049 # Parse the rest of `of S`.
1050 context = self.parse_pseudo_open(pseudo_sel, m.end(0), append_nth)
1051 else:
1052 # Use default `*|*` for `of S`.
1053 nth_sel = self.PSEUDO_SELECTORS['<nth-of-s>']
1054 self.increment_count(nth_sel.count)
1055 append_nth(nth_sel)
1056 else:
1057 if pseudo_sel == ':nth-of-type':
1058 sel.nth.append(ct.SelectorNth(s1, var, s2, True, False, ct.SelectorList()))
1059 elif pseudo_sel == ':nth-last-of-type':
1060 sel.nth.append(ct.SelectorNth(s1, var, s2, True, True, ct.SelectorList()))
1061 has_selector = True
1062 return has_selector, context
1064 def parse_pseudo_open(
1065 self,
1066 name: str,
1067 index: int,
1068 on_close: Callable[[ct.SelectorList], None] | None = None,
1069 is_relative: bool = False
1070 ) -> _SelectorContext:
1071 """Create a new context for nested pseudo-class selectors."""
1073 flags = FLG_PSEUDO | FLG_OPEN
1074 if name == ':not':
1075 flags |= FLG_NOT
1076 elif name == ':has':
1077 if is_relative and not self.nostrict:
1078 raise SelectorSyntaxError(
1079 f":has() was found to be nested under :has() at position {index}",
1080 self.pattern,
1081 index
1082 )
1083 flags |= FLG_RELATIVE
1084 elif name in (':where', ':is'):
1085 flags |= FLG_FORGIVE
1087 return self.create_context(flags, index, on_close)
1089 def create_context(
1090 self,
1091 flags: int,
1092 index: int,
1093 on_close: Callable[[ct.SelectorList], None] | None = None
1094 ) -> _SelectorContext:
1095 """Create a new selector context."""
1097 context = _SelectorContext(flags, index, self.count, on_close)
1098 if self.debug: # pragma: no cover
1099 context.debug()
1100 return context
1102 def parse_has_combinator(
1103 self,
1104 sel: _Selector,
1105 m: Match[str],
1106 has_selector: bool,
1107 selectors: list[_Selector],
1108 rel_type: str,
1109 index: int
1110 ) -> tuple[bool, _Selector, str]:
1111 """Parse combinator tokens."""
1113 combinator = m.group('relation')
1114 if not combinator:
1115 combinator = WS_COMBINATOR
1116 if combinator == COMMA_COMBINATOR:
1117 if not has_selector:
1118 raise SelectorSyntaxError(
1119 f"The combinator '{combinator}' at position {index}, must have a selector before it",
1120 self.pattern,
1121 index
1122 )
1123 sel.rel_type = rel_type
1124 selectors[-1].relations.append(sel)
1125 rel_type = ":" + WS_COMBINATOR
1126 selectors.append(_Selector())
1127 else:
1128 if has_selector and self.nostrict:
1129 # End the current selector and associate the leading combinator with this selector.
1130 sel.rel_type = rel_type
1131 selectors[-1].relations.append(sel)
1132 elif has_selector:
1133 raise SelectorSyntaxError(
1134 f'Using strict rules as proposed by CSS, the complex selectors at postition {index} is not allowed',
1135 self.pattern,
1136 index
1137 )
1138 elif rel_type[1:] != WS_COMBINATOR:
1139 # It's impossible to have two whitespace combinators after each other as the patterns
1140 # will gobble up trailing whitespace. It is also impossible to have a whitespace
1141 # combinator after any other kind for the same reason. But we could have
1142 # multiple non-whitespace combinators. So if the current combinator is not a whitespace,
1143 # then we've hit the multiple combinator case, so we should fail.
1144 raise SelectorSyntaxError(
1145 f'The multiple combinators at position {index}',
1146 self.pattern,
1147 index
1148 )
1150 # Set the leading combinator for the next selector.
1151 rel_type = ':' + combinator
1153 sel = _Selector()
1154 has_selector = False
1155 return has_selector, sel, rel_type
1157 def parse_combinator(
1158 self,
1159 sel: _Selector,
1160 m: Match[str],
1161 has_selector: bool,
1162 selectors: list[_Selector],
1163 relations: list[_Selector],
1164 is_pseudo: bool,
1165 is_forgive: bool,
1166 index: int
1167 ) -> tuple[bool, _Selector]:
1168 """Parse combinator tokens."""
1170 combinator = m.group('relation')
1171 if not combinator:
1172 combinator = WS_COMBINATOR
1173 if not has_selector:
1174 if not is_forgive or combinator != COMMA_COMBINATOR:
1175 raise SelectorSyntaxError(
1176 f"The combinator '{combinator}' at position {index}, must have a selector before it",
1177 self.pattern,
1178 index
1179 )
1181 # If we are in a forgiving pseudo class, just make the selector a "no match"
1182 if combinator == COMMA_COMBINATOR:
1183 self.increment_count()
1184 del relations[:]
1185 else:
1186 if combinator == COMMA_COMBINATOR:
1187 if not sel.tag and not is_pseudo:
1188 # Implied `*`
1189 sel.tag = ct.SelectorTag('*', None)
1190 sel.relations.extend(relations)
1191 selectors.append(sel)
1192 del relations[:]
1193 else:
1194 sel.relations.extend(relations)
1195 sel.rel_type = combinator
1196 del relations[:]
1197 relations.append(sel)
1199 sel = _Selector()
1200 has_selector = False
1202 return has_selector, sel
1204 def parse_class_id(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1205 """Parse HTML classes and ids."""
1207 selector = m.group(0)
1208 if selector.startswith('.'):
1209 sel.classes.append(css_unescape(selector[1:]))
1210 else:
1211 sel.ids.append(css_unescape(selector[1:]))
1212 has_selector = True
1213 return has_selector
1215 def parse_pseudo_contains(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1216 """Parse contains."""
1218 pseudo = util.lower(css_unescape(m.group('name')))
1219 if pseudo == ":contains":
1220 warnings.warn( # noqa: B028
1221 "The pseudo class ':contains' is deprecated, ':-soup-contains' should be used moving forward.",
1222 FutureWarning
1223 )
1224 contains_own = pseudo == ":-soup-contains-own"
1225 patterns = [
1226 css_unescape(token.group('value'))
1227 for token in RE_VALUES.finditer(m.group('values'))
1228 if not token.group('split')
1229 ]
1230 sel.contains.append(ct.SelectorContains(patterns, contains_own))
1231 has_selector = True
1232 return has_selector
1234 def parse_pseudo_lang(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1235 """Parse pseudo language."""
1237 patterns = [
1238 css_unescape(token.group('value'))
1239 for token in RE_VALUES.finditer(m.group('values'))
1240 if not token.group('split')
1241 ]
1242 sel.lang.append(ct.SelectorLang(patterns))
1243 has_selector = True
1245 return has_selector
1247 def parse_pseudo_dir(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1248 """Parse pseudo direction."""
1250 value = ct.SEL_DIR_LTR if util.lower(m.group('dir')) == 'ltr' else ct.SEL_DIR_RTL
1251 sel.flags |= value
1252 has_selector = True
1253 return has_selector
1255 def parse_selectors(
1256 self,
1257 iselector: Iterator[tuple[str, Match[str]]],
1258 index: int = 0,
1259 flags: int = 0
1260 ) -> ct.SelectorList:
1261 """
1262 Parse selectors.
1264 Pseudo-classes that open a nested selector list push a new context onto a stack,
1265 and closing them pops back to the parent context, so nesting does not recurse.
1266 """
1268 ctx = self.create_context(flags, index)
1269 stack: list[_SelectorContext] = []
1271 for key, m in iselector:
1272 child: _SelectorContext | None = None
1274 if key not in ('combine', 'pseudo_close'):
1275 self.increment_count()
1277 # Handle parts
1278 if self.ignore:
1279 mi = RE_PSEUDO_IGNORE.match(m.group(0))
1280 if mi is not None and mi.group('name').lower() in self.ignore:
1281 raise SelectorSyntaxError(
1282 f"The selector '{mi.group('name')}' at position {m.start(0)}, has been disallowed",
1283 self.pattern,
1284 m.start(0)
1285 )
1286 if key == "at_rule":
1287 raise NotImplementedError(f"At-rules found at position {m.start(0)}")
1288 elif key == "amp":
1289 ctx.sel.flags |= ct.SEL_SCOPE
1290 ctx.has_selector = True
1291 elif key == 'pseudo_class_custom':
1292 ctx.has_selector = self.parse_pseudo_class_custom(ctx.sel, m, ctx.has_selector)
1293 elif key == 'pseudo_class':
1294 ctx.has_selector, ctx.is_html, child = self.parse_pseudo_class(
1295 ctx.sel, m, ctx.has_selector, ctx.is_html, ctx.is_relative
1296 )
1297 elif key == 'pseudo_element':
1298 raise NotImplementedError(f"Pseudo-element found at position {m.start(0)}")
1299 elif key == 'pseudo_contains':
1300 ctx.has_selector = self.parse_pseudo_contains(ctx.sel, m, ctx.has_selector)
1301 elif key in ('pseudo_nth_type', 'pseudo_nth_child'):
1302 ctx.has_selector, child = self.parse_pseudo_nth(ctx.sel, m, ctx.has_selector)
1303 elif key == 'pseudo_lang':
1304 ctx.has_selector = self.parse_pseudo_lang(ctx.sel, m, ctx.has_selector)
1305 elif key == 'pseudo_dir':
1306 ctx.has_selector = self.parse_pseudo_dir(ctx.sel, m, ctx.has_selector)
1307 # Currently only supports HTML
1308 ctx.is_html = True
1309 elif key == 'pseudo_close':
1310 if not ctx.has_selector:
1311 if not ctx.is_forgive:
1312 raise SelectorSyntaxError(
1313 f"Expected a selector at position {m.start(0)}",
1314 self.pattern,
1315 m.start(0)
1316 )
1317 ctx.sel.no_match = True
1318 if not ctx.is_open:
1319 raise SelectorSyntaxError(
1320 f"Unmatched pseudo-class close at position {m.start(0)}",
1321 self.pattern,
1322 m.start(0)
1323 )
1325 # Hand the completed selector list to the parent and resume parsing the parent.
1326 selectors = self.finalize_selectors(ctx)
1327 if ctx.on_close is not None:
1328 ctx.on_close(selectors)
1329 ctx = stack.pop()
1330 continue
1331 elif key == 'combine':
1332 if ctx.is_relative:
1333 ctx.has_selector, ctx.sel, ctx.rel_type = self.parse_has_combinator(
1334 ctx.sel, m, ctx.has_selector, ctx.selectors, ctx.rel_type, ctx.index
1335 )
1336 else:
1337 ctx.has_selector, ctx.sel = self.parse_combinator(
1338 ctx.sel, m, ctx.has_selector, ctx.selectors, ctx.relations,
1339 ctx.is_pseudo, ctx.is_forgive, ctx.index
1340 )
1341 elif key == 'attribute':
1342 ctx.has_selector = self.parse_attribute_selector(ctx.sel, m, ctx.has_selector)
1343 elif key == 'tag':
1344 if ctx.has_selector:
1345 raise SelectorSyntaxError(
1346 f"Tag name found at position {m.start(0)} instead of at the start",
1347 self.pattern,
1348 m.start(0)
1349 )
1350 ctx.has_selector = self.parse_tag_pattern(ctx.sel, m, ctx.has_selector)
1351 elif key in ('class', 'id'):
1352 ctx.has_selector = self.parse_class_id(ctx.sel, m, ctx.has_selector)
1354 ctx.index = m.end(0)
1356 # A pseudo-class opened a nested selector list, parse it before resuming this one.
1357 if child is not None:
1358 stack.append(ctx)
1359 ctx = child
1361 # Handle selectors that are not closed
1362 if ctx.is_open:
1363 raise SelectorSyntaxError(
1364 f"Unclosed pseudo-class at position {ctx.index}",
1365 self.pattern,
1366 ctx.index
1367 )
1369 return self.finalize_selectors(ctx)
1371 def finalize_selectors(self, ctx: _SelectorContext) -> ct.SelectorList:
1372 """Complete the selector list of a context and freeze it."""
1374 sel = ctx.sel
1375 selectors = ctx.selectors
1376 relations = ctx.relations
1377 has_selector = ctx.has_selector
1379 # Cleanup completed selector piece
1380 if has_selector:
1381 if not sel.tag and not ctx.is_pseudo:
1382 # Implied `*`
1383 sel.tag = ct.SelectorTag('*', None)
1384 if ctx.is_relative:
1385 sel.rel_type = ctx.rel_type
1386 selectors[-1].relations.append(sel)
1387 else:
1388 sel.relations.extend(relations)
1389 del relations[:]
1390 selectors.append(sel)
1392 # Forgive empty slots in pseudo-classes that have lists (and are forgiving)
1393 elif ctx.is_forgive and (not selectors or not relations):
1394 # Handle normal pseudo-classes with empty slots like `:is()` etc.
1395 self.increment_count()
1396 sel.no_match = True
1397 del relations[:]
1398 selectors.append(sel)
1399 has_selector = True
1401 if not has_selector:
1402 # We will always need to finish a selector when `:has()` is used as it leads with combining.
1403 # May apply to others as well.
1404 raise SelectorSyntaxError(
1405 f'Expected a selector at position {ctx.index}',
1406 self.pattern,
1407 ctx.index
1408 )
1410 # Some patterns require additional logic, such as default. We try to make these the
1411 # last pattern, and append the appropriate flag to that selector which communicates
1412 # to the matcher what additional logic is required.
1413 if ctx.is_default:
1414 selectors[-1].flags = ct.SEL_DEFAULT
1415 if ctx.is_indeterminate:
1416 selectors[-1].flags = ct.SEL_INDETERMINATE
1417 if ctx.is_in_range:
1418 selectors[-1].flags = ct.SEL_IN_RANGE
1419 if ctx.is_out_of_range:
1420 selectors[-1].flags = ct.SEL_OUT_OF_RANGE
1421 if ctx.is_placeholder_shown:
1422 selectors[-1].flags = ct.SEL_PLACEHOLDER_SHOWN
1423 if ctx.is_editable:
1424 selectors[-1].flags = ct.SEL_EDITABLE
1426 # Return selector list
1427 return ct.SelectorList([s.freeze() for s in selectors], ctx.is_not, ctx.is_html, self.count - ctx.count)
1429 def selector_iter(self, pattern: str) -> Iterator[tuple[str, Match[str]]]:
1430 """Iterate selector tokens."""
1432 # Ignore whitespace and comments at start and end of pattern
1433 m = RE_WS_BEGIN.search(pattern)
1434 index = m.end(0) if m else 0
1435 m = RE_WS_END.search(pattern[::-1])
1436 offset = m.end(0) if m else 0
1437 end = len(pattern) - (1 + offset)
1439 if self.debug: # pragma: no cover
1440 print(f'## PARSING: {pattern!r}')
1441 while index <= end:
1442 m = None
1443 for v in self.CSS_TOKENS:
1444 m = v.match(pattern, index, self.flags)
1445 if m:
1446 name = v.get_name()
1447 if self.debug: # pragma: no cover
1448 print(f"TOKEN: '{name}' --> {m.group(0)!r} at position {m.start(0)}")
1449 index = m.end(0)
1450 yield name, m
1451 break
1452 if m is None:
1453 c = pattern[index]
1454 # If the character represents the start of one of the known selector types,
1455 # throw an exception mentioning that the known selector type is in error;
1456 # otherwise, report the invalid character.
1457 if c == '[':
1458 msg = f"Malformed attribute selector at position {index}"
1459 elif c == '.':
1460 msg = f"Malformed class selector at position {index}"
1461 elif c == '#':
1462 msg = f"Malformed id selector at position {index}"
1463 elif c == ':':
1464 msg = f"Malformed pseudo-class selector at position {index}"
1465 else:
1466 msg = f"Invalid character {c!r} position {index}"
1467 raise SelectorSyntaxError(msg, self.pattern, index)
1468 if self.debug: # pragma: no cover
1469 print('## END PARSING')
1471 def process_selectors(self, index: int = 0, flags: int = 0) -> ct.SelectorList:
1472 """Process selectors."""
1474 return self.parse_selectors(self.selector_iter(self.pattern), index, flags)