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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

660 statements  

1""" 

2cssselect.parser 

3================ 

4 

5Tokenizer, parser and parsed objects for CSS selectors. 

6 

7 

8:copyright: (c) 2007-2012 Ian Bicking and contributors. 

9See AUTHORS for more details. 

10:license: BSD, see LICENSE for more details. 

11 

12""" 

13 

14from __future__ import annotations 

15 

16import operator 

17import re 

18import sys 

19from typing import TYPE_CHECKING, Literal, Protocol, TypeAlias, Union, cast, overload 

20 

21if TYPE_CHECKING: 

22 from collections.abc import Iterable, Iterator, Sequence 

23 

24 # typing.Self requires Python 3.11 

25 from typing_extensions import Self 

26 

27 

28def ascii_lower(string: str) -> str: 

29 """Lower-case, but only in the ASCII range.""" 

30 return string.encode("utf8").lower().decode("utf8") 

31 

32 

33class SelectorError(Exception): 

34 """Common parent for :class:`SelectorSyntaxError` and 

35 :class:`ExpressionError`. 

36 

37 You can just use ``except SelectorError:`` when calling 

38 :meth:`~GenericTranslator.css_to_xpath` and handle both exceptions types. 

39 

40 """ 

41 

42 

43class SelectorSyntaxError(SelectorError, SyntaxError): 

44 """Parsing a selector that does not match the grammar.""" 

45 

46 

47#### Parsed objects 

48 

49Tree: TypeAlias = Union[ 

50 "Element", 

51 "Hash", 

52 "Class", 

53 "Function", 

54 "Pseudo", 

55 "Attrib", 

56 "Negation", 

57 "Relation", 

58 "Matching", 

59 "SpecificityAdjustment", 

60 "CombinedSelector", 

61] 

62PseudoElement: TypeAlias = Union["FunctionalPseudoElement", str] 

63 

64 

65class Selector: 

66 """ 

67 Represents a parsed selector. 

68 

69 :meth:`~GenericTranslator.selector_to_xpath` accepts this object, 

70 but ignores :attr:`pseudo_element`. It is the user’s responsibility 

71 to account for pseudo-elements and reject selectors with unknown 

72 or unsupported pseudo-elements. 

73 

74 """ 

75 

76 def __init__(self, tree: Tree, pseudo_element: PseudoElement | None = None) -> None: 

77 self.parsed_tree = tree 

78 if pseudo_element is not None and not isinstance( 

79 pseudo_element, FunctionalPseudoElement 

80 ): 

81 pseudo_element = ascii_lower(pseudo_element) 

82 #: A :class:`FunctionalPseudoElement`, 

83 #: or the identifier for the pseudo-element as a string, 

84 # or ``None``. 

85 #: 

86 #: +-------------------------+----------------+--------------------------------+ 

87 #: | | Selector | Pseudo-element | 

88 #: +=========================+================+================================+ 

89 #: | CSS3 syntax | ``a::before`` | ``'before'`` | 

90 #: +-------------------------+----------------+--------------------------------+ 

91 #: | Older syntax | ``a:before`` | ``'before'`` | 

92 #: +-------------------------+----------------+--------------------------------+ 

93 #: | From the Lists3_ draft, | ``li::marker`` | ``'marker'`` | 

94 #: | not in Selectors3 | | | 

95 #: +-------------------------+----------------+--------------------------------+ 

96 #: | Invalid pseudo-class | ``li:marker`` | ``None`` | 

97 #: +-------------------------+----------------+--------------------------------+ 

98 #: | Functional | ``a::foo(2)`` | ``FunctionalPseudoElement(…)`` | 

99 #: +-------------------------+----------------+--------------------------------+ 

100 #: 

101 #: .. _Lists3: http://www.w3.org/TR/2011/WD-css3-lists-20110524/#marker-pseudoelement 

102 self.pseudo_element = pseudo_element 

103 

104 def __repr__(self) -> str: 

105 if isinstance(self.pseudo_element, FunctionalPseudoElement): 

106 pseudo_element = repr(self.pseudo_element) 

107 elif self.pseudo_element: 

108 pseudo_element = f"::{self.pseudo_element}" 

109 else: 

110 pseudo_element = "" 

111 return f"{self.__class__.__name__}[{self.parsed_tree!r}{pseudo_element}]" 

112 

113 def canonical(self) -> str: 

114 """Return a CSS representation for this selector (a string)""" 

115 if isinstance(self.pseudo_element, FunctionalPseudoElement): 

116 pseudo_element = f"::{self.pseudo_element.canonical()}" 

117 elif self.pseudo_element: 

118 pseudo_element = f"::{_serialize_ident(self.pseudo_element)}" 

119 else: 

120 pseudo_element = "" 

121 res = f"{self.parsed_tree.canonical()}{pseudo_element}" 

122 # Strip a redundant universal selector from e.g. "*.foo" (but not 

123 # from e.g. "* > foo"). 

124 if len(res) > 1 and res[0] == "*" and res[1] in "#.[:": 

125 res = res[1:] 

126 return res 

127 

128 def specificity(self) -> tuple[int, int, int]: 

129 """Return the specificity_ of this selector as a tuple of 3 integers. 

130 

131 .. _specificity: http://www.w3.org/TR/selectors/#specificity 

132 

133 """ 

134 a, b, c = self.parsed_tree.specificity() 

135 if self.pseudo_element: 

136 c += 1 

137 return a, b, c 

138 

139 

140class Class: 

141 """ 

142 Represents selector.class_name 

143 """ 

144 

145 def __init__(self, selector: Tree, class_name: str) -> None: 

146 self.selector = selector 

147 self.class_name = class_name 

148 

149 def __repr__(self) -> str: 

150 return f"{self.__class__.__name__}[{self.selector!r}.{self.class_name}]" 

151 

152 def canonical(self) -> str: 

153 return f"{self.selector.canonical()}.{_serialize_ident(self.class_name)}" 

154 

155 def specificity(self) -> tuple[int, int, int]: 

156 a, b, c = self.selector.specificity() 

157 b += 1 

158 return a, b, c 

159 

160 

161class FunctionalPseudoElement: 

162 """ 

163 Represents selector::name(arguments) 

164 

165 .. attribute:: name 

166 

167 The name (identifier) of the pseudo-element, as a string. 

168 

169 .. attribute:: arguments 

170 

171 The arguments of the pseudo-element, as a list of tokens. 

172 

173 **Note:** tokens are not part of the public API, 

174 and may change between cssselect versions. 

175 Use at your own risks. 

176 

177 """ 

178 

179 def __init__(self, name: str, arguments: Sequence[Token]): 

180 self.name = ascii_lower(name) 

181 self.arguments = arguments 

182 

183 def __repr__(self) -> str: 

184 token_values = [token.value for token in self.arguments] 

185 return f"{self.__class__.__name__}[::{self.name}({token_values!r})]" 

186 

187 def argument_types(self) -> list[str]: 

188 return [token.type for token in self.arguments] 

189 

190 def canonical(self) -> str: 

191 args = "".join(token.css() for token in self.arguments) 

192 return f"{_serialize_ident(self.name)}({args})" 

193 

194 

195class Function: 

196 """ 

197 Represents selector:name(expr) 

198 """ 

199 

200 def __init__(self, selector: Tree, name: str, arguments: Sequence[Token]) -> None: 

201 self.selector = selector 

202 self.name = ascii_lower(name) 

203 self.arguments = arguments 

204 

205 def __repr__(self) -> str: 

206 token_values = [token.value for token in self.arguments] 

207 return f"{self.__class__.__name__}[{self.selector!r}:{self.name}({token_values!r})]" 

208 

209 def argument_types(self) -> list[str]: 

210 return [token.type for token in self.arguments] 

211 

212 def canonical(self) -> str: 

213 args = "".join(token.css() for token in self.arguments) 

214 return f"{self.selector.canonical()}:{_serialize_ident(self.name)}({args})" 

215 

216 def specificity(self) -> tuple[int, int, int]: 

217 a, b, c = self.selector.specificity() 

218 b += 1 

219 return a, b, c 

220 

221 

222class Pseudo: 

223 """ 

224 Represents selector:ident 

225 """ 

226 

227 def __init__(self, selector: Tree, ident: str) -> None: 

228 self.selector = selector 

229 self.ident = ascii_lower(ident) 

230 

231 def __repr__(self) -> str: 

232 return f"{self.__class__.__name__}[{self.selector!r}:{self.ident}]" 

233 

234 def canonical(self) -> str: 

235 return f"{self.selector.canonical()}:{_serialize_ident(self.ident)}" 

236 

237 def specificity(self) -> tuple[int, int, int]: 

238 a, b, c = self.selector.specificity() 

239 b += 1 

240 return a, b, c 

241 

242 

243class Negation: 

244 """ 

245 Represents selector:not(subselector) 

246 """ 

247 

248 def __init__(self, selector: Tree, subselector: Tree) -> None: 

249 self.selector = selector 

250 self.subselector = subselector 

251 

252 def __repr__(self) -> str: 

253 return f"{self.__class__.__name__}[{self.selector!r}:not({self.subselector!r})]" 

254 

255 def canonical(self) -> str: 

256 subsel = self.subselector.canonical() 

257 # Strip a redundant universal selector from e.g. "*.foo" (but not 

258 # from e.g. "* > foo"). 

259 if len(subsel) > 1 and subsel[0] == "*" and subsel[1] in "#.[:": 

260 subsel = subsel[1:] 

261 return f"{self.selector.canonical()}:not({subsel})" 

262 

263 def specificity(self) -> tuple[int, int, int]: 

264 a1, b1, c1 = self.selector.specificity() 

265 a2, b2, c2 = self.subselector.specificity() 

266 return a1 + a2, b1 + b2, c1 + c2 

267 

268 

269class Relation: 

270 """ 

271 Represents selector:has(subselector) 

272 """ 

273 

274 def __init__(self, selector: Tree, combinator: Token, subselector: Selector): 

275 self.selector = selector 

276 self.combinator = combinator 

277 self.subselector = subselector 

278 

279 def _combinator_prefix(self) -> str: 

280 # The descendant combinator is implicit in :has() arguments. 

281 if self.combinator.value == " ": 

282 return "" 

283 return f"{self.combinator.value} " 

284 

285 def __repr__(self) -> str: 

286 return ( 

287 f"{self.__class__.__name__}[{self.selector!r}" 

288 f":has({self._combinator_prefix()}{self.subselector!r})]" 

289 ) 

290 

291 def canonical(self) -> str: 

292 subsel = self.subselector.canonical() 

293 if len(subsel) > 1: 

294 subsel = subsel.lstrip("*") 

295 return f"{self.selector.canonical()}:has({self._combinator_prefix()}{subsel})" 

296 

297 def specificity(self) -> tuple[int, int, int]: 

298 a1, b1, c1 = self.selector.specificity() 

299 a2, b2, c2 = self.subselector.specificity() 

300 return a1 + a2, b1 + b2, c1 + c2 

301 

302 

303class Matching: 

304 """ 

305 Represents selector:is(selector_list) 

306 """ 

307 

308 def __init__(self, selector: Tree, selector_list: Iterable[Tree]): 

309 self.selector = selector 

310 self.selector_list = selector_list 

311 

312 def __repr__(self) -> str: 

313 args_str = ", ".join(repr(s) for s in self.selector_list) 

314 return f"{self.__class__.__name__}[{self.selector!r}:is({args_str})]" 

315 

316 def canonical(self) -> str: 

317 selector_arguments = [] 

318 for s in self.selector_list: 

319 selarg = s.canonical() 

320 if len(selarg) > 1: 

321 selarg = selarg.lstrip("*") 

322 selector_arguments.append(selarg) 

323 args_str = ", ".join(selector_arguments) 

324 return f"{self.selector.canonical()}:is({args_str})" 

325 

326 def specificity(self) -> tuple[int, int, int]: 

327 a1, b1, c1 = self.selector.specificity() 

328 a2, b2, c2 = max(x.specificity() for x in self.selector_list) 

329 return a1 + a2, b1 + b2, c1 + c2 

330 

331 

332class SpecificityAdjustment: 

333 """ 

334 Represents selector:where(selector_list) 

335 Same as selector:is(selector_list), but its specificity is always 0 

336 """ 

337 

338 def __init__(self, selector: Tree, selector_list: list[Tree]): 

339 self.selector = selector 

340 self.selector_list = selector_list 

341 

342 def __repr__(self) -> str: 

343 args_str = ", ".join(repr(s) for s in self.selector_list) 

344 return f"{self.__class__.__name__}[{self.selector!r}:where({args_str})]" 

345 

346 def canonical(self) -> str: 

347 selector_arguments = [] 

348 for s in self.selector_list: 

349 selarg = s.canonical() 

350 if len(selarg) > 1: 

351 selarg = selarg.lstrip("*") 

352 selector_arguments.append(selarg) 

353 args_str = ", ".join(selector_arguments) 

354 return f"{self.selector.canonical()}:where({args_str})" 

355 

356 def specificity(self) -> tuple[int, int, int]: 

357 # :where() itself contributes no specificity, but the compound 

358 # selector it applies to does. 

359 return self.selector.specificity() 

360 

361 

362class Attrib: 

363 """ 

364 Represents selector[namespace|attrib operator value] 

365 """ 

366 

367 @overload 

368 def __init__( 

369 self, 

370 selector: Tree, 

371 namespace: str | None, 

372 attrib: str, 

373 operator: Literal["exists"], 

374 value: None, 

375 ) -> None: ... 

376 

377 @overload 

378 def __init__( 

379 self, 

380 selector: Tree, 

381 namespace: str | None, 

382 attrib: str, 

383 operator: str, 

384 value: Token, 

385 ) -> None: ... 

386 

387 def __init__( 

388 self, 

389 selector: Tree, 

390 namespace: str | None, 

391 attrib: str, 

392 operator: str, 

393 value: Token | None, 

394 ) -> None: 

395 self.selector = selector 

396 self.namespace = namespace 

397 self.attrib = attrib 

398 self.operator = operator 

399 self.value = value 

400 

401 def __repr__(self) -> str: 

402 attrib = f"{self.namespace}|{self.attrib}" if self.namespace else self.attrib 

403 if self.operator == "exists": 

404 return f"{self.__class__.__name__}[{self.selector!r}[{attrib}]]" 

405 assert self.value is not None 

406 return f"{self.__class__.__name__}[{self.selector!r}[{attrib} {self.operator} {self.value.value!r}]]" 

407 

408 def canonical(self) -> str: 

409 attrib = _serialize_ident(self.attrib) 

410 if self.namespace: 

411 attrib = f"{_serialize_ident(self.namespace)}|{attrib}" 

412 

413 if self.operator == "exists": 

414 op = attrib 

415 else: 

416 assert self.value is not None 

417 op = f"{attrib}{self.operator}{self.value.css()}" 

418 

419 return f"{self.selector.canonical()}[{op}]" 

420 

421 def specificity(self) -> tuple[int, int, int]: 

422 a, b, c = self.selector.specificity() 

423 b += 1 

424 return a, b, c 

425 

426 

427class Element: 

428 """ 

429 Represents namespace|element 

430 

431 `None` is for the universal selector '*' 

432 

433 """ 

434 

435 def __init__( 

436 self, namespace: str | None = None, element: str | None = None 

437 ) -> None: 

438 self.namespace = namespace 

439 self.element = element 

440 

441 def __repr__(self) -> str: 

442 return f"{self.__class__.__name__}[{self.canonical()}]" 

443 

444 def canonical(self) -> str: 

445 element = _serialize_ident(self.element) if self.element else "*" 

446 if self.namespace: 

447 element = f"{_serialize_ident(self.namespace)}|{element}" 

448 return element 

449 

450 def specificity(self) -> tuple[int, int, int]: 

451 if self.element: 

452 return 0, 0, 1 

453 return 0, 0, 0 

454 

455 

456class Hash: 

457 """ 

458 Represents selector#id 

459 """ 

460 

461 def __init__(self, selector: Tree, id: str) -> None: # noqa: A002 

462 self.selector = selector 

463 self.id = id 

464 

465 def __repr__(self) -> str: 

466 return f"{self.__class__.__name__}[{self.selector!r}#{self.id}]" 

467 

468 def canonical(self) -> str: 

469 return f"{self.selector.canonical()}#{_serialize_ident(self.id)}" 

470 

471 def specificity(self) -> tuple[int, int, int]: 

472 a, b, c = self.selector.specificity() 

473 a += 1 

474 return a, b, c 

475 

476 

477class CombinedSelector: 

478 def __init__(self, selector: Tree, combinator: str, subselector: Tree) -> None: 

479 assert selector is not None 

480 self.selector = selector 

481 self.combinator = combinator 

482 self.subselector = subselector 

483 

484 def __repr__(self) -> str: 

485 comb = "<followed>" if self.combinator == " " else self.combinator 

486 return ( 

487 f"{self.__class__.__name__}[{self.selector!r} {comb} {self.subselector!r}]" 

488 ) 

489 

490 def canonical(self) -> str: 

491 subsel = self.subselector.canonical() 

492 if len(subsel) > 1: 

493 subsel = subsel.lstrip("*") 

494 combinator = " " if self.combinator == " " else f" {self.combinator} " 

495 return f"{self.selector.canonical()}{combinator}{subsel}" 

496 

497 def specificity(self) -> tuple[int, int, int]: 

498 a1, b1, c1 = self.selector.specificity() 

499 a2, b2, c2 = self.subselector.specificity() 

500 return a1 + a2, b1 + b2, c1 + c2 

501 

502 

503#### Parser 

504 

505# foo 

506_el_re = re.compile(r"^[ \t\r\n\f]*([a-zA-Z]+)[ \t\r\n\f]*$") 

507 

508# foo#bar or #bar 

509_id_re = re.compile(r"^[ \t\r\n\f]*([a-zA-Z]*)#([a-zA-Z0-9_-]+)[ \t\r\n\f]*$") 

510 

511# foo.bar or .bar 

512_class_re = re.compile( 

513 r"^[ \t\r\n\f]*([a-zA-Z]*)\.([a-zA-Z][a-zA-Z0-9_-]*)[ \t\r\n\f]*$" 

514) 

515 

516 

517def parse(css: str) -> list[Selector]: 

518 """Parse a CSS *group of selectors*. 

519 

520 If you don't care about pseudo-elements or selector specificity, 

521 you can skip this and use :meth:`~GenericTranslator.css_to_xpath`. 

522 

523 :param css: 

524 A *group of selectors* as a string. 

525 :raises: 

526 :class:`SelectorSyntaxError` on invalid selectors. 

527 :returns: 

528 A list of parsed :class:`Selector` objects, one for each 

529 selector in the comma-separated group. 

530 

531 """ 

532 # Fast path for simple cases 

533 match = _el_re.match(css) 

534 if match: 

535 return [Selector(Element(element=match.group(1)))] 

536 match = _id_re.match(css) 

537 if match is not None: 

538 return [Selector(Hash(Element(element=match.group(1) or None), match.group(2)))] 

539 match = _class_re.match(css) 

540 if match is not None: 

541 return [ 

542 Selector(Class(Element(element=match.group(1) or None), match.group(2))) 

543 ] 

544 

545 stream = TokenStream(tokenize(css)) 

546 stream.source = css 

547 return list(parse_selector_group(stream)) 

548 

549 

550# except SelectorSyntaxError: 

551# e = sys.exc_info()[1] 

552# message = "%s at %s -> %r" % ( 

553# e, stream.used, stream.peek()) 

554# e.msg = message 

555# e.args = tuple([message]) 

556# raise 

557 

558 

559def parse_selector_group(stream: TokenStream) -> Iterator[Selector]: 

560 stream.skip_whitespace() 

561 while 1: 

562 yield Selector(*parse_selector(stream)) 

563 if stream.peek() == ("DELIM", ","): 

564 stream.next() 

565 stream.skip_whitespace() 

566 else: 

567 break 

568 

569 

570def parse_selector(stream: TokenStream) -> tuple[Tree, PseudoElement | None]: 

571 result, pseudo_element = parse_simple_selector(stream) 

572 while 1: 

573 stream.skip_whitespace() 

574 peek = stream.peek() 

575 if peek in (("EOF", None), ("DELIM", ",")): 

576 break 

577 if pseudo_element: 

578 raise SelectorSyntaxError( 

579 f"Got pseudo-element ::{pseudo_element} not at the end of a selector" 

580 ) 

581 if peek.is_delim("+", ">", "~"): 

582 # A combinator 

583 combinator = cast("str", stream.next().value) 

584 stream.skip_whitespace() 

585 else: 

586 # By exclusion, the last parse_simple_selector() ended 

587 # at peek == ' ' 

588 combinator = " " 

589 next_selector, pseudo_element = parse_simple_selector(stream) 

590 result = CombinedSelector(result, combinator, next_selector) 

591 return result, pseudo_element 

592 

593 

594def parse_simple_selector( 

595 stream: TokenStream, 

596 inside_negation: bool = False, 

597 inside_selector_list: bool = False, 

598) -> tuple[Tree, PseudoElement | None]: 

599 stream.skip_whitespace() 

600 selector_start = len(stream.used) 

601 peek = stream.peek() 

602 if peek.type == "IDENT" or peek == ("DELIM", "*"): 

603 if peek.type == "IDENT": 

604 namespace = stream.next().value 

605 else: 

606 stream.next() 

607 namespace = None 

608 if stream.peek() == ("DELIM", "|"): 

609 stream.next() 

610 element = stream.next_ident_or_star() 

611 else: 

612 element = namespace 

613 namespace = None 

614 else: 

615 element = namespace = None 

616 result: Tree = Element(namespace, element) 

617 pseudo_element: PseudoElement | None = None 

618 while 1: 

619 peek = stream.peek() 

620 if ( 

621 peek.type in ("S", "EOF") 

622 or peek.is_delim(",", "+", ">", "~") 

623 or (inside_negation and peek == ("DELIM", ")")) 

624 ): 

625 break 

626 if pseudo_element: 

627 raise SelectorSyntaxError( 

628 f"Got pseudo-element ::{pseudo_element} not at the end of a selector" 

629 ) 

630 if peek.type == "HASH": 

631 result = Hash(result, cast("str", stream.next().value)) 

632 elif peek == ("DELIM", "."): 

633 stream.next() 

634 result = Class(result, stream.next_ident()) 

635 elif peek == ("DELIM", "|"): 

636 # The explicit "no namespace" syntax, e.g. |div: only valid at 

637 # the very start of a simple selector. 

638 if len(stream.used) != selector_start: 

639 raise SelectorSyntaxError(f"Expected selector, got {peek}") 

640 stream.next() 

641 result = Element(None, stream.next_ident_or_star()) 

642 elif peek == ("DELIM", "["): 

643 stream.next() 

644 result = parse_attrib(result, stream) 

645 elif peek == ("DELIM", ":"): 

646 stream.next() 

647 if stream.peek() == ("DELIM", ":"): 

648 stream.next() 

649 pseudo_element = stream.next_ident() 

650 if stream.peek() == ("DELIM", "("): 

651 stream.next() 

652 pseudo_element = FunctionalPseudoElement( 

653 pseudo_element, parse_arguments(stream) 

654 ) 

655 continue 

656 ident = stream.next_ident() 

657 if ident.lower() in ("first-line", "first-letter", "before", "after"): 

658 # Special case: CSS 2.1 pseudo-elements can have a single ':' 

659 # Any new pseudo-element must have two. 

660 pseudo_element = str(ident) 

661 continue 

662 if stream.peek() != ("DELIM", "("): 

663 result = Pseudo(result, ident) 

664 if result.ident == "scope": 

665 # :scope is only supported at the start of a selector, 

666 # i.e. never in :is()/:where()/:matches() arguments 

667 # (where a preceding comma separates arguments, not 

668 # selectors), and otherwise only when the tokens 

669 # preceding its compound selector are the start of the 

670 # input or a comma. 

671 preceding = stream.used[:selector_start] 

672 while preceding and preceding[-1].type == "S": 

673 preceding = preceding[:-1] 

674 if inside_selector_list or ( 

675 preceding and not preceding[-1].is_delim(",") 

676 ): 

677 raise SelectorSyntaxError( 

678 'Got pseudo-class ":scope" not at the start of a selector' 

679 ) 

680 continue 

681 stream.next() 

682 stream.skip_whitespace() 

683 if ident.lower() == "not": 

684 if inside_selector_list: 

685 raise SelectorSyntaxError( 

686 ":not() is not supported inside :is(), :where() and :matches()" 

687 ) 

688 if inside_negation: 

689 raise SelectorSyntaxError("Got nested :not()") 

690 argument, argument_pseudo_element = parse_simple_selector( 

691 stream, inside_negation=True 

692 ) 

693 while 1: 

694 # Whitespace before the closing parenthesis is not a 

695 # descendant combinator. 

696 stream.skip_whitespace() 

697 peek = stream.peek() 

698 if argument_pseudo_element: 

699 raise SelectorSyntaxError( 

700 f"Got pseudo-element ::{argument_pseudo_element} inside :not() at {peek.pos}" 

701 ) 

702 if peek == ("DELIM", ")"): 

703 stream.next() 

704 break 

705 if peek.is_delim("+", ">", "~"): 

706 argument_combinator = cast("str", stream.next().value) 

707 stream.skip_whitespace() 

708 elif peek.type == "EOF" or peek.is_delim(","): 

709 # A selector list is not supported in :not(). 

710 raise SelectorSyntaxError(f"Expected ')', got {peek}") 

711 else: 

712 argument_combinator = " " 

713 next_selector, argument_pseudo_element = parse_simple_selector( 

714 stream, inside_negation=True 

715 ) 

716 argument = CombinedSelector( 

717 argument, argument_combinator, next_selector 

718 ) 

719 result = Negation(result, argument) 

720 elif ident.lower() == "has": 

721 combinator, arguments = parse_relative_selector(stream) 

722 result = Relation(result, combinator, arguments) 

723 

724 elif ident.lower() in ("matches", "is"): 

725 selectors = parse_simple_selector_arguments(stream) 

726 result = Matching(result, selectors) 

727 elif ident.lower() == "where": 

728 selectors = parse_simple_selector_arguments(stream) 

729 result = SpecificityAdjustment(result, selectors) 

730 else: 

731 result = Function(result, ident, parse_arguments(stream)) 

732 else: 

733 raise SelectorSyntaxError(f"Expected selector, got {peek}") 

734 if len(stream.used) == selector_start: 

735 raise SelectorSyntaxError(f"Expected selector, got {stream.peek()}") 

736 return result, pseudo_element 

737 

738 

739def parse_arguments(stream: TokenStream) -> list[Token]: # noqa: RET503 

740 arguments: list[Token] = [] 

741 while 1: 

742 stream.skip_whitespace() 

743 next_ = stream.next() 

744 if next_.type in ("IDENT", "STRING", "NUMBER") or next_ in [ 

745 ("DELIM", "+"), 

746 ("DELIM", "-"), 

747 ]: 

748 arguments.append(next_) 

749 elif next_ == ("DELIM", ")"): 

750 return arguments 

751 else: 

752 raise SelectorSyntaxError(f"Expected an argument, got {next_}") 

753 

754 

755def parse_relative_selector(stream: TokenStream) -> tuple[Token, Selector]: 

756 stream.skip_whitespace() 

757 subselector_tokens: list[Token] = [] 

758 next_ = stream.next() 

759 

760 if next_ in [("DELIM", "+"), ("DELIM", ">"), ("DELIM", "~")]: 

761 combinator = next_ 

762 stream.skip_whitespace() 

763 next_ = stream.next() 

764 else: 

765 combinator = Token("DELIM", " ", pos=0) 

766 

767 seen_whitespace = False 

768 while 1: 

769 if next_.type == "S": 

770 # Whitespace is valid before the closing parenthesis; anywhere 

771 # else it would be a descendant combinator, which is not 

772 # supported in :has() arguments. 

773 seen_whitespace = True 

774 elif next_.type == "IDENT" or next_ in [("DELIM", "."), ("DELIM", "*")]: 

775 if seen_whitespace: 

776 raise SelectorSyntaxError(f"Expected an argument, got {next_}") 

777 subselector_tokens.append(next_) 

778 elif next_ == ("DELIM", ")"): 

779 break 

780 else: 

781 raise SelectorSyntaxError(f"Expected an argument, got {next_}") 

782 next_ = stream.next() 

783 

784 # Reparse the collected tokens instead of their concatenated source 

785 # text, so that escaped identifiers are preserved. 

786 subselector_tokens.append(EOFToken(next_.pos)) 

787 result, _ = parse_simple_selector(TokenStream(subselector_tokens)) 

788 return combinator, Selector(result) 

789 

790 

791def parse_simple_selector_arguments(stream: TokenStream) -> list[Tree]: 

792 arguments = [] 

793 while 1: 

794 result, pseudo_element = parse_simple_selector( 

795 stream, inside_negation=True, inside_selector_list=True 

796 ) 

797 if pseudo_element: 

798 raise SelectorSyntaxError( 

799 f"Got pseudo-element ::{pseudo_element} inside function" 

800 ) 

801 stream.skip_whitespace() 

802 next_ = stream.next() 

803 if next_ == ("DELIM", ","): 

804 stream.skip_whitespace() 

805 arguments.append(result) 

806 elif next_ == ("DELIM", ")"): 

807 arguments.append(result) 

808 break 

809 else: 

810 raise SelectorSyntaxError(f"Expected an argument, got {next_}") 

811 return arguments 

812 

813 

814def parse_attrib(selector: Tree, stream: TokenStream) -> Attrib: 

815 stream.skip_whitespace() 

816 attrib = stream.next_ident_or_star() 

817 if attrib is None and stream.peek() != ("DELIM", "|"): 

818 raise SelectorSyntaxError(f"Expected '|', got {stream.peek()}") 

819 namespace: str | None 

820 op: str | None 

821 if stream.peek() == ("DELIM", "|"): 

822 stream.next() 

823 if stream.peek() == ("DELIM", "="): 

824 namespace = None 

825 stream.next() 

826 op = "|=" 

827 else: 

828 namespace = attrib 

829 attrib = stream.next_ident() 

830 op = None 

831 else: 

832 namespace = op = None 

833 if op is None: 

834 stream.skip_whitespace() 

835 next_ = stream.next() 

836 if next_ == ("DELIM", "]"): 

837 return Attrib(selector, namespace, cast("str", attrib), "exists", None) 

838 if next_ == ("DELIM", "="): 

839 op = "=" 

840 elif next_.is_delim("^", "$", "*", "~", "|", "!") and ( 

841 stream.peek() == ("DELIM", "=") 

842 ): 

843 op = cast("str", next_.value) + "=" 

844 stream.next() 

845 else: 

846 raise SelectorSyntaxError(f"Operator expected, got {next_}") 

847 stream.skip_whitespace() 

848 value = stream.next() 

849 if value.type not in ("IDENT", "STRING"): 

850 raise SelectorSyntaxError(f"Expected string or ident, got {value}") 

851 stream.skip_whitespace() 

852 next_ = stream.next() 

853 if next_ != ("DELIM", "]"): 

854 raise SelectorSyntaxError(f"Expected ']', got {next_}") 

855 return Attrib(selector, namespace, cast("str", attrib), op, value) 

856 

857 

858def parse_series(tokens: Iterable[Token]) -> tuple[int, int]: 

859 """Parses the arguments for :nth-child() and friends.""" 

860 for token in tokens: 

861 if token.type == "STRING": 

862 raise ValueError("String tokens not allowed in series.") 

863 # The An+B microsyntax is ASCII-case-insensitive: 2N+1, EVEN, Odd... 

864 s = ascii_lower("".join(cast("str", token.value) for token in tokens).strip()) 

865 if s == "odd": 

866 return 2, 1 

867 if s == "even": 

868 return 2, 0 

869 if s == "n": 

870 return 1, 0 

871 if "n" not in s: 

872 # Just b 

873 return 0, int(s) 

874 a, b = s.split("n", 1) 

875 a_as_int: int 

876 if not a: 

877 a_as_int = 1 

878 elif a in {"-", "+"}: 

879 a_as_int = int(a + "1") 

880 else: 

881 a_as_int = int(a) 

882 b_as_int = int(b) if b else 0 

883 return a_as_int, b_as_int 

884 

885 

886#### Token objects 

887 

888 

889class Token(tuple[str, str | None]): # noqa: SLOT001 

890 @overload 

891 def __new__( 

892 cls, 

893 type_: Literal["IDENT", "HASH", "STRING", "S", "DELIM", "NUMBER"], 

894 value: str, 

895 pos: int, 

896 ) -> Self: ... 

897 

898 @overload 

899 def __new__(cls, type_: Literal["EOF"], value: None, pos: int) -> Self: ... 

900 

901 def __new__(cls, type_: str, value: str | None, pos: int) -> Self: 

902 obj = tuple.__new__(cls, (type_, value)) 

903 obj.pos = pos 

904 return obj 

905 

906 def __repr__(self) -> str: 

907 return f"<{self.type} '{self.value}' at {self.pos}>" 

908 

909 def is_delim(self, *values: str) -> bool: 

910 return self.type == "DELIM" and self.value in values 

911 

912 pos: int 

913 

914 @property 

915 def type(self) -> str: 

916 return self[0] 

917 

918 @property 

919 def value(self) -> str | None: 

920 return self[1] 

921 

922 def css(self) -> str: 

923 if self.type == "STRING": 

924 # Escape as CSS (repr() would use Python escapes, which mean 

925 # something else in CSS, e.g. '\n' is just the letter 'n'). 

926 escaped = cast("str", self.value).replace("\\", "\\\\").replace("'", "\\'") 

927 escaped = _sub_string_control_char(_replace_string_control_char, escaped) 

928 return f"'{escaped}'" 

929 if self.type == "IDENT": 

930 return _serialize_ident(cast("str", self.value)) 

931 return cast("str", self.value) 

932 

933 

934class EOFToken(Token): 

935 def __new__(cls, pos: int) -> Self: 

936 return Token.__new__(cls, "EOF", None, pos) 

937 

938 def __repr__(self) -> str: 

939 return f"<{self.type} at {self.pos}>" 

940 

941 

942#### Tokenizer 

943 

944 

945class TokenMacros: 

946 unicode_escape = r"\\([0-9a-f]{1,6})(?:\r\n|[ \n\r\t\f])?" 

947 escape = unicode_escape + r"|\\[^\n\r\f0-9a-f]" 

948 string_escape = r"\\(?:\n|\r\n|\r|\f)|" + escape 

949 nonascii = r"[^\0-\177]" 

950 nmchar = f"[_a-z0-9-]|{escape}|{nonascii}" 

951 nmstart = f"[_a-z]|{escape}|{nonascii}" 

952 

953 

954class MatchFunc(Protocol): 

955 def __call__( 

956 self, string: str, pos: int = ..., endpos: int = ... 

957 ) -> re.Match[str] | None: ... 

958 

959 

960def _compile(pattern: str) -> MatchFunc: 

961 return re.compile(pattern % vars(TokenMacros), re.IGNORECASE).match 

962 

963 

964_match_whitespace = _compile(r"[ \t\r\n\f]+") 

965_match_number = _compile(r"[+-]?(?:[0-9]*\.[0-9]+|[0-9]+)") 

966_match_hash = _compile("#(?:%(nmchar)s)+") 

967_match_ident = _compile("-?(?:%(nmstart)s)(?:%(nmchar)s)*") 

968_match_string_by_quote = { 

969 "'": _compile(r"([^\n\r\f\\']|%(string_escape)s)*"), 

970 '"': _compile(r'([^\n\r\f\\"]|%(string_escape)s)*'), 

971} 

972 

973_sub_simple_escape = re.compile(r"\\(.)").sub 

974_sub_unicode_escape = re.compile(TokenMacros.unicode_escape, re.IGNORECASE).sub 

975_sub_newline_escape = re.compile(r"\\(?:\n|\r\n|\r|\f)").sub 

976_sub_string_control_char = re.compile(r"[\x00-\x1f\x7f]").sub 

977 

978# Same as r'\1', but faster on CPython 

979_replace_simple = operator.methodcaller("group", 1) 

980 

981 

982def _replace_unicode(match: re.Match[str]) -> str: 

983 codepoint = int(match.group(1), 16) 

984 if codepoint > sys.maxunicode or 0xD800 <= codepoint <= 0xDFFF: 

985 codepoint = 0xFFFD 

986 return chr(codepoint) 

987 

988 

989def _replace_string_control_char(match: re.Match[str]) -> str: 

990 # The trailing space ends the escape sequence, in case the next 

991 # character is a hexadecimal digit. 

992 return f"\\{ord(match.group()):x} " 

993 

994 

995def unescape_ident(value: str) -> str: 

996 value = _sub_unicode_escape(_replace_unicode, value) 

997 return _sub_simple_escape(_replace_simple, value) 

998 

999 

1000def _serialize_ident(value: str) -> str: 

1001 """Serialize a string as a CSS identifier, escaping special characters. 

1002 

1003 Implements the CSSOM "serialize an identifier" algorithm: 

1004 https://drafts.csswg.org/cssom/#serialize-an-identifier 

1005 """ 

1006 result = [] 

1007 for i, char in enumerate(value): 

1008 code = ord(char) 

1009 serialized = char 

1010 if code == 0: 

1011 serialized = "\N{REPLACEMENT CHARACTER}" 

1012 elif code <= 0x1F or code == 0x7F: 

1013 serialized = f"\\{code:x} " 

1014 elif "0" <= char <= "9": 

1015 if i == 0 or (i == 1 and value[0] == "-"): 

1016 # An identifier cannot start with a digit 

1017 # (or a '-' followed by a digit). 

1018 serialized = f"\\{code:x} " 

1019 elif char == "-": 

1020 if len(value) == 1 or (i == 0 and value[1] == "-"): 

1021 # CSSOM leaves a leading "--" unescaped (such identifiers 

1022 # are valid since CSS Syntax 3), but the tokenizer only 

1023 # implements the CSS 2.1 identifier grammar and would not 

1024 # be able to parse the result, so escape the first "-". 

1025 serialized = "\\-" 

1026 elif not ( 

1027 code >= 0x80 or char == "_" or "a" <= char <= "z" or "A" <= char <= "Z" 

1028 ): 

1029 serialized = f"\\{char}" 

1030 result.append(serialized) 

1031 return "".join(result) 

1032 

1033 

1034def tokenize(s: str) -> Iterator[Token]: 

1035 pos = 0 

1036 len_s = len(s) 

1037 while pos < len_s: 

1038 match = _match_whitespace(s, pos=pos) 

1039 if match: 

1040 yield Token("S", " ", pos) 

1041 pos = match.end() 

1042 continue 

1043 

1044 match = _match_ident(s, pos=pos) 

1045 if match: 

1046 value = unescape_ident(match.group()) 

1047 yield Token("IDENT", value, pos) 

1048 pos = match.end() 

1049 continue 

1050 

1051 match = _match_hash(s, pos=pos) 

1052 if match: 

1053 value = unescape_ident(match.group()[1:]) 

1054 yield Token("HASH", value, pos) 

1055 pos = match.end() 

1056 continue 

1057 

1058 quote = s[pos] 

1059 if quote in _match_string_by_quote: 

1060 match = _match_string_by_quote[quote](s, pos=pos + 1) 

1061 assert match, "Should have found at least an empty match" 

1062 end_pos = match.end() 

1063 if end_pos == len_s: 

1064 raise SelectorSyntaxError(f"Unclosed string at {pos}") 

1065 if s[end_pos] != quote: 

1066 raise SelectorSyntaxError(f"Invalid string at {pos}") 

1067 value = _sub_simple_escape( 

1068 _replace_simple, 

1069 _sub_unicode_escape( 

1070 _replace_unicode, _sub_newline_escape("", match.group()) 

1071 ), 

1072 ) 

1073 yield Token("STRING", value, pos) 

1074 pos = end_pos + 1 

1075 continue 

1076 

1077 match = _match_number(s, pos=pos) 

1078 if match: 

1079 value = match.group() 

1080 yield Token("NUMBER", value, pos) 

1081 pos = match.end() 

1082 continue 

1083 

1084 pos2 = pos + 2 

1085 if s[pos:pos2] == "/*": 

1086 pos = s.find("*/", pos2) 

1087 if pos == -1: 

1088 pos = len_s 

1089 else: 

1090 pos += 2 

1091 continue 

1092 

1093 yield Token("DELIM", s[pos], pos) 

1094 pos += 1 

1095 

1096 assert pos == len_s 

1097 yield EOFToken(pos) 

1098 

1099 

1100class TokenStream: 

1101 def __init__(self, tokens: Iterable[Token], source: str | None = None) -> None: 

1102 self.used: list[Token] = [] 

1103 self.tokens = iter(tokens) 

1104 self.source = source 

1105 self.peeked: Token | None = None 

1106 self._peeking = False 

1107 self.next_token = self.tokens.__next__ 

1108 

1109 def next(self) -> Token: 

1110 if self._peeking: 

1111 self._peeking = False 

1112 assert self.peeked is not None 

1113 self.used.append(self.peeked) 

1114 return self.peeked 

1115 next_ = self.next_token() 

1116 self.used.append(next_) 

1117 return next_ 

1118 

1119 def peek(self) -> Token: 

1120 if not self._peeking: 

1121 self.peeked = self.next_token() 

1122 self._peeking = True 

1123 assert self.peeked is not None 

1124 return self.peeked 

1125 

1126 def next_ident(self) -> str: 

1127 next_ = self.next() 

1128 if next_.type != "IDENT": 

1129 raise SelectorSyntaxError(f"Expected ident, got {next_}") 

1130 return cast("str", next_.value) 

1131 

1132 def next_ident_or_star(self) -> str | None: 

1133 next_ = self.next() 

1134 if next_.type == "IDENT": 

1135 return next_.value 

1136 if next_ == ("DELIM", "*"): 

1137 return None 

1138 raise SelectorSyntaxError(f"Expected ident or '*', got {next_}") 

1139 

1140 def skip_whitespace(self) -> None: 

1141 # A comment between two whitespace runs yields two consecutive 

1142 # whitespace tokens, so a single check is not enough. 

1143 while self.peek().type == "S": 

1144 self.next()