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