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

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

628 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, Callable, Iterator, Iterable, Mapping, cast 

11from dataclasses import dataclass 

12from collections import UserDict 

13import threading 

14 

15__all__ = ('SelectorSyntaxError',) 

16 

17RE_LOCK = threading.Lock() 

18SEL_LOCK = threading.RLock() 

19 

20UNICODE_REPLACEMENT_CHAR = 0xFFFD 

21 

22# Non-ASCII identifier code point ranges (inclusive): https://drafts.csswg.org/css-syntax-3/#non-ascii-ident-code-point 

23NON_ASCII_IDENT_RANGES = ( 

24 (0x00B7, 0x00B7), 

25 (0x00C0, 0x00D6), 

26 (0x00D8, 0x00F6), 

27 (0x00F8, 0x037D), 

28 (0x037F, 0x1FFF), 

29 (0x200C, 0x200D), 

30 (0x203F, 0x2040), 

31 (0x2070, 0x218F), 

32 (0x2C00, 0x2FEF), 

33 (0x3001, 0xD7FF), 

34 (0xF900, 0xFDCF), 

35 (0xFDF0, 0xFFFD), 

36 (0x10000, 0x10FFFF) 

37) 

38 

39SELECTOR_LIMIT = 8192 

40 

41# Simple pseudo classes that take no parameters 

42PSEUDO_SIMPLE = { 

43 ":any-link", 

44 ":empty", 

45 ":first-child", 

46 ":first-of-type", 

47 ":in-range", 

48 ":open", 

49 ":out-of-range", 

50 ":last-child", 

51 ":last-of-type", 

52 ":link", 

53 ":only-child", 

54 ":only-of-type", 

55 ":root", 

56 ':checked', 

57 ':default', 

58 ':disabled', 

59 ':enabled', 

60 ':indeterminate', 

61 ':optional', 

62 ':placeholder-shown', 

63 ':read-only', 

64 ':read-write', 

65 ':required', 

66 ':scope', 

67 ':defined', 

68 ':muted' 

69} 

70 

71# Supported, simple pseudo classes that match nothing in the Soup Sieve environment 

72PSEUDO_SIMPLE_NO_MATCH = { 

73 ':active', 

74 ':autofill', 

75 ':buffering', 

76 ':current', 

77 ':focus', 

78 ':focus-visible', 

79 ':focus-within', 

80 ':fullscreen', 

81 ':future', 

82 ':host', 

83 ':hover', 

84 ':local-link', 

85 ':past', 

86 ':paused', 

87 ':picture-in-picture', 

88 ':playing', 

89 ':popover-open', 

90 ':seeking', 

91 ':stalled', 

92 ':target', 

93 ':target-within', 

94 ':user-invalid', 

95 ':volume-locked', 

96 ':visited' 

97} 

98 

99# Attributes whose values are matched case insensitively on HTML elements in HTML documents 

100# https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors 

101HTML_CASE_INSENSITIVE_ATTRIBUTES = frozenset( 

102 ( 

103 'accept', 'accept-charset', 'align', 'alink', 'axis', 'bgcolor', 'charset', 'checked', 'clear', 

104 'codetype', 'color', 'compact', 'declare', 'defer', 'dir', 'direction', 'disabled', 'enctype', 

105 'face', 'frame', 'hreflang', 'http-equiv', 'lang', 'language', 'link', 'media', 'method', 

106 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap', 'readonly', 'rel', 'rev', 'rules', 'scope', 

107 'scrolling', 'selected', 'shape', 'target', 'text', 'type', 'valign', 'valuetype', 'vlink' 

108 ) 

109) 

110 

111# Complex pseudo classes that take selector lists 

112PSEUDO_COMPLEX = { 

113 ':contains', 

114 ':-soup-contains', 

115 ':-soup-contains-own', 

116 ':has', 

117 ':is', 

118 ':matches', 

119 ':not', 

120 ':where' 

121} 

122 

123PSEUDO_COMPLEX_NO_MATCH = { 

124 ':current', 

125 ':host', 

126 ':host-context' 

127} 

128 

129# Complex pseudo classes that take very specific parameters and are handled special 

130PSEUDO_SPECIAL = { 

131 ':dir', 

132 ':lang', 

133 ':nth-child', 

134 ':nth-last-child', 

135 ':nth-last-of-type', 

136 ':nth-of-type' 

137} 

138 

139PSEUDO_SUPPORTED = PSEUDO_SIMPLE | PSEUDO_SIMPLE_NO_MATCH | PSEUDO_COMPLEX | PSEUDO_COMPLEX_NO_MATCH | PSEUDO_SPECIAL 

140 

141# Sub-patterns parts 

142# Whitespace 

143NEWLINE = r'(?>\r\n|[\n\f\r])' 

144WS = fr'(?:[ \t]|{NEWLINE})' 

145# Comments 

146COMMENTS = r'(?:/\*(?:[^*]|\*(?!/))*+\*/)' 

147# Whitespace with comments included 

148WSC = fr'(?:{WS}|{COMMENTS})' 

149# CSS escapes 

150CSS_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}+{WS}?+|[^\r\n\f]|$))' 

151CSS_STRING_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}+{WS}?+|{NEWLINE}|[^\r\n\f]|$))' 

152# CSS identifier code points (browsers treat all non-ASCII code points as identifier code points) 

153IDENT_START = r'[A-Za-z_\x80-\U0010ffff]' 

154IDENT_CHAR = r'[-0-9A-Za-z_\x80-\U0010ffff]' 

155# CSS Identifier 

156IDENTIFIER = fr''' 

157(?:(?:--|-?+(?:{IDENT_START}|{CSS_ESCAPES})) 

158(?:{IDENT_CHAR}|{CSS_ESCAPES})*+) 

159''' 

160# `nth` content 

161NTH = fr'[-+]?+(?:[0-9]++n?+|n)(?:(?<=n){WSC}*+[-+]{WSC}*+[0-9]++)?+' 

162# Value: quoted string or identifier 

163VALUE = fr''' 

164(?:"(?:\\(?:{NEWLINE}|[^\r\n\f])|[^\\"\r\n\f])*+"| 

165'(?:\\(?:{NEWLINE}|[^\r\n\f])|[^\\'\r\n\f])*+'| 

166{IDENTIFIER}) 

167''' 

168# Attribute value comparison. `!=` is handled special as it is non-standard. 

169ATTR = fr'(?:{WSC}*+(?P<cmp>[!~^|*$]?+=){WSC}*+(?P<value>{VALUE})(?:{WSC}*+(?P<case>[is]))?+)?+' 

170 

171# Selector patterns 

172# IDs (`#id`) 

173PAT_ID = fr'\#{IDENTIFIER}' 

174# Classes (`.class`) 

175PAT_CLASS = fr'\.{IDENTIFIER}' 

176# Prefix:Tag (`prefix|tag`) 

177PAT_TAG = fr'(?P<tag_ns>(?:{IDENTIFIER}|\*)?+\|)?+(?P<tag_name>{IDENTIFIER}|\*)' 

178# Attributes (`[attr]`, `[attr=value]`, etc.) 

179PAT_ATTR = fr'\[{WSC}*+(?P<attr_ns>(?:{IDENTIFIER}|\*)?+\|(?!=))?+(?P<attr_name>{IDENTIFIER}){ATTR}{WSC}*+\]' 

180# Pseudo class (`:pseudo-class`, `:pseudo-class(`) 

181PAT_PSEUDO_CLASS = fr'(?P<name>:{IDENTIFIER})(?P<open>\({WSC}*+)?+' 

182# Pseudo class special patterns. Matches `:pseudo-class(` for special case pseudo classes. 

183PAT_PSEUDO_CLASS_SPECIAL = fr'(?P<name>:{IDENTIFIER})(?P<open>\({WSC}*+)' 

184# Custom pseudo class (`:--custom-pseudo`) 

185PAT_PSEUDO_CLASS_CUSTOM = fr'(?P<name>:(?=--){IDENTIFIER})' 

186# Nesting ampersand selector. Matches `&` 

187PAT_AMP = r'&' 

188# Closing pseudo group (`)`) 

189PAT_PSEUDO_CLOSE = fr'{WSC}*+\)' 

190# Pseudo element (`::pseudo-element`) 

191PAT_PSEUDO_ELEMENT = fr':{PAT_PSEUDO_CLASS}' 

192# At rule (`@page`, etc.) (not supported) 

193PAT_AT_RULE = fr'@P{IDENTIFIER}' 

194# Pseudo class `nth-child` (`:nth-child(an+b [of S]?)`, `:first-child`, etc.) 

195PAT_PSEUDO_NTH_CHILD = fr''' 

196(?P<pseudo_nth_child>{PAT_PSEUDO_CLASS_SPECIAL} 

197(?P<nth_child>{NTH}|even|odd))(?:{WSC}*+\)|(?P<of>{COMMENTS}*+{WS}{WSC}*+of{COMMENTS}*+{WS}{WSC}*+)) 

198''' 

199# Pseudo class `nth-of-type` (`:nth-of-type(an+b)`, `:first-of-type`, etc.) 

200PAT_PSEUDO_NTH_TYPE = fr''' 

201(?P<pseudo_nth_type>{PAT_PSEUDO_CLASS_SPECIAL} 

202(?P<nth_type>{NTH}|even|odd)){WSC}*+\) 

203''' 

204# Pseudo class language (`:lang("*-de", en)`) 

205PAT_PSEUDO_LANG = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<values>{VALUE}(?:{WSC}*+,{WSC}*+{VALUE})*+){WSC}*+\)' 

206# Pseudo class direction (`:dir(ltr)`) 

207PAT_PSEUDO_DIR = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<dir>{IDENTIFIER}){WSC}*+\)' 

208# Combining characters (`>`, `~`, ` `, `+`, `,`) along with any comments and whitespace around them. 

209# A run of comments and whitespace is matched all at once. `ws` captures whether the run has whitespace 

210# before the relation, as a run of only comments does not combine selectors. 

211PAT_COMBINE = fr''' 

212(?=[,+>~]|{WS}|/\*){COMMENTS}*+(?:(?P<ws>{WS}){WSC}*+)?+(?P<relation>[,+>~])?+{WSC}*+ 

213''' 

214# Extra: Contains (`:contains(text)`) 

215PAT_PSEUDO_CONTAINS = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<values>{VALUE}(?:{WSC}*+,{WSC}*+{VALUE})*+){WSC}*+\)' 

216 

217# Regular expressions 

218# CSS escape pattern 

219RE_CSS_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}+{WSC}?+)|(\\[^\r\n\f])|(\\$))', re.I) 

220RE_CSS_STR_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}+{WS}?+)|(\\[^\r\n\f])|(\\$)|(\\{NEWLINE}))', re.I) 

221# Pattern to break up `nth` specifiers 

222RE_NTH = re.compile(fr'(?P<s1>[-+])?+(?P<a>[0-9]++n?+|n)(?:(?<=n){WSC}*+(?P<s2>[-+]){WSC}*+(?P<b>[0-9]++))?+', re.I) 

223# Pattern to iterate multiple values. 

224RE_VALUES = re.compile(fr'(?:(?P<value>{VALUE})|(?P<split>{WSC}*+,{WSC}*+))', re.X) 

225# Whitespace checks 

226RE_WS = re.compile(WS) 

227RE_CUSTOM = re.compile(fr'^{PAT_PSEUDO_CLASS_CUSTOM}$', re.X) 

228RE_PSEUDO_CLASS_SPECIAL = re.compile(PAT_PSEUDO_CLASS_SPECIAL, re.I | re.X | re.U) 

229RE_PSEUDO_IGNORE = re.compile(PAT_PSEUDO_CLASS, re.X | re.I | re.U) 

230 

231QUOTED = ("'", '"') 

232 

233# Constants 

234# List split token 

235COMMA_COMBINATOR = ',' 

236# Relation token for descendant 

237WS_COMBINATOR = " " 

238 

239# Parse flags 

240FLG_PSEUDO = 0x01 

241FLG_NOT = 0x02 

242FLG_RELATIVE = 0x04 

243FLG_DEFAULT = 0x08 

244FLG_HTML = 0x10 

245FLG_INDETERMINATE = 0x20 

246FLG_OPEN = 0x40 

247FLG_IN_RANGE = 0x80 

248FLG_OUT_OF_RANGE = 0x100 

249FLG_PLACEHOLDER_SHOWN = 0x200 

250FLG_FORGIVE = 0x400 

251FLG_EDITABLE = 0x800 

252FLG_CHECKED = 0x1000 

253 

254# Maximum cached patterns to store 

255_MAXCACHE = 500 

256 

257 

258@lru_cache(maxsize=_MAXCACHE) 

259def _cached_css_compile( 

260 pattern: str, 

261 namespaces: ct.Namespaces | None, 

262 custom: ct.CustomSelectors | None, 

263 ignore: tuple[str] | None = None, 

264 flags: int = 0, 

265 max_selectors: int = SELECTOR_LIMIT 

266) -> cm.SoupSieve: 

267 """Cached CSS compile.""" 

268 

269 return cm.SoupSieve( 

270 pattern, 

271 CSSParser( 

272 pattern, 

273 custom=custom, 

274 ignore=ignore, 

275 flags=flags, 

276 max_selectors=max_selectors 

277 ).process_selectors(), 

278 namespaces, 

279 custom, 

280 flags 

281 ) 

282 

283 

284def _purge_cache() -> None: 

285 """Purge the cache.""" 

286 

287 _cached_css_compile.cache_clear() 

288 

289 

290def css_unescape(content: str) -> str: 

291 """ 

292 Unescape CSS value. 

293 

294 Strings allow for spanning the value on multiple strings by escaping a new line. 

295 """ 

296 

297 def replace(m: Match[str]) -> str: 

298 """Replace with the appropriate substitute.""" 

299 

300 if m.group(1): 

301 codepoint = int(m.group(1)[1:], 16) 

302 # A surrogate, or a value past Unicode, is `U+FFFD`. Zero already is. 

303 if ( 

304 codepoint == 0 

305 or codepoint > 0x10FFFF 

306 or 0xD800 <= codepoint <= 0xDFFF 

307 ): 

308 codepoint = UNICODE_REPLACEMENT_CHAR 

309 value = chr(codepoint) 

310 elif m.group(2): 

311 value = m.group(2)[1:] 

312 elif m.group(3): 

313 value = '\ufffd' 

314 else: 

315 value = '' 

316 

317 return value 

318 

319 quoted = content.startswith(QUOTED) 

320 return (RE_CSS_STR_ESC if quoted else RE_CSS_ESC).sub( 

321 replace, 

322 content[1:-1] if quoted else content 

323 ) 

324 

325 

326def escape(ident: str) -> str: 

327 """Escape identifier.""" 

328 

329 string = [] 

330 length = len(ident) 

331 start_dash = length > 0 and ident[0] == '-' 

332 if length == 1 and start_dash: 

333 # Need to escape identifier that is a single `-` with no other characters 

334 string.append(f'\\{ident}') 

335 else: 

336 for index, c in enumerate(ident): 

337 codepoint = ord(c) 

338 if codepoint == 0x00: 

339 string.append('\ufffd') 

340 elif (0x01 <= codepoint <= 0x1F) or codepoint == 0x7F: 

341 string.append(f'\\{codepoint:x} ') 

342 elif (index == 0 or (start_dash and index == 1)) and (0x30 <= codepoint <= 0x39): 

343 string.append(f'\\{codepoint:x} ') 

344 elif ( 

345 codepoint in (0x2D, 0x5F) or (0x30 <= codepoint <= 0x39) or 

346 (0x41 <= codepoint <= 0x5A) or (0x61 <= codepoint <= 0x7A) or 

347 (codepoint >= 0x80 and any(start <= codepoint <= end for start, end in NON_ASCII_IDENT_RANGES)) 

348 ): 

349 string.append(c) 

350 else: 

351 string.append(f'\\{c}') 

352 return ''.join(string) 

353 

354 

355class SelectorPattern: 

356 """ 

357 Selector pattern. 

358 

359 Patterns are shared by all parsers, so they must not hold any per-match state. 

360 `match` returns the token name along with the match; a name of `None` means the 

361 match is consumed without being processed further. 

362 """ 

363 

364 def __init__(self, name: str, pattern: str) -> None: 

365 """Initialize.""" 

366 

367 self.name = name 

368 self.pattern = pattern 

369 self._re_pattern: re.Pattern[str] | None = None 

370 

371 @property 

372 def re_pattern(self) -> re.Pattern[str]: 

373 """Retrieve the compiled regular expression pattern.""" 

374 

375 with RE_LOCK: 

376 if self._re_pattern is None: 

377 self._re_pattern = re.compile(self.pattern, re.I | re.X | re.U) 

378 return self._re_pattern 

379 

380 def match(self, selector: str, index: int, flags: int) -> tuple[str | None, Match[str]] | None: 

381 """Match the selector.""" 

382 

383 m = self.re_pattern.match(selector, index) 

384 return None if m is None else (self.name, m) 

385 

386 

387class SelectorPatternCombinator(SelectorPattern): 

388 """ 

389 A run of comments and whitespace might be a combinator or might just be consumed. 

390 

391 Process all combinators except whitespace at the start or end, and runs of only comments. 

392 """ 

393 

394 def match(self, selector: str, index: int, flags: int) -> tuple[str | None, Match[str]] | None: 

395 """ 

396 Match the selector. 

397 

398 If we match at the start or end of the string and have a non-whitespace combinator, the match is bogus. 

399 If we are at the start or end and the combinator is white space, then it can be consumed with no further 

400 processing. Comments are not whitespace, so a run of only comments is consumed as well. 

401 """ 

402 

403 m = self.re_pattern.match(selector, index) 

404 # An unterminated comment matches nothing, which must not be consumed or the parser would never advance. 

405 if m is None or m.end(0) == index: 

406 return None 

407 relation = m.group('relation') 

408 if m.start(0) == 0 or m.end(0) == len(selector): 

409 return None if relation else (None, m) 

410 return (self.name if relation or m.group('ws') else None), m 

411 

412 

413class SpecialPseudoPattern(SelectorPattern): 

414 """Selector pattern.""" 

415 

416 def __init__(self, patterns: tuple[tuple[str, tuple[str, ...], str, type[SelectorPattern]], ...]) -> None: 

417 """Initialize.""" 

418 

419 self.patterns = {} 

420 for p in patterns: 

421 name = p[0] 

422 pattern = p[3](name, p[2]) 

423 for pseudo in p[1]: 

424 self.patterns[pseudo] = pattern 

425 

426 def match(self, selector: str, index: int, flags: int) -> tuple[str | None, Match[str]] | None: 

427 """Match the selector.""" 

428 

429 m = RE_PSEUDO_CLASS_SPECIAL.match(selector, index) 

430 if m: 

431 pattern = self.patterns.get(util.lower(css_unescape(m.group('name')))) 

432 if pattern: 

433 return pattern.match(selector, index, flags) 

434 return None 

435 

436 

437class _Selector: 

438 """ 

439 Intermediate selector class. 

440 

441 This stores selector data for a compound selector as we are acquiring them. 

442 Once we are done collecting the data for a compound selector, we freeze 

443 the data in an object that can be pickled and hashed. 

444 """ 

445 

446 def __init__(self, **kwargs: Any) -> None: 

447 """Initialize.""" 

448 

449 self.tag: ct.SelectorTag | None = kwargs.get('tag', None) 

450 self.ids: list[str] = kwargs.get('ids', []) 

451 self.classes: list[str] = kwargs.get('classes', []) 

452 self.attributes: list[ct.SelectorAttribute] = kwargs.get('attributes', []) 

453 self.nth: list[ct.SelectorNth] = kwargs.get('nth', []) 

454 self.selectors: list[ct.SelectorList] = kwargs.get('selectors', []) 

455 self.relations: list[_Selector] = kwargs.get('relations', []) 

456 self.rel_type: str | None = kwargs.get('rel_type', None) 

457 self.contains: list[ct.SelectorContains] = kwargs.get('contains', []) 

458 self.lang: list[ct.SelectorLang] = kwargs.get('lang', []) 

459 self.flags: int = kwargs.get('flags', 0) 

460 self.no_match: bool = kwargs.get('no_match', False) 

461 

462 def _freeze(self, relation: ct.SelectorList) -> ct.Selector | ct.SelectorNull: 

463 """Freeze self with an already frozen relation.""" 

464 

465 if self.no_match: 

466 return ct.Null 

467 else: 

468 return ct.Selector( 

469 self.tag, 

470 tuple(self.ids), 

471 tuple(self.classes), 

472 tuple(self.attributes), 

473 tuple(self.nth), 

474 tuple(self.selectors), 

475 relation, 

476 self.rel_type, 

477 tuple(self.contains), 

478 tuple(self.lang), 

479 self.flags 

480 ) 

481 

482 def freeze(self) -> ct.Selector | ct.SelectorNull: 

483 """ 

484 Freeze self. 

485 

486 Relations form a chain where each selector's first relation is the next link and any 

487 remaining relations are carried down to follow it. As the far end of the chain must be 

488 frozen first, flatten the chain and then freeze it in reverse to avoid recursion. 

489 """ 

490 

491 chain: list[_Selector] = [] 

492 pending = [self] 

493 while pending: 

494 sel = pending.pop() 

495 chain.append(sel) 

496 if sel.no_match: 

497 break 

498 pending.extend(reversed(sel.relations)) 

499 

500 relation = ct.SelectorList() 

501 for sel in reversed(chain): 

502 frozen = sel._freeze(relation) 

503 relation = ct.SelectorList([frozen]) 

504 return frozen 

505 

506 def __str__(self) -> str: # pragma: no cover 

507 """String representation.""" 

508 

509 return ( 

510 f'_Selector(tag={self.tag!r}, ids={self.ids!r}, classes={self.classes!r}, attributes={self.attributes!r}, ' 

511 f'nth={self.nth!r}, selectors={self.selectors!r}, relations={self.relations!r}, ' 

512 f'rel_type={self.rel_type!r}, contains={self.contains!r}, lang={self.lang!r}, flags={self.flags!r}, ' 

513 f'no_match={self.no_match!r})' 

514 ) 

515 

516 __repr__ = __str__ 

517 

518 

519class _SelectorContext: 

520 """ 

521 Parsing state for a selector list. 

522 

523 Context are created for each nesting level to avoid recursion. 

524 """ 

525 

526 def __init__( 

527 self, 

528 flags: int, 

529 index: int, 

530 count: int, 

531 on_close: Callable[[ct.SelectorList], None] | None = None 

532 ) -> None: 

533 """Initialize.""" 

534 

535 self.sel = _Selector() 

536 self.selectors: list[_Selector] = [] 

537 self.relations: list[_Selector] = [] 

538 self.has_selector = False 

539 self.rel_type = ":" + WS_COMBINATOR 

540 self.index = index 

541 self.count = count 

542 # Receives the frozen selector list when an open pseudo-class is closed. 

543 self.on_close = on_close 

544 

545 # Setup various flags 

546 self.is_open = bool(flags & FLG_OPEN) 

547 self.is_pseudo = bool(flags & FLG_PSEUDO) 

548 self.is_relative = bool(flags & FLG_RELATIVE) 

549 self.is_not = bool(flags & FLG_NOT) 

550 self.is_html = bool(flags & FLG_HTML) 

551 self.is_default = bool(flags & FLG_DEFAULT) 

552 self.is_indeterminate = bool(flags & FLG_INDETERMINATE) 

553 self.is_in_range = bool(flags & FLG_IN_RANGE) 

554 self.is_out_of_range = bool(flags & FLG_OUT_OF_RANGE) 

555 self.is_placeholder_shown = bool(flags & FLG_PLACEHOLDER_SHOWN) 

556 self.is_forgive = bool(flags & FLG_FORGIVE) 

557 self.is_editable = bool(flags & FLG_EDITABLE) 

558 self.is_checked = bool(flags & FLG_CHECKED) 

559 

560 # The algorithm for relative selectors require an initial selector in the selector list 

561 if self.is_relative: 

562 self.selectors.append(_Selector()) 

563 

564 def debug(self) -> None: # pragma: no cover 

565 """Print out useful debug stuff.""" 

566 

567 for name in ( 

568 'is_pseudo', 'is_open', 'is_relative', 'is_not', 'is_html', 'is_default', 'is_indeterminate', 

569 'is_in_range', 'is_out_of_range', 'is_placeholder_shown', 'is_forgive', 'is_editable', 'is_checked' 

570 ): 

571 if getattr(self, name): 

572 print(f' {name}: True') 

573 

574 

575@dataclass 

576class CSSPattern: 

577 """A CSS pattern that hasn't been processed by `CSSParser` yet.""" 

578 

579 selector: str 

580 flags: int 

581 

582 

583class PseudoSelectorMap(UserDict[str, CSSPattern | ct.SelectorList]): 

584 """Pseudo selector map.""" 

585 

586 def __setitem__(self, key: str, value: CSSPattern | ct.SelectorList) -> None: 

587 """Set item.""" 

588 

589 self.data[key] = value 

590 

591 def __getitem__(self, key: str) -> ct.SelectorList: 

592 """Get item.""" 

593 

594 with SEL_LOCK: 

595 value = self.data[key] 

596 if isinstance(value, CSSPattern): 

597 value = CSSParser(value.selector).process_selectors(flags=value.flags) 

598 self.data[key] = value 

599 

600 return value 

601 

602 

603# An `input` is in the Text state when its `type` is missing, `text`, or not one of 

604# the other keywords, so an invalid value like `type="foo"` is a text input: 

605# https://html.spec.whatwg.org/multipage/input.html#attr-input-type 

606INPUT_TEXT_STATE = ( 

607 ':not([type=hidden], [type=search], [type=tel], [type=url], [type=email], [type=password], ' 

608 '[type=date], [type=month], [type=week], [type=time], [type=datetime-local], [type=number], ' 

609 '[type=range], [type=color], [type=checkbox], [type=radio], [type=file], [type=submit], ' 

610 '[type=image], [type=reset], [type=button])' 

611) 

612 

613# CSS pattern for `:link` and `:any-link` 

614CSS_LINK = CSSPattern('html|*:is(a, area)[href]', FLG_PSEUDO | FLG_HTML) 

615# CSS pattern for `:checked` 

616CSS_CHECKED = CSSPattern( 

617 ''' 

618 html|input[type=checkbox][checked], 

619 

620 /* 

621 This pattern must be at the end. 

622 Special logic is applied to the last selector. 

623 */ 

624 html|*:is(input[type=radio][checked], option) 

625 ''', 

626 FLG_PSEUDO | FLG_HTML | FLG_CHECKED 

627) 

628# CSS pattern for `:default` 

629CSS_DEFAULT = CSSPattern( 

630 ''' 

631 html|*:is(input[type=checkbox], input[type=radio])[checked], 

632 html|option[selected], 

633 

634 /* 

635 This pattern must be at the end. 

636 Special logic is applied to the last selector. 

637 */ 

638 html|*:is( 

639 button:is([type=submit], :not([type=button], [type=reset], [commandfor], [command])), 

640 input:is([type=submit], [type=image]) 

641 ) 

642 ''', 

643 FLG_PSEUDO | FLG_HTML | FLG_DEFAULT 

644) 

645# CSS pattern for `:indeterminate` 

646CSS_INDETERMINATE = CSSPattern( 

647 ''' 

648 html|input[type="checkbox"][indeterminate], 

649 html|input[type="radio"]:is(:not([name]), [name=""]):not([checked]), 

650 html|progress:not([value]), 

651 

652 /* 

653 This pattern must be at the end. 

654 Special logic is applied to the last selector. 

655 */ 

656 html|input[type="radio"][name]:not([name='']):not([checked]) 

657 ''', 

658 FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE 

659) 

660# CSS pattern for `:disabled` 

661CSS_DISABLED = CSSPattern( 

662 ''' 

663 html|*:is(input, button, select, textarea, fieldset, optgroup, option, fieldset)[disabled], 

664 html|optgroup[disabled] > html|option, 

665 html|fieldset[disabled] > html|*:is(input, button, select, textarea, fieldset), 

666 html|fieldset[disabled] > 

667 html|*:not(legend:nth-of-type(1)) html|*:is(input, button, select, textarea, fieldset), 

668 html|*:is( 

669 html|select[disabled], 

670 html|fieldset[disabled] > html|select, 

671 html|fieldset[disabled] > html|*:not(legend:nth-of-type(1)) html|select 

672 ) html|*:is(optgroup, option) 

673 ''', 

674 FLG_PSEUDO | FLG_HTML 

675) 

676# CSS pattern for `:enabled` 

677CSS_ENABLED = CSSPattern( 

678 ''' 

679 html|*:is(input, button, select, textarea, fieldset, optgroup, option, fieldset):not(:disabled) 

680 ''', 

681 FLG_PSEUDO | FLG_HTML 

682) 

683# CSS pattern for `:required` 

684# The `required` attribute does not apply to `input` elements of these types. 

685INPUT_REQUIRED_NOT_APPLIES = ( 

686 '[type=hidden], [type=range], [type=color], [type=submit], [type=image], [type=reset], [type=button]' 

687) 

688CSS_REQUIRED = CSSPattern( 

689 f''' 

690 html|input:not({INPUT_REQUIRED_NOT_APPLIES})[required], 

691 html|*:is(textarea, select)[required] 

692 ''', 

693 FLG_PSEUDO | FLG_HTML 

694) 

695# CSS pattern for `:optional` 

696CSS_OPTIONAL = CSSPattern( 

697 f''' 

698 html|*:is(input, textarea, select):not([required]), 

699 html|input:is({INPUT_REQUIRED_NOT_APPLIES}) 

700 ''', 

701 FLG_PSEUDO | FLG_HTML 

702) 

703# CSS pattern for `:placeholder-shown` 

704CSS_PLACEHOLDER_SHOWN = CSSPattern( 

705 f''' 

706 html|input:is( 

707 {INPUT_TEXT_STATE}, 

708 [type=search], 

709 [type=url], 

710 [type=tel], 

711 [type=email], 

712 [type=password], 

713 [type=number] 

714 )[placeholder]:not([placeholder='']):is(:not([value]), [value=""]), 

715 html|textarea[placeholder]:not([placeholder='']) 

716 ''', 

717 FLG_PSEUDO | FLG_HTML | FLG_PLACEHOLDER_SHOWN 

718) 

719# CSS pattern for `:read-write` (CSS_DISABLED must be compiled first) 

720CSS_READ_WRITE = CSSPattern( 

721 f''' 

722 html|*:is( 

723 textarea, 

724 input:is( 

725 {INPUT_TEXT_STATE}, 

726 [type=search], 

727 [type=url], 

728 [type=tel], 

729 [type=email], 

730 [type=number], 

731 [type=password], 

732 [type=date], 

733 [type=datetime-local], 

734 [type=month], 

735 [type=time], 

736 [type=week] 

737 ) 

738 ):not([readonly], :disabled), 

739 html|*:not(input, textarea, [contenteditable="false" i]) 

740 ''', 

741 FLG_PSEUDO | FLG_HTML | FLG_EDITABLE 

742) 

743# CSS pattern for `:read-only` 

744CSS_READ_ONLY = CSSPattern('html|*:not(:read-write)', FLG_PSEUDO | FLG_HTML) 

745# CSS pattern for `:in-range` 

746CSS_IN_RANGE = CSSPattern( 

747 ''' 

748 html|input:is( 

749 [type="date"], 

750 [type="month"], 

751 [type="week"], 

752 [type="time"], 

753 [type="datetime-local"], 

754 [type="number"], 

755 [type="range"] 

756 ):is( 

757 [min], 

758 [max] 

759 ) 

760 ''', 

761 FLG_PSEUDO | FLG_HTML | FLG_IN_RANGE 

762) 

763# CSS pattern for `:out-of-range` 

764CSS_OUT_OF_RANGE = CSSPattern( 

765 ''' 

766 html|input:is( 

767 [type="date"], 

768 [type="month"], 

769 [type="week"], 

770 [type="time"], 

771 [type="datetime-local"], 

772 [type="number"], 

773 [type="range"] 

774 ):is( 

775 [min], 

776 [max] 

777 ) 

778 ''', 

779 FLG_PSEUDO | FLG_HTML | FLG_OUT_OF_RANGE 

780) 

781# CSS pattern for :open 

782CSS_OPEN = CSSPattern('html|*:is(details, dialog)[open]', FLG_PSEUDO | FLG_HTML) 

783# CSS pattern for :muted 

784CSS_MUTED = CSSPattern('html|*:is(video, audio)[muted]', FLG_PSEUDO | FLG_HTML) 

785# CSS pattern default for `:nth-child` "of S" feature 

786CSS_NTH_OF_S_DEFAULT = CSSPattern("*|*", FLG_PSEUDO) 

787 

788 

789class CSSParser: 

790 """Parse CSS selectors.""" 

791 

792 CSS_TOKENS = ( 

793 SelectorPattern("pseudo_close", PAT_PSEUDO_CLOSE), 

794 SpecialPseudoPattern( 

795 ( 

796 ( 

797 "pseudo_contains", 

798 (':contains', ':-soup-contains', ':-soup-contains-own'), 

799 PAT_PSEUDO_CONTAINS, 

800 SelectorPattern 

801 ), 

802 ("pseudo_nth_child", (':nth-child', ':nth-last-child'), PAT_PSEUDO_NTH_CHILD, SelectorPattern), 

803 ("pseudo_nth_type", (':nth-of-type', ':nth-last-of-type'), PAT_PSEUDO_NTH_TYPE, SelectorPattern), 

804 ("pseudo_lang", (':lang',), PAT_PSEUDO_LANG, SelectorPattern), 

805 ("pseudo_dir", (':dir',), PAT_PSEUDO_DIR, SelectorPattern) 

806 ) 

807 ), 

808 SelectorPattern("pseudo_class_custom", PAT_PSEUDO_CLASS_CUSTOM), 

809 SelectorPattern("pseudo_class", PAT_PSEUDO_CLASS), 

810 SelectorPattern("pseudo_element", PAT_PSEUDO_ELEMENT), 

811 SelectorPattern("amp", PAT_AMP), 

812 SelectorPattern("at_rule", PAT_AT_RULE), 

813 SelectorPattern("id", PAT_ID), 

814 SelectorPattern("class", PAT_CLASS), 

815 SelectorPattern("tag", PAT_TAG), 

816 SelectorPattern("attribute", PAT_ATTR), 

817 SelectorPatternCombinator("combine", PAT_COMBINE) 

818 ) 

819 

820 # Pseudos that expand to selectors 

821 PSEUDO_SELECTORS = PseudoSelectorMap( 

822 { 

823 ':link': CSS_LINK, 

824 ':any-link': CSS_LINK, 

825 ':checked': CSS_CHECKED, 

826 ':default': CSS_DEFAULT, 

827 ':indeterminate': CSS_INDETERMINATE, 

828 ':disabled': CSS_DISABLED, 

829 ':enabled': CSS_ENABLED, 

830 ':required': CSS_REQUIRED, 

831 ':muted': CSS_MUTED, 

832 ':open': CSS_OPEN, 

833 ':optional': CSS_OPTIONAL, 

834 ':read-only': CSS_READ_ONLY, 

835 ':read-write': CSS_READ_WRITE, 

836 ':in-range': CSS_IN_RANGE, 

837 ':out-of-range': CSS_OUT_OF_RANGE, 

838 ':placeholder-shown': CSS_PLACEHOLDER_SHOWN, 

839 '<nth-of-s>': CSS_NTH_OF_S_DEFAULT 

840 } 

841 ) 

842 

843 def __init__( 

844 self, 

845 selector: str, 

846 *, 

847 custom: Mapping[str, ct.SelectorList] | None = None, 

848 ignore: Iterable[str] | None = None, 

849 flags: int = 0, 

850 max_selectors: int = SELECTOR_LIMIT 

851 ) -> None: 

852 """Initialize.""" 

853 

854 self.pattern = selector.replace('\x00', '\ufffd') 

855 self.flags = flags 

856 self.debug = self.flags & util.DEBUG 

857 self.nostrict = self.flags & util.NOSTRICT 

858 self.custom = custom 

859 self.ignore = frozenset([] if ignore is None else ignore) 

860 self.count = 0 

861 self.maxsel = max_selectors 

862 

863 def increment_count(self, increment: int = 1) -> None: 

864 """Check the current selector count.""" 

865 

866 self.count += increment 

867 if self.count > self.maxsel: 

868 raise ValueError(f'Selector exceeds pseudo-class nesting limit of {self.maxsel}') 

869 

870 def parse_attribute_selector(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool: 

871 """Create attribute selector from the returned regex match.""" 

872 

873 op = m.group('cmp') 

874 case = util.lower(m.group('case')) if m.group('case') else None 

875 ns = css_unescape(m.group('attr_ns')[:-1]) if m.group('attr_ns') else '' 

876 attr = css_unescape(m.group('attr_name')) 

877 html_insensitive = False 

878 pattern = None 

879 pattern2 = None 

880 inverse = False 

881 

882 if case: 

883 ignore_case = case == 'i' 

884 elif util.lower(attr) in HTML_CASE_INSENSITIVE_ATTRIBUTES: 

885 ignore_case = True 

886 html_insensitive = True 

887 else: 

888 ignore_case = False 

889 

890 if op: 

891 value = css_unescape(m.group('value')) 

892 pattern = self.parse_attribute_pattern(op, util.lower(value) if ignore_case else value) 

893 if html_insensitive: 

894 # Outside HTML, the value is matched case sensitive. 

895 pattern2 = self.parse_attribute_pattern(op, value) 

896 inverse = op.startswith('!') 

897 

898 # Append the attribute selector 

899 sel_attr = ct.SelectorAttribute(attr, ns, pattern, pattern2, inverse, ignore_case) 

900 sel.attributes.append(sel_attr) 

901 

902 has_selector = True 

903 return has_selector 

904 

905 @staticmethod 

906 def parse_attribute_pattern(op: str, value: str) -> re.Pattern[str]: 

907 """ 

908 Create the pattern for an attribute selector's value. 

909 

910 The pattern is applied with `search()`, so it must not need a leading `.*?` to find the value, which would make 

911 the regular expression engine retry the value at every position instead of scanning for it as a literal. 

912 

913 A case insensitive pattern is compiled case sensitive for the value converted to ASCII lowercase and is matched 

914 against the attribute value converted the same way. We purposely avoid using `re.I` as the regular expression 

915 engine cannot then scan as a literal, making the search take quadratic time depending on the pattern. 

916 """ 

917 

918 if op.startswith('^'): 

919 # Value start with 

920 # `^=` should match nothing if the value is empty, so use `(?!)` which cannot be matched. 

921 source = r'^(?!)' if not value else fr'^{re.escape(value)}' 

922 elif op.startswith('$'): 

923 # Value ends with 

924 # `$=` should match nothing if the value is empty, so use `(?!)` which cannot be matched. 

925 source = r'(?!)' if not value else fr'{re.escape(value)}\Z' 

926 elif op.startswith('*'): 

927 # Value contains 

928 # `*=` should match nothing if the value is empty, so use `(?!)` which cannot be matched. 

929 source = r'(?!)' if not value else re.escape(value) 

930 elif op.startswith('~'): 

931 # Value contains word within space separated list 

932 # `~=` should match nothing if the value is empty, so use `(?!)` which cannot be matched. 

933 value = r'(?!)' if not value or RE_WS.search(value) else re.escape(value) 

934 source = fr'(?:(?<=^)|(?<=[ \t\r\n\f])){value}(?=(?:[ \t\r\n\f]|\Z))' 

935 elif op.startswith('|'): 

936 # Value starts with word in dash separated list 

937 source = fr'^{re.escape(value)}(?:-.*)?\Z' 

938 elif op.startswith('!'): 

939 # Value does not matches 

940 source = fr'^(?!{re.escape(value)}\Z)' 

941 else: 

942 # Value matches 

943 source = fr'^{re.escape(value)}\Z' 

944 return re.compile(source, re.DOTALL) 

945 

946 def parse_tag_pattern(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool: 

947 """Parse tag pattern from regex match.""" 

948 

949 prefix = css_unescape(m.group('tag_ns')[:-1]) if m.group('tag_ns') else None 

950 tag = css_unescape(m.group('tag_name')) 

951 sel.tag = ct.SelectorTag(tag, prefix) 

952 has_selector = True 

953 return has_selector 

954 

955 def parse_pseudo_class_custom(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool: 

956 """ 

957 Parse custom pseudo class alias. 

958 

959 Compile custom selectors as we need them. When compiling a custom selector, 

960 set it to `None` in the dictionary so we can avoid an infinite loop. 

961 """ 

962 

963 pseudo = util.lower(css_unescape(m.group('name'))) 

964 selector = self.custom.get(pseudo) if self.custom is not None else None 

965 if selector is None: 

966 raise SelectorSyntaxError( 

967 f"Undefined custom selector '{pseudo}' found at position {m.end(0)}", 

968 self.pattern, 

969 m.end(0) 

970 ) 

971 

972 self.increment_count(selector.count) 

973 sel.selectors.append(selector) 

974 has_selector = True 

975 return has_selector 

976 

977 def parse_pseudo_class( 

978 self, 

979 sel: _Selector, 

980 m: Match[str], 

981 has_selector: bool, 

982 is_relative: bool 

983 ) -> tuple[bool, _SelectorContext | None]: 

984 """ 

985 Parse pseudo class. 

986 

987 If the pseudo-class opens a selector list, a new context is returned to parse its contents. 

988 """ 

989 

990 context = None 

991 complex_pseudo = False 

992 pseudo = util.lower(css_unescape(m.group('name'))) 

993 if m.group('open'): 

994 complex_pseudo = True 

995 if complex_pseudo and pseudo in PSEUDO_COMPLEX: 

996 context = self.parse_pseudo_open(pseudo, m.end(0), sel.selectors.append, is_relative) 

997 has_selector = True 

998 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE: 

999 if pseudo == ':root': 

1000 sel.flags |= ct.SEL_ROOT 

1001 elif pseudo == ':defined': 

1002 sel.flags |= ct.SEL_DEFINED 

1003 elif pseudo == ':scope': 

1004 sel.flags |= ct.SEL_SCOPE 

1005 elif pseudo == ':empty': 

1006 sel.flags |= ct.SEL_EMPTY 

1007 elif pseudo in self.PSEUDO_SELECTORS: 

1008 pseudo_selector = self.PSEUDO_SELECTORS[pseudo] 

1009 self.increment_count(pseudo_selector.count) 

1010 sel.selectors.append(pseudo_selector) 

1011 elif pseudo == ':first-child': 

1012 sel.nth.append(ct.SelectorNth(0, False, 1, False, False, ct.SelectorList())) 

1013 elif pseudo == ':last-child': 

1014 sel.nth.append(ct.SelectorNth(0, False, 1, False, True, ct.SelectorList())) 

1015 elif pseudo == ':first-of-type': 

1016 sel.nth.append(ct.SelectorNth(0, False, 1, True, False, ct.SelectorList())) 

1017 elif pseudo == ':last-of-type': 

1018 sel.nth.append(ct.SelectorNth(0, False, 1, True, True, ct.SelectorList())) 

1019 elif pseudo == ':only-child': 

1020 sel.nth.extend( 

1021 [ 

1022 ct.SelectorNth(0, False, 1, False, False, ct.SelectorList()), 

1023 ct.SelectorNth(0, False, 1, False, True, ct.SelectorList()) 

1024 ] 

1025 ) 

1026 elif pseudo == ':only-of-type': 

1027 sel.nth.extend( 

1028 [ 

1029 ct.SelectorNth(0, False, 1, True, False, ct.SelectorList()), 

1030 ct.SelectorNth(0, False, 1, True, True, ct.SelectorList()) 

1031 ] 

1032 ) 

1033 has_selector = True 

1034 elif complex_pseudo and pseudo in PSEUDO_COMPLEX_NO_MATCH: 

1035 context = self.parse_pseudo_open(pseudo, m.end(0)) 

1036 sel.no_match = True 

1037 has_selector = True 

1038 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE_NO_MATCH: 

1039 sel.no_match = True 

1040 has_selector = True 

1041 elif pseudo in PSEUDO_SUPPORTED: 

1042 raise SelectorSyntaxError( 

1043 f"Invalid syntax for pseudo class '{pseudo}'", 

1044 self.pattern, 

1045 m.start(0) 

1046 ) 

1047 else: 

1048 raise SelectorSyntaxError( 

1049 f"'{pseudo}' was detected as a pseudo-class and is either unsupported or invalid. " 

1050 "If the syntax was not intended to be recognized as a pseudo-class, please escape the colon.", 

1051 self.pattern, 

1052 m.start(0) 

1053 ) 

1054 

1055 return has_selector, context 

1056 

1057 def parse_pseudo_nth( 

1058 self, 

1059 sel: _Selector, 

1060 m: Match[str], 

1061 has_selector: bool 

1062 ) -> tuple[bool, _SelectorContext | None]: 

1063 """ 

1064 Parse `nth` pseudo. 

1065 

1066 If `of S` is specified, a new context is returned to parse `S`. 

1067 """ 

1068 

1069 context = None 

1070 mdict = m.groupdict() 

1071 if mdict.get('pseudo_nth_child'): 

1072 postfix = '_child' 

1073 else: 

1074 postfix = '_type' 

1075 mdict['name'] = util.lower(css_unescape(mdict['name'])) 

1076 content = util.lower(mdict.get('nth' + postfix)) 

1077 if content == 'even': 

1078 # 2n 

1079 s1 = 2 

1080 s2 = 0 

1081 var = True 

1082 elif content == 'odd': 

1083 # 2n+1 

1084 s1 = 2 

1085 s2 = 1 

1086 var = True 

1087 else: 

1088 nth_parts = cast(Match[str], RE_NTH.match(content)) 

1089 _s1 = '-' if nth_parts.group('s1') and nth_parts.group('s1') == '-' else '' 

1090 _s2 = '' 

1091 a = nth_parts.group('a') 

1092 var = a.endswith('n') 

1093 if a.startswith('n'): 

1094 _s1 += '1' 

1095 elif var: 

1096 _s1 += a[:-1] 

1097 else: 

1098 _s2 = _s1 + a 

1099 _s1 = '' 

1100 if nth_parts.group('s2') and nth_parts.group('s2') == '-': 

1101 _s2 = '-' 

1102 if nth_parts.group('b'): 

1103 _s2 += nth_parts.group('b') 

1104 s1 = int(_s1, 10) if _s1 else 0 

1105 s2 = int(_s2, 10) if _s2 else 0 

1106 

1107 pseudo_sel = mdict['name'] 

1108 if postfix == '_child': 

1109 last = pseudo_sel == ':nth-last-child' 

1110 

1111 def append_nth(nth_sel: ct.SelectorList) -> None: 

1112 """Append to this selector's `nth` attribute.""" 

1113 

1114 sel.nth.append(ct.SelectorNth(s1, var, s2, False, last, nth_sel)) 

1115 

1116 if m.group('of'): 

1117 # Parse the rest of `of S`. 

1118 context = self.parse_pseudo_open(pseudo_sel, m.end(0), append_nth) 

1119 else: 

1120 # Use default `*|*` for `of S`. 

1121 nth_sel = self.PSEUDO_SELECTORS['<nth-of-s>'] 

1122 self.increment_count(nth_sel.count) 

1123 append_nth(nth_sel) 

1124 else: 

1125 if pseudo_sel == ':nth-of-type': 

1126 sel.nth.append(ct.SelectorNth(s1, var, s2, True, False, ct.SelectorList())) 

1127 elif pseudo_sel == ':nth-last-of-type': 

1128 sel.nth.append(ct.SelectorNth(s1, var, s2, True, True, ct.SelectorList())) 

1129 has_selector = True 

1130 return has_selector, context 

1131 

1132 def parse_pseudo_open( 

1133 self, 

1134 name: str, 

1135 index: int, 

1136 on_close: Callable[[ct.SelectorList], None] | None = None, 

1137 is_relative: bool = False 

1138 ) -> _SelectorContext: 

1139 """Create a new context for nested pseudo-class selectors.""" 

1140 

1141 flags = FLG_PSEUDO | FLG_OPEN 

1142 if name == ':not': 

1143 flags |= FLG_NOT 

1144 elif name == ':has': 

1145 if is_relative and not self.nostrict: 

1146 raise SelectorSyntaxError( 

1147 f":has() was found to be nested under :has() at position {index}", 

1148 self.pattern, 

1149 index 

1150 ) 

1151 flags |= FLG_RELATIVE 

1152 elif name in (':where', ':is'): 

1153 flags |= FLG_FORGIVE 

1154 

1155 return self.create_context(flags, index, on_close) 

1156 

1157 def create_context( 

1158 self, 

1159 flags: int, 

1160 index: int, 

1161 on_close: Callable[[ct.SelectorList], None] | None = None 

1162 ) -> _SelectorContext: 

1163 """Create a new selector context.""" 

1164 

1165 context = _SelectorContext(flags, index, self.count, on_close) 

1166 if self.debug: # pragma: no cover 

1167 context.debug() 

1168 return context 

1169 

1170 def parse_has_combinator( 

1171 self, 

1172 sel: _Selector, 

1173 m: Match[str], 

1174 has_selector: bool, 

1175 selectors: list[_Selector], 

1176 rel_type: str, 

1177 index: int 

1178 ) -> tuple[bool, _Selector, str]: 

1179 """Parse combinator tokens.""" 

1180 

1181 combinator = m.group('relation') 

1182 if not combinator: 

1183 combinator = WS_COMBINATOR 

1184 if combinator == COMMA_COMBINATOR: 

1185 if not has_selector: 

1186 raise SelectorSyntaxError( 

1187 f"The combinator '{combinator}' at position {index}, must have a selector before it", 

1188 self.pattern, 

1189 index 

1190 ) 

1191 sel.rel_type = rel_type 

1192 selectors[-1].relations.append(sel) 

1193 rel_type = ":" + WS_COMBINATOR 

1194 selectors.append(_Selector()) 

1195 else: 

1196 if has_selector and self.nostrict: 

1197 # End the current selector and associate the leading combinator with this selector. 

1198 sel.rel_type = rel_type 

1199 selectors[-1].relations.append(sel) 

1200 elif has_selector: 

1201 raise SelectorSyntaxError( 

1202 f'Using strict rules as proposed by CSS, the complex selectors at postition {index} is not allowed', 

1203 self.pattern, 

1204 index 

1205 ) 

1206 elif rel_type[1:] != WS_COMBINATOR: 

1207 # It's impossible to have two whitespace combinators after each other as the patterns 

1208 # will gobble up trailing whitespace. It is also impossible to have a whitespace 

1209 # combinator after any other kind for the same reason. But we could have 

1210 # multiple non-whitespace combinators. So if the current combinator is not a whitespace, 

1211 # then we've hit the multiple combinator case, so we should fail. 

1212 raise SelectorSyntaxError( 

1213 f'The multiple combinators at position {index}', 

1214 self.pattern, 

1215 index 

1216 ) 

1217 

1218 # Set the leading combinator for the next selector. 

1219 rel_type = ':' + combinator 

1220 

1221 sel = _Selector() 

1222 has_selector = False 

1223 return has_selector, sel, rel_type 

1224 

1225 def parse_combinator( 

1226 self, 

1227 sel: _Selector, 

1228 m: Match[str], 

1229 has_selector: bool, 

1230 selectors: list[_Selector], 

1231 relations: list[_Selector], 

1232 is_pseudo: bool, 

1233 is_forgive: bool, 

1234 index: int 

1235 ) -> tuple[bool, _Selector]: 

1236 """Parse combinator tokens.""" 

1237 

1238 combinator = m.group('relation') 

1239 if not combinator: 

1240 combinator = WS_COMBINATOR 

1241 if not has_selector: 

1242 if not is_forgive or combinator != COMMA_COMBINATOR: 

1243 raise SelectorSyntaxError( 

1244 f"The combinator '{combinator}' at position {index}, must have a selector before it", 

1245 self.pattern, 

1246 index 

1247 ) 

1248 

1249 # If we are in a forgiving pseudo class, just make the selector a "no match" 

1250 if combinator == COMMA_COMBINATOR: 

1251 self.increment_count() 

1252 del relations[:] 

1253 else: 

1254 if combinator == COMMA_COMBINATOR: 

1255 if not sel.tag and not is_pseudo: 

1256 # Implied `*` 

1257 sel.tag = ct.SelectorTag('*', None) 

1258 sel.relations.extend(relations) 

1259 selectors.append(sel) 

1260 del relations[:] 

1261 else: 

1262 sel.relations.extend(relations) 

1263 sel.rel_type = combinator 

1264 del relations[:] 

1265 relations.append(sel) 

1266 

1267 sel = _Selector() 

1268 has_selector = False 

1269 

1270 return has_selector, sel 

1271 

1272 def parse_class_id(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool: 

1273 """Parse HTML classes and ids.""" 

1274 

1275 selector = m.group(0) 

1276 if selector.startswith('.'): 

1277 sel.classes.append(css_unescape(selector[1:])) 

1278 else: 

1279 sel.ids.append(css_unescape(selector[1:])) 

1280 has_selector = True 

1281 return has_selector 

1282 

1283 def parse_pseudo_contains(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool: 

1284 """Parse contains.""" 

1285 

1286 pseudo = util.lower(css_unescape(m.group('name'))) 

1287 if pseudo == ":contains": 

1288 warnings.warn( # noqa: B028 

1289 "The pseudo class ':contains' is deprecated, ':-soup-contains' should be used moving forward.", 

1290 FutureWarning 

1291 ) 

1292 contains_own = pseudo == ":-soup-contains-own" 

1293 patterns = [ 

1294 css_unescape(token.group('value')) 

1295 for token in RE_VALUES.finditer(m.group('values')) 

1296 if not token.group('split') 

1297 ] 

1298 sel.contains.append(ct.SelectorContains(patterns, contains_own)) 

1299 has_selector = True 

1300 return has_selector 

1301 

1302 def parse_pseudo_lang(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool: 

1303 """Parse pseudo language.""" 

1304 

1305 patterns = [ 

1306 css_unescape(token.group('value')) 

1307 for token in RE_VALUES.finditer(m.group('values')) 

1308 if not token.group('split') 

1309 ] 

1310 sel.lang.append(ct.SelectorLang(patterns)) 

1311 has_selector = True 

1312 

1313 return has_selector 

1314 

1315 def parse_pseudo_dir(self, sel: _Selector, m: Match[str], has_selector: bool) -> bool: 

1316 """Parse pseudo direction.""" 

1317 

1318 value = util.lower(css_unescape(m.group('dir'))) 

1319 if value == 'ltr': 

1320 sel.flags |= ct.SEL_DIR_LTR 

1321 elif value == 'rtl': 

1322 sel.flags |= ct.SEL_DIR_RTL 

1323 else: 

1324 # Any other identifier is valid, but does not match anything. 

1325 sel.no_match = True 

1326 has_selector = True 

1327 return has_selector 

1328 

1329 def parse_selectors( 

1330 self, 

1331 iselector: Iterator[tuple[str, Match[str]]], 

1332 index: int = 0, 

1333 flags: int = 0 

1334 ) -> ct.SelectorList: 

1335 """ 

1336 Parse selectors. 

1337 

1338 Pseudo-classes that open a nested selector list push a new context onto a stack, 

1339 and closing them pops back to the parent context, so nesting does not recurse. 

1340 """ 

1341 

1342 ctx = self.create_context(flags, index) 

1343 stack: list[_SelectorContext] = [] 

1344 

1345 for key, m in iselector: 

1346 child: _SelectorContext | None = None 

1347 

1348 if key not in ('combine', 'pseudo_close'): 

1349 self.increment_count() 

1350 

1351 # Handle parts 

1352 if self.ignore: 

1353 mi = RE_PSEUDO_IGNORE.match(m.group(0)) 

1354 if mi is not None and mi.group('name').lower() in self.ignore: 

1355 raise SelectorSyntaxError( 

1356 f"The selector '{mi.group('name')}' at position {m.start(0)}, has been disallowed", 

1357 self.pattern, 

1358 m.start(0) 

1359 ) 

1360 if key == "at_rule": 

1361 raise NotImplementedError(f"At-rules found at position {m.start(0)}") 

1362 elif key == "amp": 

1363 ctx.sel.flags |= ct.SEL_SCOPE 

1364 ctx.has_selector = True 

1365 elif key == 'pseudo_class_custom': 

1366 ctx.has_selector = self.parse_pseudo_class_custom(ctx.sel, m, ctx.has_selector) 

1367 elif key == 'pseudo_class': 

1368 ctx.has_selector, child = self.parse_pseudo_class(ctx.sel, m, ctx.has_selector, ctx.is_relative) 

1369 elif key == 'pseudo_element': 

1370 raise NotImplementedError(f"Pseudo-element found at position {m.start(0)}") 

1371 elif key == 'pseudo_contains': 

1372 ctx.has_selector = self.parse_pseudo_contains(ctx.sel, m, ctx.has_selector) 

1373 elif key in ('pseudo_nth_type', 'pseudo_nth_child'): 

1374 ctx.has_selector, child = self.parse_pseudo_nth(ctx.sel, m, ctx.has_selector) 

1375 elif key == 'pseudo_lang': 

1376 ctx.has_selector = self.parse_pseudo_lang(ctx.sel, m, ctx.has_selector) 

1377 elif key == 'pseudo_dir': 

1378 ctx.has_selector = self.parse_pseudo_dir(ctx.sel, m, ctx.has_selector) 

1379 elif key == 'pseudo_close': 

1380 if not ctx.has_selector: 

1381 if not ctx.is_forgive: 

1382 raise SelectorSyntaxError( 

1383 f"Expected a selector at position {m.start(0)}", 

1384 self.pattern, 

1385 m.start(0) 

1386 ) 

1387 ctx.sel.no_match = True 

1388 if not ctx.is_open: 

1389 raise SelectorSyntaxError( 

1390 f"Unmatched pseudo-class close at position {m.start(0)}", 

1391 self.pattern, 

1392 m.start(0) 

1393 ) 

1394 

1395 # Hand the completed selector list to the parent and resume parsing the parent. 

1396 selectors = self.finalize_selectors(ctx) 

1397 if ctx.on_close is not None: 

1398 ctx.on_close(selectors) 

1399 ctx = stack.pop() 

1400 continue 

1401 elif key == 'combine': 

1402 if ctx.is_relative: 

1403 ctx.has_selector, ctx.sel, ctx.rel_type = self.parse_has_combinator( 

1404 ctx.sel, m, ctx.has_selector, ctx.selectors, ctx.rel_type, ctx.index 

1405 ) 

1406 else: 

1407 ctx.has_selector, ctx.sel = self.parse_combinator( 

1408 ctx.sel, m, ctx.has_selector, ctx.selectors, ctx.relations, 

1409 ctx.is_pseudo, ctx.is_forgive, ctx.index 

1410 ) 

1411 elif key == 'attribute': 

1412 ctx.has_selector = self.parse_attribute_selector(ctx.sel, m, ctx.has_selector) 

1413 elif key == 'tag': 

1414 if ctx.has_selector: 

1415 raise SelectorSyntaxError( 

1416 f"Tag name found at position {m.start(0)} instead of at the start", 

1417 self.pattern, 

1418 m.start(0) 

1419 ) 

1420 ctx.has_selector = self.parse_tag_pattern(ctx.sel, m, ctx.has_selector) 

1421 elif key in ('class', 'id'): 

1422 ctx.has_selector = self.parse_class_id(ctx.sel, m, ctx.has_selector) 

1423 

1424 ctx.index = m.end(0) 

1425 

1426 # A pseudo-class opened a nested selector list, parse it before resuming this one. 

1427 if child is not None: 

1428 stack.append(ctx) 

1429 ctx = child 

1430 

1431 # Handle selectors that are not closed 

1432 if ctx.is_open: 

1433 raise SelectorSyntaxError( 

1434 f"Unclosed pseudo-class at position {ctx.index}", 

1435 self.pattern, 

1436 ctx.index 

1437 ) 

1438 

1439 return self.finalize_selectors(ctx) 

1440 

1441 def finalize_selectors(self, ctx: _SelectorContext) -> ct.SelectorList: 

1442 """Complete the selector list of a context and freeze it.""" 

1443 

1444 sel = ctx.sel 

1445 selectors = ctx.selectors 

1446 relations = ctx.relations 

1447 has_selector = ctx.has_selector 

1448 

1449 # Cleanup completed selector piece 

1450 if has_selector: 

1451 if not sel.tag and not ctx.is_pseudo: 

1452 # Implied `*` 

1453 sel.tag = ct.SelectorTag('*', None) 

1454 if ctx.is_relative: 

1455 sel.rel_type = ctx.rel_type 

1456 selectors[-1].relations.append(sel) 

1457 else: 

1458 sel.relations.extend(relations) 

1459 del relations[:] 

1460 selectors.append(sel) 

1461 

1462 # Forgive empty slots in pseudo-classes that have lists (and are forgiving) 

1463 elif ctx.is_forgive and (not selectors or not relations): 

1464 # Handle normal pseudo-classes with empty slots like `:is()` etc. 

1465 self.increment_count() 

1466 sel.no_match = True 

1467 del relations[:] 

1468 selectors.append(sel) 

1469 has_selector = True 

1470 

1471 if not has_selector: 

1472 # We will always need to finish a selector when `:has()` is used as it leads with combining. 

1473 # May apply to others as well. 

1474 raise SelectorSyntaxError( 

1475 f'Expected a selector at position {ctx.index}', 

1476 self.pattern, 

1477 ctx.index 

1478 ) 

1479 

1480 # Some patterns require additional logic, such as default. We try to make these the 

1481 # last pattern, and append the appropriate flag to that selector which communicates 

1482 # to the matcher what additional logic is required. 

1483 if ctx.is_default: 

1484 selectors[-1].flags = ct.SEL_DEFAULT 

1485 if ctx.is_indeterminate: 

1486 selectors[-1].flags = ct.SEL_INDETERMINATE 

1487 if ctx.is_in_range: 

1488 selectors[-1].flags = ct.SEL_IN_RANGE 

1489 if ctx.is_out_of_range: 

1490 selectors[-1].flags = ct.SEL_OUT_OF_RANGE 

1491 if ctx.is_placeholder_shown: 

1492 selectors[-1].flags = ct.SEL_PLACEHOLDER_SHOWN 

1493 if ctx.is_editable: 

1494 selectors[-1].flags = ct.SEL_EDITABLE 

1495 if ctx.is_checked: 

1496 selectors[-1].flags = ct.SEL_CHECKED 

1497 

1498 # Return selector list 

1499 return ct.SelectorList([s.freeze() for s in selectors], ctx.is_not, ctx.is_html, self.count - ctx.count) 

1500 

1501 def selector_iter(self, pattern: str) -> Iterator[tuple[str, Match[str]]]: 

1502 """Iterate selector tokens.""" 

1503 

1504 index = 0 

1505 end = len(pattern) - 1 

1506 

1507 if self.debug: # pragma: no cover 

1508 print(f'## PARSING: {pattern!r}') 

1509 while index <= end: 

1510 token = None 

1511 for v in self.CSS_TOKENS: 

1512 token = v.match(pattern, index, self.flags) 

1513 if token is not None: 

1514 name, m = token 

1515 if self.debug: # pragma: no cover 

1516 print(f"TOKEN: '{name or 'consumed'}' --> {m.group(0)!r} at position {m.start(0)}") 

1517 index = m.end(0) 

1518 if name is not None: 

1519 yield name, m 

1520 break 

1521 if token is None: 

1522 c = pattern[index] 

1523 # If the character represents the start of one of the known selector types, 

1524 # throw an exception mentioning that the known selector type is in error; 

1525 # otherwise, report the invalid character. 

1526 if c == '[': 

1527 msg = f"Malformed attribute selector at position {index}" 

1528 elif c == '.': 

1529 msg = f"Malformed class selector at position {index}" 

1530 elif c == '#': 

1531 msg = f"Malformed id selector at position {index}" 

1532 elif c == ':': 

1533 msg = f"Malformed pseudo-class selector at position {index}" 

1534 else: 

1535 msg = f"Invalid character {c!r} position {index}" 

1536 raise SelectorSyntaxError(msg, self.pattern, index) 

1537 if self.debug: # pragma: no cover 

1538 print('## END PARSING') 

1539 

1540 def process_selectors(self, index: int = 0, flags: int = 0) -> ct.SelectorList: 

1541 """Process selectors.""" 

1542 

1543 return self.parse_selectors(self.selector_iter(self.pattern), index, flags)