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

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

603 statements  

1"""CSS selector parser.""" 

2from __future__ import annotations 

3import re 

4from functools import lru_cache 

5from . import util 

6from . import css_match as cm 

7from . import css_types as ct 

8from .util import SelectorSyntaxError 

9import warnings 

10from typing import Match, Any, Iterator, Iterable, cast 

11from dataclasses import dataclass 

12from collections import UserDict 

13import threading 

14 

15RE_LOCK = threading.Lock() 

16SEL_LOCK = threading.RLock() 

17 

18UNICODE_REPLACEMENT_CHAR = 0xFFFD 

19 

20SELECTOR_LIMIT = 8192 

21 

22# Simple pseudo classes that take no parameters 

23PSEUDO_SIMPLE = { 

24 ":any-link", 

25 ":empty", 

26 ":first-child", 

27 ":first-of-type", 

28 ":in-range", 

29 ":open", 

30 ":out-of-range", 

31 ":last-child", 

32 ":last-of-type", 

33 ":link", 

34 ":only-child", 

35 ":only-of-type", 

36 ":root", 

37 ':checked', 

38 ':default', 

39 ':disabled', 

40 ':enabled', 

41 ':indeterminate', 

42 ':optional', 

43 ':placeholder-shown', 

44 ':read-only', 

45 ':read-write', 

46 ':required', 

47 ':scope', 

48 ':defined', 

49 ':muted' 

50} 

51 

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

53PSEUDO_SIMPLE_NO_MATCH = { 

54 ':active', 

55 ':autofill', 

56 ':buffering', 

57 ':current', 

58 ':focus', 

59 ':focus-visible', 

60 ':focus-within', 

61 ':fullscreen', 

62 ':future', 

63 ':host', 

64 ':hover', 

65 ':local-link', 

66 ':past', 

67 ':paused', 

68 ':picture-in-picture', 

69 ':playing', 

70 ':popover-open', 

71 ':seeking', 

72 ':stalled', 

73 ':target', 

74 ':target-within', 

75 ':user-invalid', 

76 ':volume-locked', 

77 ':visited' 

78} 

79 

80# Complex pseudo classes that take selector lists 

81PSEUDO_COMPLEX = { 

82 ':contains', 

83 ':-soup-contains', 

84 ':-soup-contains-own', 

85 ':has', 

86 ':is', 

87 ':matches', 

88 ':not', 

89 ':where' 

90} 

91 

92PSEUDO_COMPLEX_NO_MATCH = { 

93 ':current', 

94 ':host', 

95 ':host-context' 

96} 

97 

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

99PSEUDO_SPECIAL = { 

100 ':dir', 

101 ':lang', 

102 ':nth-child', 

103 ':nth-last-child', 

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

105 ':nth-of-type' 

106} 

107 

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

109 

110# Sub-patterns parts 

111# Whitespace 

112NEWLINE = r'(?:\r\n|(?!\r\n)[\n\f\r])' 

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

114# Comments 

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

116# Whitespace with comments included 

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

118# CSS escapes 

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

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

121# CSS Identifier 

122IDENTIFIER = fr''' 

123(?:(?:--|-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})) 

124(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})*) 

125''' 

126# `nth` content 

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

128# Value: quoted string or identifier 

129VALUE = fr''' 

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

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

132{IDENTIFIER}) 

133''' 

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

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

136 

137# Selector patterns 

138# IDs (`#id`) 

139PAT_ID = fr'\#{IDENTIFIER}' 

140# Classes (`.class`) 

141PAT_CLASS = fr'\.{IDENTIFIER}' 

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

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

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

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

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

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

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

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

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

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

152# Nesting ampersand selector. Matches `&` 

153PAT_AMP = r'&' 

154# Closing pseudo group (`)`) 

155PAT_PSEUDO_CLOSE = fr'{WSC}*\)' 

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

157PAT_PSEUDO_ELEMENT = fr':{PAT_PSEUDO_CLASS}' 

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

159PAT_AT_RULE = fr'@P{IDENTIFIER}' 

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

161PAT_PSEUDO_NTH_CHILD = fr''' 

162(?P<pseudo_nth_child>{PAT_PSEUDO_CLASS_SPECIAL} 

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

164''' 

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

166PAT_PSEUDO_NTH_TYPE = fr''' 

167(?P<pseudo_nth_type>{PAT_PSEUDO_CLASS_SPECIAL} 

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

169''' 

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

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

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

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

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

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

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

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

178 

179# Regular expressions 

180# CSS escape pattern 

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

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

183# Pattern to break up `nth` specifiers 

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

185# Pattern to iterate multiple values. 

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

187# Whitespace checks 

188RE_WS = re.compile(WS) 

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

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

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

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

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

194 

195QUOTED = ("'", '"') 

196 

197# Constants 

198# List split token 

199COMMA_COMBINATOR = ',' 

200# Relation token for descendant 

201WS_COMBINATOR = " " 

202 

203# Parse flags 

204FLG_PSEUDO = 0x01 

205FLG_NOT = 0x02 

206FLG_RELATIVE = 0x04 

207FLG_DEFAULT = 0x08 

208FLG_HTML = 0x10 

209FLG_INDETERMINATE = 0x20 

210FLG_OPEN = 0x40 

211FLG_IN_RANGE = 0x80 

212FLG_OUT_OF_RANGE = 0x100 

213FLG_PLACEHOLDER_SHOWN = 0x200 

214FLG_FORGIVE = 0x400 

215 

216# Maximum cached patterns to store 

217_MAXCACHE = 500 

218 

219 

220@lru_cache(maxsize=_MAXCACHE) 

221def _cached_css_compile( 

222 pattern: str, 

223 namespaces: ct.Namespaces | None, 

224 custom: ct.CustomSelectors | None, 

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

226 flags: int = 0 

227) -> cm.SoupSieve: 

228 """Cached CSS compile.""" 

229 

230 custom_selectors = process_custom(custom) 

231 return cm.SoupSieve( 

232 pattern, 

233 CSSParser( 

234 pattern, 

235 custom=custom_selectors, 

236 ignore=ignore, 

237 flags=flags 

238 ).process_selectors(), 

239 namespaces, 

240 custom, 

241 flags 

242 ) 

243 

244 

245def _purge_cache() -> None: 

246 """Purge the cache.""" 

247 

248 _cached_css_compile.cache_clear() 

249 

250 

251def process_custom(custom: ct.CustomSelectors | None) -> dict[str, str | ct.SelectorList]: 

252 """Process custom.""" 

253 

254 custom_selectors = {} 

255 if custom is not None: 

256 for key, value in custom.items(): 

257 name = util.lower(key) 

258 if RE_CUSTOM.match(name) is None: 

259 raise SelectorSyntaxError(f"The name '{name}' is not a valid custom pseudo-class name") 

260 if name in custom_selectors: 

261 raise KeyError(f"The custom selector '{name}' has already been registered") 

262 custom_selectors[css_unescape(name)] = value 

263 return custom_selectors 

264 

265 

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

267 """ 

268 Unescape CSS value. 

269 

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

271 """ 

272 

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

274 """Replace with the appropriate substitute.""" 

275 

276 if m.group(1): 

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

278 if codepoint == 0: 

279 codepoint = UNICODE_REPLACEMENT_CHAR 

280 value = chr(codepoint) 

281 elif m.group(2): 

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

283 elif m.group(3): 

284 value = '\ufffd' 

285 else: 

286 value = '' 

287 

288 return value 

289 

290 quoted = content.startswith(QUOTED) 

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

292 replace, 

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

294 ) 

295 

296 

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

298 """Escape identifier.""" 

299 

300 string = [] 

301 length = len(ident) 

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

303 if length == 1 and start_dash: 

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

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

306 else: 

307 for index, c in enumerate(ident): 

308 codepoint = ord(c) 

309 if codepoint == 0x00: 

310 string.append('\ufffd') 

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

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

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

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

315 elif ( 

316 codepoint in (0x2D, 0x5F) or codepoint >= 0x80 or (0x30 <= codepoint <= 0x39) or 

317 (0x30 <= codepoint <= 0x39) or (0x41 <= codepoint <= 0x5A) or (0x61 <= codepoint <= 0x7A) 

318 ): 

319 string.append(c) 

320 else: 

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

322 return ''.join(string) 

323 

324 

325class SelectorPattern: 

326 """Selector pattern.""" 

327 

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

329 """Initialize.""" 

330 

331 self.name = name 

332 self.pattern = pattern 

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

334 

335 @property 

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

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

338 

339 with RE_LOCK: 

340 if self._re_pattern is None: 

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

342 return self._re_pattern 

343 

344 def get_name(self) -> str: 

345 """Get name.""" 

346 

347 return self.name 

348 

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

350 """Match the selector.""" 

351 

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

353 

354 

355class SpecialPseudoPattern(SelectorPattern): 

356 """Selector pattern.""" 

357 

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

359 """Initialize.""" 

360 

361 self.patterns = {} 

362 for p in patterns: 

363 name = p[0] 

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

365 for pseudo in p[1]: 

366 self.patterns[pseudo] = pattern 

367 

368 self.matched_name = None # type: SelectorPattern | None 

369 

370 def get_name(self) -> str: 

371 """Get name.""" 

372 

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

374 

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

376 """Match the selector.""" 

377 

378 pseudo = None 

379 m = RE_PSEUDO_CLASS_SPECIAL.match(selector, index) 

380 if m: 

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

382 pattern = self.patterns.get(name) 

383 if pattern: 

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

385 if pseudo: 

386 self.matched_name = pattern 

387 

388 return pseudo 

389 

390 

391class _Selector: 

392 """ 

393 Intermediate selector class. 

394 

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

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

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

398 """ 

399 

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

401 """Initialize.""" 

402 

403 self.tag = kwargs.get('tag', None) # type: ct.SelectorTag | None 

404 self.ids = kwargs.get('ids', []) # type: list[str] 

405 self.classes = kwargs.get('classes', []) # type: list[str] 

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

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

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

409 self.relations = kwargs.get('relations', []) # type: list[_Selector] 

410 self.rel_type = kwargs.get('rel_type', None) # type: str | None 

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

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

413 self.flags = kwargs.get('flags', 0) # type: int 

414 self.no_match = kwargs.get('no_match', False) # type: bool 

415 

416 def _freeze_relations(self, relations: list[_Selector]) -> ct.SelectorList: 

417 """Freeze relation.""" 

418 

419 if relations: 

420 sel = relations[0] 

421 sel.relations.extend(relations[1:]) 

422 return ct.SelectorList([sel.freeze()]) 

423 else: 

424 return ct.SelectorList() 

425 

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

427 """Freeze self.""" 

428 

429 if self.no_match: 

430 return ct.Null 

431 else: 

432 return ct.Selector( 

433 self.tag, 

434 tuple(self.ids), 

435 tuple(self.classes), 

436 tuple(self.attributes), 

437 tuple(self.nth), 

438 tuple(self.selectors), 

439 self._freeze_relations(self.relations), 

440 self.rel_type, 

441 tuple(self.contains), 

442 tuple(self.lang), 

443 self.flags 

444 ) 

445 

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

447 """String representation.""" 

448 

449 return ( 

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

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

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

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

454 ) 

455 

456 __repr__ = __str__ 

457 

458 

459@dataclass 

460class CSSPattern: 

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

462 

463 selector: str 

464 flags: int 

465 

466 

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

468 """Pseudo selector map.""" 

469 

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

471 """Set item.""" 

472 

473 self.data[key] = value 

474 

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

476 """Get item.""" 

477 

478 with SEL_LOCK: 

479 value = self.data[key] 

480 if isinstance(value, CSSPattern): 

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

482 self.data[key] = value 

483 

484 return value 

485 

486 

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

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

489# CSS pattern for `:checked` 

490CSS_CHECKED = CSSPattern( 

491 ''' 

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

493 ''', 

494 FLG_PSEUDO | FLG_HTML 

495) 

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

497CSS_DEFAULT = CSSPattern( 

498 ''' 

499 :checked, 

500 

501 /* 

502 This pattern must be at the end. 

503 Special logic is applied to the last selector. 

504 */ 

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

506 ''', 

507 FLG_PSEUDO | FLG_HTML | FLG_DEFAULT 

508) 

509# CSS pattern for `:indeterminate` 

510CSS_INDETERMINATE = CSSPattern( 

511 ''' 

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

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

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

515 

516 /* 

517 This pattern must be at the end. 

518 Special logic is applied to the last selector. 

519 */ 

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

521 ''', 

522 FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE 

523) 

524# CSS pattern for `:disabled` 

525CSS_DISABLED = CSSPattern( 

526 ''' 

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

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

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

530 html|fieldset[disabled] > 

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

532 ''', 

533 FLG_PSEUDO | FLG_HTML 

534) 

535# CSS pattern for `:enabled` 

536CSS_ENABLED = CSSPattern( 

537 ''' 

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

539 ''', 

540 FLG_PSEUDO | FLG_HTML 

541) 

542# CSS pattern for `:required` 

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

544# CSS pattern for `:optional` 

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

546# CSS pattern for `:placeholder-shown` 

547CSS_PLACEHOLDER_SHOWN = CSSPattern( 

548 ''' 

549 html|input:is( 

550 :not([type]), 

551 [type=""], 

552 [type=text], 

553 [type=search], 

554 [type=url], 

555 [type=tel], 

556 [type=email], 

557 [type=password], 

558 [type=number] 

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

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

561 ''', 

562 FLG_PSEUDO | FLG_HTML | FLG_PLACEHOLDER_SHOWN 

563) 

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

565CSS_READ_WRITE = CSSPattern( 

566 ''' 

567 html|*:is( 

568 textarea, 

569 input:is( 

570 :not([type]), 

571 [type=""], 

572 [type=text], 

573 [type=search], 

574 [type=url], 

575 [type=tel], 

576 [type=email], 

577 [type=number], 

578 [type=password], 

579 [type=date], 

580 [type=datetime-local], 

581 [type=month], 

582 [type=time], 

583 [type=week] 

584 ) 

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

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

587 ''', 

588 FLG_PSEUDO | FLG_HTML 

589) 

590# CSS pattern for `:read-only` 

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

592# CSS pattern for `:in-range` 

593CSS_IN_RANGE = CSSPattern( 

594 ''' 

595 html|input:is( 

596 [type="date"], 

597 [type="month"], 

598 [type="week"], 

599 [type="time"], 

600 [type="datetime-local"], 

601 [type="number"], 

602 [type="range"] 

603 ):is( 

604 [min], 

605 [max] 

606 ) 

607 ''', 

608 FLG_PSEUDO | FLG_HTML | FLG_IN_RANGE 

609) 

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

611CSS_OUT_OF_RANGE = CSSPattern( 

612 ''' 

613 html|input:is( 

614 [type="date"], 

615 [type="month"], 

616 [type="week"], 

617 [type="time"], 

618 [type="datetime-local"], 

619 [type="number"], 

620 [type="range"] 

621 ):is( 

622 [min], 

623 [max] 

624 ) 

625 ''', 

626 FLG_PSEUDO | FLG_HTML | FLG_OUT_OF_RANGE 

627) 

628# CSS pattern for :open 

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

630# CSS pattern for :muted 

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

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

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

634 

635 

636class CSSParser: 

637 """Parse CSS selectors.""" 

638 

639 CSS_TOKENS = ( 

640 SelectorPattern("pseudo_close", PAT_PSEUDO_CLOSE), 

641 SpecialPseudoPattern( 

642 ( 

643 ( 

644 "pseudo_contains", 

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

646 PAT_PSEUDO_CONTAINS, 

647 SelectorPattern 

648 ), 

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

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

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

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

653 ) 

654 ), 

655 SelectorPattern("pseudo_class_custom", PAT_PSEUDO_CLASS_CUSTOM), 

656 SelectorPattern("pseudo_class", PAT_PSEUDO_CLASS), 

657 SelectorPattern("pseudo_element", PAT_PSEUDO_ELEMENT), 

658 SelectorPattern("amp", PAT_AMP), 

659 SelectorPattern("at_rule", PAT_AT_RULE), 

660 SelectorPattern("id", PAT_ID), 

661 SelectorPattern("class", PAT_CLASS), 

662 SelectorPattern("tag", PAT_TAG), 

663 SelectorPattern("attribute", PAT_ATTR), 

664 SelectorPattern("combine", PAT_COMBINE) 

665 ) 

666 

667 # Pseudos that expand to selectors 

668 PSEUDO_SELECTORS = PseudoSelectorMap( 

669 { 

670 ':link': CSS_LINK, 

671 ':any-link': CSS_LINK, 

672 ':checked': CSS_CHECKED, 

673 ':default': CSS_DEFAULT, 

674 ':indeterminate': CSS_INDETERMINATE, 

675 ':disabled': CSS_DISABLED, 

676 ':enabled': CSS_ENABLED, 

677 ':required': CSS_REQUIRED, 

678 ':muted': CSS_MUTED, 

679 ':open': CSS_OPEN, 

680 ':optional': CSS_OPTIONAL, 

681 ':read-only': CSS_READ_ONLY, 

682 ':read-write': CSS_READ_WRITE, 

683 ':in-range': CSS_IN_RANGE, 

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

685 ':placeholder-shown': CSS_PLACEHOLDER_SHOWN, 

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

687 } 

688 ) 

689 

690 def __init__( 

691 self, 

692 selector: str, 

693 *, 

694 custom: dict[str, str | ct.SelectorList] | None = None, 

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

696 flags: int = 0 

697 ) -> None: 

698 """Initialize.""" 

699 

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

701 self.flags = flags 

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

703 self.custom = {} if custom is None else custom 

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

705 self.count = 0 

706 

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

708 """Check the current selector count.""" 

709 

710 self.count += increment 

711 if self.count > SELECTOR_LIMIT: 

712 raise ValueError(f'Selector exceeds pseudo-class nesting limit of {SELECTOR_LIMIT}') 

713 

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

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

716 

717 inverse = False 

718 op = m.group('cmp') 

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

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

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

722 is_type = False 

723 pattern2 = None 

724 value = '' 

725 

726 if case: 

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

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

729 flags = re.I | re.DOTALL 

730 is_type = True 

731 else: 

732 flags = re.DOTALL 

733 

734 if op: 

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

736 

737 if not op: 

738 # Attribute name 

739 pattern = None 

740 elif op.startswith('^'): 

741 # Value start with 

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

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

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

745 elif op.startswith('$'): 

746 # Value ends with 

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

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

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

750 elif op.startswith('*'): 

751 # Value contains 

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

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

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

755 elif op.startswith('~'): 

756 # Value contains word within space separated list 

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

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

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

760 elif op.startswith('|'): 

761 # Value starts with word in dash separated list 

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

763 else: 

764 # Value matches 

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

766 if op.startswith('!'): 

767 # Equivalent to `:not([attr=value])` 

768 inverse = True 

769 if is_type and pattern: 

770 pattern2 = re.compile(pattern.pattern) 

771 

772 # Append the attribute selector 

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

774 if inverse: 

775 # If we are using `!=`, we need to nest the pattern under a `:not()`. 

776 sub_sel = _Selector() 

777 sub_sel.attributes.append(sel_attr) 

778 not_list = ct.SelectorList([sub_sel.freeze()], True, False) 

779 sel.selectors.append(not_list) 

780 else: 

781 sel.attributes.append(sel_attr) 

782 

783 has_selector = True 

784 return has_selector 

785 

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

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

788 

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

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

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

792 has_selector = True 

793 return has_selector 

794 

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

796 """ 

797 Parse custom pseudo class alias. 

798 

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

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

801 """ 

802 

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

804 selector = self.custom.get(pseudo) 

805 if selector is None: 

806 raise SelectorSyntaxError( 

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

808 self.pattern, 

809 m.end(0) 

810 ) 

811 

812 if not isinstance(selector, ct.SelectorList): 

813 del self.custom[pseudo] 

814 selector = CSSParser( 

815 selector, custom=self.custom, flags=self.flags, ignore=self.ignore 

816 ).process_selectors(flags=FLG_PSEUDO) 

817 self.custom[pseudo] = selector 

818 

819 self.increment_count(selector.count) 

820 sel.selectors.append(selector) 

821 has_selector = True 

822 return has_selector 

823 

824 def parse_pseudo_class( 

825 self, 

826 sel: _Selector, 

827 m: Match[str], 

828 has_selector: bool, 

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

830 is_html: bool 

831 ) -> tuple[bool, bool]: 

832 """Parse pseudo class.""" 

833 

834 complex_pseudo = False 

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

836 if m.group('open'): 

837 complex_pseudo = True 

838 if complex_pseudo and pseudo in PSEUDO_COMPLEX: 

839 has_selector = self.parse_pseudo_open(sel, pseudo, has_selector, iselector, m.end(0)) 

840 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE: 

841 if pseudo == ':root': 

842 sel.flags |= ct.SEL_ROOT 

843 elif pseudo == ':defined': 

844 sel.flags |= ct.SEL_DEFINED 

845 is_html = True 

846 elif pseudo == ':scope': 

847 sel.flags |= ct.SEL_SCOPE 

848 elif pseudo == ':empty': 

849 sel.flags |= ct.SEL_EMPTY 

850 elif pseudo in self.PSEUDO_SELECTORS: 

851 pseudo_selector = self.PSEUDO_SELECTORS[pseudo] 

852 self.increment_count(pseudo_selector.count) 

853 sel.selectors.append(pseudo_selector) 

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

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

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

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

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

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

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

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

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

863 sel.nth.extend( 

864 [ 

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

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

867 ] 

868 ) 

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

870 sel.nth.extend( 

871 [ 

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

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

874 ] 

875 ) 

876 has_selector = True 

877 elif complex_pseudo and pseudo in PSEUDO_COMPLEX_NO_MATCH: 

878 self.parse_selectors(iselector, m.end(0), FLG_PSEUDO | FLG_OPEN) 

879 sel.no_match = True 

880 has_selector = True 

881 elif not complex_pseudo and pseudo in PSEUDO_SIMPLE_NO_MATCH: 

882 sel.no_match = True 

883 has_selector = True 

884 elif pseudo in PSEUDO_SUPPORTED: 

885 raise SelectorSyntaxError( 

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

887 self.pattern, 

888 m.start(0) 

889 ) 

890 else: 

891 raise SelectorSyntaxError( 

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

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

894 self.pattern, 

895 m.start(0) 

896 ) 

897 

898 return has_selector, is_html 

899 

900 def parse_pseudo_nth( 

901 self, 

902 sel: _Selector, 

903 m: Match[str], 

904 has_selector: bool, 

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

906 ) -> bool: 

907 """Parse `nth` pseudo.""" 

908 

909 mdict = m.groupdict() 

910 if mdict.get('pseudo_nth_child'): 

911 postfix = '_child' 

912 else: 

913 postfix = '_type' 

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

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

916 if content == 'even': 

917 # 2n 

918 s1 = 2 

919 s2 = 0 

920 var = True 

921 elif content == 'odd': 

922 # 2n+1 

923 s1 = 2 

924 s2 = 1 

925 var = True 

926 else: 

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

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

929 _s2 = '' 

930 a = nth_parts.group('a') 

931 var = a.endswith('n') 

932 if a.startswith('n'): 

933 _s1 += '1' 

934 elif var: 

935 _s1 += a[:-1] 

936 else: 

937 _s2 = _s1 + a 

938 _s1 = '' 

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

940 _s2 = '-' 

941 if nth_parts.group('b'): 

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

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

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

945 

946 pseudo_sel = mdict['name'] 

947 if postfix == '_child': 

948 if m.group('of'): 

949 # Parse the rest of `of S`. 

950 nth_sel = self.parse_selectors(iselector, m.end(0), FLG_PSEUDO | FLG_OPEN) 

951 else: 

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

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

954 self.increment_count(nth_sel.count) 

955 if pseudo_sel == ':nth-child': 

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

957 elif pseudo_sel == ':nth-last-child': 

958 sel.nth.append(ct.SelectorNth(s1, var, s2, False, True, nth_sel)) 

959 else: 

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

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

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

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

964 has_selector = True 

965 return has_selector 

966 

967 def parse_pseudo_open( 

968 self, 

969 sel: _Selector, 

970 name: str, 

971 has_selector: bool, 

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

973 index: int 

974 ) -> bool: 

975 """Parse pseudo with opening bracket.""" 

976 

977 flags = FLG_PSEUDO | FLG_OPEN 

978 if name == ':not': 

979 flags |= FLG_NOT 

980 elif name == ':has': 

981 flags |= FLG_RELATIVE 

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

983 flags |= FLG_FORGIVE 

984 

985 sel.selectors.append(self.parse_selectors(iselector, index, flags)) 

986 has_selector = True 

987 

988 return has_selector 

989 

990 def parse_has_combinator( 

991 self, 

992 sel: _Selector, 

993 m: Match[str], 

994 has_selector: bool, 

995 selectors: list[_Selector], 

996 rel_type: str, 

997 index: int 

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

999 """Parse combinator tokens.""" 

1000 

1001 combinator = m.group('relation') 

1002 if not combinator: 

1003 combinator = WS_COMBINATOR 

1004 if combinator == COMMA_COMBINATOR: 

1005 if not has_selector: 

1006 raise SelectorSyntaxError( 

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

1008 self.pattern, 

1009 index 

1010 ) 

1011 sel.rel_type = rel_type 

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

1013 rel_type = ":" + WS_COMBINATOR 

1014 selectors.append(_Selector()) 

1015 else: 

1016 if has_selector: 

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

1018 sel.rel_type = rel_type 

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

1020 elif rel_type[1:] != WS_COMBINATOR: 

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

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

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

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

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

1026 raise SelectorSyntaxError( 

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

1028 self.pattern, 

1029 index 

1030 ) 

1031 

1032 # Set the leading combinator for the next selector. 

1033 rel_type = ':' + combinator 

1034 

1035 sel = _Selector() 

1036 has_selector = False 

1037 return has_selector, sel, rel_type 

1038 

1039 def parse_combinator( 

1040 self, 

1041 sel: _Selector, 

1042 m: Match[str], 

1043 has_selector: bool, 

1044 selectors: list[_Selector], 

1045 relations: list[_Selector], 

1046 is_pseudo: bool, 

1047 is_forgive: bool, 

1048 index: int 

1049 ) -> tuple[bool, _Selector]: 

1050 """Parse combinator tokens.""" 

1051 

1052 combinator = m.group('relation') 

1053 if not combinator: 

1054 combinator = WS_COMBINATOR 

1055 if not has_selector: 

1056 if not is_forgive or combinator != COMMA_COMBINATOR: 

1057 raise SelectorSyntaxError( 

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

1059 self.pattern, 

1060 index 

1061 ) 

1062 

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

1064 if combinator == COMMA_COMBINATOR: 

1065 self.increment_count() 

1066 del relations[:] 

1067 else: 

1068 if combinator == COMMA_COMBINATOR: 

1069 if not sel.tag and not is_pseudo: 

1070 # Implied `*` 

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

1072 sel.relations.extend(relations) 

1073 selectors.append(sel) 

1074 del relations[:] 

1075 else: 

1076 sel.relations.extend(relations) 

1077 sel.rel_type = combinator 

1078 del relations[:] 

1079 relations.append(sel) 

1080 

1081 sel = _Selector() 

1082 has_selector = False 

1083 

1084 return has_selector, sel 

1085 

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

1087 """Parse HTML classes and ids.""" 

1088 

1089 selector = m.group(0) 

1090 if selector.startswith('.'): 

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

1092 else: 

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

1094 has_selector = True 

1095 return has_selector 

1096 

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

1098 """Parse contains.""" 

1099 

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

1101 if pseudo == ":contains": 

1102 warnings.warn( # noqa: B028 

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

1104 FutureWarning 

1105 ) 

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

1107 patterns = [ 

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

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

1110 if not token.group('split') 

1111 ] 

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

1113 has_selector = True 

1114 return has_selector 

1115 

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

1117 """Parse pseudo language.""" 

1118 

1119 patterns = [ 

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

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

1122 if not token.group('split') 

1123 ] 

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

1125 has_selector = True 

1126 

1127 return has_selector 

1128 

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

1130 """Parse pseudo direction.""" 

1131 

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

1133 sel.flags |= value 

1134 has_selector = True 

1135 return has_selector 

1136 

1137 def parse_selectors( 

1138 self, 

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

1140 index: int = 0, 

1141 flags: int = 0 

1142 ) -> ct.SelectorList: 

1143 """Parse selectors.""" 

1144 

1145 # Initialize important variables 

1146 sel = _Selector() 

1147 selectors = [] 

1148 has_selector = False 

1149 closed = False 

1150 relations = [] # type: list[_Selector] 

1151 rel_type = ":" + WS_COMBINATOR 

1152 count = self.count 

1153 

1154 # Setup various flags 

1155 is_open = bool(flags & FLG_OPEN) 

1156 is_pseudo = bool(flags & FLG_PSEUDO) 

1157 is_relative = bool(flags & FLG_RELATIVE) 

1158 is_not = bool(flags & FLG_NOT) 

1159 is_html = bool(flags & FLG_HTML) 

1160 is_default = bool(flags & FLG_DEFAULT) 

1161 is_indeterminate = bool(flags & FLG_INDETERMINATE) 

1162 is_in_range = bool(flags & FLG_IN_RANGE) 

1163 is_out_of_range = bool(flags & FLG_OUT_OF_RANGE) 

1164 is_placeholder_shown = bool(flags & FLG_PLACEHOLDER_SHOWN) 

1165 is_forgive = bool(flags & FLG_FORGIVE) 

1166 

1167 # Print out useful debug stuff 

1168 if self.debug: # pragma: no cover 

1169 if is_pseudo: 

1170 print(' is_pseudo: True') 

1171 if is_open: 

1172 print(' is_open: True') 

1173 if is_relative: 

1174 print(' is_relative: True') 

1175 if is_not: 

1176 print(' is_not: True') 

1177 if is_html: 

1178 print(' is_html: True') 

1179 if is_default: 

1180 print(' is_default: True') 

1181 if is_indeterminate: 

1182 print(' is_indeterminate: True') 

1183 if is_in_range: 

1184 print(' is_in_range: True') 

1185 if is_out_of_range: 

1186 print(' is_out_of_range: True') 

1187 if is_placeholder_shown: 

1188 print(' is_placeholder_shown: True') 

1189 if is_forgive: 

1190 print(' is_forgive: True') 

1191 

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

1193 if is_relative: 

1194 selectors.append(_Selector()) 

1195 

1196 try: 

1197 while True: 

1198 key, m = next(iselector) 

1199 

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

1201 self.increment_count() 

1202 

1203 # Handle parts 

1204 if self.ignore: 

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

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

1207 raise SelectorSyntaxError( 

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

1209 self.pattern, 

1210 m.start(0) 

1211 ) 

1212 if key == "at_rule": 

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

1214 elif key == "amp": 

1215 sel.flags |= ct.SEL_SCOPE 

1216 has_selector = True 

1217 elif key == 'pseudo_class_custom': 

1218 has_selector = self.parse_pseudo_class_custom(sel, m, has_selector) 

1219 elif key == 'pseudo_class': 

1220 has_selector, is_html = self.parse_pseudo_class(sel, m, has_selector, iselector, is_html) 

1221 elif key == 'pseudo_element': 

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

1223 elif key == 'pseudo_contains': 

1224 has_selector = self.parse_pseudo_contains(sel, m, has_selector) 

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

1226 has_selector = self.parse_pseudo_nth(sel, m, has_selector, iselector) 

1227 elif key == 'pseudo_lang': 

1228 has_selector = self.parse_pseudo_lang(sel, m, has_selector) 

1229 elif key == 'pseudo_dir': 

1230 has_selector = self.parse_pseudo_dir(sel, m, has_selector) 

1231 # Currently only supports HTML 

1232 is_html = True 

1233 elif key == 'pseudo_close': 

1234 if not has_selector: 

1235 if not is_forgive: 

1236 raise SelectorSyntaxError( 

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

1238 self.pattern, 

1239 m.start(0) 

1240 ) 

1241 sel.no_match = True 

1242 if is_open: 

1243 closed = True 

1244 break 

1245 else: 

1246 raise SelectorSyntaxError( 

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

1248 self.pattern, 

1249 m.start(0) 

1250 ) 

1251 elif key == 'combine': 

1252 if is_relative: 

1253 has_selector, sel, rel_type = self.parse_has_combinator( 

1254 sel, m, has_selector, selectors, rel_type, index 

1255 ) 

1256 else: 

1257 has_selector, sel = self.parse_combinator( 

1258 sel, m, has_selector, selectors, relations, is_pseudo, is_forgive, index 

1259 ) 

1260 elif key == 'attribute': 

1261 has_selector = self.parse_attribute_selector(sel, m, has_selector) 

1262 elif key == 'tag': 

1263 if has_selector: 

1264 raise SelectorSyntaxError( 

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

1266 self.pattern, 

1267 m.start(0) 

1268 ) 

1269 has_selector = self.parse_tag_pattern(sel, m, has_selector) 

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

1271 has_selector = self.parse_class_id(sel, m, has_selector) 

1272 

1273 index = m.end(0) 

1274 except StopIteration: 

1275 pass 

1276 

1277 # Handle selectors that are not closed 

1278 if is_open and not closed: 

1279 raise SelectorSyntaxError( 

1280 f"Unclosed pseudo-class at position {index}", 

1281 self.pattern, 

1282 index 

1283 ) 

1284 

1285 # Cleanup completed selector piece 

1286 if has_selector: 

1287 if not sel.tag and not is_pseudo: 

1288 # Implied `*` 

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

1290 if is_relative: 

1291 sel.rel_type = rel_type 

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

1293 else: 

1294 sel.relations.extend(relations) 

1295 del relations[:] 

1296 selectors.append(sel) 

1297 

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

1299 elif is_forgive and (not selectors or not relations): 

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

1301 self.increment_count() 

1302 sel.no_match = True 

1303 del relations[:] 

1304 selectors.append(sel) 

1305 has_selector = True 

1306 

1307 if not has_selector: 

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

1309 # May apply to others as well. 

1310 raise SelectorSyntaxError( 

1311 f'Expected a selector at position {index}', 

1312 self.pattern, 

1313 index 

1314 ) 

1315 

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

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

1318 # to the matcher what additional logic is required. 

1319 if is_default: 

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

1321 if is_indeterminate: 

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

1323 if is_in_range: 

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

1325 if is_out_of_range: 

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

1327 if is_placeholder_shown: 

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

1329 

1330 # Return selector list 

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

1332 

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

1334 """Iterate selector tokens.""" 

1335 

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

1337 m = RE_WS_BEGIN.search(pattern) 

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

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

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

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

1342 

1343 if self.debug: # pragma: no cover 

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

1345 while index <= end: 

1346 m = None 

1347 for v in self.CSS_TOKENS: 

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

1349 if m: 

1350 name = v.get_name() 

1351 if self.debug: # pragma: no cover 

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

1353 index = m.end(0) 

1354 yield name, m 

1355 break 

1356 if m is None: 

1357 c = pattern[index] 

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

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

1360 # otherwise, report the invalid character. 

1361 if c == '[': 

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

1363 elif c == '.': 

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

1365 elif c == '#': 

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

1367 elif c == ':': 

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

1369 else: 

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

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

1372 if self.debug: # pragma: no cover 

1373 print('## END PARSING') 

1374 

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

1376 """Process selectors.""" 

1377 

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