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

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

623 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 

237FLG_EDITABLE = 0x800 

238 

239# Maximum cached patterns to store 

240_MAXCACHE = 500 

241 

242 

243@lru_cache(maxsize=_MAXCACHE) 

244def _cached_css_compile( 

245 pattern: str, 

246 namespaces: ct.Namespaces | None, 

247 custom: ct.CustomSelectors | None, 

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

249 flags: int = 0, 

250 max_selectors: int = SELECTOR_LIMIT 

251) -> cm.SoupSieve: 

252 """Cached CSS compile.""" 

253 

254 return cm.SoupSieve( 

255 pattern, 

256 CSSParser( 

257 pattern, 

258 custom=custom, 

259 ignore=ignore, 

260 flags=flags, 

261 max_selectors=max_selectors 

262 ).process_selectors(), 

263 namespaces, 

264 custom, 

265 flags 

266 ) 

267 

268 

269def _purge_cache() -> None: 

270 """Purge the cache.""" 

271 

272 _cached_css_compile.cache_clear() 

273 

274 

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

276 """ 

277 Unescape CSS value. 

278 

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

280 """ 

281 

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

283 """Replace with the appropriate substitute.""" 

284 

285 if m.group(1): 

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

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

288 if ( 

289 codepoint == 0 

290 or codepoint > 0x10FFFF 

291 or 0xD800 <= codepoint <= 0xDFFF 

292 ): 

293 codepoint = UNICODE_REPLACEMENT_CHAR 

294 value = chr(codepoint) 

295 elif m.group(2): 

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

297 elif m.group(3): 

298 value = '\ufffd' 

299 else: 

300 value = '' 

301 

302 return value 

303 

304 quoted = content.startswith(QUOTED) 

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

306 replace, 

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

308 ) 

309 

310 

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

312 """Escape identifier.""" 

313 

314 string = [] 

315 length = len(ident) 

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

317 if length == 1 and start_dash: 

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

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

320 else: 

321 for index, c in enumerate(ident): 

322 codepoint = ord(c) 

323 if codepoint == 0x00: 

324 string.append('\ufffd') 

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

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

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

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

329 elif ( 

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

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

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

333 ): 

334 string.append(c) 

335 else: 

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

337 return ''.join(string) 

338 

339 

340class SelectorPattern: 

341 """Selector pattern.""" 

342 

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

344 """Initialize.""" 

345 

346 self.name = name 

347 self.pattern = pattern 

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

349 

350 @property 

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

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

353 

354 with RE_LOCK: 

355 if self._re_pattern is None: 

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

357 return self._re_pattern 

358 

359 def get_name(self) -> str: 

360 """Get name.""" 

361 

362 return self.name 

363 

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

365 """Match the selector.""" 

366 

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

368 

369 

370class SpecialPseudoPattern(SelectorPattern): 

371 """Selector pattern.""" 

372 

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

374 """Initialize.""" 

375 

376 self.patterns = {} 

377 for p in patterns: 

378 name = p[0] 

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

380 for pseudo in p[1]: 

381 self.patterns[pseudo] = pattern 

382 

383 self.matched_name: SelectorPattern | None = None 

384 

385 def get_name(self) -> str: 

386 """Get name.""" 

387 

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

389 

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

391 """Match the selector.""" 

392 

393 pseudo = None 

394 m = RE_PSEUDO_CLASS_SPECIAL.match(selector, index) 

395 if m: 

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

397 pattern = self.patterns.get(name) 

398 if pattern: 

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

400 if pseudo: 

401 self.matched_name = pattern 

402 

403 return pseudo 

404 

405 

406class _Selector: 

407 """ 

408 Intermediate selector class. 

409 

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

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

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

413 """ 

414 

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

416 """Initialize.""" 

417 

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

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

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

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

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

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

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

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

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

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

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

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

430 

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

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

433 

434 if self.no_match: 

435 return ct.Null 

436 else: 

437 return ct.Selector( 

438 self.tag, 

439 tuple(self.ids), 

440 tuple(self.classes), 

441 tuple(self.attributes), 

442 tuple(self.nth), 

443 tuple(self.selectors), 

444 relation, 

445 self.rel_type, 

446 tuple(self.contains), 

447 tuple(self.lang), 

448 self.flags 

449 ) 

450 

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

452 """ 

453 Freeze self. 

454 

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

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

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

458 """ 

459 

460 chain: list[_Selector] = [] 

461 pending = [self] 

462 while pending: 

463 sel = pending.pop() 

464 chain.append(sel) 

465 if sel.no_match: 

466 break 

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

468 

469 relation = ct.SelectorList() 

470 for sel in reversed(chain): 

471 frozen = sel._freeze(relation) 

472 relation = ct.SelectorList([frozen]) 

473 return frozen 

474 

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

476 """String representation.""" 

477 

478 return ( 

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

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

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

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

483 ) 

484 

485 __repr__ = __str__ 

486 

487 

488class _SelectorContext: 

489 """ 

490 Parsing state for a selector list. 

491 

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

493 """ 

494 

495 def __init__( 

496 self, 

497 flags: int, 

498 index: int, 

499 count: int, 

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

501 ) -> None: 

502 """Initialize.""" 

503 

504 self.sel = _Selector() 

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

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

507 self.has_selector = False 

508 self.rel_type = ":" + WS_COMBINATOR 

509 self.index = index 

510 self.count = count 

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

512 self.on_close = on_close 

513 

514 # Setup various flags 

515 self.is_open = bool(flags & FLG_OPEN) 

516 self.is_pseudo = bool(flags & FLG_PSEUDO) 

517 self.is_relative = bool(flags & FLG_RELATIVE) 

518 self.is_not = bool(flags & FLG_NOT) 

519 self.is_html = bool(flags & FLG_HTML) 

520 self.is_default = bool(flags & FLG_DEFAULT) 

521 self.is_indeterminate = bool(flags & FLG_INDETERMINATE) 

522 self.is_in_range = bool(flags & FLG_IN_RANGE) 

523 self.is_out_of_range = bool(flags & FLG_OUT_OF_RANGE) 

524 self.is_placeholder_shown = bool(flags & FLG_PLACEHOLDER_SHOWN) 

525 self.is_forgive = bool(flags & FLG_FORGIVE) 

526 self.is_editable = bool(flags & FLG_EDITABLE) 

527 

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

529 if self.is_relative: 

530 self.selectors.append(_Selector()) 

531 

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

533 """Print out useful debug stuff.""" 

534 

535 for name in ( 

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

537 'is_in_range', 'is_out_of_range', 'is_placeholder_shown', 'is_forgive', 'is_editable' 

538 ): 

539 if getattr(self, name): 

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

541 

542 

543@dataclass 

544class CSSPattern: 

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

546 

547 selector: str 

548 flags: int 

549 

550 

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

552 """Pseudo selector map.""" 

553 

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

555 """Set item.""" 

556 

557 self.data[key] = value 

558 

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

560 """Get item.""" 

561 

562 with SEL_LOCK: 

563 value = self.data[key] 

564 if isinstance(value, CSSPattern): 

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

566 self.data[key] = value 

567 

568 return value 

569 

570 

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

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

573# CSS pattern for `:checked` 

574CSS_CHECKED = CSSPattern( 

575 ''' 

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

577 ''', 

578 FLG_PSEUDO | FLG_HTML 

579) 

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

581CSS_DEFAULT = CSSPattern( 

582 ''' 

583 :checked, 

584 

585 /* 

586 This pattern must be at the end. 

587 Special logic is applied to the last selector. 

588 */ 

589 html|form html|*:is( 

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

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

592 ) 

593 ''', 

594 FLG_PSEUDO | FLG_HTML | FLG_DEFAULT 

595) 

596# CSS pattern for `:indeterminate` 

597CSS_INDETERMINATE = CSSPattern( 

598 ''' 

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

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

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

602 

603 /* 

604 This pattern must be at the end. 

605 Special logic is applied to the last selector. 

606 */ 

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

608 ''', 

609 FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE 

610) 

611# CSS pattern for `:disabled` 

612CSS_DISABLED = CSSPattern( 

613 ''' 

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

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

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

617 html|fieldset[disabled] > 

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

619 html|*:is( 

620 html|select[disabled], 

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

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

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

624 ''', 

625 FLG_PSEUDO | FLG_HTML 

626) 

627# CSS pattern for `:enabled` 

628CSS_ENABLED = CSSPattern( 

629 ''' 

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

631 ''', 

632 FLG_PSEUDO | FLG_HTML 

633) 

634# CSS pattern for `:required` 

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

636# CSS pattern for `:optional` 

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

638# CSS pattern for `:placeholder-shown` 

639CSS_PLACEHOLDER_SHOWN = CSSPattern( 

640 ''' 

641 html|input:is( 

642 :not([type]), 

643 [type=""], 

644 [type=text], 

645 [type=search], 

646 [type=url], 

647 [type=tel], 

648 [type=email], 

649 [type=password], 

650 [type=number] 

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

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

653 ''', 

654 FLG_PSEUDO | FLG_HTML | FLG_PLACEHOLDER_SHOWN 

655) 

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

657CSS_READ_WRITE = CSSPattern( 

658 ''' 

659 html|*:is( 

660 textarea, 

661 input:is( 

662 :not([type]), 

663 [type=""], 

664 [type=text], 

665 [type=search], 

666 [type=url], 

667 [type=tel], 

668 [type=email], 

669 [type=number], 

670 [type=password], 

671 [type=date], 

672 [type=datetime-local], 

673 [type=month], 

674 [type=time], 

675 [type=week] 

676 ) 

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

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

679 ''', 

680 FLG_PSEUDO | FLG_HTML | FLG_EDITABLE 

681) 

682# CSS pattern for `:read-only` 

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

684# CSS pattern for `:in-range` 

685CSS_IN_RANGE = CSSPattern( 

686 ''' 

687 html|input:is( 

688 [type="date"], 

689 [type="month"], 

690 [type="week"], 

691 [type="time"], 

692 [type="datetime-local"], 

693 [type="number"], 

694 [type="range"] 

695 ):is( 

696 [min], 

697 [max] 

698 ) 

699 ''', 

700 FLG_PSEUDO | FLG_HTML | FLG_IN_RANGE 

701) 

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

703CSS_OUT_OF_RANGE = CSSPattern( 

704 ''' 

705 html|input:is( 

706 [type="date"], 

707 [type="month"], 

708 [type="week"], 

709 [type="time"], 

710 [type="datetime-local"], 

711 [type="number"], 

712 [type="range"] 

713 ):is( 

714 [min], 

715 [max] 

716 ) 

717 ''', 

718 FLG_PSEUDO | FLG_HTML | FLG_OUT_OF_RANGE 

719) 

720# CSS pattern for :open 

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

722# CSS pattern for :muted 

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

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

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

726 

727 

728class CSSParser: 

729 """Parse CSS selectors.""" 

730 

731 CSS_TOKENS = ( 

732 SelectorPattern("pseudo_close", PAT_PSEUDO_CLOSE), 

733 SpecialPseudoPattern( 

734 ( 

735 ( 

736 "pseudo_contains", 

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

738 PAT_PSEUDO_CONTAINS, 

739 SelectorPattern 

740 ), 

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

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

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

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

745 ) 

746 ), 

747 SelectorPattern("pseudo_class_custom", PAT_PSEUDO_CLASS_CUSTOM), 

748 SelectorPattern("pseudo_class", PAT_PSEUDO_CLASS), 

749 SelectorPattern("pseudo_element", PAT_PSEUDO_ELEMENT), 

750 SelectorPattern("amp", PAT_AMP), 

751 SelectorPattern("at_rule", PAT_AT_RULE), 

752 SelectorPattern("id", PAT_ID), 

753 SelectorPattern("class", PAT_CLASS), 

754 SelectorPattern("tag", PAT_TAG), 

755 SelectorPattern("attribute", PAT_ATTR), 

756 SelectorPattern("combine", PAT_COMBINE) 

757 ) 

758 

759 # Pseudos that expand to selectors 

760 PSEUDO_SELECTORS = PseudoSelectorMap( 

761 { 

762 ':link': CSS_LINK, 

763 ':any-link': CSS_LINK, 

764 ':checked': CSS_CHECKED, 

765 ':default': CSS_DEFAULT, 

766 ':indeterminate': CSS_INDETERMINATE, 

767 ':disabled': CSS_DISABLED, 

768 ':enabled': CSS_ENABLED, 

769 ':required': CSS_REQUIRED, 

770 ':muted': CSS_MUTED, 

771 ':open': CSS_OPEN, 

772 ':optional': CSS_OPTIONAL, 

773 ':read-only': CSS_READ_ONLY, 

774 ':read-write': CSS_READ_WRITE, 

775 ':in-range': CSS_IN_RANGE, 

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

777 ':placeholder-shown': CSS_PLACEHOLDER_SHOWN, 

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

779 } 

780 ) 

781 

782 def __init__( 

783 self, 

784 selector: str, 

785 *, 

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

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

788 flags: int = 0, 

789 max_selectors: int = SELECTOR_LIMIT 

790 ) -> None: 

791 """Initialize.""" 

792 

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

794 self.flags = flags 

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

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

797 self.custom = custom 

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

799 self.count = 0 

800 self.maxsel = max_selectors 

801 

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

803 """Check the current selector count.""" 

804 

805 self.count += increment 

806 if self.count > self.maxsel: 

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

808 

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

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

811 

812 op = m.group('cmp') 

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

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

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

816 is_type = False 

817 pattern2 = None 

818 value = '' 

819 inverse = False 

820 

821 if case: 

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

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

824 flags = re.I | re.DOTALL 

825 is_type = True 

826 else: 

827 flags = re.DOTALL 

828 

829 if op: 

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

831 

832 if not op: 

833 # Attribute name 

834 pattern = None 

835 elif op.startswith('^'): 

836 # Value start with 

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 ends with 

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

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

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

845 elif op.startswith('*'): 

846 # Value contains 

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

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

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

850 elif op.startswith('~'): 

851 # Value contains word within space separated list 

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

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

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

855 elif op.startswith('|'): 

856 # Value starts with word in dash separated list 

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

858 elif op.startswith('!'): 

859 # Value does not matches 

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

861 inverse = True 

862 else: 

863 # Value matches 

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

865 

866 if is_type and pattern: 

867 pattern2 = re.compile(pattern.pattern) 

868 

869 # Append the attribute selector 

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

871 sel.attributes.append(sel_attr) 

872 

873 has_selector = True 

874 return has_selector 

875 

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

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

878 

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

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

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

882 has_selector = True 

883 return has_selector 

884 

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

886 """ 

887 Parse custom pseudo class alias. 

888 

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

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

891 """ 

892 

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

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

895 if selector is None: 

896 raise SelectorSyntaxError( 

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

898 self.pattern, 

899 m.end(0) 

900 ) 

901 

902 self.increment_count(selector.count) 

903 sel.selectors.append(selector) 

904 has_selector = True 

905 return has_selector 

906 

907 def parse_pseudo_class( 

908 self, 

909 sel: _Selector, 

910 m: Match[str], 

911 has_selector: bool, 

912 is_html: bool, 

913 is_relative: bool 

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

915 """ 

916 Parse pseudo class. 

917 

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

919 """ 

920 

921 context = None 

922 complex_pseudo = False 

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

924 if m.group('open'): 

925 complex_pseudo = True 

926 if complex_pseudo and pseudo in PSEUDO_COMPLEX: 

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

928 has_selector = True 

929 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE: 

930 if pseudo == ':root': 

931 sel.flags |= ct.SEL_ROOT 

932 elif pseudo == ':defined': 

933 sel.flags |= ct.SEL_DEFINED 

934 is_html = True 

935 elif pseudo == ':scope': 

936 sel.flags |= ct.SEL_SCOPE 

937 elif pseudo == ':empty': 

938 sel.flags |= ct.SEL_EMPTY 

939 elif pseudo in self.PSEUDO_SELECTORS: 

940 pseudo_selector = self.PSEUDO_SELECTORS[pseudo] 

941 self.increment_count(pseudo_selector.count) 

942 sel.selectors.append(pseudo_selector) 

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

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

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

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

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

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

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

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

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

952 sel.nth.extend( 

953 [ 

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

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

956 ] 

957 ) 

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

959 sel.nth.extend( 

960 [ 

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

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

963 ] 

964 ) 

965 has_selector = True 

966 elif complex_pseudo and pseudo in PSEUDO_COMPLEX_NO_MATCH: 

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

968 sel.no_match = True 

969 has_selector = True 

970 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE_NO_MATCH: 

971 sel.no_match = True 

972 has_selector = True 

973 elif pseudo in PSEUDO_SUPPORTED: 

974 raise SelectorSyntaxError( 

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

976 self.pattern, 

977 m.start(0) 

978 ) 

979 else: 

980 raise SelectorSyntaxError( 

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

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

983 self.pattern, 

984 m.start(0) 

985 ) 

986 

987 return has_selector, is_html, context 

988 

989 def parse_pseudo_nth( 

990 self, 

991 sel: _Selector, 

992 m: Match[str], 

993 has_selector: bool 

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

995 """ 

996 Parse `nth` pseudo. 

997 

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

999 """ 

1000 

1001 context = None 

1002 mdict = m.groupdict() 

1003 if mdict.get('pseudo_nth_child'): 

1004 postfix = '_child' 

1005 else: 

1006 postfix = '_type' 

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

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

1009 if content == 'even': 

1010 # 2n 

1011 s1 = 2 

1012 s2 = 0 

1013 var = True 

1014 elif content == 'odd': 

1015 # 2n+1 

1016 s1 = 2 

1017 s2 = 1 

1018 var = True 

1019 else: 

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

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

1022 _s2 = '' 

1023 a = nth_parts.group('a') 

1024 var = a.endswith('n') 

1025 if a.startswith('n'): 

1026 _s1 += '1' 

1027 elif var: 

1028 _s1 += a[:-1] 

1029 else: 

1030 _s2 = _s1 + a 

1031 _s1 = '' 

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

1033 _s2 = '-' 

1034 if nth_parts.group('b'): 

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

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

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

1038 

1039 pseudo_sel = mdict['name'] 

1040 if postfix == '_child': 

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

1042 

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

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

1045 

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

1047 

1048 if m.group('of'): 

1049 # Parse the rest of `of S`. 

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

1051 else: 

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

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

1054 self.increment_count(nth_sel.count) 

1055 append_nth(nth_sel) 

1056 else: 

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

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

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

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

1061 has_selector = True 

1062 return has_selector, context 

1063 

1064 def parse_pseudo_open( 

1065 self, 

1066 name: str, 

1067 index: int, 

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

1069 is_relative: bool = False 

1070 ) -> _SelectorContext: 

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

1072 

1073 flags = FLG_PSEUDO | FLG_OPEN 

1074 if name == ':not': 

1075 flags |= FLG_NOT 

1076 elif name == ':has': 

1077 if is_relative and not self.nostrict: 

1078 raise SelectorSyntaxError( 

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

1080 self.pattern, 

1081 index 

1082 ) 

1083 flags |= FLG_RELATIVE 

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

1085 flags |= FLG_FORGIVE 

1086 

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

1088 

1089 def create_context( 

1090 self, 

1091 flags: int, 

1092 index: int, 

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

1094 ) -> _SelectorContext: 

1095 """Create a new selector context.""" 

1096 

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

1098 if self.debug: # pragma: no cover 

1099 context.debug() 

1100 return context 

1101 

1102 def parse_has_combinator( 

1103 self, 

1104 sel: _Selector, 

1105 m: Match[str], 

1106 has_selector: bool, 

1107 selectors: list[_Selector], 

1108 rel_type: str, 

1109 index: int 

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

1111 """Parse combinator tokens.""" 

1112 

1113 combinator = m.group('relation') 

1114 if not combinator: 

1115 combinator = WS_COMBINATOR 

1116 if combinator == COMMA_COMBINATOR: 

1117 if not has_selector: 

1118 raise SelectorSyntaxError( 

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

1120 self.pattern, 

1121 index 

1122 ) 

1123 sel.rel_type = rel_type 

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

1125 rel_type = ":" + WS_COMBINATOR 

1126 selectors.append(_Selector()) 

1127 else: 

1128 if has_selector and self.nostrict: 

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

1130 sel.rel_type = rel_type 

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

1132 elif has_selector: 

1133 raise SelectorSyntaxError( 

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

1135 self.pattern, 

1136 index 

1137 ) 

1138 elif rel_type[1:] != WS_COMBINATOR: 

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

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

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

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

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

1144 raise SelectorSyntaxError( 

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

1146 self.pattern, 

1147 index 

1148 ) 

1149 

1150 # Set the leading combinator for the next selector. 

1151 rel_type = ':' + combinator 

1152 

1153 sel = _Selector() 

1154 has_selector = False 

1155 return has_selector, sel, rel_type 

1156 

1157 def parse_combinator( 

1158 self, 

1159 sel: _Selector, 

1160 m: Match[str], 

1161 has_selector: bool, 

1162 selectors: list[_Selector], 

1163 relations: list[_Selector], 

1164 is_pseudo: bool, 

1165 is_forgive: bool, 

1166 index: int 

1167 ) -> tuple[bool, _Selector]: 

1168 """Parse combinator tokens.""" 

1169 

1170 combinator = m.group('relation') 

1171 if not combinator: 

1172 combinator = WS_COMBINATOR 

1173 if not has_selector: 

1174 if not is_forgive or combinator != COMMA_COMBINATOR: 

1175 raise SelectorSyntaxError( 

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

1177 self.pattern, 

1178 index 

1179 ) 

1180 

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

1182 if combinator == COMMA_COMBINATOR: 

1183 self.increment_count() 

1184 del relations[:] 

1185 else: 

1186 if combinator == COMMA_COMBINATOR: 

1187 if not sel.tag and not is_pseudo: 

1188 # Implied `*` 

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

1190 sel.relations.extend(relations) 

1191 selectors.append(sel) 

1192 del relations[:] 

1193 else: 

1194 sel.relations.extend(relations) 

1195 sel.rel_type = combinator 

1196 del relations[:] 

1197 relations.append(sel) 

1198 

1199 sel = _Selector() 

1200 has_selector = False 

1201 

1202 return has_selector, sel 

1203 

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

1205 """Parse HTML classes and ids.""" 

1206 

1207 selector = m.group(0) 

1208 if selector.startswith('.'): 

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

1210 else: 

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

1212 has_selector = True 

1213 return has_selector 

1214 

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

1216 """Parse contains.""" 

1217 

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

1219 if pseudo == ":contains": 

1220 warnings.warn( # noqa: B028 

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

1222 FutureWarning 

1223 ) 

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

1225 patterns = [ 

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

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

1228 if not token.group('split') 

1229 ] 

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

1231 has_selector = True 

1232 return has_selector 

1233 

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

1235 """Parse pseudo language.""" 

1236 

1237 patterns = [ 

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

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

1240 if not token.group('split') 

1241 ] 

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

1243 has_selector = True 

1244 

1245 return has_selector 

1246 

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

1248 """Parse pseudo direction.""" 

1249 

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

1251 sel.flags |= value 

1252 has_selector = True 

1253 return has_selector 

1254 

1255 def parse_selectors( 

1256 self, 

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

1258 index: int = 0, 

1259 flags: int = 0 

1260 ) -> ct.SelectorList: 

1261 """ 

1262 Parse selectors. 

1263 

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

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

1266 """ 

1267 

1268 ctx = self.create_context(flags, index) 

1269 stack: list[_SelectorContext] = [] 

1270 

1271 for key, m in iselector: 

1272 child: _SelectorContext | None = None 

1273 

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

1275 self.increment_count() 

1276 

1277 # Handle parts 

1278 if self.ignore: 

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

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

1281 raise SelectorSyntaxError( 

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

1283 self.pattern, 

1284 m.start(0) 

1285 ) 

1286 if key == "at_rule": 

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

1288 elif key == "amp": 

1289 ctx.sel.flags |= ct.SEL_SCOPE 

1290 ctx.has_selector = True 

1291 elif key == 'pseudo_class_custom': 

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

1293 elif key == 'pseudo_class': 

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

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

1296 ) 

1297 elif key == 'pseudo_element': 

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

1299 elif key == 'pseudo_contains': 

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

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

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

1303 elif key == 'pseudo_lang': 

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

1305 elif key == 'pseudo_dir': 

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

1307 # Currently only supports HTML 

1308 ctx.is_html = True 

1309 elif key == 'pseudo_close': 

1310 if not ctx.has_selector: 

1311 if not ctx.is_forgive: 

1312 raise SelectorSyntaxError( 

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

1314 self.pattern, 

1315 m.start(0) 

1316 ) 

1317 ctx.sel.no_match = True 

1318 if not ctx.is_open: 

1319 raise SelectorSyntaxError( 

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

1321 self.pattern, 

1322 m.start(0) 

1323 ) 

1324 

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

1326 selectors = self.finalize_selectors(ctx) 

1327 if ctx.on_close is not None: 

1328 ctx.on_close(selectors) 

1329 ctx = stack.pop() 

1330 continue 

1331 elif key == 'combine': 

1332 if ctx.is_relative: 

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

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

1335 ) 

1336 else: 

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

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

1339 ctx.is_pseudo, ctx.is_forgive, ctx.index 

1340 ) 

1341 elif key == 'attribute': 

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

1343 elif key == 'tag': 

1344 if ctx.has_selector: 

1345 raise SelectorSyntaxError( 

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

1347 self.pattern, 

1348 m.start(0) 

1349 ) 

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

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

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

1353 

1354 ctx.index = m.end(0) 

1355 

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

1357 if child is not None: 

1358 stack.append(ctx) 

1359 ctx = child 

1360 

1361 # Handle selectors that are not closed 

1362 if ctx.is_open: 

1363 raise SelectorSyntaxError( 

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

1365 self.pattern, 

1366 ctx.index 

1367 ) 

1368 

1369 return self.finalize_selectors(ctx) 

1370 

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

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

1373 

1374 sel = ctx.sel 

1375 selectors = ctx.selectors 

1376 relations = ctx.relations 

1377 has_selector = ctx.has_selector 

1378 

1379 # Cleanup completed selector piece 

1380 if has_selector: 

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

1382 # Implied `*` 

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

1384 if ctx.is_relative: 

1385 sel.rel_type = ctx.rel_type 

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

1387 else: 

1388 sel.relations.extend(relations) 

1389 del relations[:] 

1390 selectors.append(sel) 

1391 

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

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

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

1395 self.increment_count() 

1396 sel.no_match = True 

1397 del relations[:] 

1398 selectors.append(sel) 

1399 has_selector = True 

1400 

1401 if not has_selector: 

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

1403 # May apply to others as well. 

1404 raise SelectorSyntaxError( 

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

1406 self.pattern, 

1407 ctx.index 

1408 ) 

1409 

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

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

1412 # to the matcher what additional logic is required. 

1413 if ctx.is_default: 

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

1415 if ctx.is_indeterminate: 

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

1417 if ctx.is_in_range: 

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

1419 if ctx.is_out_of_range: 

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

1421 if ctx.is_placeholder_shown: 

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

1423 if ctx.is_editable: 

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

1425 

1426 # Return selector list 

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

1428 

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

1430 """Iterate selector tokens.""" 

1431 

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

1433 m = RE_WS_BEGIN.search(pattern) 

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

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

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

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

1438 

1439 if self.debug: # pragma: no cover 

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

1441 while index <= end: 

1442 m = None 

1443 for v in self.CSS_TOKENS: 

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

1445 if m: 

1446 name = v.get_name() 

1447 if self.debug: # pragma: no cover 

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

1449 index = m.end(0) 

1450 yield name, m 

1451 break 

1452 if m is None: 

1453 c = pattern[index] 

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

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

1456 # otherwise, report the invalid character. 

1457 if c == '[': 

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

1459 elif c == '.': 

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

1461 elif c == '#': 

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

1463 elif c == ':': 

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

1465 else: 

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

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

1468 if self.debug: # pragma: no cover 

1469 print('## END PARSING') 

1470 

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

1472 """Process selectors.""" 

1473 

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