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

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

1185 statements  

1"""CSS matcher.""" 

2from __future__ import annotations 

3from bisect import bisect_right 

4from datetime import datetime 

5from math import isfinite 

6from collections.abc import Hashable 

7from . import bidi 

8from . import util 

9import re 

10from . import css_types as ct 

11import bs4 

12from typing import ( 

13 Generator, 

14 Iterator, 

15 Iterable, 

16 Any, 

17 Callable, 

18 Sequence, 

19 overload, 

20 Literal, 

21 NamedTuple, 

22 cast, 

23 TYPE_CHECKING 

24) # noqa: F401, F811 

25 

26# Empty tag pattern (whitespace okay) 

27RE_NOT_EMPTY = re.compile('[^ \t\r\n\f]') 

28 

29RE_NOT_WS = re.compile('[^ \t\r\n\f]+') 

30 

31# Relationships 

32REL_PARENT = ' ' 

33REL_CLOSE_PARENT = '>' 

34REL_SIBLING = '~' 

35REL_CLOSE_SIBLING = '+' 

36 

37# Relationships for :has() (forward looking) 

38REL_HAS_PARENT = ': ' 

39REL_HAS_CLOSE_PARENT = ':>' 

40REL_HAS_SIBLING = ':~' 

41REL_HAS_CLOSE_SIBLING = ':+' 

42 

43# `bs4` imports this module while it is still initializing, 

44# so these must only be evaluated when type checking. 

45if TYPE_CHECKING: # pragma: no cover 

46 MatchRequest = tuple[bs4.Tag, ct.SelectorList] 

47 MatchGenerator = Generator[MatchRequest, bool, bool] 

48 

49NS_XHTML = 'http://www.w3.org/1999/xhtml' 

50NS_XML = 'http://www.w3.org/XML/1998/namespace' 

51NS_SVG = 'http://www.w3.org/2000/svg' 

52 

53# Values of `type` that put an `input` in a state that does not use its value for `dir="auto"`. 

54# A missing or invalid value is the Text state, which does. 

55NON_AUTO_DIR_INPUT_TYPES = frozenset(( 

56 'date', 'month', 'week', 'time', 'datetime-local', 'number', 'range', 'color', 'checkbox', 

57 'radio', 'file', 'image' 

58)) 

59 

60# HTML elements whose text is ignored when resolving `dir="auto"` on an ancestor. 

61AUTO_DIR_IGNORED_ELEMENTS = frozenset(('bdi', 'script', 'style', 'textarea')) 

62 

63DIR_FLAGS = ct.SEL_DIR_LTR | ct.SEL_DIR_RTL 

64RANGES = ct.SEL_IN_RANGE | ct.SEL_OUT_OF_RANGE 

65 

66DIR_MAP = { 

67 'ltr': ct.SEL_DIR_LTR, 

68 'rtl': ct.SEL_DIR_RTL, 

69 'auto': 0 

70} 

71 

72RE_NUM = re.compile(r"^(?P<value>-?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?)\Z") 

73RE_TIME = re.compile(r'^(?P<hour>[0-9]{2}):(?P<minutes>[0-9]{2})(?::(?P<seconds>[0-9]{2}(?:\.[0-9]{1,3})?))?\Z') 

74RE_MONTH = re.compile(r'^(?P<year>[0-9]{4,})-(?P<month>[0-9]{2})\Z') 

75RE_WEEK = re.compile(r'^(?P<year>[0-9]{4,})-W(?P<week>[0-9]{2})\Z') 

76RE_DATE = re.compile(r'^(?P<year>[0-9]{4,})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})\Z') 

77RE_DATETIME = re.compile( 

78 r'^(?P<year>[0-9]{4,})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})[T ](?P<hour>[0-9]{2}):(?P<minutes>[0-9]{2})' 

79 r'(?::(?P<seconds>[0-9]{2}(?:\.[0-9]{1,3})?))?\Z' 

80) 

81RE_SIZE = re.compile(r'[ \t\r\n\f]*\+?(?P<value>[0-9]+)') 

82# BCP 47 syntax for an extended language range (RFC 4647) and a language tag (RFC 5646), as browsers check it: 

83# subtags of 1 to 8 ASCII letters and digits, the first only letters, and `*` subtags only in a range. 

84# Subtags are not checked against the registry. 

85RE_LANG_RANGE = re.compile(r'(?:[a-zA-Z]{1,8}|\*)(?:-(?:[a-zA-Z0-9]{1,8}|\*))*') 

86RE_LANG_TAG = re.compile(r'[a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*') 

87 

88MONTHS_30 = (4, 6, 9, 11) # April, June, September, and November 

89FEB = 2 

90SHORT_MONTH = 30 

91LONG_MONTH = 31 

92FEB_MONTH = 28 

93FEB_LEAP_MONTH = 29 

94DAYS_IN_WEEK = 7 

95 

96 

97class TreeForms(NamedTuple): 

98 """The form information of a tree. Elements are tracked by `id()` as `bs4` hashes them by their content.""" 

99 

100 # The form owner of each `input` and `button` 

101 owners: dict[int, bs4.Tag | None] 

102 # The default button of each form 

103 defaults: dict[int, bs4.Tag] 

104 # The last radio button with a `checked` attribute of each radio button group, by form owner and name 

105 checked: dict[tuple[int, str], bs4.Tag] 

106 

107 

108class _DocumentNav: 

109 """Navigate a Beautiful Soup document.""" 

110 

111 @classmethod 

112 def assert_valid_input(cls, tag: Any) -> None: 

113 """Check if valid input tag or document.""" 

114 

115 # Fail on unexpected types. 

116 if not cls.is_tag(tag): 

117 raise TypeError(f"Expected a BeautifulSoup 'Tag', but instead received type {type(tag)}") 

118 

119 @staticmethod 

120 def is_doc(obj: bs4.element.PageElement | None) -> bool: 

121 """Is `BeautifulSoup` object.""" 

122 return isinstance(obj, bs4.BeautifulSoup) 

123 

124 @staticmethod 

125 def is_tag(obj: bs4.element.PageElement | None) -> bool: 

126 """Is tag.""" 

127 return isinstance(obj, bs4.Tag) 

128 

129 @staticmethod 

130 def is_declaration(obj: bs4.element.PageElement | None) -> bool: # pragma: no cover 

131 """Is declaration.""" 

132 return isinstance(obj, bs4.Declaration) 

133 

134 @staticmethod 

135 def is_cdata(obj: bs4.element.PageElement | None) -> bool: 

136 """Is CDATA.""" 

137 return isinstance(obj, bs4.CData) 

138 

139 @staticmethod 

140 def is_processing_instruction(obj: bs4.element.PageElement | None) -> bool: # pragma: no cover 

141 """Is processing instruction.""" 

142 return isinstance(obj, bs4.ProcessingInstruction) 

143 

144 @staticmethod 

145 def is_navigable_string(obj: bs4.element.PageElement | None) -> bool: 

146 """Is navigable string.""" 

147 return isinstance(obj, bs4.element.NavigableString) 

148 

149 @staticmethod 

150 def is_special_string(obj: bs4.element.PageElement | None) -> bool: 

151 """Is special string.""" 

152 return isinstance(obj, (bs4.Comment, bs4.Declaration, bs4.CData, bs4.ProcessingInstruction, bs4.Doctype)) 

153 

154 @classmethod 

155 def is_content_string(cls, obj: bs4.element.PageElement | None) -> bool: 

156 """Check if node is content string.""" 

157 

158 return cls.is_navigable_string(obj) and not cls.is_special_string(obj) 

159 

160 @staticmethod 

161 def is_xml_tree(el: bs4.Tag | None) -> bool: 

162 """Check if element (or document) is from a XML tree.""" 

163 

164 return el is not None and bool(el._is_xml) 

165 

166 def is_template(self, el: bs4.Tag) -> bool: 

167 """ 

168 Check if element is an HTML `template`. 

169 

170 The contents of a `template` are not part of the document, so navigation stops at one in both directions. 

171 This is called for every element that is navigated, so avoid normalizing names that can't match. 

172 """ 

173 

174 if len(el.name) != 8: 

175 return False 

176 

177 return bool( 

178 ((el.name if self.is_xml_tree(el) else util.lower(el.name)) == 'template') and 

179 self.is_html_tag(el) # type: ignore[attr-defined] 

180 ) 

181 

182 def is_root(self, el: bs4.Tag) -> bool: 

183 """Return whether element is the root of the tree (which we have already pre-calculated).""" 

184 

185 return bool(self.root and self.root is el) # type: ignore[attr-defined] 

186 

187 def has_contents(self, el: bs4.Tag | None, template_contents: bool) -> bool: 

188 """Check if the contents of an element should be navigated.""" 

189 

190 return el is not None and (template_contents or not self.is_template(el)) 

191 

192 def get_contents( 

193 self, 

194 el: bs4.Tag | None, 

195 template_contents: bool = False 

196 ) -> Iterator[bs4.element.PageElement]: 

197 """Get contents or contents in reverse.""" 

198 

199 if self.has_contents(el, template_contents): 

200 yield from cast('bs4.Tag', el).contents 

201 

202 def get_tag_children( 

203 self, 

204 el: bs4.Tag | None, 

205 start: int | None = None, 

206 reverse: bool = False 

207 ) -> Iterator[bs4.Tag]: 

208 """Get tag children.""" 

209 

210 return self.get_children(el, start, reverse, True) 

211 

212 @overload 

213 def get_children( 

214 self, 

215 el: bs4.Tag | None, 

216 start: int | None = None, 

217 reverse: bool = False, 

218 tags: Literal[True] = ..., 

219 template_contents: bool = False 

220 ) -> Iterator[bs4.Tag]: 

221 ... 

222 

223 @overload 

224 def get_children( 

225 self, 

226 el: bs4.Tag | None, 

227 start: int | None = None, 

228 reverse: bool = False, 

229 tags: Literal[False] = ..., 

230 template_contents: bool = False 

231 ) -> Iterator[bs4.element.PageElement]: 

232 ... 

233 

234 def get_children( 

235 self, 

236 el: bs4.Tag | None, 

237 start: int | None = None, 

238 reverse: bool = False, 

239 tags: Literal[True] | Literal[False] = False, 

240 template_contents: bool = False 

241 ) -> Iterator[bs4.element.PageElement]: 

242 """Get children.""" 

243 

244 if el is not None and self.has_contents(el, template_contents): 

245 last = len(el.contents) - 1 

246 if start is None: 

247 index = last if reverse else 0 

248 else: 

249 index = start 

250 end = -1 if reverse else last + 1 

251 incr = -1 if reverse else 1 

252 

253 if 0 <= index <= last: 

254 for i in range(index, end, incr): 

255 node = el.contents[i] 

256 if not tags or self.is_tag(node): 

257 yield node 

258 

259 def get_tag_descendants( 

260 self, 

261 el: bs4.Tag | None, 

262 template_contents: bool = False 

263 ) -> Iterator[bs4.Tag]: 

264 """Specifically get tag descendants.""" 

265 

266 yield from self.get_descendants( # type: ignore[misc] 

267 el, tags=True, template_contents=template_contents 

268 ) 

269 

270 def get_descendants( 

271 self, 

272 el: bs4.Tag | None, 

273 tags: bool = False, 

274 template_contents: bool = False 

275 ) -> Iterator[bs4.element.PageElement]: 

276 """ 

277 Get descendants. 

278 

279 `template_contents` only allows entering `el`, any `template` found within it is still not entered. 

280 """ 

281 

282 if el is not None and self.has_contents(el, template_contents): 

283 next_good = None 

284 for child in el.descendants: 

285 

286 if next_good is not None: 

287 if child is not next_good: 

288 continue 

289 next_good = None 

290 

291 if isinstance(child, bs4.Tag): 

292 if self.is_template(child): 

293 if child.next_sibling is not None: 

294 next_good = child.next_sibling 

295 else: 

296 last_child: bs4.element.PageElement = child 

297 while isinstance(last_child, bs4.Tag) and last_child.contents: 

298 last_child = last_child.contents[-1] 

299 next_good = last_child.next_element 

300 yield child 

301 if next_good is None: 

302 break 

303 # Coverage isn't seeing this even though it's executed 

304 continue # pragma: no cover 

305 yield child 

306 

307 elif not tags: 

308 yield child 

309 

310 def get_parent( 

311 self, 

312 el: bs4.Tag | None, 

313 no_doc: bool = True, 

314 no_template: bool = True 

315 ) -> bs4.Tag | None: 

316 """Get parent.""" 

317 

318 parent = el.parent if el is not None else None 

319 if no_template and parent is not None and self.is_template(parent): 

320 parent = None 

321 if no_doc and self.is_doc(parent): 

322 parent = None 

323 return parent 

324 

325 @staticmethod 

326 def get_tag_name(el: bs4.Tag | None) -> str | None: 

327 """Get tag.""" 

328 

329 return el.name if el is not None else None 

330 

331 @staticmethod 

332 def get_prefix_name(el: bs4.Tag) -> str | None: 

333 """Get prefix.""" 

334 

335 return el.prefix 

336 

337 @staticmethod 

338 def get_uri(el: bs4.Tag | None) -> str | None: 

339 """Get namespace `URI`.""" 

340 

341 return el.namespace if el is not None else None 

342 

343 @classmethod 

344 def get_next_tag(cls, el: bs4.Tag) -> bs4.Tag | None: 

345 """Get next sibling tag.""" 

346 

347 return cls.get_next(el, tags=True) # type: ignore[return-value] 

348 

349 @classmethod 

350 def get_next(cls, el: bs4.Tag, tags: bool = False) -> bs4.element.PageElement | None: 

351 """Get next sibling tag.""" 

352 

353 sibling = el.next_sibling 

354 while tags and not isinstance(sibling, bs4.Tag) and sibling is not None: 

355 sibling = sibling.next_sibling 

356 

357 if tags and not isinstance(sibling, bs4.Tag): 

358 sibling = None 

359 

360 return sibling 

361 

362 @classmethod 

363 def get_previous_tag(cls, el: bs4.Tag, tags: bool = True) -> bs4.Tag | None: 

364 """Get previous sibling tag.""" 

365 

366 return cls.get_previous(el, True) # type: ignore[return-value] 

367 

368 @classmethod 

369 def get_previous(cls, el: bs4.Tag, tags: bool = False) -> bs4.element.PageElement | None: 

370 """Get previous sibling tag.""" 

371 

372 sibling = el.previous_sibling 

373 while tags and not isinstance(sibling, bs4.Tag) and sibling is not None: 

374 sibling = sibling.previous_sibling 

375 

376 if tags and not isinstance(sibling, bs4.Tag): 

377 sibling = None 

378 

379 return sibling 

380 

381 @staticmethod 

382 def has_html_ns(el: bs4.Tag | None) -> bool: 

383 """ 

384 Check if element has an HTML namespace. 

385 

386 This is a bit different than whether a element is treated as having an HTML namespace, 

387 like we do in the case of `is_html_tag`. 

388 """ 

389 

390 ns = getattr(el, 'namespace') if el is not None else None # noqa: B009 

391 return bool(ns and ns == NS_XHTML) 

392 

393 @staticmethod 

394 def split_namespace(el: bs4.Tag | None, attr_name: str) -> tuple[str | None, str | None]: 

395 """Return namespace and attribute name without the prefix.""" 

396 

397 if el is None: # pragma: no cover 

398 return None, None 

399 

400 return getattr(attr_name, 'namespace', None), getattr(attr_name, 'name', None) 

401 

402 @classmethod 

403 def get_attribute_by_name( 

404 cls, 

405 el: bs4.Tag, 

406 name: str, 

407 default: str | Sequence[str] | None = None 

408 ) -> str | Sequence[str] | None: 

409 """Get attribute by name.""" 

410 

411 value = default 

412 if el._is_xml: 

413 if name in el.attrs: 

414 v = el.attrs[name] 

415 value = '' if v is None else v 

416 else: 

417 for k, v in el.attrs.items(): 

418 if util.lower(k) == name: 

419 value = '' if v is None else v 

420 break 

421 return value 

422 

423 @classmethod 

424 def iter_attributes(cls, el: bs4.Tag | None) -> Iterator[tuple[str, str | Sequence[str] | None]]: 

425 """Iterate attributes.""" 

426 

427 if el is not None: 

428 for k, v in el.attrs.items(): 

429 yield k, '' if v is None else v 

430 

431 @classmethod 

432 def get_classes(cls, el: bs4.Tag) -> Sequence[str]: 

433 """Get classes.""" 

434 

435 classes = cls.get_attribute_by_name(el, 'class', []) 

436 if isinstance(classes, str): 

437 classes = RE_NOT_WS.findall(classes) 

438 return cast(Sequence[str], classes) 

439 

440 def get_text(self, el: bs4.Tag) -> str: 

441 """Get text.""" 

442 

443 return ''.join( 

444 [ 

445 node for node in self.get_descendants(el) # type: ignore[misc] 

446 if self.is_content_string(node) 

447 ] 

448 ) 

449 

450 def get_own_text(self, el: bs4.Tag) -> list[str]: 

451 """Get Own Text.""" 

452 

453 return [ 

454 node for node in self.get_contents(el) if self.is_content_string(node) # type: ignore[misc] 

455 ] 

456 

457 

458class Inputs: 

459 """Class for parsing and validating input items.""" 

460 

461 @staticmethod 

462 def validate_day(year: int, month: int, day: int) -> bool: 

463 """Validate day.""" 

464 

465 max_days = LONG_MONTH 

466 if month == FEB: 

467 max_days = FEB_LEAP_MONTH if ((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0) else FEB_MONTH 

468 elif month in MONTHS_30: 

469 max_days = SHORT_MONTH 

470 return 1 <= day <= max_days 

471 

472 @staticmethod 

473 def validate_week(year: int, week: int) -> bool: 

474 """Validate week.""" 

475 

476 # Validate an ISO week number for `year`. 

477 # 

478 # Per ISO 8601 rules, the last ISO week of a year is the week 

479 # containing Dec 28. Using Dec 28 guarantees we obtain the 

480 # correct ISO week-number for the final week of `year`, even in 

481 # years where Dec 31 falls in ISO week 01 of the following year. 

482 # 

483 # Example: if Dec 31 is a Thursday the year's last ISO week will 

484 # be week 53; if Dec 31 is a Monday and that week is counted as 

485 # week 1 of the next year, Dec 28 still belongs to the final 

486 # week of the current ISO year and yields the correct max week. 

487 # 

488 # `datetime` stops at year 9999, but the calendar repeats every 400 

489 # years (a whole number of weeks), so use the matching year from 2000 

490 # to 2399, as `calendar.weekday()` does. 

491 max_week = datetime(2000 + year % 400, 12, 28).isocalendar()[1] 

492 return 1 <= week <= max_week 

493 

494 @staticmethod 

495 def validate_month(month: int) -> bool: 

496 """Validate month.""" 

497 

498 return 1 <= month <= 12 

499 

500 @staticmethod 

501 def validate_year(year: int) -> bool: 

502 """Validate year.""" 

503 

504 return 1 <= year 

505 

506 @staticmethod 

507 def parse_year(year: str) -> int: 

508 """ 

509 Parse year. 

510 

511 A year is four or more digits with no upper limit, but `int()` refuses digit strings longer than 

512 `sys.get_int_max_str_digits()`, so treat a year that long as invalid. 

513 """ 

514 

515 try: 

516 return int(year, 10) 

517 except ValueError: 

518 return 0 

519 

520 @staticmethod 

521 def validate_hour(hour: int) -> bool: 

522 """Validate hour.""" 

523 

524 return 0 <= hour <= 23 

525 

526 @staticmethod 

527 def validate_minutes(minutes: int) -> bool: 

528 """Validate minutes.""" 

529 

530 return 0 <= minutes <= 59 

531 

532 @staticmethod 

533 def validate_seconds(seconds: float) -> bool: 

534 """Validate seconds.""" 

535 

536 return 0 <= seconds < 60 

537 

538 @classmethod 

539 def parse_value(cls, itype: str, value: str | None) -> tuple[float, ...] | None: 

540 """Parse the input value.""" 

541 

542 parsed: tuple[float, ...] | None = None 

543 if value is None: 

544 return value 

545 if itype == "date": 

546 m = RE_DATE.match(value) 

547 if m: 

548 year = cls.parse_year(m.group('year')) 

549 month = int(m.group('month'), 10) 

550 day = int(m.group('day'), 10) 

551 if cls.validate_year(year) and cls.validate_month(month) and cls.validate_day(year, month, day): 

552 parsed = (year, month, day) 

553 elif itype == "month": 

554 m = RE_MONTH.match(value) 

555 if m: 

556 year = cls.parse_year(m.group('year')) 

557 month = int(m.group('month'), 10) 

558 if cls.validate_year(year) and cls.validate_month(month): 

559 parsed = (year, month) 

560 elif itype == "week": 

561 m = RE_WEEK.match(value) 

562 if m: 

563 year = cls.parse_year(m.group('year')) 

564 week = int(m.group('week'), 10) 

565 if cls.validate_year(year) and cls.validate_week(year, week): 

566 parsed = (year, week) 

567 elif itype == "time": 

568 m = RE_TIME.match(value) 

569 if m: 

570 hour = int(m.group('hour'), 10) 

571 minutes = int(m.group('minutes'), 10) 

572 seconds = float(m.group('seconds') or 0) 

573 if cls.validate_hour(hour) and cls.validate_minutes(minutes) and cls.validate_seconds(seconds): 

574 parsed = (hour, minutes, seconds) 

575 elif itype == "datetime-local": 

576 m = RE_DATETIME.match(value) 

577 if m: 

578 year = cls.parse_year(m.group('year')) 

579 month = int(m.group('month'), 10) 

580 day = int(m.group('day'), 10) 

581 hour = int(m.group('hour'), 10) 

582 minutes = int(m.group('minutes'), 10) 

583 seconds = float(m.group('seconds') or 0) 

584 if ( 

585 cls.validate_year(year) and cls.validate_month(month) and cls.validate_day(year, month, day) and 

586 cls.validate_hour(hour) and cls.validate_minutes(minutes) and cls.validate_seconds(seconds) 

587 ): 

588 parsed = (year, month, day, hour, minutes, seconds) 

589 elif itype in ("number", "range"): 

590 m = RE_NUM.match(value) 

591 if m: 

592 number = float(m.group('value')) 

593 if isfinite(number): 

594 parsed = (number,) 

595 return parsed 

596 

597 

598class CSSMatch(_DocumentNav): 

599 """Perform CSS matching.""" 

600 

601 def __init__( 

602 self, 

603 selectors: ct.SelectorList, 

604 scope: bs4.Tag | None, 

605 namespaces: ct.Namespaces | None, 

606 flags: int 

607 ) -> None: 

608 """Initialize.""" 

609 

610 self.assert_valid_input(scope) 

611 self.tag = scope 

612 self.cached_meta_lang: list[tuple[str, str]] = [] 

613 self.cached_forms: dict[int, TreeForms] = {} 

614 self.cached_options: dict[int, dict[int, bool]] = {} 

615 self.selectors = selectors 

616 self.namespaces: ct.Namespaces | dict[str, str] = {} if namespaces is None else namespaces 

617 self.flags = flags 

618 self.enable_cache = not bool(self.flags & util.NOCACHE) 

619 self.nth_cache: dict[Hashable, list[int]] = {} 

620 self.sib_cache: dict[Hashable, int] = {} 

621 self.anc_cache: dict[tuple[int, int], bool] = {} 

622 self.desc_cache: dict[tuple[int, int], bool] = {} 

623 self.dir_cache: dict[int, int] = {} 

624 self.lang_cache: dict[int, str] = {} 

625 self.edit_cache: dict[int, bool] = {} 

626 self.tree_cache: dict[int, bs4.Tag] = {} 

627 self.select_cache: dict[int, bs4.Tag | None] = {} 

628 self.index_cache: dict[int, dict[int, int]] = {} 

629 

630 # Find the root element for the whole tree 

631 doc = scope 

632 parent = self.get_parent(doc, no_template=False) 

633 while parent: 

634 doc = parent 

635 parent = self.get_parent(doc, no_template=False) 

636 root: bs4.Tag | None = None 

637 if not self.is_doc(doc): 

638 root = doc 

639 else: 

640 for child in self.get_tag_children(doc): 

641 root = child 

642 break 

643 

644 self.root = root 

645 self.scope = scope if scope is not doc else root 

646 self.has_html_namespace = self.has_html_ns(root) 

647 

648 # Parsers either record a namespace for every element or for none. The root 

649 # alone can't be expected to be HTML as a non-HTML element may have been detached. 

650 self.has_namespaces = bool(self.get_uri(root)) 

651 

652 # A document can be both XML and HTML (XHTML) 

653 self.is_xml = self.is_xml_tree(doc) 

654 self.is_html = not self.is_xml or self.has_html_namespace 

655 

656 def reset(self) -> None: # pragma: no cover 

657 """Reset.""" 

658 

659 self.nth_cache.clear() 

660 self.sib_cache.clear() 

661 self.anc_cache.clear() 

662 self.desc_cache.clear() 

663 self.dir_cache.clear() 

664 self.lang_cache.clear() 

665 self.edit_cache.clear() 

666 self.tree_cache.clear() 

667 self.select_cache.clear() 

668 self.index_cache.clear() 

669 

670 def supports_namespaces(self) -> bool: 

671 """Check if namespaces are supported in the HTML type.""" 

672 

673 return self.is_xml or self.has_namespaces 

674 

675 def get_tag_ns(self, el: bs4.Tag | None) -> str: 

676 """Get tag namespace.""" 

677 

678 namespace = '' 

679 if el is None: # pragma: no cover 

680 return namespace 

681 

682 if self.supports_namespaces(): 

683 ns = self.get_uri(el) 

684 if ns: 

685 namespace = ns 

686 else: 

687 namespace = NS_XHTML 

688 return namespace 

689 

690 def is_html_tag(self, el: bs4.Tag | None) -> bool: 

691 """Check if tag is in HTML namespace.""" 

692 

693 return self.get_tag_ns(el) == NS_XHTML 

694 

695 def get_tag(self, el: bs4.Tag | None) -> str | None: 

696 """Get tag.""" 

697 

698 name = self.get_tag_name(el) 

699 return util.lower(name) if name is not None and not self.is_xml else name 

700 

701 def get_prefix(self, el: bs4.Tag) -> str | None: 

702 """Get prefix.""" 

703 

704 prefix = self.get_prefix_name(el) 

705 return util.lower(prefix) if prefix is not None and not self.is_xml else prefix 

706 

707 @staticmethod 

708 def find_text_dir(text: str) -> int | None: 

709 """Get directionality from the first character of the text with a strong direction.""" 

710 

711 for c in text: 

712 direction = bidi.DIRECTIONS[bisect_right(bidi.STARTS, ord(c)) - 1] 

713 if direction != 'N': 

714 return ct.SEL_DIR_LTR if direction == 'L' else ct.SEL_DIR_RTL 

715 return None 

716 

717 def find_bidi(self, el: bs4.Tag) -> int | None: 

718 """Get directionality from element text.""" 

719 

720 stack = [self.get_children(el)] 

721 while stack: 

722 for node in stack[-1]: 

723 

724 # Analyze child text nodes 

725 if self.is_tag(node): 

726 

727 # Avoid analyzing certain elements specified in the specification. This only applies 

728 # to HTML elements: the text of others (SVG, MathML, etc.) is analyzed like any other. 

729 if self.is_html_tag(node) and ( 

730 self.get_tag(node) in AUTO_DIR_IGNORED_ELEMENTS or 

731 DIR_MAP.get(util.lower(self.get_attribute_by_name(node, 'dir', '')), None) is not None 

732 ): 

733 continue # pragma: no cover 

734 

735 # Check directionality of this node's text, then resume with the remaining siblings. 

736 stack.append(self.get_children(node)) 

737 break 

738 

739 # Skip `doctype` comments, etc. 

740 if self.is_special_string(node): 

741 continue 

742 

743 # Analyze text nodes for directionality. 

744 direction = self.find_text_dir(cast('bs4.element.NavigableString', node)) 

745 if direction is not None: 

746 return direction 

747 else: 

748 # All children have been analyzed and direction could not be determined. 

749 stack.pop() 

750 return None 

751 

752 def extended_language_filter(self, lang_range: str, lang_tag: str) -> bool: 

753 """ 

754 Filter the language tags. 

755 

756 `lang_tag` is a well-formed language tag, or an empty string if the language is untagged. 

757 """ 

758 

759 # Only an empty range matches an untagged language, and a range that is not well-formed matches nothing. 

760 if not lang_range or not lang_tag: 

761 return not lang_range and not lang_tag 

762 if RE_LANG_RANGE.fullmatch(lang_range) is None: 

763 return False 

764 

765 match = True 

766 # Wildcards after the first subtag are ignored (RFC 4647 section 3.3.2, step 3.A), as subtags in between are 

767 # already skipped implicitly. So `en-*` is the same as `en` and `de-*-DE` the same as `de-DE`. 

768 first, *rest = lang_range.lower().split('-') 

769 ranges = [first, *(r for r in rest if r != '*')] 

770 subtags = lang_tag.lower().split('-') 

771 length = len(ranges) 

772 rindex = 0 

773 sindex = 0 

774 r = ranges[rindex] 

775 s = subtags[sindex] 

776 

777 # Primary tag needs to match 

778 if r != '*' and r != s: 

779 match = False 

780 

781 rindex += 1 

782 sindex += 1 

783 

784 # Match until we run out of ranges 

785 while match and rindex < length: 

786 r = ranges[rindex] 

787 try: 

788 s = subtags[sindex] 

789 except IndexError: 

790 # Ran out of subtags, 

791 # but we still have ranges 

792 match = False 

793 continue 

794 

795 # Matched range 

796 if s == r: 

797 rindex += 1 

798 

799 # Implicit wildcard cannot match 

800 # singletons 

801 elif len(s) == 1: 

802 match = False 

803 continue 

804 

805 # Implicitly matched, so grab next subtag 

806 sindex += 1 

807 

808 return match 

809 

810 def match_attribute_name( 

811 self, 

812 el: bs4.Tag, 

813 attr: str, 

814 prefix: str | None 

815 ) -> str | Sequence[str] | None: 

816 """Match attribute name and return value if it exists.""" 

817 

818 value = None 

819 if self.supports_namespaces(): 

820 value = None 

821 # If we have not defined namespaces, we can't very well find them, so don't bother trying. 

822 if prefix: 

823 ns = self.namespaces.get(prefix) 

824 if ns is None and prefix != '*': 

825 return None 

826 else: 

827 ns = None 

828 

829 for k, v in self.iter_attributes(el): 

830 

831 # Get attribute parts 

832 namespace, name = self.split_namespace(el, k) 

833 

834 # Can't match a prefix attribute as we haven't specified one to match 

835 # Try to match it normally as a whole `p:a` as selector may be trying `p\:a`. 

836 if ns is None: 

837 if (self.is_xml and attr == k) or (not self.is_xml and util.lower(attr) == util.lower(k)): 

838 value = v 

839 break 

840 # Coverage is not finding this even though it is executed. 

841 # Adding a print statement before this (and erasing coverage) causes coverage to find the line. 

842 # Ignore the false positive message. 

843 continue # pragma: no cover 

844 

845 # We can't match our desired prefix attribute as the attribute doesn't have a prefix 

846 if namespace is None or (ns != namespace and prefix != '*'): 

847 continue 

848 

849 # The attribute doesn't match. 

850 if (util.lower(attr) != util.lower(name)) if not self.is_xml else (attr != name): 

851 continue 

852 

853 value = v 

854 break 

855 else: 

856 for k, v in self.iter_attributes(el): 

857 if util.lower(attr) != util.lower(k): 

858 continue 

859 value = v 

860 break 

861 return value 

862 

863 def match_namespace(self, el: bs4.Tag, tag: ct.SelectorTag) -> bool: 

864 """Match the namespace of the element.""" 

865 

866 match = True 

867 namespace = self.get_tag_ns(el) 

868 default_namespace = self.namespaces.get('') 

869 tag_ns = '' if tag.prefix is None else self.namespaces.get(tag.prefix) 

870 # We must match the default namespace if one is not provided 

871 if tag.prefix is None and (default_namespace is not None and namespace != default_namespace): 

872 match = False 

873 # If we specified `|tag`, we must not have a namespace. 

874 elif (tag.prefix is not None and tag.prefix == '' and namespace): 

875 match = False 

876 # Verify prefix matches 

877 elif ( 

878 tag.prefix and 

879 tag.prefix != '*' and (tag_ns is None or namespace != tag_ns) 

880 ): 

881 match = False 

882 return match 

883 

884 def match_attributes(self, el: bs4.Tag, attributes: tuple[ct.SelectorAttribute, ...]) -> bool: 

885 """Match attributes.""" 

886 

887 match = True 

888 if attributes: 

889 for a in attributes: 

890 temp = self.match_attribute_name(el, a.attribute, a.prefix) 

891 # The case insensitive pattern only applies to HTML elements in HTML documents 

892 pattern: re.Pattern[str] | None 

893 if a.xml_type_pattern and (self.is_xml or not self.is_html_tag(el)): 

894 pattern = a.xml_type_pattern 

895 ignore_case = False 

896 else: 

897 pattern = a.pattern 

898 ignore_case = a.ignore_case 

899 if temp is None and a.inverse: 

900 continue 

901 if temp is None: 

902 match = False 

903 break 

904 if pattern is None: 

905 continue 

906 value = temp if isinstance(temp, str) else ' '.join(temp) 

907 # A case insensitive pattern is compiled for the value converted to ASCII lowercase. 

908 if ignore_case: 

909 value = util.ascii_lower(value) 

910 if pattern.search(value) is None: 

911 match = False 

912 break 

913 return match 

914 

915 def match_tagname(self, el: bs4.Tag, tag: ct.SelectorTag) -> bool: 

916 """Match tag name.""" 

917 

918 name = (util.lower(tag.name) if not self.is_xml and tag.name is not None else tag.name) 

919 return not ( 

920 name is not None and 

921 name not in (self.get_tag(el), '*') 

922 ) 

923 

924 def match_tag(self, el: bs4.Tag, tag: ct.SelectorTag | None) -> bool: 

925 """Match the tag.""" 

926 

927 match = True 

928 if tag is not None: 

929 # Verify namespace 

930 if not self.match_tagname(el, tag): 

931 match = False 

932 if match and not self.match_namespace(el, tag): 

933 match = False 

934 return match 

935 

936 def within(self, target: bs4.Tag, parent: bs4.Tag | bs4.BeautifulSoup, start: int, end: int | None = None) -> bool: 

937 """ 

938 Check if target is within the parent's contents from `start` up to `end`. 

939 

940 Searching the contents for every target is quadratic on parents with many children, 

941 so cache the index of each child the first time a parent is checked. 

942 """ 

943 

944 key = id(parent) 

945 if key not in self.index_cache: 

946 self.index_cache[key] = {id(child): i for i, child in enumerate(parent.contents)} 

947 index = self.index_cache[key].get(id(target), -1) 

948 return start <= index < (end if end is not None else len(parent.contents)) 

949 

950 def match_general_sibling(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator: 

951 """Match general sibling combinator.""" 

952 

953 found = False 

954 

955 if relation[0] is ct.Null: # pragma: no cover 

956 return found 

957 

958 pkey: int | None = None 

959 key: tuple[int, int] | None = None 

960 

961 # Get the parent and use the ID for the cache key. Siblings do not need a parent element: 

962 # they can be top level elements in the document or the top level contents of a `template`. 

963 # Only an element that has been detached has no parent, and it has no siblings. 

964 parent = self.get_parent(el, no_doc=False, no_template=False) 

965 if parent is None: 

966 return found 

967 pkey = id(parent) 

968 

969 # Check the cache to see if we already know where the first sibling is, 

970 # and if we do, check if we are on the correct side of it. 

971 # If we've previously searched and found no sibling, there is no sibling. 

972 # Lastly, if this is our first time, setup the cache. 

973 reverse = relation[0].rel_type == REL_HAS_SIBLING 

974 start = len(parent) - 1 if reverse else 0 

975 if pkey is not None: 

976 key = (pkey, id(relation)) 

977 if key in self.sib_cache: 

978 index = self.sib_cache[key] 

979 if index >= 0: 

980 a, b = (index + 1, start + 1) if reverse else (start, index) 

981 return not self.within(el, parent, a, b) 

982 else: 

983 return False 

984 self.sib_cache[key] = start 

985 

986 # Start at the furthest endpoint and walk back towards the element looking for siblings. 

987 # The current element counts as a sibling, but will not cause a match. 

988 passed = False 

989 incr = -1 if reverse else 1 

990 for child in self.get_children(parent, start=start, reverse=reverse, template_contents=True): 

991 start += incr 

992 if not isinstance(child, bs4.Tag): 

993 continue 

994 

995 # Flag that we are passing the element. 

996 # Any siblings we find aren't valid for this element. 

997 if child is el: 

998 passed = True 

999 

1000 # We found the furthest sibling. 

1001 if (yield child, relation): 

1002 found = True 

1003 break 

1004 

1005 # Cache the index of the sibling or mark as there being no siblings. 

1006 if pkey and key: 

1007 self.sib_cache[key] = start if found else -1 

1008 

1009 # If we passed the current element and then found a sibling, it doesn't count as a match. 

1010 if passed: 

1011 found = False 

1012 

1013 return found 

1014 

1015 def match_ancestor(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator: 

1016 """ 

1017 Match descendant combinator. 

1018 

1019 A selector with several descendant combinators tries every ancestor for each one, which is 

1020 a polynomial of the depth with a degree of the number of combinators. Every element visited 

1021 on the walk shares the result, so cache whether each one is or has a matching ancestor. 

1022 """ 

1023 

1024 found = False 

1025 visited: list[tuple[int, int]] = [] 

1026 parent: bs4.Tag | None = el 

1027 while not found and parent and (parent := self.get_parent(parent)): 

1028 key = (id(parent), id(relation)) 

1029 if key in self.anc_cache: 

1030 found = self.anc_cache[key] 

1031 break 

1032 visited.append(key) 

1033 found = yield parent, relation 

1034 

1035 for key in visited: 

1036 self.anc_cache[key] = found 

1037 

1038 return found 

1039 

1040 def match_past_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator: 

1041 """Match past relationship.""" 

1042 

1043 found = False 

1044 # I don't think this can ever happen, but it makes `mypy` happy 

1045 if relation[0] is ct.Null: # pragma: no cover 

1046 return found 

1047 

1048 if relation[0].rel_type == REL_PARENT: 

1049 if self.enable_cache: 

1050 found = yield from self.match_ancestor(el, relation) 

1051 else: 

1052 parent: bs4.Tag | None = el 

1053 while not found and parent and (parent := self.get_parent(parent)): 

1054 found = parent is not None and (yield parent, relation) 

1055 elif relation[0].rel_type == REL_CLOSE_PARENT: 

1056 parent = self.get_parent(el) 

1057 if parent is None: 

1058 pass 

1059 elif self.enable_cache: 

1060 # Siblings share the parent, so cache whether it matches to avoid evaluating it again for every child. 

1061 # This is inlined, instead of going through `match_ancestor`, as an extra generator is costly on 

1062 # documents where most parents have a single child and the cache rarely gets a hit. 

1063 key = (id(parent), id(relation)) 

1064 cached = self.anc_cache.get(key) 

1065 if cached is None: 

1066 cached = self.anc_cache[key] = yield parent, relation 

1067 found = cached 

1068 else: 

1069 found = yield parent, relation 

1070 elif relation[0].rel_type == REL_SIBLING: 

1071 if self.enable_cache: 

1072 found = yield from self.match_general_sibling(el, relation) 

1073 else: 

1074 sibling: bs4.Tag | None = el 

1075 while not found and sibling and (sibling := self.get_previous_tag(sibling)): 

1076 found = sibling is not None and (yield sibling, relation) 

1077 elif relation[0].rel_type == REL_CLOSE_SIBLING: 

1078 sibling = self.get_previous_tag(el) 

1079 found = sibling is not None and (yield sibling, relation) 

1080 return found 

1081 

1082 def match_future_child( 

1083 self, 

1084 parent: bs4.Tag, 

1085 relation: ct.SelectorList, 

1086 recursive: bool = False 

1087 ) -> MatchGenerator: 

1088 """Match future child.""" 

1089 

1090 match = False 

1091 if recursive: 

1092 children: Callable[..., Iterator[bs4.Tag]] = self.get_tag_descendants 

1093 else: 

1094 children = self.get_tag_children 

1095 for child in children(parent): 

1096 if (yield child, relation): 

1097 match = True 

1098 break 

1099 return match 

1100 

1101 def match_descendant(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator: 

1102 """ 

1103 Match descendant relation of `:has()`. 

1104 

1105 Every element matched by `:has()` searches all of its descendants, which is quadratic in the depth 

1106 of the document. Every element visited on the search shares the result, so cache whether each one 

1107 is or has a matching descendant: an element searched without a match is cached along with all of 

1108 its descendants, and when there is a match, the element and all of those it is nested in are cached. 

1109 """ 

1110 

1111 found = False 

1112 # Elements whose contents are being searched, outermost first, and elements searched without a match 

1113 searching: list[tuple[int, int]] = [] 

1114 searched: list[tuple[int, int]] = [] 

1115 stack = [self.get_tag_children(el)] 

1116 while stack and not found: 

1117 for child in stack[-1]: 

1118 key = (id(child), id(relation)) 

1119 if key in self.desc_cache: 

1120 if self.desc_cache[key]: 

1121 found = True 

1122 break 

1123 continue 

1124 searching.append(key) 

1125 if (yield child, relation): 

1126 found = True 

1127 else: 

1128 stack.append(self.get_tag_children(child)) 

1129 break 

1130 else: 

1131 stack.pop() 

1132 if searching: 

1133 searched.append(searching.pop()) 

1134 

1135 for key in searching: 

1136 self.desc_cache[key] = True 

1137 for key in searched: 

1138 self.desc_cache[key] = False 

1139 

1140 return found 

1141 

1142 def match_future_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator: 

1143 """Match future relationship.""" 

1144 

1145 found = False 

1146 # I don't think this can ever happen, but it makes `mypy` happy 

1147 if relation[0] is ct.Null: # pragma: no cover 

1148 return found 

1149 

1150 if relation[0].rel_type == REL_HAS_PARENT: 

1151 if self.enable_cache: 

1152 found = yield from self.match_descendant(el, relation) 

1153 else: 

1154 found = yield from self.match_future_child(el, relation, True) 

1155 elif relation[0].rel_type == REL_HAS_CLOSE_PARENT: 

1156 found = yield from self.match_future_child(el, relation) 

1157 elif relation[0].rel_type == REL_HAS_SIBLING: 

1158 if self.enable_cache: 

1159 found = yield from self.match_general_sibling(el, relation) 

1160 else: 

1161 sibling: bs4.Tag | None = el 

1162 while not found and sibling and (sibling := self.get_next_tag(sibling)): 

1163 found = yield sibling, relation 

1164 elif relation[0].rel_type == REL_HAS_CLOSE_SIBLING: 

1165 sibling = self.get_next_tag(el) 

1166 found = sibling is not None and (yield sibling, relation) 

1167 return found 

1168 

1169 def match_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator: 

1170 """Match relationship to other elements.""" 

1171 

1172 sel = relation[0] 

1173 if sel is not ct.Null and sel.rel_type is not None and sel.rel_type.startswith(':'): 

1174 return self.match_future_relations(el, relation) 

1175 return self.match_past_relations(el, relation) 

1176 

1177 def match_id(self, el: bs4.Tag, ids: tuple[str, ...]) -> bool: 

1178 """Match element's ID.""" 

1179 

1180 found = True 

1181 for i in ids: 

1182 if i != self.get_attribute_by_name(el, 'id', ''): 

1183 found = False 

1184 break 

1185 return found 

1186 

1187 def match_classes(self, el: bs4.Tag, classes: tuple[str, ...]) -> bool: 

1188 """Match element's classes.""" 

1189 

1190 current_classes = self.get_classes(el) 

1191 found = True 

1192 for c in classes: 

1193 if c not in current_classes: 

1194 found = False 

1195 break 

1196 return found 

1197 

1198 def match_root(self, el: bs4.Tag) -> bool: 

1199 """Match element as root.""" 

1200 

1201 is_root = self.is_root(el) 

1202 if is_root: 

1203 sibling: Any = self.get_previous(el) 

1204 while is_root and sibling is not None: 

1205 if ( 

1206 self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or 

1207 self.is_cdata(sibling) 

1208 ): 

1209 is_root = False 

1210 else: 

1211 sibling = self.get_previous(sibling) 

1212 if is_root: 

1213 sibling = self.get_next(el) 

1214 while is_root and sibling is not None: 

1215 if ( 

1216 self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or 

1217 self.is_cdata(sibling) 

1218 ): 

1219 is_root = False 

1220 else: 

1221 sibling = self.get_next(sibling) 

1222 return is_root 

1223 

1224 def match_scope(self, el: bs4.Tag) -> bool: 

1225 """Match element as scope.""" 

1226 

1227 return self.scope is el 

1228 

1229 def match_nth_tag_type(self, el: bs4.Tag, child: bs4.Tag) -> bool: 

1230 """Match tag type for `nth` matches.""" 

1231 

1232 return ( 

1233 (self.get_tag(child) == self.get_tag(el)) and 

1234 (self.get_tag_ns(child) == self.get_tag_ns(el)) 

1235 ) 

1236 

1237 def match_nth(self, el: bs4.Tag, nth: tuple[ct.SelectorNth, ...]) -> MatchGenerator: 

1238 """Match `nth` elements.""" 

1239 

1240 key: tuple[int, int, str | None, str | None] | None = None 

1241 start = rindex = 0 

1242 incr = rincr = 0 

1243 

1244 # Get the parent and use the ID for the cache key. The top level 

1245 # contents of a `template` have no parent element, but they do have siblings. 

1246 parent = self.get_parent(el, no_doc=False, no_template=False) 

1247 pkey = id(parent) if self.enable_cache and parent else None 

1248 

1249 # Test element against the `nth` selectors. 

1250 matched = True 

1251 for n in nth: 

1252 matched = False 

1253 last = n.last 

1254 key = None 

1255 

1256 # Prepare the child iterator and get the starting, real index and the relative index 

1257 if pkey is not None and parent: 

1258 # Get last info from the cache 

1259 if pkey: 

1260 key = ( 

1261 (pkey, id(n), self.get_tag(el), self.get_tag_ns(el)) 

1262 if n.of_type 

1263 else (pkey, id(n), None, None) 

1264 ) 

1265 

1266 valid = False 

1267 if key and key in self.nth_cache: 

1268 start, rindex = self.nth_cache[key] 

1269 if self.within(el, parent, start): 

1270 last = False 

1271 rincr = -1 if n.last else 1 

1272 valid = True 

1273 

1274 # Start/overwrite the cache if the cache was empty or invalid 

1275 if not valid: 

1276 start, rindex = len(parent) - 1 if last else 0, 0 

1277 self.nth_cache[key] = [start, rindex] 

1278 rincr = 1 

1279 

1280 incr = 1 if not last else -1 

1281 children = self.get_children(parent, start=start, reverse=last, template_contents=True) 

1282 

1283 # Non-cached handling of parented element 

1284 elif parent: 

1285 rindex = 0 

1286 start = len(parent) - 1 if last else 0 

1287 rincr = incr = 1 

1288 children = self.get_children(parent, start=start, reverse=last, template_contents=True) 

1289 

1290 # No parent, just evaluate the element against the selectors 

1291 else: 

1292 start = rindex = 0 

1293 rincr = incr = 1 

1294 children = iter([el]) 

1295 

1296 # Find index of element compared to its siblings and check the index conditions 

1297 child: bs4.Tag 

1298 for child in children: 

1299 start += incr 

1300 

1301 # We only care about tags 

1302 if not self.is_tag(child): 

1303 continue 

1304 

1305 # Handle `of S` in `nth-child` and handle `of-type` 

1306 if ( 

1307 (n.selectors and not (yield child, n.selectors)) or 

1308 (n.of_type and not self.match_nth_tag_type(el, child)) 

1309 ): 

1310 if child is el: 

1311 # Cached "last" lookups count down from `rindex`, so it must be the 

1312 # position `el` would have had, even though `el` did not match. 

1313 if last: 

1314 rindex += 1 

1315 break 

1316 continue 

1317 

1318 # Test the relative index against the `nth` requirement. 

1319 rindex += rincr 

1320 if child is el: 

1321 if n.a != 0: 

1322 v = (rindex - n.b) / n.a 

1323 matched = v.is_integer() and v >= 0 

1324 else: 

1325 matched = rindex == n.b and n.b >= 1 

1326 break 

1327 

1328 # "Last index" selectors evaluate first from the bottom and then evaluate 

1329 # from the first found element top-down. Start will be incremented in the 

1330 # wrong direction, so increment it and step over the current index. 

1331 if last: 

1332 start += 2 

1333 

1334 # Update the cache 

1335 if key: 

1336 self.nth_cache[key] = [start, rindex] 

1337 

1338 # If we failed to match any `nth` selectors, quit. 

1339 if not matched: 

1340 break 

1341 

1342 return matched 

1343 

1344 def match_empty(self, el: bs4.Tag) -> bool: 

1345 """Check if element is empty (if requested).""" 

1346 

1347 is_empty = True 

1348 child: Any 

1349 for child in self.get_children(el): 

1350 if self.is_tag(child): 

1351 is_empty = False 

1352 break 

1353 elif self.is_content_string(child) and RE_NOT_EMPTY.search(child): 

1354 is_empty = False 

1355 break 

1356 return is_empty 

1357 

1358 def match_subselectors(self, el: bs4.Tag, selectors: tuple[ct.SelectorList, ...]) -> MatchGenerator: 

1359 """Match selectors.""" 

1360 

1361 match = True 

1362 for sel in selectors: 

1363 if not (yield el, sel): 

1364 match = False 

1365 return match 

1366 

1367 def match_contains(self, el: bs4.Tag, contains: tuple[ct.SelectorContains, ...]) -> bool: 

1368 """Match element if it contains text.""" 

1369 

1370 match = True 

1371 content: str | None = None 

1372 own_content: Sequence[str] | None = None 

1373 for contain_list in contains: 

1374 found = False 

1375 for text in contain_list.text: 

1376 if contain_list.own: 

1377 if own_content is None: 

1378 own_content = self.get_own_text(el) 

1379 for c in own_content: 

1380 if text in c: 

1381 found = True 

1382 break 

1383 if found: 

1384 break 

1385 else: 

1386 if content is None: 

1387 content = self.get_text(el) 

1388 if text in content: 

1389 found = True 

1390 break 

1391 if not found: 

1392 match = False 

1393 return match 

1394 

1395 def get_tree(self, el: bs4.Tag) -> bs4.Tag: 

1396 """Get the top of the tree that an element is in: its document or `template`.""" 

1397 

1398 # Every element visited shares the top of the tree, so cache it to avoid walking the same ancestors again. 

1399 visited: list[int] = [] 

1400 top = el 

1401 parent = self.get_parent(top, no_doc=False, no_template=False) 

1402 while parent is not None: 

1403 key = id(top) 

1404 if key in self.tree_cache: 

1405 top = self.tree_cache[key] 

1406 break 

1407 visited.append(key) 

1408 top = parent 

1409 if self.is_template(top): 

1410 break 

1411 parent = self.get_parent(top, no_doc=False, no_template=False) 

1412 if self.enable_cache: 

1413 for key in visited: 

1414 self.tree_cache[key] = top 

1415 return top 

1416 

1417 def is_submit_button(self, el: bs4.Tag) -> bool: 

1418 """Check if an `input` or `button` is a submit button.""" 

1419 

1420 v = util.lower(cast(str, self.get_attribute_by_name(el, 'type', ''))) 

1421 if self.get_tag(el) == 'input': 

1422 return v in ('submit', 'image') 

1423 

1424 # A button with a missing or invalid `type` submits, unless it invokes a command or is 

1425 # a child of a `select`. 

1426 return v == 'submit' or ( 

1427 v not in ('button', 'reset') and 

1428 self.get_attribute_by_name(el, 'command') is None and 

1429 self.get_attribute_by_name(el, 'commandfor') is None and 

1430 self.get_tag(self.get_parent(el)) != 'select' 

1431 ) 

1432 

1433 def get_forms(self, tree: bs4.Tag) -> TreeForms: 

1434 """ 

1435 Get the form information of a tree. 

1436 

1437 A form owner is the form that the `form` attribute refers to, if there is one, otherwise the nearest 

1438 ancestor form. The attribute is only used by elements connected to a document, which the contents of 

1439 a `template` are not. A `template` holds its own tree, so it is not entered. 

1440 

1441 This requires a pass through the whole tree as the `form` attribute can refer to any form, so it is 

1442 cached instead of searching the tree again for every element. 

1443 

1444 A form in a form only exists in XML and in a `template`. Anywhere else, it can only have come from a 

1445 parser that doesn't follow the HTML specification. A browser's parser would have ignored the nested 

1446 form and taken its end tag for that of the parent, so what it holds belongs to the parent, and 

1447 what follows it does not. 

1448 """ 

1449 

1450 key = id(tree) 

1451 forms = self.cached_forms.get(key) 

1452 if forms is not None: 

1453 return forms 

1454 

1455 ids: dict[str, bs4.Tag] = {} 

1456 controls: list[tuple[bs4.Tag, bs4.Tag | None]] = [] 

1457 closed: set[int] = set() 

1458 nesting = self.is_xml or self.is_template(tree) 

1459 stack: list[tuple[bs4.Tag | None, bs4.Tag | None]] = [ 

1460 (el, None) for el in self.get_children(tree, reverse=True, tags=True, template_contents=True) 

1461 ] 

1462 while stack: 

1463 el, form = stack.pop() 

1464 

1465 # The end of a nested form, which ends the form it is in 

1466 if el is None: 

1467 closed.add(id(form)) 

1468 continue 

1469 

1470 if id(form) in closed: 

1471 form = None 

1472 name = self.get_tag(el) if self.is_html_tag(el) else None 

1473 if name == 'form' and form is not None and not nesting: 

1474 stack.append((None, form)) 

1475 else: 

1476 el_id = cast(str, self.get_attribute_by_name(el, 'id', '')) 

1477 if el_id: 

1478 ids.setdefault(el_id, el) 

1479 if name == 'form': 

1480 form = el 

1481 elif name in ('input', 'button'): 

1482 controls.append((el, form)) 

1483 stack.extend((child, form) for child in self.get_tag_children(el, reverse=True)) 

1484 

1485 forms = TreeForms({}, {}, {}) 

1486 connected = not self.is_template(tree) 

1487 for el, form in controls: 

1488 form_id = self.get_attribute_by_name(el, 'form') 

1489 if connected and form_id is not None: 

1490 form = ids.get(cast(str, form_id)) 

1491 if form is not None and not (self.get_tag(form) == 'form' and self.is_html_tag(form)): 

1492 form = None 

1493 forms.owners[id(el)] = form 

1494 

1495 # Controls are in tree order, so the first submit button of a form is its default button, 

1496 # and the last radio button with `checked` in a radio button group is the one left checked. 

1497 radio_name = cast(str, self.get_attribute_by_name(el, 'name', '')) 

1498 if form is not None and self.is_submit_button(el): 

1499 forms.defaults.setdefault(id(form), el) 

1500 elif ( 

1501 radio_name and 

1502 self.get_attribute_by_name(el, 'checked') is not None and 

1503 util.lower(cast(str, self.get_attribute_by_name(el, 'type', ''))) == 'radio' 

1504 ): 

1505 forms.checked[(id(form), radio_name)] = el 

1506 

1507 self.cached_forms[key] = forms 

1508 return forms 

1509 

1510 def match_default(self, el: bs4.Tag) -> bool: 

1511 """Match the default button of a form, which is the first submit button in the tree that it owns.""" 

1512 

1513 forms = self.get_forms(self.get_tree(el)) 

1514 form = forms.owners.get(id(el)) 

1515 return form is not None and forms.defaults.get(id(form)) is el 

1516 

1517 def get_radio_group(self, el: bs4.Tag, forms: TreeForms) -> tuple[int, str]: 

1518 """Get the key of the radio button group of a radio button with a name.""" 

1519 

1520 return id(forms.owners.get(id(el))), cast(str, self.get_attribute_by_name(el, 'name')) 

1521 

1522 def match_indeterminate(self, el: bs4.Tag) -> bool: 

1523 """Match a radio button in a radio button group that has no checked radio buttons.""" 

1524 

1525 forms = self.get_forms(self.get_tree(el)) 

1526 return self.get_radio_group(el, forms) not in forms.checked 

1527 

1528 def match_checked_radio(self, el: bs4.Tag) -> bool: 

1529 """ 

1530 Match a radio button with a `checked` attribute if it is still checked. 

1531 

1532 A radio button that is checked unchecks the others in its group when it is added to a document, so 

1533 only the last one is left checked. The contents of a `template` are never added to a document, 

1534 so all of them stay checked. 

1535 """ 

1536 

1537 if not self.get_attribute_by_name(el, 'name'): 

1538 return True 

1539 

1540 tree = self.get_tree(el) 

1541 if self.is_template(tree): 

1542 return True 

1543 

1544 forms = self.get_forms(tree) 

1545 return forms.checked.get(self.get_radio_group(el, forms)) is el 

1546 

1547 def get_options(self, select: bs4.Tag) -> dict[int, bool]: 

1548 """ 

1549 Get the selectedness of each option in the list of options of a `select`. 

1550 

1551 An option that is not in a `select` with `multiple` is left selected if it is the last one with 

1552 a `selected` attribute, and if there are none, the first enabled option is selected when 

1553 the `select` shows a single option. 

1554 

1555 Other than in XML, a nested `option`, `optgroup`, or `select` can only have come from a parser that 

1556 doesn't follow the HTML specification. A browser's parser would have ended the first one, so 

1557 an `option` or `optgroup` is treated as a sibling, and a `select` ends the list of options. 

1558 """ 

1559 

1560 key = id(select) 

1561 selectedness = self.cached_options.get(key) 

1562 if selectedness is not None: 

1563 return selectedness 

1564 

1565 options: list[tuple[bs4.Tag, bool]] = [] 

1566 stack: list[tuple[bs4.Tag, bool | None]] = [ 

1567 (el, None) for el in self.get_tag_children(select, reverse=True) 

1568 ] 

1569 while stack: 

1570 el, group = stack.pop() 

1571 name = self.get_tag(el) if self.is_html_tag(el) else None 

1572 if name == 'select' and not self.is_xml: 

1573 break 

1574 if name == 'option': 

1575 options.append((el, bool(group))) 

1576 if name in ('select', 'hr', 'datalist') or ( 

1577 self.is_xml and (name == 'option' or (name == 'optgroup' and group is not None)) 

1578 ): 

1579 continue 

1580 if name == 'optgroup': 

1581 group = self.get_attribute_by_name(el, 'disabled') is not None 

1582 stack.extend((child, group) for child in self.get_tag_children(el, reverse=True)) 

1583 

1584 marked = [el for el, _ in options if self.get_attribute_by_name(el, 'selected') is not None] 

1585 if self.get_attribute_by_name(select, 'multiple') is None: 

1586 size = RE_SIZE.match(cast(str, self.get_attribute_by_name(select, 'size', ''))) 

1587 if marked: 

1588 marked = marked[-1:] 

1589 elif size is None or size.group('value').lstrip('0') in ('', '1'): 

1590 marked = [ 

1591 el for el, disabled in options 

1592 if not disabled and self.get_attribute_by_name(el, 'disabled') is None 

1593 ][:1] 

1594 

1595 selected = {id(el) for el in marked} 

1596 selectedness = {id(el): id(el) in selected for el, _ in options} 

1597 self.cached_options[key] = selectedness 

1598 return selectedness 

1599 

1600 def match_checked_option(self, el: bs4.Tag) -> bool: 

1601 """Match a selected option. Without a `select`, an option is only selected by its `selected` attribute.""" 

1602 

1603 # Every element visited shares the `select`, so cache it to avoid walking the same ancestors again. 

1604 visited: list[int] = [] 

1605 node = el 

1606 while True: 

1607 key = id(node) 

1608 if key in self.select_cache: 

1609 parent = self.select_cache[key] 

1610 break 

1611 visited.append(key) 

1612 parent = self.get_parent(node) 

1613 if parent is None or (self.get_tag(parent) == 'select' and self.is_html_tag(parent)): 

1614 break 

1615 node = parent 

1616 if self.enable_cache: 

1617 for key in visited: 

1618 self.select_cache[key] = parent 

1619 

1620 selected = self.get_options(parent).get(id(el)) if parent is not None else None 

1621 return self.get_attribute_by_name(el, 'selected') is not None if selected is None else selected 

1622 

1623 def match_lang(self, el: bs4.Tag, langs: tuple[ct.SelectorLang, ...]) -> bool: 

1624 """Match languages.""" 

1625 

1626 # Every element visited while resolving shares the language, 

1627 # so cache it to avoid walking the same ancestors again for each element. 

1628 visited: list[int] = [] 

1629 found_lang = self.resolve_lang(el, visited) 

1630 if self.enable_cache: 

1631 for key in visited: 

1632 self.lang_cache[key] = found_lang 

1633 

1634 # A language tag that is not well-formed matches nothing, not even `:lang('')` or `:lang('*')`. 

1635 if found_lang and RE_LANG_TAG.fullmatch(found_lang) is None: 

1636 return False 

1637 

1638 match = False 

1639 for patterns in langs: 

1640 match = False 

1641 for pattern in patterns: 

1642 if self.extended_language_filter(pattern, found_lang): 

1643 match = True 

1644 if not match: 

1645 break 

1646 

1647 return match 

1648 

1649 def resolve_lang(self, el: bs4.Tag, visited: list[int]) -> str: 

1650 """Resolve the language of an element, which is an empty string if it is unknown.""" 

1651 

1652 has_ns = self.supports_namespaces() 

1653 root = self.root 

1654 has_html_namespace = self.has_html_namespace 

1655 

1656 # Walk parents looking for `lang` (HTML) or `xml:lang` XML property. 

1657 parent: bs4.Tag | None = el 

1658 found_lang = None 

1659 last = None 

1660 while found_lang is None: 

1661 key = id(parent) 

1662 if key in self.lang_cache: 

1663 return self.lang_cache[key] 

1664 visited.append(key) 

1665 has_html_ns = self.has_html_ns(parent) 

1666 # SVG elements also use a `lang` attribute in no namespace, but `xml:lang` takes precedence. 

1667 has_svg_ns = has_ns and getattr(parent, 'namespace', None) == NS_SVG 

1668 svg_lang = None 

1669 for k, v in self.iter_attributes(parent): 

1670 attr_ns, attr = self.split_namespace(parent, k) 

1671 if ( 

1672 ((not has_ns or has_html_ns) and (util.lower(k) if not self.is_xml else k) == 'lang') or 

1673 ( 

1674 has_ns and not has_html_ns and attr_ns == NS_XML and 

1675 (util.lower(attr) if not self.is_xml and attr is not None else attr) == 'lang' 

1676 ) 

1677 ): 

1678 found_lang = v 

1679 break 

1680 if has_svg_ns and attr_ns is None and (util.lower(k) if not self.is_xml else k) == 'lang': 

1681 svg_lang = v 

1682 if found_lang is None: 

1683 found_lang = svg_lang 

1684 last = parent 

1685 parent = self.get_parent(parent) 

1686 

1687 if parent is None: 

1688 root = last 

1689 has_html_namespace = self.has_html_ns(root) 

1690 parent = last 

1691 break 

1692 

1693 # Use cached meta language. 

1694 if found_lang is None and self.cached_meta_lang: 

1695 for cache in self.cached_meta_lang: 

1696 if root is not None and cast(str, root) is cache[0]: 

1697 return cache[1] 

1698 

1699 # If we couldn't find a language, and the document is HTML, look to meta to determine language. 

1700 if found_lang is None and (not self.is_xml or (has_html_namespace and root and root.name == 'html')): 

1701 # Find head 

1702 found = False 

1703 for tag in ('html', 'head'): 

1704 found = False 

1705 for child in self.get_tag_children(parent): 

1706 if self.get_tag(child) == tag and self.is_html_tag(child): 

1707 found = True 

1708 parent = child 

1709 break 

1710 if found: # pragma: no cover 

1711 break 

1712 

1713 # Search meta tags 

1714 if found and parent is not None: 

1715 for child2 in parent: 

1716 if isinstance(child2, bs4.Tag) and self.get_tag(child2) == 'meta' and self.is_html_tag(parent): 

1717 c_lang = False 

1718 content = None 

1719 for k, v in self.iter_attributes(child2): 

1720 if util.lower(k) == 'http-equiv' and util.lower(v) == 'content-language': 

1721 c_lang = True 

1722 if util.lower(k) == 'content': 

1723 content = v 

1724 if c_lang and content: 

1725 found_lang = content 

1726 self.cached_meta_lang.append((cast(str, root), cast(str, found_lang))) 

1727 break 

1728 if found_lang is not None: 

1729 break 

1730 if found_lang is None: 

1731 self.cached_meta_lang.append((cast(str, root), '')) 

1732 

1733 # Without any language information, the language is unknown, which is an empty language tag. 

1734 return '' if found_lang is None else cast(str, found_lang) 

1735 

1736 def match_dir(self, el: bs4.Tag, directionality: int) -> bool: 

1737 """Check directionality.""" 

1738 

1739 # Direction is only supported in HTML documents. 

1740 if not self.is_html: 

1741 return False 

1742 

1743 # If we have to match both left and right, we can't match either. 

1744 if directionality & ct.SEL_DIR_LTR and directionality & ct.SEL_DIR_RTL: 

1745 return False 

1746 

1747 # Every element visited while resolving shares the resolved direction, 

1748 # so cache it to avoid walking the same ancestors again for each element. 

1749 visited: list[int] = [] 

1750 direction = self.resolve_dir(el, visited) 

1751 if self.enable_cache: 

1752 for key in visited: 

1753 self.dir_cache[key] = direction 

1754 return direction == directionality 

1755 

1756 def resolve_dir(self, el: bs4.Tag, visited: list[int]) -> int: 

1757 """Resolve the direction of an element.""" 

1758 

1759 while True: 

1760 key = id(el) 

1761 if key in self.dir_cache: 

1762 return self.dir_cache[key] 

1763 visited.append(key) 

1764 

1765 # `dir` is only defined for HTML elements, so other elements (SVG, MathML, etc.) 

1766 # always inherit the direction of their parent. 

1767 if self.is_html_tag(el): 

1768 

1769 # Element has defined direction of left to right or right to left 

1770 direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(el, 'dir', '')), None) 

1771 if direction not in (None, 0): 

1772 return direction 

1773 

1774 # If `input[type=telephone]` and no direction is assigned, assume left to right. 

1775 name = self.get_tag(el) 

1776 is_input = name == 'input' 

1777 is_textarea = name == 'textarea' 

1778 is_bdi = name == 'bdi' 

1779 itype = util.lower(self.get_attribute_by_name(el, 'type', '')) if is_input else '' 

1780 if is_input and itype == 'tel' and direction is None: 

1781 return ct.SEL_DIR_LTR 

1782 

1783 # Auto handling for inputs whose value is used 

1784 if ((is_input and itype not in NON_AUTO_DIR_INPUT_TYPES) or is_textarea) and direction == 0: 

1785 if is_textarea: 

1786 value = ''.join( 

1787 node for node in self.get_contents(el) # type: ignore[misc] 

1788 if self.is_content_string(node) 

1789 ) 

1790 else: 

1791 value = cast(str, self.get_attribute_by_name(el, 'value', '')) 

1792 direction = self.find_text_dir(value) 

1793 # No strong directional characters (or no value), assume left to right 

1794 return direction if direction is not None else ct.SEL_DIR_LTR 

1795 

1796 # Auto handling for `bdi` and other non text inputs. 

1797 if (is_bdi and direction is None) or direction == 0: 

1798 direction = self.find_bidi(el) 

1799 if direction is not None: 

1800 return direction 

1801 # Auto directionality could not be determined, assume left to right 

1802 return ct.SEL_DIR_LTR 

1803 

1804 # Match parents direction. There may be no parent element, and not just for the root: 

1805 # a fragment can have many top level elements, and a `template` has its own contents. 

1806 # Without one, assume left to right. 

1807 parent = self.get_parent(el) 

1808 if parent is None: 

1809 return ct.SEL_DIR_LTR 

1810 el = parent 

1811 

1812 def match_range(self, el: bs4.Tag, condition: int) -> bool: 

1813 """ 

1814 Match range. 

1815 

1816 Behavior is modeled after what we see in browsers. Browsers seem to evaluate 

1817 if the value is out of range, and if not, it is in range. So a missing value 

1818 will not evaluate out of range; therefore, value is in range. Personally, I 

1819 feel like this should evaluate as neither in or out of range. 

1820 """ 

1821 

1822 out_of_range = False 

1823 

1824 itype = util.lower(cast(str, self.get_attribute_by_name(el, 'type', ''))) 

1825 mn = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'min', None))) 

1826 mx = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'max', None))) 

1827 

1828 # There is no valid min or max, so we cannot evaluate a range 

1829 if mn is None and mx is None: 

1830 return False 

1831 

1832 value = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'value', None))) 

1833 if value is not None: 

1834 if itype in ("date", "datetime-local", "month", "week", "number", "range"): 

1835 if mn is not None and value < mn: 

1836 out_of_range = True 

1837 if not out_of_range and mx is not None and value > mx: 

1838 out_of_range = True 

1839 elif itype == "time": 

1840 if mn is not None and mx is not None and mn > mx: 

1841 # Time is periodic, so this is a reversed/discontinuous range 

1842 if value < mn and value > mx: 

1843 out_of_range = True 

1844 else: 

1845 if mn is not None and value < mn: 

1846 out_of_range = True 

1847 if not out_of_range and mx is not None and value > mx: 

1848 out_of_range = True 

1849 

1850 return not out_of_range if condition & ct.SEL_IN_RANGE else out_of_range 

1851 

1852 def match_defined(self, el: bs4.Tag) -> bool: 

1853 """ 

1854 Match defined. 

1855 

1856 `:defined` is related to custom elements in a browser. 

1857 

1858 - If the document is XML (not XHTML), no tags will match. 

1859 - Tags that are not custom (don't have a hyphen) are marked defined. 

1860 - If the tag has a prefix (without or without a namespace), it will not match. 

1861 - HTML elements with an `is` attribute (customized built-in elements) are created as undefined, 

1862 like custom tags, so they don't match. 

1863 

1864 This is of course requires the parser to provide us with the proper prefix and namespace info, 

1865 if it doesn't, there is nothing we can do. 

1866 """ 

1867 

1868 name = self.get_tag(el) 

1869 return ( 

1870 self.is_html and name is not None and ( 

1871 (name.find('-') == -1 and not self.is_customized_builtin(el)) or 

1872 name.find(':') != -1 or 

1873 self.get_prefix(el) is not None 

1874 ) 

1875 ) 

1876 

1877 def is_customized_builtin(self, el: bs4.Tag) -> bool: 

1878 """Check if an HTML element has an `is` attribute, which makes it a customized built-in element.""" 

1879 

1880 return self.is_html_tag(el) and self.get_attribute_by_name(el, 'is') is not None 

1881 

1882 def match_placeholder_shown(self, el: bs4.Tag) -> bool: 

1883 """ 

1884 Match placeholder shown according to HTML spec. 

1885 

1886 - text area should be checked if they have content. A single newline does not count as content. 

1887 

1888 """ 

1889 

1890 match = False 

1891 content = self.get_text(el) 

1892 if content in ('', '\n'): 

1893 match = True 

1894 

1895 return match 

1896 

1897 def match_editable(self, el: bs4.Tag) -> bool: 

1898 """ 

1899 Match editing hosts and the elements that are editable inside them. 

1900 

1901 The nearest `contenteditable` attribute with a valid state decides: an empty value, `true` and 

1902 `plaintext-only` make the element editable, and `false` does not. Any other value inherits 

1903 the state of the parent. The attribute only applies to HTML elements, others (SVG, MathML, 

1904 etc.) always inherit the state of their parent. 

1905 """ 

1906 

1907 # Every element visited shares the state, so cache it to avoid walking the same ancestors again. 

1908 visited: list[int] = [] 

1909 editable = False 

1910 node = el # type: bs4.Tag | None 

1911 while node is not None: 

1912 key = id(node) 

1913 if key in self.edit_cache: 

1914 editable = self.edit_cache[key] 

1915 break 

1916 visited.append(key) 

1917 state = self.get_attribute_by_name(node, 'contenteditable') if self.is_html_tag(node) else None 

1918 if isinstance(state, str): 

1919 state = util.lower(state) 

1920 if state in ('', 'true', 'plaintext-only'): 

1921 editable = True 

1922 break 

1923 if state == 'false': 

1924 break 

1925 node = self.get_parent(node) 

1926 if self.enable_cache: 

1927 for key in visited: 

1928 self.edit_cache[key] = editable 

1929 return editable 

1930 

1931 def match_selectors(self, el: bs4.Tag, selectors: ct.SelectorList) -> bool: 

1932 """ 

1933 Check if element matches one of the selectors. 

1934 

1935 Since some selectors can have complex relationships, they may need to recursively 

1936 check if another relative element matches specific selectors. To avoid recursion, 

1937 generators are yielded instead whenever it needs to check if a relative element 

1938 satisfies specific selectors. These can be compared and the result sent back. 

1939 We can check the end result of the generator once it is satisfied. 

1940 """ 

1941 

1942 # Most matches never need a nested match, so avoid setting up the stack unless needed. 

1943 # `frame` yields a `bool` when it is done. 

1944 frame = self._match_selectors(el, selectors) 

1945 item = next(frame) 

1946 if item.__class__ is bool: 

1947 return item 

1948 

1949 # We need to setup a stack as a selector has sent back a match request. 

1950 stack = [frame, self._match_selectors(*cast('MatchRequest', item))] 

1951 result: bool | None = None 

1952 while True: 

1953 item = stack[-1].send(result) # type: ignore[arg-type] 

1954 if item.__class__ is bool: 

1955 stack.pop() 

1956 if not stack: 

1957 return item 

1958 result = item 

1959 else: 

1960 stack.append(self._match_selectors(*cast('MatchRequest', item))) 

1961 result = None 

1962 

1963 def _match_selectors(self, el: bs4.Tag, selectors: ct.SelectorList) -> Generator[MatchRequest | bool, bool, None]: 

1964 """ 

1965 Check if element matches one of the selectors, yielding requests to match nested selectors. 

1966 

1967 Yields `MatchRequests` or affirmation as to whether a selector was matched. The match request 

1968 requires a boolean to be sent back to communicate whether the match was successful. 

1969 """ 

1970 

1971 match = False 

1972 is_not = selectors.is_not 

1973 is_html = selectors.is_html 

1974 

1975 # Internal selector lists that use the HTML flag, will automatically get the `html` namespace. 

1976 if is_html: 

1977 namespaces = self.namespaces 

1978 self.namespaces = {'html': NS_XHTML} 

1979 

1980 if not is_html or self.is_html: 

1981 for selector in selectors: 

1982 match = is_not 

1983 # We have a un-matchable situation (like `:focus` as you can focus an element in this environment) 

1984 if selector is ct.Null: 

1985 continue 

1986 # Verify tag matches 

1987 if not self.match_tag(el, selector.tag): 

1988 continue 

1989 # Verify tag is defined 

1990 if selector.flags & ct.SEL_DEFINED and not self.match_defined(el): 

1991 continue 

1992 # Verify element is root 

1993 if selector.flags & ct.SEL_ROOT and not self.match_root(el): 

1994 continue 

1995 # Verify element is scope 

1996 if selector.flags & ct.SEL_SCOPE and not self.match_scope(el): 

1997 continue 

1998 # Verify element has placeholder shown 

1999 if selector.flags & ct.SEL_PLACEHOLDER_SHOWN and not self.match_placeholder_shown(el): 

2000 continue 

2001 # Verify element is an editing host or editable 

2002 if selector.flags & ct.SEL_EDITABLE and not self.match_editable(el): 

2003 continue 

2004 # Verify `nth` matches 

2005 if selector.nth and not (yield from self.match_nth(el, selector.nth)): 

2006 continue 

2007 if selector.flags & ct.SEL_EMPTY and not self.match_empty(el): 

2008 continue 

2009 # Verify id matches 

2010 if selector.ids and not self.match_id(el, selector.ids): 

2011 continue 

2012 # Verify classes match 

2013 if selector.classes and not self.match_classes(el, selector.classes): 

2014 continue 

2015 # Verify attribute(s) match 

2016 if not self.match_attributes(el, selector.attributes): 

2017 continue 

2018 # Verify ranges 

2019 if selector.flags & RANGES and not self.match_range(el, selector.flags & RANGES): 

2020 continue 

2021 # Verify language patterns 

2022 if selector.lang and not self.match_lang(el, selector.lang): 

2023 continue 

2024 # Verify pseudo selector patterns 

2025 if selector.selectors and not (yield from self.match_subselectors(el, selector.selectors)): 

2026 continue 

2027 # Verify relationship selectors 

2028 if selector.relation and not (yield from self.match_relations(el, selector.relation)): 

2029 continue 

2030 # Validate that the current default selector match corresponds to the first submit button in the form 

2031 if selector.flags & ct.SEL_DEFAULT and not self.match_default(el): 

2032 continue 

2033 # Validate that the unset radio button is among radio buttons with the same name in a form that are 

2034 # also not set. 

2035 if selector.flags & ct.SEL_INDETERMINATE and not self.match_indeterminate(el): 

2036 continue 

2037 # Validate that the radio button is left checked in its group or that the option is selected. 

2038 if selector.flags & ct.SEL_CHECKED and not ( 

2039 self.match_checked_option(el) if self.get_tag(el) == 'option' else self.match_checked_radio(el) 

2040 ): 

2041 continue 

2042 # Validate element directionality 

2043 if selector.flags & DIR_FLAGS and not self.match_dir(el, selector.flags & DIR_FLAGS): 

2044 continue 

2045 # Validate that the tag contains the specified text. 

2046 if selector.contains and not self.match_contains(el, selector.contains): 

2047 continue 

2048 match = not is_not 

2049 break 

2050 

2051 # Restore actual namespaces being used for external selector lists 

2052 if is_html: 

2053 self.namespaces = namespaces 

2054 

2055 yield match 

2056 

2057 def select(self, limit: int = 0) -> Iterator[bs4.Tag]: 

2058 """Match all tags under the targeted tag.""" 

2059 

2060 lim = None if limit < 1 else limit 

2061 

2062 # Selecting from a `template` is how its contents are reached, but only for that `template`. 

2063 for child in self.get_tag_descendants(self.tag, template_contents=True): 

2064 if self.match(child): 

2065 yield child 

2066 if lim is not None: 

2067 lim -= 1 

2068 if lim < 1: 

2069 break 

2070 

2071 def closest(self) -> bs4.Tag | None: 

2072 """Match closest ancestor.""" 

2073 

2074 current: bs4.Tag | None = self.tag 

2075 closest = None 

2076 while closest is None and current is not None: 

2077 if self.match(current): 

2078 closest = current 

2079 else: 

2080 current = self.get_parent(current) 

2081 return closest 

2082 

2083 def filter(self) -> list[bs4.Tag]: # noqa A001 

2084 """Filter tag's children.""" 

2085 

2086 return [ 

2087 tag for tag in self.get_contents(self.tag, template_contents=True) 

2088 if isinstance(tag, bs4.Tag) and self.match(tag) 

2089 ] 

2090 

2091 def match(self, el: bs4.Tag) -> bool: 

2092 """Match.""" 

2093 

2094 return not self.is_doc(el) and self.is_tag(el) and self.match_selectors(el, self.selectors) 

2095 

2096 

2097class SoupSieve(ct.Immutable): 

2098 """Compiled Soup Sieve selector matching object.""" 

2099 

2100 pattern: str 

2101 selectors: ct.SelectorList 

2102 namespaces: ct.Namespaces | None 

2103 custom: dict[str, str] 

2104 flags: int 

2105 

2106 __slots__ = ("pattern", "selectors", "namespaces", "custom", "flags", "_hash") 

2107 

2108 def __init__( 

2109 self, 

2110 pattern: str, 

2111 selectors: ct.SelectorList, 

2112 namespaces: ct.Namespaces | None, 

2113 custom: ct.CustomSelectors | None, 

2114 flags: int 

2115 ): 

2116 """Initialize.""" 

2117 

2118 super().__init__( 

2119 pattern=pattern, 

2120 selectors=selectors, 

2121 namespaces=namespaces, 

2122 custom=custom, 

2123 flags=flags 

2124 ) 

2125 

2126 def match(self, tag: bs4.Tag) -> bool: 

2127 """Match.""" 

2128 

2129 return CSSMatch(self.selectors, tag, self.namespaces, self.flags).match(tag) 

2130 

2131 def closest(self, tag: bs4.Tag) -> bs4.Tag | None: 

2132 """Match closest ancestor.""" 

2133 

2134 return CSSMatch(self.selectors, tag, self.namespaces, self.flags).closest() 

2135 

2136 def filter(self, iterable: Iterable[bs4.Tag]) -> list[bs4.Tag]: # noqa A001 

2137 """ 

2138 Filter. 

2139 

2140 `CSSMatch` can cache certain searches for tags of the same document, 

2141 so if we are given a tag, all tags are from the same document, 

2142 and we can take advantage of the optimization. 

2143 

2144 Any other kind of iterable could have tags from different documents or detached tags, 

2145 so for those, we use a new `CSSMatch` for each item in the iterable. 

2146 """ 

2147 

2148 if isinstance(iterable, bs4.Tag): 

2149 return CSSMatch(self.selectors, iterable, self.namespaces, self.flags).filter() 

2150 else: 

2151 # There is no guarantee that elements are from the same document, evaluate them separately. 

2152 return [node for node in iterable if not CSSMatch.is_navigable_string(node) and self.match(node)] 

2153 

2154 def select_one(self, tag: bs4.Tag) -> bs4.Tag | None: 

2155 """Select a single tag.""" 

2156 

2157 tags = self.select(tag, limit=1) 

2158 return tags[0] if tags else None 

2159 

2160 def select(self, tag: bs4.Tag, limit: int = 0) -> list[bs4.Tag]: 

2161 """Select the specified tags.""" 

2162 

2163 return list(self.iselect(tag, limit)) 

2164 

2165 def iselect(self, tag: bs4.Tag, limit: int = 0) -> Iterator[bs4.Tag]: 

2166 """Iterate the specified tags.""" 

2167 

2168 yield from CSSMatch(self.selectors, tag, self.namespaces, self.flags).select(limit) 

2169 

2170 def __repr__(self) -> str: # pragma: no cover 

2171 """Representation.""" 

2172 

2173 return ( 

2174 f"SoupSieve(pattern={self.pattern!r}, namespaces={self.namespaces!r}, " 

2175 f"custom={self.custom!r}, flags={self.flags!r})" 

2176 ) 

2177 

2178 __str__ = __repr__ 

2179 

2180 

2181ct.pickle_register(SoupSieve)