Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/soupsieve/css_parser.py: 25%
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, Iterator, Iterable, cast
11from dataclasses import dataclass
12from collections import UserDict
13import threading
15RE_LOCK = threading.Lock()
16SEL_LOCK = threading.RLock()
18UNICODE_REPLACEMENT_CHAR = 0xFFFD
20SELECTOR_LIMIT = 8192
22# Simple pseudo classes that take no parameters
23PSEUDO_SIMPLE = {
24 ":any-link",
25 ":empty",
26 ":first-child",
27 ":first-of-type",
28 ":in-range",
29 ":open",
30 ":out-of-range",
31 ":last-child",
32 ":last-of-type",
33 ":link",
34 ":only-child",
35 ":only-of-type",
36 ":root",
37 ':checked',
38 ':default',
39 ':disabled',
40 ':enabled',
41 ':indeterminate',
42 ':optional',
43 ':placeholder-shown',
44 ':read-only',
45 ':read-write',
46 ':required',
47 ':scope',
48 ':defined',
49 ':muted'
50}
52# Supported, simple pseudo classes that match nothing in the Soup Sieve environment
53PSEUDO_SIMPLE_NO_MATCH = {
54 ':active',
55 ':autofill',
56 ':buffering',
57 ':current',
58 ':focus',
59 ':focus-visible',
60 ':focus-within',
61 ':fullscreen',
62 ':future',
63 ':host',
64 ':hover',
65 ':local-link',
66 ':past',
67 ':paused',
68 ':picture-in-picture',
69 ':playing',
70 ':popover-open',
71 ':seeking',
72 ':stalled',
73 ':target',
74 ':target-within',
75 ':user-invalid',
76 ':volume-locked',
77 ':visited'
78}
80# Complex pseudo classes that take selector lists
81PSEUDO_COMPLEX = {
82 ':contains',
83 ':-soup-contains',
84 ':-soup-contains-own',
85 ':has',
86 ':is',
87 ':matches',
88 ':not',
89 ':where'
90}
92PSEUDO_COMPLEX_NO_MATCH = {
93 ':current',
94 ':host',
95 ':host-context'
96}
98# Complex pseudo classes that take very specific parameters and are handled special
99PSEUDO_SPECIAL = {
100 ':dir',
101 ':lang',
102 ':nth-child',
103 ':nth-last-child',
104 ':nth-last-of-type',
105 ':nth-of-type'
106}
108PSEUDO_SUPPORTED = PSEUDO_SIMPLE | PSEUDO_SIMPLE_NO_MATCH | PSEUDO_COMPLEX | PSEUDO_COMPLEX_NO_MATCH | PSEUDO_SPECIAL
110# Sub-patterns parts
111# Whitespace
112NEWLINE = r'(?:\r\n|(?!\r\n)[\n\f\r])'
113WS = fr'(?:[ \t]|{NEWLINE})'
114# Comments
115COMMENTS = r'(?:/\*(?:[^*]|\*(?!/))*\*/)'
116# Whitespace with comments included
117WSC = fr'(?:{WS}|{COMMENTS})'
118# CSS escapes
119CSS_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}{WS}?|[^\r\n\f]|$))'
120CSS_STRING_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}{WS}?|{NEWLINE}|[^\r\n\f]|$))'
121# CSS Identifier
122IDENTIFIER = fr'''
123(?:(?:--|-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES}))
124(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})*)
125'''
126# `nth` content
127NTH = fr'[-+]?(?:[0-9]+n?|n)(?:(?<=n){WSC}*[-+]{WSC}*[0-9]+)?'
128# Value: quoted string or identifier
129VALUE = fr'''
130(?:"(?:\\(?:{NEWLINE}|[^\r\n\f])|[^\\"\r\n\f])*"|
131'(?:\\(?:{NEWLINE}|[^\r\n\f])|[^\\'\r\n\f])*'|
132{IDENTIFIER})
133'''
134# Attribute value comparison. `!=` is handled special as it is non-standard.
135ATTR = fr'(?:{WSC}*(?P<cmp>[!~^|*$]?=){WSC}*(?P<value>{VALUE})(?:{WSC}*(?P<case>[is]))?)?{WSC}*'
137# Selector patterns
138# IDs (`#id`)
139PAT_ID = fr'\#{IDENTIFIER}'
140# Classes (`.class`)
141PAT_CLASS = fr'\.{IDENTIFIER}'
142# Prefix:Tag (`prefix|tag`)
143PAT_TAG = fr'(?P<tag_ns>(?:{IDENTIFIER}|\*)?\|)?(?P<tag_name>{IDENTIFIER}|\*)'
144# Attributes (`[attr]`, `[attr=value]`, etc.)
145PAT_ATTR = fr'\[{WSC}*(?P<attr_ns>(?:{IDENTIFIER}|\*)?\|(?!=))?(?P<attr_name>{IDENTIFIER}){ATTR}{WSC}*\]'
146# Pseudo class (`:pseudo-class`, `:pseudo-class(`)
147PAT_PSEUDO_CLASS = fr'(?P<name>:{IDENTIFIER})(?P<open>\({WSC}*)?'
148# Pseudo class special patterns. Matches `:pseudo-class(` for special case pseudo classes.
149PAT_PSEUDO_CLASS_SPECIAL = fr'(?P<name>:{IDENTIFIER})(?P<open>\({WSC}*)'
150# Custom pseudo class (`:--custom-pseudo`)
151PAT_PSEUDO_CLASS_CUSTOM = fr'(?P<name>:(?=--){IDENTIFIER})'
152# Nesting ampersand selector. Matches `&`
153PAT_AMP = r'&'
154# Closing pseudo group (`)`)
155PAT_PSEUDO_CLOSE = fr'{WSC}*\)'
156# Pseudo element (`::pseudo-element`)
157PAT_PSEUDO_ELEMENT = fr':{PAT_PSEUDO_CLASS}'
158# At rule (`@page`, etc.) (not supported)
159PAT_AT_RULE = fr'@P{IDENTIFIER}'
160# Pseudo class `nth-child` (`:nth-child(an+b [of S]?)`, `:first-child`, etc.)
161PAT_PSEUDO_NTH_CHILD = fr'''
162(?P<pseudo_nth_child>{PAT_PSEUDO_CLASS_SPECIAL}
163(?P<nth_child>{NTH}|even|odd))(?:{WSC}*\)|(?P<of>{COMMENTS}*{WS}{WSC}*of{COMMENTS}*{WS}{WSC}*))
164'''
165# Pseudo class `nth-of-type` (`:nth-of-type(an+b)`, `:first-of-type`, etc.)
166PAT_PSEUDO_NTH_TYPE = fr'''
167(?P<pseudo_nth_type>{PAT_PSEUDO_CLASS_SPECIAL}
168(?P<nth_type>{NTH}|even|odd)){WSC}*\)
169'''
170# Pseudo class language (`:lang("*-de", en)`)
171PAT_PSEUDO_LANG = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<values>{VALUE}(?:{WSC}*,{WSC}*{VALUE})*){WSC}*\)'
172# Pseudo class direction (`:dir(ltr)`)
173PAT_PSEUDO_DIR = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<dir>ltr|rtl){WSC}*\)'
174# Combining characters (`>`, `~`, ` `, `+`, `,`)
175PAT_COMBINE = fr'{COMMENTS}*(?={WS}|[,+>~]){WSC}*(?P<relation>[,+>~])?{WSC}*'
176# Extra: Contains (`:contains(text)`)
177PAT_PSEUDO_CONTAINS = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<values>{VALUE}(?:{WSC}*,{WSC}*{VALUE})*){WSC}*\)'
179# Regular expressions
180# CSS escape pattern
181RE_CSS_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}{WSC}?)|(\\[^\r\n\f])|(\\$))', re.I)
182RE_CSS_STR_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}{WS}?)|(\\[^\r\n\f])|(\\$)|(\\{NEWLINE}))', re.I)
183# Pattern to break up `nth` specifiers
184RE_NTH = re.compile(fr'(?P<s1>[-+])?(?P<a>[0-9]+n?|n)(?:(?<=n){WSC}*(?P<s2>[-+]){WSC}*(?P<b>[0-9]+))?', re.I)
185# Pattern to iterate multiple values.
186RE_VALUES = re.compile(fr'(?:(?P<value>{VALUE})|(?P<split>{WSC}*,{WSC}*))', re.X)
187# Whitespace checks
188RE_WS = re.compile(WS)
189RE_WS_BEGIN = re.compile(fr'^{WSC}*')
190RE_WS_END = re.compile(fr'^(?:[ \t]|(?:\n\r|(?!\n\r)[\n\f\r])|{COMMENTS})*')
191RE_CUSTOM = re.compile(fr'^{PAT_PSEUDO_CLASS_CUSTOM}$', re.X)
192RE_PSEUDO_CLASS_SPECIAL = re.compile(PAT_PSEUDO_CLASS_SPECIAL, re.I | re.X | re.U)
193RE_PSEUDO_IGNORE = re.compile(PAT_PSEUDO_CLASS, re.X | re.I | re.U)
195QUOTED = ("'", '"')
197# Constants
198# List split token
199COMMA_COMBINATOR = ','
200# Relation token for descendant
201WS_COMBINATOR = " "
203# Parse flags
204FLG_PSEUDO = 0x01
205FLG_NOT = 0x02
206FLG_RELATIVE = 0x04
207FLG_DEFAULT = 0x08
208FLG_HTML = 0x10
209FLG_INDETERMINATE = 0x20
210FLG_OPEN = 0x40
211FLG_IN_RANGE = 0x80
212FLG_OUT_OF_RANGE = 0x100
213FLG_PLACEHOLDER_SHOWN = 0x200
214FLG_FORGIVE = 0x400
216# Maximum cached patterns to store
217_MAXCACHE = 500
220@lru_cache(maxsize=_MAXCACHE)
221def _cached_css_compile(
222 pattern: str,
223 namespaces: ct.Namespaces | None,
224 custom: ct.CustomSelectors | None,
225 ignore: tuple[str] | None = None,
226 flags: int = 0
227) -> cm.SoupSieve:
228 """Cached CSS compile."""
230 custom_selectors = process_custom(custom)
231 return cm.SoupSieve(
232 pattern,
233 CSSParser(
234 pattern,
235 custom=custom_selectors,
236 ignore=ignore,
237 flags=flags
238 ).process_selectors(),
239 namespaces,
240 custom,
241 flags
242 )
245def _purge_cache() -> None:
246 """Purge the cache."""
248 _cached_css_compile.cache_clear()
251def process_custom(custom: ct.CustomSelectors | None) -> dict[str, str | ct.SelectorList]:
252 """Process custom."""
254 custom_selectors = {}
255 if custom is not None:
256 for key, value in custom.items():
257 name = util.lower(key)
258 if RE_CUSTOM.match(name) is None:
259 raise SelectorSyntaxError(f"The name '{name}' is not a valid custom pseudo-class name")
260 if name in custom_selectors:
261 raise KeyError(f"The custom selector '{name}' has already been registered")
262 custom_selectors[css_unescape(name)] = value
263 return custom_selectors
266def css_unescape(content: str) -> str:
267 """
268 Unescape CSS value.
270 Strings allow for spanning the value on multiple strings by escaping a new line.
271 """
273 def replace(m: Match[str]) -> str:
274 """Replace with the appropriate substitute."""
276 if m.group(1):
277 codepoint = int(m.group(1)[1:], 16)
278 if codepoint == 0:
279 codepoint = UNICODE_REPLACEMENT_CHAR
280 value = chr(codepoint)
281 elif m.group(2):
282 value = m.group(2)[1:]
283 elif m.group(3):
284 value = '\ufffd'
285 else:
286 value = ''
288 return value
290 quoted = content.startswith(QUOTED)
291 return (RE_CSS_STR_ESC if quoted else RE_CSS_ESC).sub(
292 replace,
293 content[1:-1] if quoted else content
294 )
297def escape(ident: str) -> str:
298 """Escape identifier."""
300 string = []
301 length = len(ident)
302 start_dash = length > 0 and ident[0] == '-'
303 if length == 1 and start_dash:
304 # Need to escape identifier that is a single `-` with no other characters
305 string.append(f'\\{ident}')
306 else:
307 for index, c in enumerate(ident):
308 codepoint = ord(c)
309 if codepoint == 0x00:
310 string.append('\ufffd')
311 elif (0x01 <= codepoint <= 0x1F) or codepoint == 0x7F:
312 string.append(f'\\{codepoint:x} ')
313 elif (index == 0 or (start_dash and index == 1)) and (0x30 <= codepoint <= 0x39):
314 string.append(f'\\{codepoint:x} ')
315 elif (
316 codepoint in (0x2D, 0x5F) or codepoint >= 0x80 or (0x30 <= codepoint <= 0x39) or
317 (0x30 <= codepoint <= 0x39) or (0x41 <= codepoint <= 0x5A) or (0x61 <= codepoint <= 0x7A)
318 ):
319 string.append(c)
320 else:
321 string.append(f'\\{c}')
322 return ''.join(string)
325class SelectorPattern:
326 """Selector pattern."""
328 def __init__(self, name: str, pattern: str) -> None:
329 """Initialize."""
331 self.name = name
332 self.pattern = pattern
333 self._re_pattern: re.Pattern[str] | None = None
335 @property
336 def re_pattern(self) -> re.Pattern[str]:
337 """Retrieve the compiled regular expression pattern."""
339 with RE_LOCK:
340 if self._re_pattern is None:
341 self._re_pattern = re.compile(self.pattern, re.I | re.X | re.U)
342 return self._re_pattern
344 def get_name(self) -> str:
345 """Get name."""
347 return self.name
349 def match(self, selector: str, index: int, flags: int) -> Match[str] | None:
350 """Match the selector."""
352 return self.re_pattern.match(selector, index)
355class SpecialPseudoPattern(SelectorPattern):
356 """Selector pattern."""
358 def __init__(self, patterns: tuple[tuple[str, tuple[str, ...], str, type[SelectorPattern]], ...]) -> None:
359 """Initialize."""
361 self.patterns = {}
362 for p in patterns:
363 name = p[0]
364 pattern = p[3](name, p[2])
365 for pseudo in p[1]:
366 self.patterns[pseudo] = pattern
368 self.matched_name = None # type: SelectorPattern | None
370 def get_name(self) -> str:
371 """Get name."""
373 return '' if self.matched_name is None else self.matched_name.get_name()
375 def match(self, selector: str, index: int, flags: int) -> Match[str] | None:
376 """Match the selector."""
378 pseudo = None
379 m = RE_PSEUDO_CLASS_SPECIAL.match(selector, index)
380 if m:
381 name = util.lower(css_unescape(m.group('name')))
382 pattern = self.patterns.get(name)
383 if pattern:
384 pseudo = pattern.match(selector, index, flags)
385 if pseudo:
386 self.matched_name = pattern
388 return pseudo
391class _Selector:
392 """
393 Intermediate selector class.
395 This stores selector data for a compound selector as we are acquiring them.
396 Once we are done collecting the data for a compound selector, we freeze
397 the data in an object that can be pickled and hashed.
398 """
400 def __init__(self, **kwargs: Any) -> None:
401 """Initialize."""
403 self.tag = kwargs.get('tag', None) # type: ct.SelectorTag | None
404 self.ids = kwargs.get('ids', []) # type: list[str]
405 self.classes = kwargs.get('classes', []) # type: list[str]
406 self.attributes = kwargs.get('attributes', []) # type: list[ct.SelectorAttribute]
407 self.nth = kwargs.get('nth', []) # type: list[ct.SelectorNth]
408 self.selectors = kwargs.get('selectors', []) # type: list[ct.SelectorList]
409 self.relations = kwargs.get('relations', []) # type: list[_Selector]
410 self.rel_type = kwargs.get('rel_type', None) # type: str | None
411 self.contains = kwargs.get('contains', []) # type: list[ct.SelectorContains]
412 self.lang = kwargs.get('lang', []) # type: list[ct.SelectorLang]
413 self.flags = kwargs.get('flags', 0) # type: int
414 self.no_match = kwargs.get('no_match', False) # type: bool
416 def _freeze_relations(self, relations: list[_Selector]) -> ct.SelectorList:
417 """Freeze relation."""
419 if relations:
420 sel = relations[0]
421 sel.relations.extend(relations[1:])
422 return ct.SelectorList([sel.freeze()])
423 else:
424 return ct.SelectorList()
426 def freeze(self) -> ct.Selector | ct.SelectorNull:
427 """Freeze self."""
429 if self.no_match:
430 return ct.Null
431 else:
432 return ct.Selector(
433 self.tag,
434 tuple(self.ids),
435 tuple(self.classes),
436 tuple(self.attributes),
437 tuple(self.nth),
438 tuple(self.selectors),
439 self._freeze_relations(self.relations),
440 self.rel_type,
441 tuple(self.contains),
442 tuple(self.lang),
443 self.flags
444 )
446 def __str__(self) -> str: # pragma: no cover
447 """String representation."""
449 return (
450 f'_Selector(tag={self.tag!r}, ids={self.ids!r}, classes={self.classes!r}, attributes={self.attributes!r}, '
451 f'nth={self.nth!r}, selectors={self.selectors!r}, relations={self.relations!r}, '
452 f'rel_type={self.rel_type!r}, contains={self.contains!r}, lang={self.lang!r}, flags={self.flags!r}, '
453 f'no_match={self.no_match!r})'
454 )
456 __repr__ = __str__
459@dataclass
460class CSSPattern:
461 """A CSS pattern that hasn't been processed by `CSSParser` yet."""
463 selector: str
464 flags: int
467class PseudoSelectorMap(UserDict[str, CSSPattern | ct.SelectorList]):
468 """Pseudo selector map."""
470 def __setitem__(self, key: str, value: CSSPattern | ct.SelectorList) -> None:
471 """Set item."""
473 self.data[key] = value
475 def __getitem__(self, key: str) -> ct.SelectorList:
476 """Get item."""
478 with SEL_LOCK:
479 value = self.data[key]
480 if isinstance(value, CSSPattern):
481 value = CSSParser(value.selector).process_selectors(flags=value.flags)
482 self.data[key] = value
484 return value
487# CSS pattern for `:link` and `:any-link`
488CSS_LINK = CSSPattern('html|*:is(a, area)[href]', FLG_PSEUDO | FLG_HTML)
489# CSS pattern for `:checked`
490CSS_CHECKED = CSSPattern(
491 '''
492 html|*:is(input[type=checkbox], input[type=radio])[checked], html|option[selected]
493 ''',
494 FLG_PSEUDO | FLG_HTML
495)
496# CSS pattern for `:default` (must compile CSS_CHECKED first)
497CSS_DEFAULT = CSSPattern(
498 '''
499 :checked,
501 /*
502 This pattern must be at the end.
503 Special logic is applied to the last selector.
504 */
505 html|form html|*:is(button, input)[type="submit"]
506 ''',
507 FLG_PSEUDO | FLG_HTML | FLG_DEFAULT
508)
509# CSS pattern for `:indeterminate`
510CSS_INDETERMINATE = CSSPattern(
511 '''
512 html|input[type="checkbox"][indeterminate],
513 html|input[type="radio"]:is(:not([name]), [name=""]):not([checked]),
514 html|progress:not([value]),
516 /*
517 This pattern must be at the end.
518 Special logic is applied to the last selector.
519 */
520 html|input[type="radio"][name]:not([name='']):not([checked])
521 ''',
522 FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE
523)
524# CSS pattern for `:disabled`
525CSS_DISABLED = CSSPattern(
526 '''
527 html|*:is(input:not([type=hidden]), button, select, textarea, fieldset, optgroup, option, fieldset)[disabled],
528 html|optgroup[disabled] > html|option,
529 html|fieldset[disabled] > html|*:is(input:not([type=hidden]), button, select, textarea, fieldset),
530 html|fieldset[disabled] >
531 html|*:not(legend:nth-of-type(1)) html|*:is(input:not([type=hidden]), button, select, textarea, fieldset)
532 ''',
533 FLG_PSEUDO | FLG_HTML
534)
535# CSS pattern for `:enabled`
536CSS_ENABLED = CSSPattern(
537 '''
538 html|*:is(input:not([type=hidden]), button, select, textarea, fieldset, optgroup, option, fieldset):not(:disabled)
539 ''',
540 FLG_PSEUDO | FLG_HTML
541)
542# CSS pattern for `:required`
543CSS_REQUIRED = CSSPattern('html|*:is(input, textarea, select)[required]', FLG_PSEUDO | FLG_HTML)
544# CSS pattern for `:optional`
545CSS_OPTIONAL = CSSPattern('html|*:is(input, textarea, select):not([required])', FLG_PSEUDO | FLG_HTML)
546# CSS pattern for `:placeholder-shown`
547CSS_PLACEHOLDER_SHOWN = CSSPattern(
548 '''
549 html|input:is(
550 :not([type]),
551 [type=""],
552 [type=text],
553 [type=search],
554 [type=url],
555 [type=tel],
556 [type=email],
557 [type=password],
558 [type=number]
559 )[placeholder]:not([placeholder='']):is(:not([value]), [value=""]),
560 html|textarea[placeholder]:not([placeholder=''])
561 ''',
562 FLG_PSEUDO | FLG_HTML | FLG_PLACEHOLDER_SHOWN
563)
564# CSS pattern for `:read-write` (CSS_DISABLED must be compiled first)
565CSS_READ_WRITE = CSSPattern(
566 '''
567 html|*:is(
568 textarea,
569 input:is(
570 :not([type]),
571 [type=""],
572 [type=text],
573 [type=search],
574 [type=url],
575 [type=tel],
576 [type=email],
577 [type=number],
578 [type=password],
579 [type=date],
580 [type=datetime-local],
581 [type=month],
582 [type=time],
583 [type=week]
584 )
585 ):not([readonly], :disabled),
586 html|*:is([contenteditable=""], [contenteditable="true" i])
587 ''',
588 FLG_PSEUDO | FLG_HTML
589)
590# CSS pattern for `:read-only`
591CSS_READ_ONLY = CSSPattern('html|*:not(:read-write)', FLG_PSEUDO | FLG_HTML)
592# CSS pattern for `:in-range`
593CSS_IN_RANGE = CSSPattern(
594 '''
595 html|input:is(
596 [type="date"],
597 [type="month"],
598 [type="week"],
599 [type="time"],
600 [type="datetime-local"],
601 [type="number"],
602 [type="range"]
603 ):is(
604 [min],
605 [max]
606 )
607 ''',
608 FLG_PSEUDO | FLG_HTML | FLG_IN_RANGE
609)
610# CSS pattern for `:out-of-range`
611CSS_OUT_OF_RANGE = CSSPattern(
612 '''
613 html|input:is(
614 [type="date"],
615 [type="month"],
616 [type="week"],
617 [type="time"],
618 [type="datetime-local"],
619 [type="number"],
620 [type="range"]
621 ):is(
622 [min],
623 [max]
624 )
625 ''',
626 FLG_PSEUDO | FLG_HTML | FLG_OUT_OF_RANGE
627)
628# CSS pattern for :open
629CSS_OPEN = CSSPattern('html|*:is(details, dialog)[open]', FLG_PSEUDO | FLG_HTML)
630# CSS pattern for :muted
631CSS_MUTED = CSSPattern('html|*:is(video, audio)[muted]', FLG_PSEUDO | FLG_HTML)
632# CSS pattern default for `:nth-child` "of S" feature
633CSS_NTH_OF_S_DEFAULT = CSSPattern("*|*", FLG_PSEUDO)
636class CSSParser:
637 """Parse CSS selectors."""
639 CSS_TOKENS = (
640 SelectorPattern("pseudo_close", PAT_PSEUDO_CLOSE),
641 SpecialPseudoPattern(
642 (
643 (
644 "pseudo_contains",
645 (':contains', ':-soup-contains', ':-soup-contains-own'),
646 PAT_PSEUDO_CONTAINS,
647 SelectorPattern
648 ),
649 ("pseudo_nth_child", (':nth-child', ':nth-last-child'), PAT_PSEUDO_NTH_CHILD, SelectorPattern),
650 ("pseudo_nth_type", (':nth-of-type', ':nth-last-of-type'), PAT_PSEUDO_NTH_TYPE, SelectorPattern),
651 ("pseudo_lang", (':lang',), PAT_PSEUDO_LANG, SelectorPattern),
652 ("pseudo_dir", (':dir',), PAT_PSEUDO_DIR, SelectorPattern)
653 )
654 ),
655 SelectorPattern("pseudo_class_custom", PAT_PSEUDO_CLASS_CUSTOM),
656 SelectorPattern("pseudo_class", PAT_PSEUDO_CLASS),
657 SelectorPattern("pseudo_element", PAT_PSEUDO_ELEMENT),
658 SelectorPattern("amp", PAT_AMP),
659 SelectorPattern("at_rule", PAT_AT_RULE),
660 SelectorPattern("id", PAT_ID),
661 SelectorPattern("class", PAT_CLASS),
662 SelectorPattern("tag", PAT_TAG),
663 SelectorPattern("attribute", PAT_ATTR),
664 SelectorPattern("combine", PAT_COMBINE)
665 )
667 # Pseudos that expand to selectors
668 PSEUDO_SELECTORS = PseudoSelectorMap(
669 {
670 ':link': CSS_LINK,
671 ':any-link': CSS_LINK,
672 ':checked': CSS_CHECKED,
673 ':default': CSS_DEFAULT,
674 ':indeterminate': CSS_INDETERMINATE,
675 ':disabled': CSS_DISABLED,
676 ':enabled': CSS_ENABLED,
677 ':required': CSS_REQUIRED,
678 ':muted': CSS_MUTED,
679 ':open': CSS_OPEN,
680 ':optional': CSS_OPTIONAL,
681 ':read-only': CSS_READ_ONLY,
682 ':read-write': CSS_READ_WRITE,
683 ':in-range': CSS_IN_RANGE,
684 ':out-of-range': CSS_OUT_OF_RANGE,
685 ':placeholder-shown': CSS_PLACEHOLDER_SHOWN,
686 '<nth-of-s>': CSS_NTH_OF_S_DEFAULT
687 }
688 )
690 def __init__(
691 self,
692 selector: str,
693 *,
694 custom: dict[str, str | ct.SelectorList] | None = None,
695 ignore: Iterable[str] | None = None,
696 flags: int = 0
697 ) -> None:
698 """Initialize."""
700 self.pattern = selector.replace('\x00', '\ufffd')
701 self.flags = flags
702 self.debug = self.flags & util.DEBUG
703 self.custom = {} if custom is None else custom
704 self.ignore = frozenset([] if ignore is None else ignore)
705 self.count = 0
707 def increment_count(self, increment: int = 1) -> None:
708 """Check the current selector count."""
710 self.count += increment
711 if self.count > SELECTOR_LIMIT:
712 raise ValueError(f'Selector exceeds pseudo-class nesting limit of {SELECTOR_LIMIT}')
714 def parse_attribute_selector(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
715 """Create attribute selector from the returned regex match."""
717 inverse = False
718 op = m.group('cmp')
719 case = util.lower(m.group('case')) if m.group('case') else None
720 ns = css_unescape(m.group('attr_ns')[:-1]) if m.group('attr_ns') else ''
721 attr = css_unescape(m.group('attr_name'))
722 is_type = False
723 pattern2 = None
724 value = ''
726 if case:
727 flags = (re.I if case == 'i' else 0) | re.DOTALL
728 elif util.lower(attr) == 'type':
729 flags = re.I | re.DOTALL
730 is_type = True
731 else:
732 flags = re.DOTALL
734 if op:
735 value = css_unescape(m.group('value'))
737 if not op:
738 # Attribute name
739 pattern = None
740 elif op.startswith('^'):
741 # Value start with
742 # `^=` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
743 value = r'(?!)' if not value else re.escape(value)
744 pattern = re.compile(r'^%s.*' % value, flags)
745 elif op.startswith('$'):
746 # Value ends with
747 # `$=` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
748 value = r'(?!)' if not value else re.escape(value)
749 pattern = re.compile(r'.*?%s$' % value, flags)
750 elif op.startswith('*'):
751 # Value contains
752 # `*=` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
753 value = r'(?!)' if not value else re.escape(value)
754 pattern = re.compile(r'.*?%s.*' % value, flags)
755 elif op.startswith('~'):
756 # Value contains word within space separated list
757 # `*~` should match nothing if the value is empty, so use `(?!)` which cannot be matched.
758 value = r'(?!)' if not value or RE_WS.search(value) else re.escape(value)
759 pattern = re.compile(r'.*?(?:(?<=^)|(?<=[ \t\r\n\f]))%s(?=(?:[ \t\r\n\f]|$)).*' % value, flags)
760 elif op.startswith('|'):
761 # Value starts with word in dash separated list
762 pattern = re.compile(r'^%s(?:-.*)?$' % re.escape(value), flags)
763 else:
764 # Value matches
765 pattern = re.compile(r'^%s$' % re.escape(value), flags)
766 if op.startswith('!'):
767 # Equivalent to `:not([attr=value])`
768 inverse = True
769 if is_type and pattern:
770 pattern2 = re.compile(pattern.pattern)
772 # Append the attribute selector
773 sel_attr = ct.SelectorAttribute(attr, ns, pattern, pattern2)
774 if inverse:
775 # If we are using `!=`, we need to nest the pattern under a `:not()`.
776 sub_sel = _Selector()
777 sub_sel.attributes.append(sel_attr)
778 not_list = ct.SelectorList([sub_sel.freeze()], True, False)
779 sel.selectors.append(not_list)
780 else:
781 sel.attributes.append(sel_attr)
783 has_selector = True
784 return has_selector
786 def parse_tag_pattern(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
787 """Parse tag pattern from regex match."""
789 prefix = css_unescape(m.group('tag_ns')[:-1]) if m.group('tag_ns') else None
790 tag = css_unescape(m.group('tag_name'))
791 sel.tag = ct.SelectorTag(tag, prefix)
792 has_selector = True
793 return has_selector
795 def parse_pseudo_class_custom(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
796 """
797 Parse custom pseudo class alias.
799 Compile custom selectors as we need them. When compiling a custom selector,
800 set it to `None` in the dictionary so we can avoid an infinite loop.
801 """
803 pseudo = util.lower(css_unescape(m.group('name')))
804 selector = self.custom.get(pseudo)
805 if selector is None:
806 raise SelectorSyntaxError(
807 f"Undefined custom selector '{pseudo}' found at position {m.end(0)}",
808 self.pattern,
809 m.end(0)
810 )
812 if not isinstance(selector, ct.SelectorList):
813 del self.custom[pseudo]
814 selector = CSSParser(
815 selector, custom=self.custom, flags=self.flags, ignore=self.ignore
816 ).process_selectors(flags=FLG_PSEUDO)
817 self.custom[pseudo] = selector
819 self.increment_count(selector.count)
820 sel.selectors.append(selector)
821 has_selector = True
822 return has_selector
824 def parse_pseudo_class(
825 self,
826 sel: _Selector,
827 m: Match[str],
828 has_selector: bool,
829 iselector: Iterator[tuple[str, Match[str]]],
830 is_html: bool
831 ) -> tuple[bool, bool]:
832 """Parse pseudo class."""
834 complex_pseudo = False
835 pseudo = util.lower(css_unescape(m.group('name')))
836 if m.group('open'):
837 complex_pseudo = True
838 if complex_pseudo and pseudo in PSEUDO_COMPLEX:
839 has_selector = self.parse_pseudo_open(sel, pseudo, has_selector, iselector, m.end(0))
840 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE:
841 if pseudo == ':root':
842 sel.flags |= ct.SEL_ROOT
843 elif pseudo == ':defined':
844 sel.flags |= ct.SEL_DEFINED
845 is_html = True
846 elif pseudo == ':scope':
847 sel.flags |= ct.SEL_SCOPE
848 elif pseudo == ':empty':
849 sel.flags |= ct.SEL_EMPTY
850 elif pseudo in self.PSEUDO_SELECTORS:
851 pseudo_selector = self.PSEUDO_SELECTORS[pseudo]
852 self.increment_count(pseudo_selector.count)
853 sel.selectors.append(pseudo_selector)
854 elif pseudo == ':first-child':
855 sel.nth.append(ct.SelectorNth(0, False, 1, False, False, ct.SelectorList()))
856 elif pseudo == ':last-child':
857 sel.nth.append(ct.SelectorNth(0, False, 1, False, True, ct.SelectorList()))
858 elif pseudo == ':first-of-type':
859 sel.nth.append(ct.SelectorNth(0, False, 1, True, False, ct.SelectorList()))
860 elif pseudo == ':last-of-type':
861 sel.nth.append(ct.SelectorNth(0, False, 1, True, True, ct.SelectorList()))
862 elif pseudo == ':only-child':
863 sel.nth.extend(
864 [
865 ct.SelectorNth(0, False, 1, False, False, ct.SelectorList()),
866 ct.SelectorNth(0, False, 1, False, True, ct.SelectorList())
867 ]
868 )
869 elif pseudo == ':only-of-type':
870 sel.nth.extend(
871 [
872 ct.SelectorNth(0, False, 1, True, False, ct.SelectorList()),
873 ct.SelectorNth(0, False, 1, True, True, ct.SelectorList())
874 ]
875 )
876 has_selector = True
877 elif complex_pseudo and pseudo in PSEUDO_COMPLEX_NO_MATCH:
878 self.parse_selectors(iselector, m.end(0), FLG_PSEUDO | FLG_OPEN)
879 sel.no_match = True
880 has_selector = True
881 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE_NO_MATCH:
882 sel.no_match = True
883 has_selector = True
884 elif pseudo in PSEUDO_SUPPORTED:
885 raise SelectorSyntaxError(
886 f"Invalid syntax for pseudo class '{pseudo}'",
887 self.pattern,
888 m.start(0)
889 )
890 else:
891 raise SelectorSyntaxError(
892 f"'{pseudo}' was detected as a pseudo-class and is either unsupported or invalid. "
893 "If the syntax was not intended to be recognized as a pseudo-class, please escape the colon.",
894 self.pattern,
895 m.start(0)
896 )
898 return has_selector, is_html
900 def parse_pseudo_nth(
901 self,
902 sel: _Selector,
903 m: Match[str],
904 has_selector: bool,
905 iselector: Iterator[tuple[str, Match[str]]]
906 ) -> bool:
907 """Parse `nth` pseudo."""
909 mdict = m.groupdict()
910 if mdict.get('pseudo_nth_child'):
911 postfix = '_child'
912 else:
913 postfix = '_type'
914 mdict['name'] = util.lower(css_unescape(mdict['name']))
915 content = util.lower(mdict.get('nth' + postfix))
916 if content == 'even':
917 # 2n
918 s1 = 2
919 s2 = 0
920 var = True
921 elif content == 'odd':
922 # 2n+1
923 s1 = 2
924 s2 = 1
925 var = True
926 else:
927 nth_parts = cast(Match[str], RE_NTH.match(content))
928 _s1 = '-' if nth_parts.group('s1') and nth_parts.group('s1') == '-' else ''
929 _s2 = ''
930 a = nth_parts.group('a')
931 var = a.endswith('n')
932 if a.startswith('n'):
933 _s1 += '1'
934 elif var:
935 _s1 += a[:-1]
936 else:
937 _s2 = _s1 + a
938 _s1 = ''
939 if nth_parts.group('s2') and nth_parts.group('s2') == '-':
940 _s2 = '-'
941 if nth_parts.group('b'):
942 _s2 += nth_parts.group('b')
943 s1 = int(_s1, 10) if _s1 else 0
944 s2 = int(_s2, 10) if _s2 else 0
946 pseudo_sel = mdict['name']
947 if postfix == '_child':
948 if m.group('of'):
949 # Parse the rest of `of S`.
950 nth_sel = self.parse_selectors(iselector, m.end(0), FLG_PSEUDO | FLG_OPEN)
951 else:
952 # Use default `*|*` for `of S`.
953 nth_sel = self.PSEUDO_SELECTORS['<nth-of-s>']
954 self.increment_count(nth_sel.count)
955 if pseudo_sel == ':nth-child':
956 sel.nth.append(ct.SelectorNth(s1, var, s2, False, False, nth_sel))
957 elif pseudo_sel == ':nth-last-child':
958 sel.nth.append(ct.SelectorNth(s1, var, s2, False, True, nth_sel))
959 else:
960 if pseudo_sel == ':nth-of-type':
961 sel.nth.append(ct.SelectorNth(s1, var, s2, True, False, ct.SelectorList()))
962 elif pseudo_sel == ':nth-last-of-type':
963 sel.nth.append(ct.SelectorNth(s1, var, s2, True, True, ct.SelectorList()))
964 has_selector = True
965 return has_selector
967 def parse_pseudo_open(
968 self,
969 sel: _Selector,
970 name: str,
971 has_selector: bool,
972 iselector: Iterator[tuple[str, Match[str]]],
973 index: int
974 ) -> bool:
975 """Parse pseudo with opening bracket."""
977 flags = FLG_PSEUDO | FLG_OPEN
978 if name == ':not':
979 flags |= FLG_NOT
980 elif name == ':has':
981 flags |= FLG_RELATIVE
982 elif name in (':where', ':is'):
983 flags |= FLG_FORGIVE
985 sel.selectors.append(self.parse_selectors(iselector, index, flags))
986 has_selector = True
988 return has_selector
990 def parse_has_combinator(
991 self,
992 sel: _Selector,
993 m: Match[str],
994 has_selector: bool,
995 selectors: list[_Selector],
996 rel_type: str,
997 index: int
998 ) -> tuple[bool, _Selector, str]:
999 """Parse combinator tokens."""
1001 combinator = m.group('relation')
1002 if not combinator:
1003 combinator = WS_COMBINATOR
1004 if combinator == COMMA_COMBINATOR:
1005 if not has_selector:
1006 raise SelectorSyntaxError(
1007 f"The combinator '{combinator}' at position {index}, must have a selector before it",
1008 self.pattern,
1009 index
1010 )
1011 sel.rel_type = rel_type
1012 selectors[-1].relations.append(sel)
1013 rel_type = ":" + WS_COMBINATOR
1014 selectors.append(_Selector())
1015 else:
1016 if has_selector:
1017 # End the current selector and associate the leading combinator with this selector.
1018 sel.rel_type = rel_type
1019 selectors[-1].relations.append(sel)
1020 elif rel_type[1:] != WS_COMBINATOR:
1021 # It's impossible to have two whitespace combinators after each other as the patterns
1022 # will gobble up trailing whitespace. It is also impossible to have a whitespace
1023 # combinator after any other kind for the same reason. But we could have
1024 # multiple non-whitespace combinators. So if the current combinator is not a whitespace,
1025 # then we've hit the multiple combinator case, so we should fail.
1026 raise SelectorSyntaxError(
1027 f'The multiple combinators at position {index}',
1028 self.pattern,
1029 index
1030 )
1032 # Set the leading combinator for the next selector.
1033 rel_type = ':' + combinator
1035 sel = _Selector()
1036 has_selector = False
1037 return has_selector, sel, rel_type
1039 def parse_combinator(
1040 self,
1041 sel: _Selector,
1042 m: Match[str],
1043 has_selector: bool,
1044 selectors: list[_Selector],
1045 relations: list[_Selector],
1046 is_pseudo: bool,
1047 is_forgive: bool,
1048 index: int
1049 ) -> tuple[bool, _Selector]:
1050 """Parse combinator tokens."""
1052 combinator = m.group('relation')
1053 if not combinator:
1054 combinator = WS_COMBINATOR
1055 if not has_selector:
1056 if not is_forgive or combinator != COMMA_COMBINATOR:
1057 raise SelectorSyntaxError(
1058 f"The combinator '{combinator}' at position {index}, must have a selector before it",
1059 self.pattern,
1060 index
1061 )
1063 # If we are in a forgiving pseudo class, just make the selector a "no match"
1064 if combinator == COMMA_COMBINATOR:
1065 self.increment_count()
1066 del relations[:]
1067 else:
1068 if combinator == COMMA_COMBINATOR:
1069 if not sel.tag and not is_pseudo:
1070 # Implied `*`
1071 sel.tag = ct.SelectorTag('*', None)
1072 sel.relations.extend(relations)
1073 selectors.append(sel)
1074 del relations[:]
1075 else:
1076 sel.relations.extend(relations)
1077 sel.rel_type = combinator
1078 del relations[:]
1079 relations.append(sel)
1081 sel = _Selector()
1082 has_selector = False
1084 return has_selector, sel
1086 def parse_class_id(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1087 """Parse HTML classes and ids."""
1089 selector = m.group(0)
1090 if selector.startswith('.'):
1091 sel.classes.append(css_unescape(selector[1:]))
1092 else:
1093 sel.ids.append(css_unescape(selector[1:]))
1094 has_selector = True
1095 return has_selector
1097 def parse_pseudo_contains(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1098 """Parse contains."""
1100 pseudo = util.lower(css_unescape(m.group('name')))
1101 if pseudo == ":contains":
1102 warnings.warn( # noqa: B028
1103 "The pseudo class ':contains' is deprecated, ':-soup-contains' should be used moving forward.",
1104 FutureWarning
1105 )
1106 contains_own = pseudo == ":-soup-contains-own"
1107 patterns = [
1108 css_unescape(token.group('value'))
1109 for token in RE_VALUES.finditer(m.group('values'))
1110 if not token.group('split')
1111 ]
1112 sel.contains.append(ct.SelectorContains(patterns, contains_own))
1113 has_selector = True
1114 return has_selector
1116 def parse_pseudo_lang(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1117 """Parse pseudo language."""
1119 patterns = [
1120 css_unescape(token.group('value'))
1121 for token in RE_VALUES.finditer(m.group('values'))
1122 if not token.group('split')
1123 ]
1124 sel.lang.append(ct.SelectorLang(patterns))
1125 has_selector = True
1127 return has_selector
1129 def parse_pseudo_dir(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool:
1130 """Parse pseudo direction."""
1132 value = ct.SEL_DIR_LTR if util.lower(m.group('dir')) == 'ltr' else ct.SEL_DIR_RTL
1133 sel.flags |= value
1134 has_selector = True
1135 return has_selector
1137 def parse_selectors(
1138 self,
1139 iselector: Iterator[tuple[str, Match[str]]],
1140 index: int = 0,
1141 flags: int = 0
1142 ) -> ct.SelectorList:
1143 """Parse selectors."""
1145 # Initialize important variables
1146 sel = _Selector()
1147 selectors = []
1148 has_selector = False
1149 closed = False
1150 relations = [] # type: list[_Selector]
1151 rel_type = ":" + WS_COMBINATOR
1152 count = self.count
1154 # Setup various flags
1155 is_open = bool(flags & FLG_OPEN)
1156 is_pseudo = bool(flags & FLG_PSEUDO)
1157 is_relative = bool(flags & FLG_RELATIVE)
1158 is_not = bool(flags & FLG_NOT)
1159 is_html = bool(flags & FLG_HTML)
1160 is_default = bool(flags & FLG_DEFAULT)
1161 is_indeterminate = bool(flags & FLG_INDETERMINATE)
1162 is_in_range = bool(flags & FLG_IN_RANGE)
1163 is_out_of_range = bool(flags & FLG_OUT_OF_RANGE)
1164 is_placeholder_shown = bool(flags & FLG_PLACEHOLDER_SHOWN)
1165 is_forgive = bool(flags & FLG_FORGIVE)
1167 # Print out useful debug stuff
1168 if self.debug: # pragma: no cover
1169 if is_pseudo:
1170 print(' is_pseudo: True')
1171 if is_open:
1172 print(' is_open: True')
1173 if is_relative:
1174 print(' is_relative: True')
1175 if is_not:
1176 print(' is_not: True')
1177 if is_html:
1178 print(' is_html: True')
1179 if is_default:
1180 print(' is_default: True')
1181 if is_indeterminate:
1182 print(' is_indeterminate: True')
1183 if is_in_range:
1184 print(' is_in_range: True')
1185 if is_out_of_range:
1186 print(' is_out_of_range: True')
1187 if is_placeholder_shown:
1188 print(' is_placeholder_shown: True')
1189 if is_forgive:
1190 print(' is_forgive: True')
1192 # The algorithm for relative selectors require an initial selector in the selector list
1193 if is_relative:
1194 selectors.append(_Selector())
1196 try:
1197 while True:
1198 key, m = next(iselector)
1200 if key not in ('combine', 'pseudo_close'):
1201 self.increment_count()
1203 # Handle parts
1204 if self.ignore:
1205 mi = RE_PSEUDO_IGNORE.match(m.group(0))
1206 if mi is not None and mi.group('name').lower() in self.ignore:
1207 raise SelectorSyntaxError(
1208 f"The selector '{mi.group('name')}' at position {m.start(0)}, has been disallowed",
1209 self.pattern,
1210 m.start(0)
1211 )
1212 if key == "at_rule":
1213 raise NotImplementedError(f"At-rules found at position {m.start(0)}")
1214 elif key == "amp":
1215 sel.flags |= ct.SEL_SCOPE
1216 has_selector = True
1217 elif key == 'pseudo_class_custom':
1218 has_selector = self.parse_pseudo_class_custom(sel, m, has_selector)
1219 elif key == 'pseudo_class':
1220 has_selector, is_html = self.parse_pseudo_class(sel, m, has_selector, iselector, is_html)
1221 elif key == 'pseudo_element':
1222 raise NotImplementedError(f"Pseudo-element found at position {m.start(0)}")
1223 elif key == 'pseudo_contains':
1224 has_selector = self.parse_pseudo_contains(sel, m, has_selector)
1225 elif key in ('pseudo_nth_type', 'pseudo_nth_child'):
1226 has_selector = self.parse_pseudo_nth(sel, m, has_selector, iselector)
1227 elif key == 'pseudo_lang':
1228 has_selector = self.parse_pseudo_lang(sel, m, has_selector)
1229 elif key == 'pseudo_dir':
1230 has_selector = self.parse_pseudo_dir(sel, m, has_selector)
1231 # Currently only supports HTML
1232 is_html = True
1233 elif key == 'pseudo_close':
1234 if not has_selector:
1235 if not is_forgive:
1236 raise SelectorSyntaxError(
1237 f"Expected a selector at position {m.start(0)}",
1238 self.pattern,
1239 m.start(0)
1240 )
1241 sel.no_match = True
1242 if is_open:
1243 closed = True
1244 break
1245 else:
1246 raise SelectorSyntaxError(
1247 f"Unmatched pseudo-class close at position {m.start(0)}",
1248 self.pattern,
1249 m.start(0)
1250 )
1251 elif key == 'combine':
1252 if is_relative:
1253 has_selector, sel, rel_type = self.parse_has_combinator(
1254 sel, m, has_selector, selectors, rel_type, index
1255 )
1256 else:
1257 has_selector, sel = self.parse_combinator(
1258 sel, m, has_selector, selectors, relations, is_pseudo, is_forgive, index
1259 )
1260 elif key == 'attribute':
1261 has_selector = self.parse_attribute_selector(sel, m, has_selector)
1262 elif key == 'tag':
1263 if has_selector:
1264 raise SelectorSyntaxError(
1265 f"Tag name found at position {m.start(0)} instead of at the start",
1266 self.pattern,
1267 m.start(0)
1268 )
1269 has_selector = self.parse_tag_pattern(sel, m, has_selector)
1270 elif key in ('class', 'id'):
1271 has_selector = self.parse_class_id(sel, m, has_selector)
1273 index = m.end(0)
1274 except StopIteration:
1275 pass
1277 # Handle selectors that are not closed
1278 if is_open and not closed:
1279 raise SelectorSyntaxError(
1280 f"Unclosed pseudo-class at position {index}",
1281 self.pattern,
1282 index
1283 )
1285 # Cleanup completed selector piece
1286 if has_selector:
1287 if not sel.tag and not is_pseudo:
1288 # Implied `*`
1289 sel.tag = ct.SelectorTag('*', None)
1290 if is_relative:
1291 sel.rel_type = rel_type
1292 selectors[-1].relations.append(sel)
1293 else:
1294 sel.relations.extend(relations)
1295 del relations[:]
1296 selectors.append(sel)
1298 # Forgive empty slots in pseudo-classes that have lists (and are forgiving)
1299 elif is_forgive and (not selectors or not relations):
1300 # Handle normal pseudo-classes with empty slots like `:is()` etc.
1301 self.increment_count()
1302 sel.no_match = True
1303 del relations[:]
1304 selectors.append(sel)
1305 has_selector = True
1307 if not has_selector:
1308 # We will always need to finish a selector when `:has()` is used as it leads with combining.
1309 # May apply to others as well.
1310 raise SelectorSyntaxError(
1311 f'Expected a selector at position {index}',
1312 self.pattern,
1313 index
1314 )
1316 # Some patterns require additional logic, such as default. We try to make these the
1317 # last pattern, and append the appropriate flag to that selector which communicates
1318 # to the matcher what additional logic is required.
1319 if is_default:
1320 selectors[-1].flags = ct.SEL_DEFAULT
1321 if is_indeterminate:
1322 selectors[-1].flags = ct.SEL_INDETERMINATE
1323 if is_in_range:
1324 selectors[-1].flags = ct.SEL_IN_RANGE
1325 if is_out_of_range:
1326 selectors[-1].flags = ct.SEL_OUT_OF_RANGE
1327 if is_placeholder_shown:
1328 selectors[-1].flags = ct.SEL_PLACEHOLDER_SHOWN
1330 # Return selector list
1331 return ct.SelectorList([s.freeze() for s in selectors], is_not, is_html, self.count - count)
1333 def selector_iter(self, pattern: str) -> Iterator[tuple[str, Match[str]]]:
1334 """Iterate selector tokens."""
1336 # Ignore whitespace and comments at start and end of pattern
1337 m = RE_WS_BEGIN.search(pattern)
1338 index = m.end(0) if m else 0
1339 m = RE_WS_END.search(pattern[::-1])
1340 offset = m.end(0) if m else 0
1341 end = len(pattern) - (1 + offset)
1343 if self.debug: # pragma: no cover
1344 print(f'## PARSING: {pattern!r}')
1345 while index <= end:
1346 m = None
1347 for v in self.CSS_TOKENS:
1348 m = v.match(pattern, index, self.flags)
1349 if m:
1350 name = v.get_name()
1351 if self.debug: # pragma: no cover
1352 print(f"TOKEN: '{name}' --> {m.group(0)!r} at position {m.start(0)}")
1353 index = m.end(0)
1354 yield name, m
1355 break
1356 if m is None:
1357 c = pattern[index]
1358 # If the character represents the start of one of the known selector types,
1359 # throw an exception mentioning that the known selector type is in error;
1360 # otherwise, report the invalid character.
1361 if c == '[':
1362 msg = f"Malformed attribute selector at position {index}"
1363 elif c == '.':
1364 msg = f"Malformed class selector at position {index}"
1365 elif c == '#':
1366 msg = f"Malformed id selector at position {index}"
1367 elif c == ':':
1368 msg = f"Malformed pseudo-class selector at position {index}"
1369 else:
1370 msg = f"Invalid character {c!r} position {index}"
1371 raise SelectorSyntaxError(msg, self.pattern, index)
1372 if self.debug: # pragma: no cover
1373 print('## END PARSING')
1375 def process_selectors(self, index: int = 0, flags: int = 0) -> ct.SelectorList:
1376 """Process selectors."""
1378 return self.parse_selectors(self.selector_iter(self.pattern), index, flags)