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

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

619 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# Complex pseudo classes that take selector lists 

100PSEUDO_COMPLEX = { 

101 ':contains', 

102 ':-soup-contains', 

103 ':-soup-contains-own', 

104 ':has', 

105 ':is', 

106 ':matches', 

107 ':not', 

108 ':where' 

109} 

110 

111PSEUDO_COMPLEX_NO_MATCH = { 

112 ':current', 

113 ':host', 

114 ':host-context' 

115} 

116 

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

118PSEUDO_SPECIAL = { 

119 ':dir', 

120 ':lang', 

121 ':nth-child', 

122 ':nth-last-child', 

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

124 ':nth-of-type' 

125} 

126 

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

128 

129# Sub-patterns parts 

130# Whitespace 

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

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

133# Comments 

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

135# Whitespace with comments included 

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

137# CSS escapes 

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

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

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

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

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

143# CSS Identifier 

144IDENTIFIER = fr''' 

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

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

147''' 

148# `nth` content 

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

150# Value: quoted string or identifier 

151VALUE = fr''' 

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

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

154{IDENTIFIER}) 

155''' 

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

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

158 

159# Selector patterns 

160# IDs (`#id`) 

161PAT_ID = fr'\#{IDENTIFIER}' 

162# Classes (`.class`) 

163PAT_CLASS = fr'\.{IDENTIFIER}' 

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

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

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

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

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

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

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

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

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

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

174# Nesting ampersand selector. Matches `&` 

175PAT_AMP = r'&' 

176# Closing pseudo group (`)`) 

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

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

179PAT_PSEUDO_ELEMENT = fr':{PAT_PSEUDO_CLASS}' 

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

181PAT_AT_RULE = fr'@P{IDENTIFIER}' 

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

183PAT_PSEUDO_NTH_CHILD = fr''' 

184(?P<pseudo_nth_child>{PAT_PSEUDO_CLASS_SPECIAL} 

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

186''' 

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

188PAT_PSEUDO_NTH_TYPE = fr''' 

189(?P<pseudo_nth_type>{PAT_PSEUDO_CLASS_SPECIAL} 

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

191''' 

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

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

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

195PAT_PSEUDO_DIR = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<dir>ltr|rtl){WSC}*+\)' 

196# Combining characters (`>`, `~`, ` `, `+`, `,`) 

197PAT_COMBINE = fr'{COMMENTS}*+(?={WS}|[,+>~]){WSC}*+(?P<relation>[,+>~])?+{WSC}*+' 

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

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

200 

201# Regular expressions 

202# CSS escape pattern 

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

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

205# Pattern to break up `nth` specifiers 

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

207# Pattern to iterate multiple values. 

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

209# Whitespace checks 

210RE_WS = re.compile(WS) 

211RE_WS_BEGIN = re.compile(fr'^{WSC}*+') 

212RE_WS_END = re.compile(fr'^(?:[ \t]|(?>\n\r|[\n\f\r])|{COMMENTS})*+') 

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

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

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

216 

217QUOTED = ("'", '"') 

218 

219# Constants 

220# List split token 

221COMMA_COMBINATOR = ',' 

222# Relation token for descendant 

223WS_COMBINATOR = " " 

224 

225# Parse flags 

226FLG_PSEUDO = 0x01 

227FLG_NOT = 0x02 

228FLG_RELATIVE = 0x04 

229FLG_DEFAULT = 0x08 

230FLG_HTML = 0x10 

231FLG_INDETERMINATE = 0x20 

232FLG_OPEN = 0x40 

233FLG_IN_RANGE = 0x80 

234FLG_OUT_OF_RANGE = 0x100 

235FLG_PLACEHOLDER_SHOWN = 0x200 

236FLG_FORGIVE = 0x400 

237 

238# Maximum cached patterns to store 

239_MAXCACHE = 500 

240 

241 

242@lru_cache(maxsize=_MAXCACHE) 

243def _cached_css_compile( 

244 pattern: str, 

245 namespaces: ct.Namespaces | None, 

246 custom: ct.CustomSelectors | None, 

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

248 flags: int = 0, 

249 max_selectors: int = SELECTOR_LIMIT 

250) -> cm.SoupSieve: 

251 """Cached CSS compile.""" 

252 

253 return cm.SoupSieve( 

254 pattern, 

255 CSSParser( 

256 pattern, 

257 custom=custom, 

258 ignore=ignore, 

259 flags=flags, 

260 max_selectors=max_selectors 

261 ).process_selectors(), 

262 namespaces, 

263 custom, 

264 flags 

265 ) 

266 

267 

268def _purge_cache() -> None: 

269 """Purge the cache.""" 

270 

271 _cached_css_compile.cache_clear() 

272 

273 

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

275 """ 

276 Unescape CSS value. 

277 

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

279 """ 

280 

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

282 """Replace with the appropriate substitute.""" 

283 

284 if m.group(1): 

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

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

287 if ( 

288 codepoint == 0 

289 or codepoint > 0x10FFFF 

290 or 0xD800 <= codepoint <= 0xDFFF 

291 ): 

292 codepoint = UNICODE_REPLACEMENT_CHAR 

293 value = chr(codepoint) 

294 elif m.group(2): 

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

296 elif m.group(3): 

297 value = '\ufffd' 

298 else: 

299 value = '' 

300 

301 return value 

302 

303 quoted = content.startswith(QUOTED) 

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

305 replace, 

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

307 ) 

308 

309 

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

311 """Escape identifier.""" 

312 

313 string = [] 

314 length = len(ident) 

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

316 if length == 1 and start_dash: 

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

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

319 else: 

320 for index, c in enumerate(ident): 

321 codepoint = ord(c) 

322 if codepoint == 0x00: 

323 string.append('\ufffd') 

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

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

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

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

328 elif ( 

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

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

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

332 ): 

333 string.append(c) 

334 else: 

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

336 return ''.join(string) 

337 

338 

339class SelectorPattern: 

340 """Selector pattern.""" 

341 

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

343 """Initialize.""" 

344 

345 self.name = name 

346 self.pattern = pattern 

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

348 

349 @property 

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

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

352 

353 with RE_LOCK: 

354 if self._re_pattern is None: 

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

356 return self._re_pattern 

357 

358 def get_name(self) -> str: 

359 """Get name.""" 

360 

361 return self.name 

362 

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

364 """Match the selector.""" 

365 

366 return self.re_pattern.match(selector, index) 

367 

368 

369class SpecialPseudoPattern(SelectorPattern): 

370 """Selector pattern.""" 

371 

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

373 """Initialize.""" 

374 

375 self.patterns = {} 

376 for p in patterns: 

377 name = p[0] 

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

379 for pseudo in p[1]: 

380 self.patterns[pseudo] = pattern 

381 

382 self.matched_name: SelectorPattern | None = None 

383 

384 def get_name(self) -> str: 

385 """Get name.""" 

386 

387 return '' if self.matched_name is None else self.matched_name.get_name() 

388 

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

390 """Match the selector.""" 

391 

392 pseudo = None 

393 m = RE_PSEUDO_CLASS_SPECIAL.match(selector, index) 

394 if m: 

395 name = util.lower(css_unescape(m.group('name'))) 

396 pattern = self.patterns.get(name) 

397 if pattern: 

398 pseudo = pattern.match(selector, index, flags) 

399 if pseudo: 

400 self.matched_name = pattern 

401 

402 return pseudo 

403 

404 

405class _Selector: 

406 """ 

407 Intermediate selector class. 

408 

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

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

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

412 """ 

413 

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

415 """Initialize.""" 

416 

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

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

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

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

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

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

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

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

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

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

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

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

429 

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

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

432 

433 if self.no_match: 

434 return ct.Null 

435 else: 

436 return ct.Selector( 

437 self.tag, 

438 tuple(self.ids), 

439 tuple(self.classes), 

440 tuple(self.attributes), 

441 tuple(self.nth), 

442 tuple(self.selectors), 

443 relation, 

444 self.rel_type, 

445 tuple(self.contains), 

446 tuple(self.lang), 

447 self.flags 

448 ) 

449 

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

451 """ 

452 Freeze self. 

453 

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

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

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

457 """ 

458 

459 chain: list[_Selector] = [] 

460 pending = [self] 

461 while pending: 

462 sel = pending.pop() 

463 chain.append(sel) 

464 if sel.no_match: 

465 break 

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

467 

468 relation = ct.SelectorList() 

469 for sel in reversed(chain): 

470 frozen = sel._freeze(relation) 

471 relation = ct.SelectorList([frozen]) 

472 return frozen 

473 

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

475 """String representation.""" 

476 

477 return ( 

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

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

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

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

482 ) 

483 

484 __repr__ = __str__ 

485 

486 

487class _SelectorContext: 

488 """ 

489 Parsing state for a selector list. 

490 

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

492 """ 

493 

494 def __init__( 

495 self, 

496 flags: int, 

497 index: int, 

498 count: int, 

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

500 ) -> None: 

501 """Initialize.""" 

502 

503 self.sel = _Selector() 

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

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

506 self.has_selector = False 

507 self.rel_type = ":" + WS_COMBINATOR 

508 self.index = index 

509 self.count = count 

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

511 self.on_close = on_close 

512 

513 # Setup various flags 

514 self.is_open = bool(flags & FLG_OPEN) 

515 self.is_pseudo = bool(flags & FLG_PSEUDO) 

516 self.is_relative = bool(flags & FLG_RELATIVE) 

517 self.is_not = bool(flags & FLG_NOT) 

518 self.is_html = bool(flags & FLG_HTML) 

519 self.is_default = bool(flags & FLG_DEFAULT) 

520 self.is_indeterminate = bool(flags & FLG_INDETERMINATE) 

521 self.is_in_range = bool(flags & FLG_IN_RANGE) 

522 self.is_out_of_range = bool(flags & FLG_OUT_OF_RANGE) 

523 self.is_placeholder_shown = bool(flags & FLG_PLACEHOLDER_SHOWN) 

524 self.is_forgive = bool(flags & FLG_FORGIVE) 

525 

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

527 if self.is_relative: 

528 self.selectors.append(_Selector()) 

529 

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

531 """Print out useful debug stuff.""" 

532 

533 for name in ( 

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

535 'is_in_range', 'is_out_of_range', 'is_placeholder_shown', 'is_forgive' 

536 ): 

537 if getattr(self, name): 

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

539 

540 

541@dataclass 

542class CSSPattern: 

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

544 

545 selector: str 

546 flags: int 

547 

548 

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

550 """Pseudo selector map.""" 

551 

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

553 """Set item.""" 

554 

555 self.data[key] = value 

556 

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

558 """Get item.""" 

559 

560 with SEL_LOCK: 

561 value = self.data[key] 

562 if isinstance(value, CSSPattern): 

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

564 self.data[key] = value 

565 

566 return value 

567 

568 

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

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

571# CSS pattern for `:checked` 

572CSS_CHECKED = CSSPattern( 

573 ''' 

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

575 ''', 

576 FLG_PSEUDO | FLG_HTML 

577) 

578# CSS pattern for `:default` (must compile CSS_CHECKED first) 

579CSS_DEFAULT = CSSPattern( 

580 ''' 

581 :checked, 

582 

583 /* 

584 This pattern must be at the end. 

585 Special logic is applied to the last selector. 

586 */ 

587 html|form html|*:is(button, input)[type="submit"] 

588 ''', 

589 FLG_PSEUDO | FLG_HTML | FLG_DEFAULT 

590) 

591# CSS pattern for `:indeterminate` 

592CSS_INDETERMINATE = CSSPattern( 

593 ''' 

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

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

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

597 

598 /* 

599 This pattern must be at the end. 

600 Special logic is applied to the last selector. 

601 */ 

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

603 ''', 

604 FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE 

605) 

606# CSS pattern for `:disabled` 

607CSS_DISABLED = CSSPattern( 

608 ''' 

609 html|*:is(input:not([type=hidden]), button, select, textarea, fieldset, optgroup, option, fieldset)[disabled], 

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

611 html|fieldset[disabled] > html|*:is(input:not([type=hidden]), button, select, textarea, fieldset), 

612 html|fieldset[disabled] > 

613 html|*:not(legend:nth-of-type(1)) html|*:is(input:not([type=hidden]), button, select, textarea, fieldset) 

614 ''', 

615 FLG_PSEUDO | FLG_HTML 

616) 

617# CSS pattern for `:enabled` 

618CSS_ENABLED = CSSPattern( 

619 ''' 

620 html|*:is(input:not([type=hidden]), button, select, textarea, fieldset, optgroup, option, fieldset):not(:disabled) 

621 ''', 

622 FLG_PSEUDO | FLG_HTML 

623) 

624# CSS pattern for `:required` 

625CSS_REQUIRED = CSSPattern('html|*:is(input, textarea, select)[required]', FLG_PSEUDO | FLG_HTML) 

626# CSS pattern for `:optional` 

627CSS_OPTIONAL = CSSPattern('html|*:is(input, textarea, select):not([required])', FLG_PSEUDO | FLG_HTML) 

628# CSS pattern for `:placeholder-shown` 

629CSS_PLACEHOLDER_SHOWN = CSSPattern( 

630 ''' 

631 html|input:is( 

632 :not([type]), 

633 [type=""], 

634 [type=text], 

635 [type=search], 

636 [type=url], 

637 [type=tel], 

638 [type=email], 

639 [type=password], 

640 [type=number] 

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

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

643 ''', 

644 FLG_PSEUDO | FLG_HTML | FLG_PLACEHOLDER_SHOWN 

645) 

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

647CSS_READ_WRITE = CSSPattern( 

648 ''' 

649 html|*:is( 

650 textarea, 

651 input:is( 

652 :not([type]), 

653 [type=""], 

654 [type=text], 

655 [type=search], 

656 [type=url], 

657 [type=tel], 

658 [type=email], 

659 [type=number], 

660 [type=password], 

661 [type=date], 

662 [type=datetime-local], 

663 [type=month], 

664 [type=time], 

665 [type=week] 

666 ) 

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

668 html|*:is([contenteditable=""], [contenteditable="true" i]) 

669 ''', 

670 FLG_PSEUDO | FLG_HTML 

671) 

672# CSS pattern for `:read-only` 

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

674# CSS pattern for `:in-range` 

675CSS_IN_RANGE = CSSPattern( 

676 ''' 

677 html|input:is( 

678 [type="date"], 

679 [type="month"], 

680 [type="week"], 

681 [type="time"], 

682 [type="datetime-local"], 

683 [type="number"], 

684 [type="range"] 

685 ):is( 

686 [min], 

687 [max] 

688 ) 

689 ''', 

690 FLG_PSEUDO | FLG_HTML | FLG_IN_RANGE 

691) 

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

693CSS_OUT_OF_RANGE = CSSPattern( 

694 ''' 

695 html|input:is( 

696 [type="date"], 

697 [type="month"], 

698 [type="week"], 

699 [type="time"], 

700 [type="datetime-local"], 

701 [type="number"], 

702 [type="range"] 

703 ):is( 

704 [min], 

705 [max] 

706 ) 

707 ''', 

708 FLG_PSEUDO | FLG_HTML | FLG_OUT_OF_RANGE 

709) 

710# CSS pattern for :open 

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

712# CSS pattern for :muted 

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

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

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

716 

717 

718class CSSParser: 

719 """Parse CSS selectors.""" 

720 

721 CSS_TOKENS = ( 

722 SelectorPattern("pseudo_close", PAT_PSEUDO_CLOSE), 

723 SpecialPseudoPattern( 

724 ( 

725 ( 

726 "pseudo_contains", 

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

728 PAT_PSEUDO_CONTAINS, 

729 SelectorPattern 

730 ), 

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

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

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

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

735 ) 

736 ), 

737 SelectorPattern("pseudo_class_custom", PAT_PSEUDO_CLASS_CUSTOM), 

738 SelectorPattern("pseudo_class", PAT_PSEUDO_CLASS), 

739 SelectorPattern("pseudo_element", PAT_PSEUDO_ELEMENT), 

740 SelectorPattern("amp", PAT_AMP), 

741 SelectorPattern("at_rule", PAT_AT_RULE), 

742 SelectorPattern("id", PAT_ID), 

743 SelectorPattern("class", PAT_CLASS), 

744 SelectorPattern("tag", PAT_TAG), 

745 SelectorPattern("attribute", PAT_ATTR), 

746 SelectorPattern("combine", PAT_COMBINE) 

747 ) 

748 

749 # Pseudos that expand to selectors 

750 PSEUDO_SELECTORS = PseudoSelectorMap( 

751 { 

752 ':link': CSS_LINK, 

753 ':any-link': CSS_LINK, 

754 ':checked': CSS_CHECKED, 

755 ':default': CSS_DEFAULT, 

756 ':indeterminate': CSS_INDETERMINATE, 

757 ':disabled': CSS_DISABLED, 

758 ':enabled': CSS_ENABLED, 

759 ':required': CSS_REQUIRED, 

760 ':muted': CSS_MUTED, 

761 ':open': CSS_OPEN, 

762 ':optional': CSS_OPTIONAL, 

763 ':read-only': CSS_READ_ONLY, 

764 ':read-write': CSS_READ_WRITE, 

765 ':in-range': CSS_IN_RANGE, 

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

767 ':placeholder-shown': CSS_PLACEHOLDER_SHOWN, 

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

769 } 

770 ) 

771 

772 def __init__( 

773 self, 

774 selector: str, 

775 *, 

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

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

778 flags: int = 0, 

779 max_selectors: int = SELECTOR_LIMIT 

780 ) -> None: 

781 """Initialize.""" 

782 

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

784 self.flags = flags 

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

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

787 self.custom = custom 

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

789 self.count = 0 

790 self.maxsel = max_selectors 

791 

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

793 """Check the current selector count.""" 

794 

795 self.count += increment 

796 if self.count > self.maxsel: 

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

798 

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

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

801 

802 op = m.group('cmp') 

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

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

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

806 is_type = False 

807 pattern2 = None 

808 value = '' 

809 inverse = False 

810 

811 if case: 

812 flags = (re.I if case == 'i' else 0) | re.DOTALL 

813 elif util.lower(attr) == 'type': 

814 flags = re.I | re.DOTALL 

815 is_type = True 

816 else: 

817 flags = re.DOTALL 

818 

819 if op: 

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

821 

822 if not op: 

823 # Attribute name 

824 pattern = None 

825 elif op.startswith('^'): 

826 # Value start with 

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

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

829 pattern = re.compile(r'^%s.*' % value, flags) 

830 elif op.startswith('$'): 

831 # Value ends with 

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

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

834 pattern = re.compile(r'.*?%s$' % value, flags) 

835 elif op.startswith('*'): 

836 # Value contains 

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

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

839 pattern = re.compile(r'.*?%s.*' % value, flags) 

840 elif op.startswith('~'): 

841 # Value contains word within space separated list 

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

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

844 pattern = re.compile(r'.*?(?:(?<=^)|(?<=[ \t\r\n\f]))%s(?=(?:[ \t\r\n\f]|$)).*' % value, flags) 

845 elif op.startswith('|'): 

846 # Value starts with word in dash separated list 

847 pattern = re.compile(r'^%s(?:-.*)?$' % re.escape(value), flags) 

848 elif op.startswith('!'): 

849 # Value does not matches 

850 pattern = re.compile(r'^(?!%s).*$' % re.escape(value), flags) 

851 inverse = True 

852 else: 

853 # Value matches 

854 pattern = re.compile(r'^%s$' % re.escape(value), flags) 

855 

856 if is_type and pattern: 

857 pattern2 = re.compile(pattern.pattern) 

858 

859 # Append the attribute selector 

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

861 sel.attributes.append(sel_attr) 

862 

863 has_selector = True 

864 return has_selector 

865 

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

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

868 

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

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

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

872 has_selector = True 

873 return has_selector 

874 

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

876 """ 

877 Parse custom pseudo class alias. 

878 

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

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

881 """ 

882 

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

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

885 if selector is None: 

886 raise SelectorSyntaxError( 

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

888 self.pattern, 

889 m.end(0) 

890 ) 

891 

892 self.increment_count(selector.count) 

893 sel.selectors.append(selector) 

894 has_selector = True 

895 return has_selector 

896 

897 def parse_pseudo_class( 

898 self, 

899 sel: _Selector, 

900 m: Match[str], 

901 has_selector: bool, 

902 is_html: bool, 

903 is_relative: bool 

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

905 """ 

906 Parse pseudo class. 

907 

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

909 """ 

910 

911 context = None 

912 complex_pseudo = False 

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

914 if m.group('open'): 

915 complex_pseudo = True 

916 if complex_pseudo and pseudo in PSEUDO_COMPLEX: 

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

918 has_selector = True 

919 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE: 

920 if pseudo == ':root': 

921 sel.flags |= ct.SEL_ROOT 

922 elif pseudo == ':defined': 

923 sel.flags |= ct.SEL_DEFINED 

924 is_html = True 

925 elif pseudo == ':scope': 

926 sel.flags |= ct.SEL_SCOPE 

927 elif pseudo == ':empty': 

928 sel.flags |= ct.SEL_EMPTY 

929 elif pseudo in self.PSEUDO_SELECTORS: 

930 pseudo_selector = self.PSEUDO_SELECTORS[pseudo] 

931 self.increment_count(pseudo_selector.count) 

932 sel.selectors.append(pseudo_selector) 

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

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

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

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

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

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

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

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

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

942 sel.nth.extend( 

943 [ 

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

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

946 ] 

947 ) 

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

949 sel.nth.extend( 

950 [ 

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

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

953 ] 

954 ) 

955 has_selector = True 

956 elif complex_pseudo and pseudo in PSEUDO_COMPLEX_NO_MATCH: 

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

958 sel.no_match = True 

959 has_selector = True 

960 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE_NO_MATCH: 

961 sel.no_match = True 

962 has_selector = True 

963 elif pseudo in PSEUDO_SUPPORTED: 

964 raise SelectorSyntaxError( 

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

966 self.pattern, 

967 m.start(0) 

968 ) 

969 else: 

970 raise SelectorSyntaxError( 

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

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

973 self.pattern, 

974 m.start(0) 

975 ) 

976 

977 return has_selector, is_html, context 

978 

979 def parse_pseudo_nth( 

980 self, 

981 sel: _Selector, 

982 m: Match[str], 

983 has_selector: bool 

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

985 """ 

986 Parse `nth` pseudo. 

987 

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

989 """ 

990 

991 context = None 

992 mdict = m.groupdict() 

993 if mdict.get('pseudo_nth_child'): 

994 postfix = '_child' 

995 else: 

996 postfix = '_type' 

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

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

999 if content == 'even': 

1000 # 2n 

1001 s1 = 2 

1002 s2 = 0 

1003 var = True 

1004 elif content == 'odd': 

1005 # 2n+1 

1006 s1 = 2 

1007 s2 = 1 

1008 var = True 

1009 else: 

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

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

1012 _s2 = '' 

1013 a = nth_parts.group('a') 

1014 var = a.endswith('n') 

1015 if a.startswith('n'): 

1016 _s1 += '1' 

1017 elif var: 

1018 _s1 += a[:-1] 

1019 else: 

1020 _s2 = _s1 + a 

1021 _s1 = '' 

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

1023 _s2 = '-' 

1024 if nth_parts.group('b'): 

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

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

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

1028 

1029 pseudo_sel = mdict['name'] 

1030 if postfix == '_child': 

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

1032 

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

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

1035 

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

1037 

1038 if m.group('of'): 

1039 # Parse the rest of `of S`. 

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

1041 else: 

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

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

1044 self.increment_count(nth_sel.count) 

1045 append_nth(nth_sel) 

1046 else: 

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

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

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

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

1051 has_selector = True 

1052 return has_selector, context 

1053 

1054 def parse_pseudo_open( 

1055 self, 

1056 name: str, 

1057 index: int, 

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

1059 is_relative: bool = False 

1060 ) -> _SelectorContext: 

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

1062 

1063 flags = FLG_PSEUDO | FLG_OPEN 

1064 if name == ':not': 

1065 flags |= FLG_NOT 

1066 elif name == ':has': 

1067 if is_relative and not self.nostrict: 

1068 raise SelectorSyntaxError( 

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

1070 self.pattern, 

1071 index 

1072 ) 

1073 flags |= FLG_RELATIVE 

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

1075 flags |= FLG_FORGIVE 

1076 

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

1078 

1079 def create_context( 

1080 self, 

1081 flags: int, 

1082 index: int, 

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

1084 ) -> _SelectorContext: 

1085 """Create a new selector context.""" 

1086 

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

1088 if self.debug: # pragma: no cover 

1089 context.debug() 

1090 return context 

1091 

1092 def parse_has_combinator( 

1093 self, 

1094 sel: _Selector, 

1095 m: Match[str], 

1096 has_selector: bool, 

1097 selectors: list[_Selector], 

1098 rel_type: str, 

1099 index: int 

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

1101 """Parse combinator tokens.""" 

1102 

1103 combinator = m.group('relation') 

1104 if not combinator: 

1105 combinator = WS_COMBINATOR 

1106 if combinator == COMMA_COMBINATOR: 

1107 if not has_selector: 

1108 raise SelectorSyntaxError( 

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

1110 self.pattern, 

1111 index 

1112 ) 

1113 sel.rel_type = rel_type 

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

1115 rel_type = ":" + WS_COMBINATOR 

1116 selectors.append(_Selector()) 

1117 else: 

1118 if has_selector and self.nostrict: 

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

1120 sel.rel_type = rel_type 

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

1122 elif has_selector: 

1123 raise SelectorSyntaxError( 

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

1125 self.pattern, 

1126 index 

1127 ) 

1128 elif rel_type[1:] != WS_COMBINATOR: 

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

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

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

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

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

1134 raise SelectorSyntaxError( 

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

1136 self.pattern, 

1137 index 

1138 ) 

1139 

1140 # Set the leading combinator for the next selector. 

1141 rel_type = ':' + combinator 

1142 

1143 sel = _Selector() 

1144 has_selector = False 

1145 return has_selector, sel, rel_type 

1146 

1147 def parse_combinator( 

1148 self, 

1149 sel: _Selector, 

1150 m: Match[str], 

1151 has_selector: bool, 

1152 selectors: list[_Selector], 

1153 relations: list[_Selector], 

1154 is_pseudo: bool, 

1155 is_forgive: bool, 

1156 index: int 

1157 ) -> tuple[bool, _Selector]: 

1158 """Parse combinator tokens.""" 

1159 

1160 combinator = m.group('relation') 

1161 if not combinator: 

1162 combinator = WS_COMBINATOR 

1163 if not has_selector: 

1164 if not is_forgive or combinator != COMMA_COMBINATOR: 

1165 raise SelectorSyntaxError( 

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

1167 self.pattern, 

1168 index 

1169 ) 

1170 

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

1172 if combinator == COMMA_COMBINATOR: 

1173 self.increment_count() 

1174 del relations[:] 

1175 else: 

1176 if combinator == COMMA_COMBINATOR: 

1177 if not sel.tag and not is_pseudo: 

1178 # Implied `*` 

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

1180 sel.relations.extend(relations) 

1181 selectors.append(sel) 

1182 del relations[:] 

1183 else: 

1184 sel.relations.extend(relations) 

1185 sel.rel_type = combinator 

1186 del relations[:] 

1187 relations.append(sel) 

1188 

1189 sel = _Selector() 

1190 has_selector = False 

1191 

1192 return has_selector, sel 

1193 

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

1195 """Parse HTML classes and ids.""" 

1196 

1197 selector = m.group(0) 

1198 if selector.startswith('.'): 

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

1200 else: 

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

1202 has_selector = True 

1203 return has_selector 

1204 

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

1206 """Parse contains.""" 

1207 

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

1209 if pseudo == ":contains": 

1210 warnings.warn( # noqa: B028 

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

1212 FutureWarning 

1213 ) 

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

1215 patterns = [ 

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

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

1218 if not token.group('split') 

1219 ] 

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

1221 has_selector = True 

1222 return has_selector 

1223 

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

1225 """Parse pseudo language.""" 

1226 

1227 patterns = [ 

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

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

1230 if not token.group('split') 

1231 ] 

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

1233 has_selector = True 

1234 

1235 return has_selector 

1236 

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

1238 """Parse pseudo direction.""" 

1239 

1240 value = ct.SEL_DIR_LTR if util.lower(m.group('dir')) == 'ltr' else ct.SEL_DIR_RTL 

1241 sel.flags |= value 

1242 has_selector = True 

1243 return has_selector 

1244 

1245 def parse_selectors( 

1246 self, 

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

1248 index: int = 0, 

1249 flags: int = 0 

1250 ) -> ct.SelectorList: 

1251 """ 

1252 Parse selectors. 

1253 

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

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

1256 """ 

1257 

1258 ctx = self.create_context(flags, index) 

1259 stack: list[_SelectorContext] = [] 

1260 

1261 for key, m in iselector: 

1262 child: _SelectorContext | None = None 

1263 

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

1265 self.increment_count() 

1266 

1267 # Handle parts 

1268 if self.ignore: 

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

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

1271 raise SelectorSyntaxError( 

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

1273 self.pattern, 

1274 m.start(0) 

1275 ) 

1276 if key == "at_rule": 

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

1278 elif key == "amp": 

1279 ctx.sel.flags |= ct.SEL_SCOPE 

1280 ctx.has_selector = True 

1281 elif key == 'pseudo_class_custom': 

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

1283 elif key == 'pseudo_class': 

1284 ctx.has_selector, ctx.is_html, child = self.parse_pseudo_class( 

1285 ctx.sel, m, ctx.has_selector, ctx.is_html, ctx.is_relative 

1286 ) 

1287 elif key == 'pseudo_element': 

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

1289 elif key == 'pseudo_contains': 

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

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

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

1293 elif key == 'pseudo_lang': 

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

1295 elif key == 'pseudo_dir': 

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

1297 # Currently only supports HTML 

1298 ctx.is_html = True 

1299 elif key == 'pseudo_close': 

1300 if not ctx.has_selector: 

1301 if not ctx.is_forgive: 

1302 raise SelectorSyntaxError( 

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

1304 self.pattern, 

1305 m.start(0) 

1306 ) 

1307 ctx.sel.no_match = True 

1308 if not ctx.is_open: 

1309 raise SelectorSyntaxError( 

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

1311 self.pattern, 

1312 m.start(0) 

1313 ) 

1314 

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

1316 selectors = self.finalize_selectors(ctx) 

1317 if ctx.on_close is not None: 

1318 ctx.on_close(selectors) 

1319 ctx = stack.pop() 

1320 continue 

1321 elif key == 'combine': 

1322 if ctx.is_relative: 

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

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

1325 ) 

1326 else: 

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

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

1329 ctx.is_pseudo, ctx.is_forgive, ctx.index 

1330 ) 

1331 elif key == 'attribute': 

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

1333 elif key == 'tag': 

1334 if ctx.has_selector: 

1335 raise SelectorSyntaxError( 

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

1337 self.pattern, 

1338 m.start(0) 

1339 ) 

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

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

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

1343 

1344 ctx.index = m.end(0) 

1345 

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

1347 if child is not None: 

1348 stack.append(ctx) 

1349 ctx = child 

1350 

1351 # Handle selectors that are not closed 

1352 if ctx.is_open: 

1353 raise SelectorSyntaxError( 

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

1355 self.pattern, 

1356 ctx.index 

1357 ) 

1358 

1359 return self.finalize_selectors(ctx) 

1360 

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

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

1363 

1364 sel = ctx.sel 

1365 selectors = ctx.selectors 

1366 relations = ctx.relations 

1367 has_selector = ctx.has_selector 

1368 

1369 # Cleanup completed selector piece 

1370 if has_selector: 

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

1372 # Implied `*` 

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

1374 if ctx.is_relative: 

1375 sel.rel_type = ctx.rel_type 

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

1377 else: 

1378 sel.relations.extend(relations) 

1379 del relations[:] 

1380 selectors.append(sel) 

1381 

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

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

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

1385 self.increment_count() 

1386 sel.no_match = True 

1387 del relations[:] 

1388 selectors.append(sel) 

1389 has_selector = True 

1390 

1391 if not has_selector: 

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

1393 # May apply to others as well. 

1394 raise SelectorSyntaxError( 

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

1396 self.pattern, 

1397 ctx.index 

1398 ) 

1399 

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

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

1402 # to the matcher what additional logic is required. 

1403 if ctx.is_default: 

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

1405 if ctx.is_indeterminate: 

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

1407 if ctx.is_in_range: 

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

1409 if ctx.is_out_of_range: 

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

1411 if ctx.is_placeholder_shown: 

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

1413 

1414 # Return selector list 

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

1416 

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

1418 """Iterate selector tokens.""" 

1419 

1420 # Ignore whitespace and comments at start and end of pattern 

1421 m = RE_WS_BEGIN.search(pattern) 

1422 index = m.end(0) if m else 0 

1423 m = RE_WS_END.search(pattern[::-1]) 

1424 offset = m.end(0) if m else 0 

1425 end = len(pattern) - (1 + offset) 

1426 

1427 if self.debug: # pragma: no cover 

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

1429 while index <= end: 

1430 m = None 

1431 for v in self.CSS_TOKENS: 

1432 m = v.match(pattern, index, self.flags) 

1433 if m: 

1434 name = v.get_name() 

1435 if self.debug: # pragma: no cover 

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

1437 index = m.end(0) 

1438 yield name, m 

1439 break 

1440 if m is None: 

1441 c = pattern[index] 

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

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

1444 # otherwise, report the invalid character. 

1445 if c == '[': 

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

1447 elif c == '.': 

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

1449 elif c == '#': 

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

1451 elif c == ':': 

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

1453 else: 

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

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

1456 if self.debug: # pragma: no cover 

1457 print('## END PARSING') 

1458 

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

1460 """Process selectors.""" 

1461 

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