Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/soupsieve/css_parser.py: 70%

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

587 statements  

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)