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