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

702 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 re 

17import sys 

18from typing import ( 

19 TYPE_CHECKING, 

20 Literal, 

21 Protocol, 

22 Self, 

23 TypeAlias, 

24 Union, 

25 cast, 

26 overload, 

27) 

28 

29if TYPE_CHECKING: 

30 from collections.abc import Iterable, Iterator, Sequence 

31 

32 

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

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

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

36 

37 

38class SelectorError(Exception): 

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

40 :class:`ExpressionError`. 

41 

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

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

44 

45 """ 

46 

47 

48class SelectorSyntaxError(SelectorError, SyntaxError): 

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

50 

51 

52#### Parsed objects 

53 

54Tree: TypeAlias = Union[ 

55 "Element", 

56 "Hash", 

57 "Class", 

58 "Function", 

59 "Pseudo", 

60 "Attrib", 

61 "Negation", 

62 "Relation", 

63 "Matching", 

64 "SpecificityAdjustment", 

65 "CombinedSelector", 

66] 

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

68 

69 

70class Selector: 

71 """ 

72 Represents a parsed selector. 

73 

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

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

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

77 or unsupported pseudo-elements. 

78 

79 """ 

80 

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

82 self.parsed_tree = tree 

83 if pseudo_element is not None and not isinstance( 

84 pseudo_element, FunctionalPseudoElement 

85 ): 

86 pseudo_element = ascii_lower(pseudo_element) 

87 #: A :class:`FunctionalPseudoElement`, 

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

89 # or ``None``. 

90 #: 

91 #: +-------------------------+----------------+--------------------------------+ 

92 #: | | Selector | Pseudo-element | 

93 #: +=========================+================+================================+ 

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

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

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

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

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

99 #: | not in Selectors3 | | | 

100 #: +-------------------------+----------------+--------------------------------+ 

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

102 #: +-------------------------+----------------+--------------------------------+ 

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

104 #: +-------------------------+----------------+--------------------------------+ 

105 #: 

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

107 self.pseudo_element = pseudo_element 

108 

109 def __repr__(self) -> str: 

110 if isinstance(self.pseudo_element, FunctionalPseudoElement): 

111 pseudo_element = repr(self.pseudo_element) 

112 elif self.pseudo_element: 

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

114 else: 

115 pseudo_element = "" 

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

117 

118 def canonical(self) -> str: 

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

120 if isinstance(self.pseudo_element, FunctionalPseudoElement): 

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

122 elif self.pseudo_element: 

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

124 else: 

125 pseudo_element = "" 

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

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

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

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

130 res = res[1:] 

131 return res 

132 

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

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

135 

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

137 

138 """ 

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

140 if self.pseudo_element: 

141 c += 1 

142 return a, b, c 

143 

144 

145class Class: 

146 """ 

147 Represents selector.class_name 

148 """ 

149 

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

151 self.selector = selector 

152 self.class_name = class_name 

153 

154 def __repr__(self) -> str: 

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

156 

157 def canonical(self) -> str: 

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

159 

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

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

162 b += 1 

163 return a, b, c 

164 

165 

166class FunctionalPseudoElement: 

167 """ 

168 Represents selector::name(arguments) 

169 

170 .. attribute:: name 

171 

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

173 

174 .. attribute:: arguments 

175 

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

177 

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

179 and may change between cssselect versions. 

180 Use at your own risks. 

181 

182 """ 

183 

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

185 self.name = ascii_lower(name) 

186 self.arguments = arguments 

187 

188 def __repr__(self) -> str: 

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

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

191 

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

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

194 

195 def canonical(self) -> str: 

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

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

198 

199 

200class Function: 

201 """ 

202 Represents selector:name(expr) 

203 """ 

204 

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

206 self.selector = selector 

207 self.name = ascii_lower(name) 

208 self.arguments = arguments 

209 

210 def __repr__(self) -> str: 

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

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

213 

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

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

216 

217 def canonical(self) -> str: 

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

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

220 

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

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

223 b += 1 

224 return a, b, c 

225 

226 

227class Pseudo: 

228 """ 

229 Represents selector:ident 

230 """ 

231 

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

233 self.selector = selector 

234 self.ident = ascii_lower(ident) 

235 

236 def __repr__(self) -> str: 

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

238 

239 def canonical(self) -> str: 

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

241 

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

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

244 b += 1 

245 return a, b, c 

246 

247 

248class Negation: 

249 """ 

250 Represents selector:not(subselector) 

251 """ 

252 

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

254 self.selector = selector 

255 self.subselector = subselector 

256 

257 def __repr__(self) -> str: 

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

259 

260 def canonical(self) -> str: 

261 subsel = self.subselector.canonical() 

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

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

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

265 subsel = subsel[1:] 

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

267 

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

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

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

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

272 

273 

274def _combinator_prefix(combinator: Token) -> str: 

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

276 if combinator.value == " ": 

277 return "" 

278 return f"{combinator.value} " 

279 

280 

281class Relation: 

282 """ 

283 Represents selector:has(relative_selector_list) 

284 

285 .. attribute:: arguments 

286 

287 The relative selector list, as a list of ``(combinator, selector)`` 

288 pairs. *combinator* is the leading combinator token of the argument, 

289 which is a space token when the argument does not spell one out. 

290 

291 """ 

292 

293 def __init__(self, selector: Tree, arguments: Sequence[tuple[Token, Selector]]): 

294 self.selector = selector 

295 self.arguments = arguments 

296 

297 def __repr__(self) -> str: 

298 args_str = ", ".join( 

299 f"{_combinator_prefix(combinator)}{subselector!r}" 

300 for combinator, subselector in self.arguments 

301 ) 

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

303 

304 def canonical(self) -> str: 

305 args_str = ", ".join( 

306 f"{_combinator_prefix(combinator)}{subselector.canonical()}" 

307 for combinator, subselector in self.arguments 

308 ) 

309 return f"{self.selector.canonical()}:has({args_str})" 

310 

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

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

313 a2, b2, c2 = max(subselector.specificity() for _, subselector in self.arguments) 

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

315 

316 

317class Matching: 

318 """ 

319 Represents selector:is(selector_list) 

320 """ 

321 

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

323 self.selector = selector 

324 self.selector_list = selector_list 

325 

326 def __repr__(self) -> str: 

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

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

329 

330 def canonical(self) -> str: 

331 selector_arguments = [] 

332 for s in self.selector_list: 

333 selarg = s.canonical() 

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

335 selarg = selarg[1:] 

336 selector_arguments.append(selarg) 

337 args_str = ", ".join(selector_arguments) 

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

339 

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

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

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

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

344 

345 

346class SpecificityAdjustment: 

347 """ 

348 Represents selector:where(selector_list) 

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

350 """ 

351 

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

353 self.selector = selector 

354 self.selector_list = selector_list 

355 

356 def __repr__(self) -> str: 

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

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

359 

360 def canonical(self) -> str: 

361 selector_arguments = [] 

362 for s in self.selector_list: 

363 selarg = s.canonical() 

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

365 selarg = selarg[1:] 

366 selector_arguments.append(selarg) 

367 args_str = ", ".join(selector_arguments) 

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

369 

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

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

372 # selector it applies to does. 

373 return self.selector.specificity() 

374 

375 

376class Attrib: 

377 """ 

378 Represents selector[namespace|attrib operator value flag] 

379 

380 *flag* is ``'i'`` for a case-insensitive value match, ``'s'`` for a 

381 case-sensitive one, and `None` when the selector sets neither. 

382 """ 

383 

384 @overload 

385 def __init__( 

386 self, 

387 selector: Tree, 

388 namespace: str | None, 

389 attrib: str, 

390 operator: Literal["exists"], 

391 value: None, 

392 flag: None = None, 

393 ) -> None: ... 

394 

395 @overload 

396 def __init__( 

397 self, 

398 selector: Tree, 

399 namespace: str | None, 

400 attrib: str, 

401 operator: str, 

402 value: Token, 

403 flag: str | None = None, 

404 ) -> None: ... 

405 

406 def __init__( 

407 self, 

408 selector: Tree, 

409 namespace: str | None, 

410 attrib: str, 

411 operator: str, 

412 value: Token | None, 

413 flag: str | None = None, 

414 ) -> None: 

415 self.selector = selector 

416 self.namespace = namespace 

417 self.attrib = attrib 

418 self.operator = operator 

419 self.value = value 

420 self.flag = flag 

421 

422 def __repr__(self) -> str: 

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

424 if self.operator == "exists": 

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

426 assert self.value is not None 

427 flag = f" {self.flag}" if self.flag else "" 

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

429 

430 def canonical(self) -> str: 

431 attrib = _serialize_ident(self.attrib) 

432 if self.namespace: 

433 namespace = ( 

434 "*" if self.namespace == "*" else _serialize_ident(self.namespace) 

435 ) 

436 attrib = f"{namespace}|{attrib}" 

437 

438 if self.operator == "exists": 

439 op = attrib 

440 else: 

441 assert self.value is not None 

442 flag = f" {self.flag}" if self.flag else "" 

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

444 

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

446 

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

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

449 b += 1 

450 return a, b, c 

451 

452 

453class Element: 

454 """ 

455 Represents namespace|element 

456 

457 `None` is for the universal selector '*'. `namespace` is ``"*"`` 

458 for the explicit namespace wildcard, e.g. ``*|div``, and `None` 

459 both for an unprefixed element and for the explicit ``|div`` 

460 no-namespace syntax. ``|*`` (any element with no namespace) has 

461 `namespace` ``""``. 

462 

463 """ 

464 

465 def __init__( 

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

467 ) -> None: 

468 self.namespace = namespace 

469 self.element = element 

470 

471 def __repr__(self) -> str: 

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

473 

474 def canonical(self) -> str: 

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

476 if self.namespace is not None: 

477 namespace = ( 

478 "*" if self.namespace == "*" else _serialize_ident(self.namespace) 

479 ) 

480 element = f"{namespace}|{element}" 

481 return element 

482 

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

484 if self.element: 

485 return 0, 0, 1 

486 return 0, 0, 0 

487 

488 

489class Hash: 

490 """ 

491 Represents selector#id 

492 """ 

493 

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

495 self.selector = selector 

496 self.id = id 

497 

498 def __repr__(self) -> str: 

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

500 

501 def canonical(self) -> str: 

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

503 

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

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

506 a += 1 

507 return a, b, c 

508 

509 

510class CombinedSelector: 

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

512 assert selector is not None 

513 self.selector = selector 

514 self.combinator = combinator 

515 self.subselector = subselector 

516 

517 def __repr__(self) -> str: 

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

519 return ( 

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

521 ) 

522 

523 def canonical(self) -> str: 

524 subsel = self.subselector.canonical() 

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

526 subsel = subsel[1:] 

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

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

529 

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

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

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

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

534 

535 

536#### Parser 

537 

538# foo 

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

540 

541# foo#bar or #bar 

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

543 

544# foo.bar or .bar 

545_class_re = re.compile( 

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

547) 

548 

549 

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

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

552 

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

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

555 

556 :param css: 

557 A *group of selectors* as a string. 

558 :raises: 

559 :class:`SelectorSyntaxError` on invalid selectors. 

560 :returns: 

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

562 selector in the comma-separated group. 

563 

564 """ 

565 # Fast path for simple cases 

566 match = _el_re.match(css) 

567 if match: 

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

569 match = _id_re.match(css) 

570 if match is not None: 

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

572 match = _class_re.match(css) 

573 if match is not None: 

574 return [ 

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

576 ] 

577 

578 stream = TokenStream(tokenize(css)) 

579 stream.source = css 

580 return list(parse_selector_group(stream)) 

581 

582 

583# except SelectorSyntaxError: 

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

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

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

587# e.msg = message 

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

589# raise 

590 

591 

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

593 stream.skip_whitespace() 

594 while 1: 

595 yield Selector(*parse_selector(stream)) 

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

597 stream.next() 

598 stream.skip_whitespace() 

599 else: 

600 break 

601 

602 

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

604 result, pseudo_element = parse_simple_selector(stream) 

605 while 1: 

606 stream.skip_whitespace() 

607 peek = stream.peek() 

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

609 break 

610 if pseudo_element: 

611 raise SelectorSyntaxError( 

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

613 ) 

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

615 # A combinator 

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

617 stream.skip_whitespace() 

618 else: 

619 # By exclusion, the last parse_simple_selector() ended 

620 # at peek == ' ' 

621 combinator = " " 

622 next_selector, pseudo_element = parse_simple_selector(stream) 

623 result = CombinedSelector(result, combinator, next_selector) 

624 return result, pseudo_element 

625 

626 

627def parse_simple_selector( 

628 stream: TokenStream, 

629 inside_negation: bool = False, 

630 inside_selector_list: bool = False, 

631 inside_relative_selector: bool = False, 

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

633 stream.skip_whitespace() 

634 selector_start = len(stream.used) 

635 # Whether the tokens preceding this compound selector (ignoring 

636 # whitespace) are the start of the input or a comma, for :scope. 

637 preceding = selector_start - 1 

638 while preceding >= 0 and stream.used[preceding].type == "S": 

639 preceding -= 1 

640 at_selector_start = preceding < 0 or stream.used[preceding].is_delim(",") 

641 peek = stream.peek() 

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

643 if peek.type == "IDENT": 

644 namespace = stream.next().value 

645 else: 

646 stream.next() 

647 namespace = "*" 

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

649 if peek.type == "IDENT" and namespace == "*": 

650 # An escaped "*" (e.g. "\*|f") is a prefix named "*", 

651 # not the namespace wildcard, and no XPath prefix can be "*". 

652 raise SelectorSyntaxError(f"Expected namespace prefix, got {peek}") 

653 stream.next() 

654 element = stream.next_ident_or_star() 

655 else: 

656 # An escaped "*" (e.g. "\*") is a type selector for elements 

657 # named "*", not the universal selector. 

658 element = namespace if peek.type == "IDENT" else None 

659 namespace = None 

660 else: 

661 element = namespace = None 

662 result: Tree = Element(namespace, element) 

663 pseudo_element: PseudoElement | None = None 

664 while 1: 

665 peek = stream.peek() 

666 if peek.type in ("S", "EOF") or peek.is_delim(",", "+", ">", "~", ")"): 

667 break 

668 if pseudo_element: 

669 raise SelectorSyntaxError( 

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

671 ) 

672 if peek.type == "HASH": 

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

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

675 stream.next() 

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

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

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

679 # the very start of a simple selector. 

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

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

682 stream.next() 

683 element = stream.next_ident_or_star() 

684 # Unlike *, |* only matches elements with no namespace. 

685 result = Element("" if element is None else None, element) 

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

687 stream.next() 

688 result = parse_attrib(result, stream) 

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

690 stream.next() 

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

692 stream.next() 

693 pseudo_element = stream.next_ident() 

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

695 stream.next() 

696 pseudo_element = FunctionalPseudoElement( 

697 pseudo_element, parse_arguments(stream) 

698 ) 

699 continue 

700 ident = stream.next_ident() 

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

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

703 # Any new pseudo-element must have two. 

704 pseudo_element = str(ident) 

705 continue 

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

707 result = Pseudo(result, ident) 

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

709 # i.e. never in :is()/:where()/:matches() or :has() 

710 # arguments (where a preceding comma separates arguments, 

711 # not selectors), and otherwise only when the tokens 

712 # preceding its compound selector are the start of the 

713 # input or a comma. 

714 if result.ident == "scope" and ( 

715 inside_selector_list 

716 or inside_relative_selector 

717 or not at_selector_start 

718 ): 

719 raise SelectorSyntaxError( 

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

721 ) 

722 continue 

723 stream.next() 

724 stream.skip_whitespace() 

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

726 if inside_selector_list: 

727 raise SelectorSyntaxError( 

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

729 ) 

730 if inside_negation: 

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

732 argument, argument_pseudo_element = parse_simple_selector( 

733 stream, inside_negation=True 

734 ) 

735 while 1: 

736 # Whitespace before the closing parenthesis is not a 

737 # descendant combinator. 

738 stream.skip_whitespace() 

739 peek = stream.peek() 

740 if argument_pseudo_element: 

741 raise SelectorSyntaxError( 

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

743 ) 

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

745 stream.next() 

746 break 

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

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

749 stream.skip_whitespace() 

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

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

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

753 else: 

754 argument_combinator = " " 

755 next_selector, argument_pseudo_element = parse_simple_selector( 

756 stream, inside_negation=True 

757 ) 

758 argument = CombinedSelector( 

759 argument, argument_combinator, next_selector 

760 ) 

761 result = Negation(result, argument) 

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

763 result = Relation(result, parse_relative_selector(stream)) 

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

765 selectors = parse_simple_selector_arguments(stream) 

766 result = Matching(result, selectors) 

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

768 selectors = parse_simple_selector_arguments(stream) 

769 result = SpecificityAdjustment(result, selectors) 

770 else: 

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

772 else: 

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

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

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

776 return result, pseudo_element 

777 

778 

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

780 arguments: list[Token] = [] 

781 while 1: 

782 stream.skip_whitespace() 

783 next_ = stream.next() 

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

785 ("DELIM", "+"), 

786 ("DELIM", "-"), 

787 ]: 

788 arguments.append(next_) 

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

790 return arguments 

791 else: 

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

793 

794 

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

796 """Parse the relative selector list of a :has() argument, i.e. a 

797 comma-separated list of complex selectors, each optionally preceded by a 

798 combinator.""" 

799 arguments = [] 

800 while 1: 

801 stream.skip_whitespace() 

802 peek = stream.peek() 

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

804 combinator = stream.next() 

805 stream.skip_whitespace() 

806 else: 

807 combinator = Token("DELIM", " ", pos=peek.pos) 

808 result, pseudo_element = parse_simple_selector( 

809 stream, inside_relative_selector=True 

810 ) 

811 while 1: 

812 if pseudo_element: 

813 raise SelectorSyntaxError( 

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

815 ) 

816 # Whitespace before a comma or the closing parenthesis is not a 

817 # descendant combinator. 

818 stream.skip_whitespace() 

819 peek = stream.peek() 

820 if peek.is_delim(",", ")"): 

821 break 

822 if peek.type == "EOF": 

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

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

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

826 stream.skip_whitespace() 

827 else: 

828 argument_combinator = " " 

829 next_selector, pseudo_element = parse_simple_selector(stream) 

830 result = CombinedSelector(result, argument_combinator, next_selector) 

831 arguments.append((combinator, Selector(result))) 

832 if stream.next().is_delim(")"): 

833 break 

834 return arguments 

835 

836 

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

838 arguments = [] 

839 while 1: 

840 result, pseudo_element = parse_simple_selector( 

841 stream, inside_selector_list=True 

842 ) 

843 if pseudo_element: 

844 raise SelectorSyntaxError( 

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

846 ) 

847 stream.skip_whitespace() 

848 next_ = stream.next() 

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

850 stream.skip_whitespace() 

851 arguments.append(result) 

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

853 arguments.append(result) 

854 break 

855 else: 

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

857 return arguments 

858 

859 

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

861 stream.skip_whitespace() 

862 attrib: str | None 

863 namespace: str | None 

864 op: str | None 

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

866 # The explicit "no namespace" syntax, e.g. [|attr]. 

867 stream.next() 

868 namespace = None 

869 attrib = stream.next_ident() 

870 op = None 

871 else: 

872 attrib = stream.next_ident_or_star() 

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

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

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

876 stream.next() 

877 if attrib is not None and stream.peek() == ("DELIM", "="): 

878 namespace = None 

879 stream.next() 

880 op = "|=" 

881 else: 

882 if attrib == "*": 

883 # An escaped "*" (e.g. "[\*|attr]"), not the wildcard. 

884 raise SelectorSyntaxError( 

885 f"Expected namespace prefix, got {stream.used[-2]}" 

886 ) 

887 namespace = "*" if attrib is None else attrib 

888 attrib = stream.next_ident() 

889 op = None 

890 else: 

891 namespace = op = None 

892 if op is None: 

893 stream.skip_whitespace() 

894 next_ = stream.next() 

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

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

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

898 op = "=" 

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

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

901 ): 

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

903 stream.next() 

904 else: 

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

906 stream.skip_whitespace() 

907 value = stream.next() 

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

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

910 stream.skip_whitespace() 

911 next_ = stream.next() 

912 flag = None 

913 if next_.type == "IDENT": 

914 flag = ascii_lower(cast("str", next_.value)) 

915 if flag not in ("i", "s"): 

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

917 stream.skip_whitespace() 

918 next_ = stream.next() 

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

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

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

922 

923 

924# An An+B integer: ASCII digits only, unlike int(), which also accepts other 

925# Unicode decimal digits, underscores and surrounding whitespace. 

926_match_series_integer = re.compile("[+-]?[0-9]+").fullmatch 

927 

928 

929def _parse_series_integer(s: str) -> int: 

930 if not _match_series_integer(s): 

931 raise ValueError(f"Invalid integer in series: {s!r}") 

932 return int(s) 

933 

934 

935# The B of An+B with whitespace: only around a sign that is a token of its 

936# own, as in "2n + 1" or "2n- 1". 

937_match_series_spaced_b = re.compile(" ?[+-] ?[0-9]+").fullmatch 

938 

939 

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

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

942 parts = [] 

943 end = None 

944 for token in tokens: 

945 if token.type == "STRING": 

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

947 value = cast("str", token.value) 

948 if end is not None and token.pos != end: 

949 # Whitespace or a comment between two tokens, which must not 

950 # join them: "1 2" is not "12". An escaped identifier is longer 

951 # in the source than its value, but whitespace is allowed after 

952 # the identifier that ends An anyway. 

953 parts.append(" ") 

954 parts.append(value) 

955 end = token.pos + len(value) 

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

957 s = ascii_lower("".join(parts).strip()) 

958 if s == "odd": 

959 return 2, 1 

960 if s == "even": 

961 return 2, 0 

962 if s == "n": 

963 return 1, 0 

964 if "n" not in s: 

965 # Just b 

966 return 0, _parse_series_integer(s) 

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

968 a_as_int: int 

969 if not a: 

970 a_as_int = 1 

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

972 a_as_int = int(a + "1") 

973 else: 

974 a_as_int = _parse_series_integer(a) 

975 if " " in b: 

976 if not _match_series_spaced_b(b): 

977 raise ValueError(f"Invalid integer in series: {b!r}") 

978 b = b.replace(" ", "") 

979 b_as_int = _parse_series_integer(b) if b else 0 

980 return a_as_int, b_as_int 

981 

982 

983#### Token objects 

984 

985 

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

987 @overload 

988 def __new__( 

989 cls, 

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

991 value: str, 

992 pos: int, 

993 ) -> Self: ... 

994 

995 @overload 

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

997 

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

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

1000 obj.pos = pos 

1001 return obj 

1002 

1003 def __repr__(self) -> str: 

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

1005 

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

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

1008 

1009 pos: int 

1010 

1011 @property 

1012 def type(self) -> str: 

1013 return self[0] 

1014 

1015 @property 

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

1017 return self[1] 

1018 

1019 def css(self) -> str: 

1020 if self.type == "STRING": 

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

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

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

1024 escaped = _sub_string_control_char(_replace_string_control_char, escaped) 

1025 return f"'{escaped}'" 

1026 if self.type == "IDENT": 

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

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

1029 

1030 

1031class EOFToken(Token): 

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

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

1034 

1035 def __repr__(self) -> str: 

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

1037 

1038 

1039#### Tokenizer 

1040 

1041 

1042class TokenMacros: 

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

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

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

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

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

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

1049 

1050 

1051class MatchFunc(Protocol): 

1052 def __call__( 

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

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

1055 

1056 

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

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

1059 

1060 

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

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

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

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

1065_match_string_by_quote = { 

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

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

1068} 

1069 

1070# Each escape is decoded in a single left-to-right pass, so that the output 

1071# of one escape is never read as (part of) another. 

1072_sub_escape = re.compile(TokenMacros.unicode_escape + r"|\\(.)", re.IGNORECASE).sub 

1073_sub_string_escape = re.compile( 

1074 TokenMacros.unicode_escape + r"|\\(?:\n|\r\n|\r|\f)|\\(.)", re.IGNORECASE 

1075).sub 

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

1077 

1078# CSS Syntax Level 3, §3.3: fold a raw U+0000 or surrogate code point to U+FFFD. 

1079_sub_invalid_input_char = re.compile("[\x00\ud800-\udfff]").sub 

1080 

1081 

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

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

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

1085 codepoint = 0xFFFD 

1086 return chr(codepoint) 

1087 

1088 

1089def _replace_escape(match: re.Match[str]) -> str: 

1090 if match.group(1) is not None: 

1091 return _replace_unicode(match) 

1092 # A simple escape, or an escaped newline (only in strings). 

1093 return match.group(2) or "" 

1094 

1095 

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

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

1098 # character is a hexadecimal digit. 

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

1100 

1101 

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

1103 return _sub_escape(_replace_escape, value) 

1104 

1105 

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

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

1108 

1109 Implements the CSSOM "serialize an identifier" algorithm: 

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

1111 """ 

1112 result = [] 

1113 for i, char in enumerate(value): 

1114 code = ord(char) 

1115 serialized = char 

1116 if code == 0: 

1117 serialized = "\N{REPLACEMENT CHARACTER}" 

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

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

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

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

1122 # An identifier cannot start with a digit 

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

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

1125 elif char == "-": 

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

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

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

1129 # implements the CSS 2.1 identifier grammar and would not 

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

1131 serialized = "\\-" 

1132 elif not ( 

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

1134 ): 

1135 serialized = f"\\{char}" 

1136 result.append(serialized) 

1137 return "".join(result) 

1138 

1139 

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

1141 # Preprocess the input stream (§3.3); the substitution is length-preserving. 

1142 s = _sub_invalid_input_char("\N{REPLACEMENT CHARACTER}", s) 

1143 pos = 0 

1144 len_s = len(s) 

1145 while pos < len_s: 

1146 match = _match_whitespace(s, pos=pos) 

1147 if match: 

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

1149 pos = match.end() 

1150 continue 

1151 

1152 match = _match_ident(s, pos=pos) 

1153 if match: 

1154 value = unescape_ident(match.group()) 

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

1156 pos = match.end() 

1157 continue 

1158 

1159 match = _match_hash(s, pos=pos) 

1160 if match: 

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

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

1163 pos = match.end() 

1164 continue 

1165 

1166 quote = s[pos] 

1167 if quote in _match_string_by_quote: 

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

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

1170 end_pos = match.end() 

1171 if end_pos == len_s: 

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

1173 if s[end_pos] != quote: 

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

1175 value = _sub_string_escape(_replace_escape, match.group()) 

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

1177 pos = end_pos + 1 

1178 continue 

1179 

1180 match = _match_number(s, pos=pos) 

1181 if match: 

1182 value = match.group() 

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

1184 pos = match.end() 

1185 continue 

1186 

1187 pos2 = pos + 2 

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

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

1190 if pos == -1: 

1191 pos = len_s 

1192 else: 

1193 pos += 2 

1194 continue 

1195 

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

1197 pos += 1 

1198 

1199 assert pos == len_s 

1200 yield EOFToken(pos) 

1201 

1202 

1203class TokenStream: 

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

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

1206 self.tokens = iter(tokens) 

1207 self.source = source 

1208 self.peeked: Token | None = None 

1209 self._peeking = False 

1210 self.next_token = self.tokens.__next__ 

1211 

1212 def next(self) -> Token: 

1213 if self._peeking: 

1214 self._peeking = False 

1215 assert self.peeked is not None 

1216 self.used.append(self.peeked) 

1217 return self.peeked 

1218 next_ = self.next_token() 

1219 self.used.append(next_) 

1220 return next_ 

1221 

1222 def peek(self) -> Token: 

1223 if not self._peeking: 

1224 self.peeked = self.next_token() 

1225 self._peeking = True 

1226 assert self.peeked is not None 

1227 return self.peeked 

1228 

1229 def next_ident(self) -> str: 

1230 next_ = self.next() 

1231 if next_.type != "IDENT": 

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

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

1234 

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

1236 next_ = self.next() 

1237 if next_.type == "IDENT": 

1238 return next_.value 

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

1240 return None 

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

1242 

1243 def skip_whitespace(self) -> None: 

1244 # A comment between two whitespace runs yields two consecutive 

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

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

1247 self.next()