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

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

1018 statements  

1"""CSS matcher.""" 

2from __future__ import annotations 

3from datetime import datetime 

4from math import isfinite 

5from collections.abc import Hashable 

6from . import util 

7import re 

8from . import css_types as ct 

9import unicodedata 

10import bs4 

11from typing import ( 

12 Generator, 

13 Iterator, 

14 Iterable, 

15 Any, 

16 Callable, 

17 Sequence, 

18 overload, 

19 Literal, 

20 cast, 

21 TYPE_CHECKING 

22) # noqa: F401, F811 

23 

24# Empty tag pattern (whitespace okay) 

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

26 

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

28 

29# Relationships 

30REL_PARENT = ' ' 

31REL_CLOSE_PARENT = '>' 

32REL_SIBLING = '~' 

33REL_CLOSE_SIBLING = '+' 

34 

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

36REL_HAS_PARENT = ': ' 

37REL_HAS_CLOSE_PARENT = ':>' 

38REL_HAS_SIBLING = ':~' 

39REL_HAS_CLOSE_SIBLING = ':+' 

40 

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

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

43if TYPE_CHECKING: # pragma: no cover 

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

45 MatchGenerator = Generator[MatchRequest, bool, bool] 

46 

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

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

49 

50DIR_FLAGS = ct.SEL_DIR_LTR | ct.SEL_DIR_RTL 

51RANGES = ct.SEL_IN_RANGE | ct.SEL_OUT_OF_RANGE 

52 

53DIR_MAP = { 

54 'ltr': ct.SEL_DIR_LTR, 

55 'rtl': ct.SEL_DIR_RTL, 

56 'auto': 0 

57} 

58 

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

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

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

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

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

64RE_DATETIME = re.compile( 

65 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})' 

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

67) 

68RE_WILD_STRIP = re.compile(r'(?:(?:-\*-)(?:\*(?:-|$))*|-\*$)') 

69 

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

71FEB = 2 

72SHORT_MONTH = 30 

73LONG_MONTH = 31 

74FEB_MONTH = 28 

75FEB_LEAP_MONTH = 29 

76DAYS_IN_WEEK = 7 

77 

78 

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

80 """Check if target is within data.""" 

81 

82 contents = parent.contents 

83 return any(contents[i] is target for i in range(start, end if end is not None else len(contents))) 

84 

85 

86class _DocumentNav: 

87 """Navigate a Beautiful Soup document.""" 

88 

89 @classmethod 

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

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

92 

93 # Fail on unexpected types. 

94 if not cls.is_tag(tag): 

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

96 

97 @staticmethod 

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

99 """Is `BeautifulSoup` object.""" 

100 return isinstance(obj, bs4.BeautifulSoup) 

101 

102 @staticmethod 

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

104 """Is tag.""" 

105 return isinstance(obj, bs4.Tag) 

106 

107 @staticmethod 

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

109 """Is declaration.""" 

110 return isinstance(obj, bs4.Declaration) 

111 

112 @staticmethod 

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

114 """Is CDATA.""" 

115 return isinstance(obj, bs4.CData) 

116 

117 @staticmethod 

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

119 """Is processing instruction.""" 

120 return isinstance(obj, bs4.ProcessingInstruction) 

121 

122 @staticmethod 

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

124 """Is navigable string.""" 

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

126 

127 @staticmethod 

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

129 """Is special string.""" 

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

131 

132 @classmethod 

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

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

135 

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

137 

138 @staticmethod 

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

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

141 

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

143 

144 def is_iframe(self, el: bs4.Tag | None) -> bool: 

145 """Check if element is an `iframe`.""" 

146 

147 if el is None: # pragma: no cover 

148 return False 

149 

150 return bool( 

151 ((el.name if self.is_xml_tree(el) else util.lower(el.name)) == 'iframe') and 

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

153 ) 

154 

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

156 """ 

157 Return whether element is a root element. 

158 

159 We check that the element is the root of the tree (which we have already pre-calculated), 

160 and we check if it is the root element under an `iframe`. 

161 """ 

162 

163 root = self.root and self.root is el # type: ignore[attr-defined] 

164 if not root: 

165 parent = self.get_parent(el) 

166 root = parent is not None and self.is_html and self.is_iframe(parent) # type: ignore[attr-defined] 

167 return root 

168 

169 def get_contents(self, el: bs4.Tag | None, no_iframe: bool = False) -> Iterator[bs4.element.PageElement]: 

170 """Get contents or contents in reverse.""" 

171 

172 if el is not None: 

173 if not no_iframe or not self.is_iframe(el): 

174 yield from el.contents 

175 

176 def get_tag_children( 

177 self, 

178 el: bs4.Tag | None, 

179 start: int | None = None, 

180 reverse: bool = False, 

181 no_iframe: bool = False 

182 ) -> Iterator[bs4.Tag]: 

183 """Get tag children.""" 

184 

185 return self.get_children(el, start, reverse, True, no_iframe) 

186 

187 @overload 

188 def get_children( 

189 self, 

190 el: bs4.Tag | None, 

191 start: int | None = None, 

192 reverse: bool = False, 

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

194 no_iframe: bool = False 

195 ) -> Iterator[bs4.Tag]: 

196 ... 

197 

198 @overload 

199 def get_children( 

200 self, 

201 el: bs4.Tag | None, 

202 start: int | None = None, 

203 reverse: bool = False, 

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

205 no_iframe: bool = False 

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

207 ... 

208 

209 def get_children( 

210 self, 

211 el: bs4.Tag | None, 

212 start: int | None = None, 

213 reverse: bool = False, 

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

215 no_iframe: bool = False 

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

217 """Get children.""" 

218 

219 if el is not None and (not no_iframe or not self.is_iframe(el)): 

220 last = len(el.contents) - 1 

221 if start is None: 

222 index = last if reverse else 0 

223 else: 

224 index = start 

225 end = -1 if reverse else last + 1 

226 incr = -1 if reverse else 1 

227 

228 if 0 <= index <= last: 

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

230 node = el.contents[i] 

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

232 yield node 

233 

234 def get_tag_descendants( 

235 self, 

236 el: bs4.Tag | None, 

237 no_iframe: bool = False 

238 ) -> Iterator[bs4.Tag]: 

239 """Specifically get tag descendants.""" 

240 

241 yield from self.get_descendants(el, tags=True, no_iframe=no_iframe) # type: ignore[misc] 

242 

243 def get_descendants( 

244 self, 

245 el: bs4.Tag | None, 

246 tags: bool = False, 

247 no_iframe: bool = False 

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

249 """Get descendants.""" 

250 

251 if el is not None and (not no_iframe or not self.is_iframe(el)): 

252 next_good = None 

253 for child in el.descendants: 

254 

255 if next_good is not None: 

256 if child is not next_good: 

257 continue 

258 next_good = None 

259 

260 if isinstance(child, bs4.Tag): 

261 if no_iframe and self.is_iframe(child): 

262 if child.next_sibling is not None: 

263 next_good = child.next_sibling 

264 else: 

265 last_child: bs4.element.PageElement = child 

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

267 last_child = last_child.contents[-1] 

268 next_good = last_child.next_element 

269 yield child 

270 if next_good is None: 

271 break 

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

273 continue # pragma: no cover 

274 yield child 

275 

276 elif not tags: 

277 yield child 

278 

279 def get_parent(self, el: bs4.Tag | None, no_iframe: bool = False, no_doc: bool = True) -> bs4.Tag | None: 

280 """Get parent.""" 

281 

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

283 if no_iframe and parent is not None and self.is_iframe(parent): # pragma: no cover 

284 parent = None 

285 if no_doc and self.is_doc(parent): 

286 parent = None 

287 return parent 

288 

289 @staticmethod 

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

291 """Get tag.""" 

292 

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

294 

295 @staticmethod 

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

297 """Get prefix.""" 

298 

299 return el.prefix 

300 

301 @staticmethod 

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

303 """Get namespace `URI`.""" 

304 

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

306 

307 @classmethod 

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

309 """Get next sibling tag.""" 

310 

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

312 

313 @classmethod 

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

315 """Get next sibling tag.""" 

316 

317 sibling = el.next_sibling 

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

319 sibling = sibling.next_sibling 

320 

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

322 sibling = None 

323 

324 return sibling 

325 

326 @classmethod 

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

328 """Get previous sibling tag.""" 

329 

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

331 

332 @classmethod 

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

334 """Get previous sibling tag.""" 

335 

336 sibling = el.previous_sibling 

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

338 sibling = sibling.previous_sibling 

339 

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

341 sibling = None 

342 

343 return sibling 

344 

345 @staticmethod 

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

347 """ 

348 Check if element has an HTML namespace. 

349 

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

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

352 """ 

353 

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

355 return bool(ns and ns == NS_XHTML) 

356 

357 @staticmethod 

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

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

360 

361 if el is None: # pragma: no cover 

362 return None, None 

363 

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

365 

366 @classmethod 

367 def get_attribute_by_name( 

368 cls, 

369 el: bs4.Tag, 

370 name: str, 

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

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

373 """Get attribute by name.""" 

374 

375 value = default 

376 if el._is_xml: 

377 if name in el.attrs: 

378 v = el.attrs[name] 

379 value = '' if v is None else v 

380 else: 

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

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

383 value = '' if v is None else v 

384 break 

385 return value 

386 

387 @classmethod 

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

389 """Iterate attributes.""" 

390 

391 if el is not None: 

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

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

394 

395 @classmethod 

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

397 """Get classes.""" 

398 

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

400 if isinstance(classes, str): 

401 classes = RE_NOT_WS.findall(classes) 

402 return cast(Sequence[str], classes) 

403 

404 def get_text(self, el: bs4.Tag, no_iframe: bool = False) -> str: 

405 """Get text.""" 

406 

407 return ''.join( 

408 [ 

409 node for node in self.get_descendants(el, no_iframe=no_iframe) # type: ignore[misc] 

410 if self.is_content_string(node) 

411 ] 

412 ) 

413 

414 def get_own_text(self, el: bs4.Tag, no_iframe: bool = False) -> list[str]: 

415 """Get Own Text.""" 

416 

417 return [ 

418 node for node in self.get_contents(el, no_iframe=no_iframe) if self.is_content_string(node) # type: ignore[misc] 

419 ] 

420 

421 

422class Inputs: 

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

424 

425 @staticmethod 

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

427 """Validate day.""" 

428 

429 max_days = LONG_MONTH 

430 if month == FEB: 

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

432 elif month in MONTHS_30: 

433 max_days = SHORT_MONTH 

434 return 1 <= day <= max_days 

435 

436 @staticmethod 

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

438 """Validate week.""" 

439 

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

441 # 

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

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

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

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

446 # 

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

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

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

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

451 # 

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

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

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

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

456 return 1 <= week <= max_week 

457 

458 @staticmethod 

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

460 """Validate month.""" 

461 

462 return 1 <= month <= 12 

463 

464 @staticmethod 

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

466 """Validate year.""" 

467 

468 return 1 <= year 

469 

470 @staticmethod 

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

472 """Validate hour.""" 

473 

474 return 0 <= hour <= 23 

475 

476 @staticmethod 

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

478 """Validate minutes.""" 

479 

480 return 0 <= minutes <= 59 

481 

482 @staticmethod 

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

484 """Validate seconds.""" 

485 

486 return 0 <= seconds < 60 

487 

488 @classmethod 

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

490 """Parse the input value.""" 

491 

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

493 if value is None: 

494 return value 

495 if itype == "date": 

496 m = RE_DATE.match(value) 

497 if m: 

498 year = int(m.group('year'), 10) 

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

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

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

502 parsed = (year, month, day) 

503 elif itype == "month": 

504 m = RE_MONTH.match(value) 

505 if m: 

506 year = int(m.group('year'), 10) 

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

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

509 parsed = (year, month) 

510 elif itype == "week": 

511 m = RE_WEEK.match(value) 

512 if m: 

513 year = int(m.group('year'), 10) 

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

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

516 parsed = (year, week) 

517 elif itype == "time": 

518 m = RE_TIME.match(value) 

519 if m: 

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

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

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

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

524 parsed = (hour, minutes, seconds) 

525 elif itype == "datetime-local": 

526 m = RE_DATETIME.match(value) 

527 if m: 

528 year = int(m.group('year'), 10) 

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

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

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

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

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

534 if ( 

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

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

537 ): 

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

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

540 m = RE_NUM.match(value) 

541 if m: 

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

543 if isfinite(number): 

544 parsed = (number,) 

545 return parsed 

546 

547 

548class CSSMatch(_DocumentNav): 

549 """Perform CSS matching.""" 

550 

551 def __init__( 

552 self, 

553 selectors: ct.SelectorList, 

554 scope: bs4.Tag | None, 

555 namespaces: ct.Namespaces | None, 

556 flags: int 

557 ) -> None: 

558 """Initialize.""" 

559 

560 self.assert_valid_input(scope) 

561 self.tag = scope 

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

563 self.cached_default_forms: list[tuple[bs4.Tag, bs4.Tag]] = [] 

564 self.cached_indeterminate_forms: list[tuple[bs4.Tag, str, bool]] = [] 

565 self.selectors = selectors 

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

567 self.flags = flags 

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

569 self.iframe_restrict = False 

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

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

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

573 

574 # Find the root element for the whole tree 

575 doc = scope 

576 parent = self.get_parent(doc) 

577 while parent: 

578 doc = parent 

579 parent = self.get_parent(doc) 

580 root: bs4.Tag | None = None 

581 if not self.is_doc(doc): 

582 root = doc 

583 else: 

584 for child in self.get_tag_children(doc): 

585 root = child 

586 break 

587 

588 self.root = root 

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

590 self.has_html_namespace = self.has_html_ns(root) 

591 

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

593 self.is_xml = self.is_xml_tree(doc) 

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

595 

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

597 """Reset.""" 

598 

599 self.nth_cache.clear() 

600 self.sib_cache.clear() 

601 self.dir_cache.clear() 

602 

603 def supports_namespaces(self) -> bool: 

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

605 

606 return self.is_xml or self.has_html_namespace 

607 

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

609 """Get tag namespace.""" 

610 

611 namespace = '' 

612 if el is None: # pragma: no cover 

613 return namespace 

614 

615 if self.supports_namespaces(): 

616 ns = self.get_uri(el) 

617 if ns: 

618 namespace = ns 

619 else: 

620 namespace = NS_XHTML 

621 return namespace 

622 

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

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

625 

626 return self.get_tag_ns(el) == NS_XHTML 

627 

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

629 """Get tag.""" 

630 

631 name = self.get_tag_name(el) 

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

633 

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

635 """Get prefix.""" 

636 

637 prefix = self.get_prefix_name(el) 

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

639 

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

641 """Get directionality from element text.""" 

642 

643 stack = [self.get_children(el)] 

644 while stack: 

645 for node in stack[-1]: 

646 

647 # Analyze child text nodes 

648 if self.is_tag(node): 

649 

650 # Avoid analyzing certain elements specified in the specification. 

651 direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(node, 'dir', '')), None) 

652 name = self.get_tag(node) 

653 if ( 

654 (name and name in ('bdi', 'script', 'style', 'textarea', 'iframe')) or 

655 not self.is_html_tag(node) or 

656 direction is not None 

657 ): 

658 continue # pragma: no cover 

659 

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

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

662 break 

663 

664 # Skip `doctype` comments, etc. 

665 if self.is_special_string(node): 

666 continue 

667 

668 # Analyze text nodes for directionality. 

669 for c in cast('bs4.element.NavigableString', node): 

670 bidi = unicodedata.bidirectional(c) 

671 if bidi in ('AL', 'R', 'L'): 

672 return ct.SEL_DIR_LTR if bidi == 'L' else ct.SEL_DIR_RTL 

673 else: 

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

675 stack.pop() 

676 return None 

677 

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

679 """Filter the language tags.""" 

680 

681 match = True 

682 lang_range = RE_WILD_STRIP.sub('-', lang_range).lower() 

683 ranges = lang_range.split('-') 

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

685 length = len(ranges) 

686 slength = len(subtags) 

687 rindex = 0 

688 sindex = 0 

689 r = ranges[rindex] 

690 s = subtags[sindex] 

691 

692 # Empty specified language should match unspecified language attributes 

693 if length == 1 and slength == 1 and not r and r == s: 

694 return True 

695 

696 # Primary tag needs to match 

697 if (r != '*' and r != s) or (r == '*' and slength == 1 and not s): 

698 match = False 

699 

700 rindex += 1 

701 sindex += 1 

702 

703 # Match until we run out of ranges 

704 while match and rindex < length: 

705 r = ranges[rindex] 

706 try: 

707 s = subtags[sindex] 

708 except IndexError: 

709 # Ran out of subtags, 

710 # but we still have ranges 

711 match = False 

712 continue 

713 

714 # Empty range 

715 if not r: 

716 match = False 

717 continue 

718 

719 # Matched range 

720 elif s == r: 

721 rindex += 1 

722 

723 # Implicit wildcard cannot match 

724 # singletons 

725 elif len(s) == 1: 

726 match = False 

727 continue 

728 

729 # Implicitly matched, so grab next subtag 

730 sindex += 1 

731 

732 return match 

733 

734 def match_attribute_name( 

735 self, 

736 el: bs4.Tag, 

737 attr: str, 

738 prefix: str | None 

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

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

741 

742 value = None 

743 if self.supports_namespaces(): 

744 value = None 

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

746 if prefix: 

747 ns = self.namespaces.get(prefix) 

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

749 return None 

750 else: 

751 ns = None 

752 

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

754 

755 # Get attribute parts 

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

757 

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

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

760 if ns is None: 

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

762 value = v 

763 break 

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

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

766 # Ignore the false positive message. 

767 continue # pragma: no cover 

768 

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

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

771 continue 

772 

773 # The attribute doesn't match. 

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

775 continue 

776 

777 value = v 

778 break 

779 else: 

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

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

782 continue 

783 value = v 

784 break 

785 return value 

786 

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

788 """Match the namespace of the element.""" 

789 

790 match = True 

791 namespace = self.get_tag_ns(el) 

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

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

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

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

796 match = False 

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

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

799 match = False 

800 # Verify prefix matches 

801 elif ( 

802 tag.prefix and 

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

804 ): 

805 match = False 

806 return match 

807 

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

809 """Match attributes.""" 

810 

811 match = True 

812 if attributes: 

813 for a in attributes: 

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

815 pattern = a.xml_type_pattern if self.is_xml and a.xml_type_pattern else a.pattern 

816 if temp is None and a.inverse: 

817 continue 

818 if temp is None: 

819 match = False 

820 break 

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

822 if pattern is None: 

823 continue 

824 elif pattern.match(value) is None: 

825 match = False 

826 break 

827 return match 

828 

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

830 """Match tag name.""" 

831 

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

833 return not ( 

834 name is not None and 

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

836 ) 

837 

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

839 """Match the tag.""" 

840 

841 match = True 

842 if tag is not None: 

843 # Verify namespace 

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

845 match = False 

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

847 match = False 

848 return match 

849 

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

851 """Match general sibling combinator.""" 

852 

853 found = False 

854 

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

856 return found 

857 

858 pkey: int | None = None 

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

860 

861 # Get the parent and use the ID for the cache key 

862 parent = self.get_parent(el) 

863 if parent is None: # pragma: no cover 

864 return found 

865 pkey = id(parent) 

866 

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

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

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

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

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

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

873 if pkey is not None: 

874 key = (pkey, id(relation)) 

875 if key in self.sib_cache: 

876 index = self.sib_cache[key] 

877 if index >= 0: 

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

879 return not within(el, parent, a, b) 

880 else: 

881 return False 

882 self.sib_cache[key] = start 

883 

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

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

886 passed = False 

887 incr = -1 if reverse else 1 

888 for child in self.get_children(parent, start=start, reverse=reverse): 

889 start += incr 

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

891 continue 

892 

893 # Flag that we are passing the element. 

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

895 if child is el: 

896 passed = True 

897 

898 # We found the furthest sibling. 

899 if (yield child, relation): 

900 found = True 

901 break 

902 

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

904 if pkey and key: 

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

906 

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

908 if passed: 

909 found = False 

910 

911 return found 

912 

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

914 """Match past relationship.""" 

915 

916 found = False 

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

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

919 return found 

920 

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

922 parent: bs4.Tag | None = el 

923 while not found and parent and (parent := self.get_parent(parent, no_iframe=self.iframe_restrict)): 

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

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

926 parent = self.get_parent(el, no_iframe=self.iframe_restrict) 

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

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

929 if self.enable_cache: 

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

931 else: 

932 sibling: bs4.Tag | None = el 

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

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

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

936 sibling = self.get_previous_tag(el) 

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

938 return found 

939 

940 def match_future_child( 

941 self, 

942 parent: bs4.Tag, 

943 relation: ct.SelectorList, 

944 recursive: bool = False 

945 ) -> MatchGenerator: 

946 """Match future child.""" 

947 

948 match = False 

949 if recursive: 

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

951 else: 

952 children = self.get_tag_children 

953 for child in children(parent, no_iframe=self.iframe_restrict): 

954 if (yield child, relation): 

955 match = True 

956 break 

957 return match 

958 

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

960 """Match future relationship.""" 

961 

962 found = False 

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

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

965 return found 

966 

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

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

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

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

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

972 if self.enable_cache: 

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

974 else: 

975 sibling: bs4.Tag | None = el 

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

977 found = yield sibling, relation 

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

979 sibling = self.get_next_tag(el) 

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

981 return found 

982 

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

984 """Match relationship to other elements.""" 

985 

986 sel = relation[0] 

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

988 return self.match_future_relations(el, relation) 

989 return self.match_past_relations(el, relation) 

990 

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

992 """Match element's ID.""" 

993 

994 found = True 

995 for i in ids: 

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

997 found = False 

998 break 

999 return found 

1000 

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

1002 """Match element's classes.""" 

1003 

1004 current_classes = self.get_classes(el) 

1005 found = True 

1006 for c in classes: 

1007 if c not in current_classes: 

1008 found = False 

1009 break 

1010 return found 

1011 

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

1013 """Match element as root.""" 

1014 

1015 is_root = self.is_root(el) 

1016 if is_root: 

1017 sibling: Any = self.get_previous(el) 

1018 while is_root and sibling is not None: 

1019 if ( 

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

1021 self.is_cdata(sibling) 

1022 ): 

1023 is_root = False 

1024 else: 

1025 sibling = self.get_previous(sibling) 

1026 if is_root: 

1027 sibling = self.get_next(el) 

1028 while is_root and sibling is not None: 

1029 if ( 

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

1031 self.is_cdata(sibling) 

1032 ): 

1033 is_root = False 

1034 else: 

1035 sibling = self.get_next(sibling) 

1036 return is_root 

1037 

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

1039 """Match element as scope.""" 

1040 

1041 return self.scope is el 

1042 

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

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

1045 

1046 return ( 

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

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

1049 ) 

1050 

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

1052 """Match `nth` elements.""" 

1053 

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

1055 start = rindex = 0 

1056 incr = rincr = 0 

1057 

1058 # Get the parent and use the ID for the cache key 

1059 parent = self.get_parent(el, no_doc=False) 

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

1061 

1062 # Test element against the `nth` selectors. 

1063 matched = True 

1064 for n in nth: 

1065 matched = False 

1066 last = n.last 

1067 key = None 

1068 

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

1070 if pkey is not None and parent: 

1071 # Get last info from the cache 

1072 if pkey: 

1073 key = ( 

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

1075 if n.of_type 

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

1077 ) 

1078 

1079 valid = False 

1080 if key and key in self.nth_cache: 

1081 start, rindex = self.nth_cache[key] 

1082 if within(el, parent, start): 

1083 last = False 

1084 rincr = -1 if n.last else 1 

1085 valid = True 

1086 

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

1088 if not valid: 

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

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

1091 rincr = 1 

1092 

1093 incr = 1 if not last else -1 

1094 children = self.get_children(parent, start=start, reverse=last) 

1095 

1096 # Non-cached handling of parented element 

1097 elif parent: 

1098 rindex = 0 

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

1100 rincr = incr = 1 

1101 children = self.get_children(parent, start=start, reverse=last) 

1102 

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

1104 else: 

1105 start = rindex = 0 

1106 rincr = incr = 1 

1107 children = iter([el]) 

1108 

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

1110 child: bs4.Tag 

1111 for child in children: 

1112 start += incr 

1113 

1114 # We only care about tags 

1115 if not self.is_tag(child): 

1116 continue 

1117 

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

1119 if ( 

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

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

1122 ): 

1123 if child is el: 

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

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

1126 if last: 

1127 rindex += 1 

1128 break 

1129 continue 

1130 

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

1132 rindex += rincr 

1133 if child is el: 

1134 if n.a != 0: 

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

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

1137 else: 

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

1139 break 

1140 

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

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

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

1144 if last: 

1145 start += 2 

1146 

1147 # Update the cache 

1148 if key: 

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

1150 

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

1152 if not matched: 

1153 break 

1154 

1155 return matched 

1156 

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

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

1159 

1160 is_empty = True 

1161 child: Any 

1162 for child in self.get_children(el): 

1163 if self.is_tag(child): 

1164 is_empty = False 

1165 break 

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

1167 is_empty = False 

1168 break 

1169 return is_empty 

1170 

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

1172 """Match selectors.""" 

1173 

1174 match = True 

1175 for sel in selectors: 

1176 if not (yield el, sel): 

1177 match = False 

1178 return match 

1179 

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

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

1182 

1183 match = True 

1184 content: str | None = None 

1185 own_content: Sequence[str] | None = None 

1186 for contain_list in contains: 

1187 found = False 

1188 for text in contain_list.text: 

1189 if contain_list.own: 

1190 if own_content is None: 

1191 own_content = self.get_own_text(el, no_iframe=self.is_html) 

1192 for c in own_content: 

1193 if text in c: 

1194 found = True 

1195 break 

1196 if found: 

1197 break 

1198 else: 

1199 if content is None: 

1200 content = self.get_text(el, no_iframe=self.is_html) 

1201 if text in content: 

1202 found = True 

1203 break 

1204 if not found: 

1205 match = False 

1206 return match 

1207 

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

1209 """Match default.""" 

1210 

1211 match = False 

1212 

1213 # Find this input's form 

1214 form: bs4.Tag | None = None 

1215 parent = self.get_parent(el, no_iframe=True) 

1216 while parent and form is None: 

1217 if self.get_tag(parent) == 'form' and self.is_html_tag(parent): 

1218 form = parent 

1219 else: 

1220 parent = self.get_parent(parent, no_iframe=True) 

1221 

1222 if form is not None: 

1223 # Look in form cache to see if we've already located its default button 

1224 found_form = False 

1225 for f, t in self.cached_default_forms: 

1226 if f is form: 

1227 found_form = True 

1228 if t is el: 

1229 match = True 

1230 break 

1231 

1232 # We didn't have the form cached, so look for its default button 

1233 if not found_form: 

1234 for child in self.get_tag_descendants(form, no_iframe=True): 

1235 name = self.get_tag(child) 

1236 # Can't do nested forms (haven't figured out why we never hit this) 

1237 if name == 'form': # pragma: no cover 

1238 break 

1239 if name in ('input', 'button'): 

1240 v = util.lower(cast(str, self.get_attribute_by_name(child, 'type', ''))) 

1241 if name == 'input': 

1242 submit = v in ('submit', 'image') 

1243 else: 

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

1245 # a child of a `select`. 

1246 submit = v == 'submit' or ( 

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

1248 self.get_attribute_by_name(child, 'command') is None and 

1249 self.get_attribute_by_name(child, 'commandfor') is None and 

1250 self.get_tag(self.get_parent(child, no_iframe=True)) != 'select' 

1251 ) 

1252 if submit: 

1253 self.cached_default_forms.append((form, child)) 

1254 if el is child: 

1255 match = True 

1256 break 

1257 return match 

1258 

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

1260 """Match default.""" 

1261 

1262 match = False 

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

1264 

1265 def get_parent_form(el: bs4.Tag) -> bs4.Tag | None: 

1266 """Find this input's form.""" 

1267 form = None 

1268 parent = self.get_parent(el, no_iframe=True) 

1269 while form is None: 

1270 if self.get_tag(parent) == 'form' and self.is_html_tag(parent): 

1271 form = parent 

1272 break 

1273 last_parent = parent 

1274 parent = self.get_parent(parent, no_iframe=True) 

1275 if parent is None: 

1276 form = last_parent 

1277 break 

1278 return form 

1279 

1280 form = get_parent_form(el) 

1281 

1282 # Look in form cache to see if we've already evaluated that its fellow radio buttons are indeterminate 

1283 if form is not None: 

1284 found_form = False 

1285 for f, n, i in self.cached_indeterminate_forms: 

1286 if f is form and n == name: 

1287 found_form = True 

1288 if i is True: 

1289 match = True 

1290 break 

1291 

1292 # We didn't have the form cached, so validate that the radio button is indeterminate 

1293 if not found_form: 

1294 checked = False 

1295 for child in self.get_tag_descendants(form, no_iframe=True): 

1296 if child is el: 

1297 continue 

1298 tag_name = self.get_tag(child) 

1299 if tag_name == 'input': 

1300 is_radio = False 

1301 check = False 

1302 has_name = False 

1303 for k, v in self.iter_attributes(child): 

1304 if util.lower(k) == 'type' and util.lower(v) == 'radio': 

1305 is_radio = True 

1306 elif util.lower(k) == 'name' and v == name: 

1307 has_name = True 

1308 elif util.lower(k) == 'checked': 

1309 check = True 

1310 if is_radio and check and has_name and get_parent_form(child) is form: 

1311 checked = True 

1312 break 

1313 if checked: 

1314 break 

1315 if not checked: 

1316 match = True 

1317 self.cached_indeterminate_forms.append((form, name, match)) 

1318 

1319 return match 

1320 

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

1322 """Match languages.""" 

1323 

1324 match = False 

1325 has_ns = self.supports_namespaces() 

1326 root = self.root 

1327 has_html_namespace = self.has_html_namespace 

1328 

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

1330 parent: bs4.Tag | None = el 

1331 found_lang = None 

1332 last = None 

1333 while not found_lang: 

1334 has_html_ns = self.has_html_ns(parent) 

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

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

1337 if ( 

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

1339 ( 

1340 has_ns and not has_html_ns and attr_ns == NS_XML and 

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

1342 ) 

1343 ): 

1344 found_lang = v 

1345 break 

1346 last = parent 

1347 parent = self.get_parent(parent, no_iframe=self.is_html) 

1348 

1349 if parent is None: 

1350 root = last 

1351 has_html_namespace = self.has_html_ns(root) 

1352 parent = last 

1353 break 

1354 

1355 # Use cached meta language. 

1356 if found_lang is None and self.cached_meta_lang: 

1357 for cache in self.cached_meta_lang: 

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

1359 found_lang = cache[1] 

1360 

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

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

1363 # Find head 

1364 found = False 

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

1366 found = False 

1367 for child in self.get_tag_children(parent, no_iframe=self.is_html): 

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

1369 found = True 

1370 parent = child 

1371 break 

1372 if found: # pragma: no cover 

1373 break 

1374 

1375 # Search meta tags 

1376 if found and parent is not None: 

1377 for child2 in parent: 

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

1379 c_lang = False 

1380 content = None 

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

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

1383 c_lang = True 

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

1385 content = v 

1386 if c_lang and content: 

1387 found_lang = content 

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

1389 break 

1390 if found_lang is not None: 

1391 break 

1392 if found_lang is None: 

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

1394 

1395 # If we determined a language, compare. 

1396 if found_lang is not None: 

1397 for patterns in langs: 

1398 match = False 

1399 for pattern in patterns: 

1400 if self.extended_language_filter(pattern, cast(str, found_lang)): 

1401 match = True 

1402 if not match: 

1403 break 

1404 

1405 return match 

1406 

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

1408 """Check directionality.""" 

1409 

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

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

1412 return False 

1413 

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

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

1416 visited: list[int] = [] 

1417 direction = self.resolve_dir(el, visited) 

1418 if self.enable_cache: 

1419 for key in visited: 

1420 self.dir_cache[key] = direction 

1421 return direction == directionality 

1422 

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

1424 """Resolve the direction of an element, returning `0` if it cannot be resolved.""" 

1425 

1426 while True: 

1427 if el is None or not self.is_html_tag(el): 

1428 return 0 

1429 

1430 key = id(el) 

1431 if key in self.dir_cache: 

1432 return self.dir_cache[key] 

1433 visited.append(key) 

1434 

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

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

1437 if direction not in (None, 0): 

1438 return direction 

1439 

1440 # Element is the document element (the root) and no direction assigned, assume left to right. 

1441 is_root = self.is_root(el) 

1442 if is_root and direction is None: 

1443 return ct.SEL_DIR_LTR 

1444 

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

1446 name = self.get_tag(el) 

1447 is_input = name == 'input' 

1448 is_textarea = name == 'textarea' 

1449 is_bdi = name == 'bdi' 

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

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

1452 return ct.SEL_DIR_LTR 

1453 

1454 # Auto handling for text inputs 

1455 if ((is_input and itype in ('text', 'search', 'tel', 'url', 'email')) or is_textarea) and direction == 0: 

1456 if is_textarea: 

1457 value = ''.join( 

1458 node for node in self.get_contents(el, no_iframe=True) # type: ignore[misc] 

1459 if self.is_content_string(node) 

1460 ) 

1461 else: 

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

1463 for c in value: 

1464 bidi = unicodedata.bidirectional(c) 

1465 if bidi in ('AL', 'R', 'L'): 

1466 direction = ct.SEL_DIR_LTR if bidi == 'L' else ct.SEL_DIR_RTL 

1467 return ct.SEL_DIR_LTR if bidi == 'L' else ct.SEL_DIR_RTL 

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

1469 return ct.SEL_DIR_LTR 

1470 

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

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

1473 direction = self.find_bidi(el) 

1474 if direction is not None: 

1475 return direction 

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

1477 return ct.SEL_DIR_LTR 

1478 

1479 # Match parents direction 

1480 el = self.get_parent(el, no_iframe=True) 

1481 

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

1483 """ 

1484 Match range. 

1485 

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

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

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

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

1490 """ 

1491 

1492 out_of_range = False 

1493 

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

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

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

1497 

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

1499 if mn is None and mx is None: 

1500 return False 

1501 

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

1503 if value is not None: 

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

1505 if mn is not None and value < mn: 

1506 out_of_range = True 

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

1508 out_of_range = True 

1509 elif itype == "time": 

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

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

1512 if value < mn and value > mx: 

1513 out_of_range = True 

1514 else: 

1515 if mn is not None and value < mn: 

1516 out_of_range = True 

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

1518 out_of_range = True 

1519 

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

1521 

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

1523 """ 

1524 Match defined. 

1525 

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

1527 

1528 - If the document is XML (not XHTML), all tags will match. 

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

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

1531 

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

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

1534 """ 

1535 

1536 name = self.get_tag(el) 

1537 return ( 

1538 name is not None and ( 

1539 name.find('-') == -1 or 

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

1541 self.get_prefix(el) is not None 

1542 ) 

1543 ) 

1544 

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

1546 """ 

1547 Match placeholder shown according to HTML spec. 

1548 

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

1550 

1551 """ 

1552 

1553 match = False 

1554 content = self.get_text(el) 

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

1556 match = True 

1557 

1558 return match 

1559 

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

1561 """ 

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

1563 

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

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

1566 the state of the parent. 

1567 """ 

1568 

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

1570 while node is not None: 

1571 state = self.get_attribute_by_name(node, 'contenteditable') 

1572 if isinstance(state, str): 

1573 state = util.lower(state) 

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

1575 return True 

1576 if state == 'false': 

1577 return False 

1578 node = self.get_parent(node) 

1579 return False 

1580 

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

1582 """ 

1583 Check if element matches one of the selectors. 

1584 

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

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

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

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

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

1590 """ 

1591 

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

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

1594 frame = self._match_selectors(el, selectors) 

1595 item = next(frame) 

1596 if item.__class__ is bool: 

1597 return item 

1598 

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

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

1601 result: bool | None = None 

1602 while True: 

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

1604 if item.__class__ is bool: 

1605 stack.pop() 

1606 if not stack: 

1607 return item 

1608 result = item 

1609 else: 

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

1611 result = None 

1612 

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

1614 """ 

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

1616 

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

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

1619 """ 

1620 

1621 match = False 

1622 is_not = selectors.is_not 

1623 is_html = selectors.is_html 

1624 

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

1626 if is_html: 

1627 namespaces = self.namespaces 

1628 iframe_restrict = self.iframe_restrict 

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

1630 self.iframe_restrict = True 

1631 

1632 if not is_html or self.is_html: 

1633 for selector in selectors: 

1634 match = is_not 

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

1636 if selector is ct.Null: 

1637 continue 

1638 # Verify tag matches 

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

1640 continue 

1641 # Verify tag is defined 

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

1643 continue 

1644 # Verify element is root 

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

1646 continue 

1647 # Verify element is scope 

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

1649 continue 

1650 # Verify element has placeholder shown 

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

1652 continue 

1653 # Verify element is an editing host or editable 

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

1655 continue 

1656 # Verify `nth` matches 

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

1658 continue 

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

1660 continue 

1661 # Verify id matches 

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

1663 continue 

1664 # Verify classes match 

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

1666 continue 

1667 # Verify attribute(s) match 

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

1669 continue 

1670 # Verify ranges 

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

1672 continue 

1673 # Verify language patterns 

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

1675 continue 

1676 # Verify pseudo selector patterns 

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

1678 continue 

1679 # Verify relationship selectors 

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

1681 continue 

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

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

1684 continue 

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

1686 # also not set. 

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

1688 continue 

1689 # Validate element directionality 

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

1691 continue 

1692 # Validate that the tag contains the specified text. 

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

1694 continue 

1695 match = not is_not 

1696 break 

1697 

1698 # Restore actual namespaces being used for external selector lists 

1699 if is_html: 

1700 self.namespaces = namespaces 

1701 self.iframe_restrict = iframe_restrict 

1702 

1703 yield match 

1704 

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

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

1707 

1708 lim = None if limit < 1 else limit 

1709 

1710 for child in self.get_tag_descendants(self.tag): 

1711 if self.match(child): 

1712 yield child 

1713 if lim is not None: 

1714 lim -= 1 

1715 if lim < 1: 

1716 break 

1717 

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

1719 """Match closest ancestor.""" 

1720 

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

1722 closest = None 

1723 while closest is None and current is not None: 

1724 if self.match(current): 

1725 closest = current 

1726 else: 

1727 current = self.get_parent(current) 

1728 return closest 

1729 

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

1731 """Filter tag's children.""" 

1732 

1733 return [ 

1734 tag for tag in self.get_contents(self.tag) 

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

1736 ] 

1737 

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

1739 """Match.""" 

1740 

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

1742 

1743 

1744class SoupSieve(ct.Immutable): 

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

1746 

1747 pattern: str 

1748 selectors: ct.SelectorList 

1749 namespaces: ct.Namespaces | None 

1750 custom: dict[str, str] 

1751 flags: int 

1752 

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

1754 

1755 def __init__( 

1756 self, 

1757 pattern: str, 

1758 selectors: ct.SelectorList, 

1759 namespaces: ct.Namespaces | None, 

1760 custom: ct.CustomSelectors | None, 

1761 flags: int 

1762 ): 

1763 """Initialize.""" 

1764 

1765 super().__init__( 

1766 pattern=pattern, 

1767 selectors=selectors, 

1768 namespaces=namespaces, 

1769 custom=custom, 

1770 flags=flags 

1771 ) 

1772 

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

1774 """Match.""" 

1775 

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

1777 

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

1779 """Match closest ancestor.""" 

1780 

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

1782 

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

1784 """ 

1785 Filter. 

1786 

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

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

1789 and we can take advantage of the optimization. 

1790 

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

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

1793 """ 

1794 

1795 if isinstance(iterable, bs4.Tag): 

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

1797 else: 

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

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

1800 

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

1802 """Select a single tag.""" 

1803 

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

1805 return tags[0] if tags else None 

1806 

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

1808 """Select the specified tags.""" 

1809 

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

1811 

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

1813 """Iterate the specified tags.""" 

1814 

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

1816 

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

1818 """Representation.""" 

1819 

1820 return ( 

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

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

1823 ) 

1824 

1825 __str__ = __repr__ 

1826 

1827 

1828ct.pickle_register(SoupSieve)