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

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

1001 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 = self.get_attribute_by_name(child, 'type', '') 

1241 if v and util.lower(v) == 'submit': 

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

1243 if el is child: 

1244 match = True 

1245 break 

1246 return match 

1247 

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

1249 """Match default.""" 

1250 

1251 match = False 

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

1253 

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

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

1256 form = None 

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

1258 while form is None: 

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

1260 form = parent 

1261 break 

1262 last_parent = parent 

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

1264 if parent is None: 

1265 form = last_parent 

1266 break 

1267 return form 

1268 

1269 form = get_parent_form(el) 

1270 

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

1272 if form is not None: 

1273 found_form = False 

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

1275 if f is form and n == name: 

1276 found_form = True 

1277 if i is True: 

1278 match = True 

1279 break 

1280 

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

1282 if not found_form: 

1283 checked = False 

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

1285 if child is el: 

1286 continue 

1287 tag_name = self.get_tag(child) 

1288 if tag_name == 'input': 

1289 is_radio = False 

1290 check = False 

1291 has_name = False 

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

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

1294 is_radio = True 

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

1296 has_name = True 

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

1298 check = True 

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

1300 checked = True 

1301 break 

1302 if checked: 

1303 break 

1304 if not checked: 

1305 match = True 

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

1307 

1308 return match 

1309 

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

1311 """Match languages.""" 

1312 

1313 match = False 

1314 has_ns = self.supports_namespaces() 

1315 root = self.root 

1316 has_html_namespace = self.has_html_namespace 

1317 

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

1319 parent: bs4.Tag | None = el 

1320 found_lang = None 

1321 last = None 

1322 while not found_lang: 

1323 has_html_ns = self.has_html_ns(parent) 

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

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

1326 if ( 

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

1328 ( 

1329 has_ns and not has_html_ns and attr_ns == NS_XML and 

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

1331 ) 

1332 ): 

1333 found_lang = v 

1334 break 

1335 last = parent 

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

1337 

1338 if parent is None: 

1339 root = last 

1340 has_html_namespace = self.has_html_ns(root) 

1341 parent = last 

1342 break 

1343 

1344 # Use cached meta language. 

1345 if found_lang is None and self.cached_meta_lang: 

1346 for cache in self.cached_meta_lang: 

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

1348 found_lang = cache[1] 

1349 

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

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

1352 # Find head 

1353 found = False 

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

1355 found = False 

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

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

1358 found = True 

1359 parent = child 

1360 break 

1361 if found: # pragma: no cover 

1362 break 

1363 

1364 # Search meta tags 

1365 if found and parent is not None: 

1366 for child2 in parent: 

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

1368 c_lang = False 

1369 content = None 

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

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

1372 c_lang = True 

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

1374 content = v 

1375 if c_lang and content: 

1376 found_lang = content 

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

1378 break 

1379 if found_lang is not None: 

1380 break 

1381 if found_lang is None: 

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

1383 

1384 # If we determined a language, compare. 

1385 if found_lang is not None: 

1386 for patterns in langs: 

1387 match = False 

1388 for pattern in patterns: 

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

1390 match = True 

1391 if not match: 

1392 break 

1393 

1394 return match 

1395 

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

1397 """Check directionality.""" 

1398 

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

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

1401 return False 

1402 

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

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

1405 visited: list[int] = [] 

1406 direction = self.resolve_dir(el, visited) 

1407 if self.enable_cache: 

1408 for key in visited: 

1409 self.dir_cache[key] = direction 

1410 return direction == directionality 

1411 

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

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

1414 

1415 while True: 

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

1417 return 0 

1418 

1419 key = id(el) 

1420 if key in self.dir_cache: 

1421 return self.dir_cache[key] 

1422 visited.append(key) 

1423 

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

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

1426 if direction not in (None, 0): 

1427 return direction 

1428 

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

1430 is_root = self.is_root(el) 

1431 if is_root and direction is None: 

1432 return ct.SEL_DIR_LTR 

1433 

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

1435 name = self.get_tag(el) 

1436 is_input = name == 'input' 

1437 is_textarea = name == 'textarea' 

1438 is_bdi = name == 'bdi' 

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

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

1441 return ct.SEL_DIR_LTR 

1442 

1443 # Auto handling for text inputs 

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

1445 if is_textarea: 

1446 value = ''.join( 

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

1448 if self.is_content_string(node) 

1449 ) 

1450 else: 

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

1452 for c in value: 

1453 bidi = unicodedata.bidirectional(c) 

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

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

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

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

1458 return ct.SEL_DIR_LTR 

1459 

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

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

1462 direction = self.find_bidi(el) 

1463 if direction is not None: 

1464 return direction 

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

1466 return ct.SEL_DIR_LTR 

1467 

1468 # Match parents direction 

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

1470 

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

1472 """ 

1473 Match range. 

1474 

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

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

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

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

1479 """ 

1480 

1481 out_of_range = False 

1482 

1483 itype = util.lower(self.get_attribute_by_name(el, 'type')) 

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

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

1486 

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

1488 if mn is None and mx is None: 

1489 return False 

1490 

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

1492 if value is not None: 

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

1494 if mn is not None and value < mn: 

1495 out_of_range = True 

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

1497 out_of_range = True 

1498 elif itype == "time": 

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

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

1501 if value < mn and value > mx: 

1502 out_of_range = True 

1503 else: 

1504 if mn is not None and value < mn: 

1505 out_of_range = True 

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

1507 out_of_range = True 

1508 

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

1510 

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

1512 """ 

1513 Match defined. 

1514 

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

1516 

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

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

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

1520 

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

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

1523 """ 

1524 

1525 name = self.get_tag(el) 

1526 return ( 

1527 name is not None and ( 

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

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

1530 self.get_prefix(el) is not None 

1531 ) 

1532 ) 

1533 

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

1535 """ 

1536 Match placeholder shown according to HTML spec. 

1537 

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

1539 

1540 """ 

1541 

1542 match = False 

1543 content = self.get_text(el) 

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

1545 match = True 

1546 

1547 return match 

1548 

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

1550 """ 

1551 Check if element matches one of the selectors. 

1552 

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

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

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

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

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

1558 """ 

1559 

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

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

1562 frame = self._match_selectors(el, selectors) 

1563 item = next(frame) 

1564 if item.__class__ is bool: 

1565 return item 

1566 

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

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

1569 result: bool | None = None 

1570 while True: 

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

1572 if item.__class__ is bool: 

1573 stack.pop() 

1574 if not stack: 

1575 return item 

1576 result = item 

1577 else: 

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

1579 result = None 

1580 

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

1582 """ 

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

1584 

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

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

1587 """ 

1588 

1589 match = False 

1590 is_not = selectors.is_not 

1591 is_html = selectors.is_html 

1592 

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

1594 if is_html: 

1595 namespaces = self.namespaces 

1596 iframe_restrict = self.iframe_restrict 

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

1598 self.iframe_restrict = True 

1599 

1600 if not is_html or self.is_html: 

1601 for selector in selectors: 

1602 match = is_not 

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

1604 if selector is ct.Null: 

1605 continue 

1606 # Verify tag matches 

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

1608 continue 

1609 # Verify tag is defined 

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

1611 continue 

1612 # Verify element is root 

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

1614 continue 

1615 # Verify element is scope 

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

1617 continue 

1618 # Verify element has placeholder shown 

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

1620 continue 

1621 # Verify `nth` matches 

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

1623 continue 

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

1625 continue 

1626 # Verify id matches 

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

1628 continue 

1629 # Verify classes match 

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

1631 continue 

1632 # Verify attribute(s) match 

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

1634 continue 

1635 # Verify ranges 

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

1637 continue 

1638 # Verify language patterns 

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

1640 continue 

1641 # Verify pseudo selector patterns 

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

1643 continue 

1644 # Verify relationship selectors 

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

1646 continue 

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

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

1649 continue 

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

1651 # also not set. 

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

1653 continue 

1654 # Validate element directionality 

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

1656 continue 

1657 # Validate that the tag contains the specified text. 

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

1659 continue 

1660 match = not is_not 

1661 break 

1662 

1663 # Restore actual namespaces being used for external selector lists 

1664 if is_html: 

1665 self.namespaces = namespaces 

1666 self.iframe_restrict = iframe_restrict 

1667 

1668 yield match 

1669 

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

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

1672 

1673 lim = None if limit < 1 else limit 

1674 

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

1676 if self.match(child): 

1677 yield child 

1678 if lim is not None: 

1679 lim -= 1 

1680 if lim < 1: 

1681 break 

1682 

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

1684 """Match closest ancestor.""" 

1685 

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

1687 closest = None 

1688 while closest is None and current is not None: 

1689 if self.match(current): 

1690 closest = current 

1691 else: 

1692 current = self.get_parent(current) 

1693 return closest 

1694 

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

1696 """Filter tag's children.""" 

1697 

1698 return [ 

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

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

1701 ] 

1702 

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

1704 """Match.""" 

1705 

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

1707 

1708 

1709class SoupSieve(ct.Immutable): 

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

1711 

1712 pattern: str 

1713 selectors: ct.SelectorList 

1714 namespaces: ct.Namespaces | None 

1715 custom: dict[str, str] 

1716 flags: int 

1717 

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

1719 

1720 def __init__( 

1721 self, 

1722 pattern: str, 

1723 selectors: ct.SelectorList, 

1724 namespaces: ct.Namespaces | None, 

1725 custom: ct.CustomSelectors | None, 

1726 flags: int 

1727 ): 

1728 """Initialize.""" 

1729 

1730 super().__init__( 

1731 pattern=pattern, 

1732 selectors=selectors, 

1733 namespaces=namespaces, 

1734 custom=custom, 

1735 flags=flags 

1736 ) 

1737 

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

1739 """Match.""" 

1740 

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

1742 

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

1744 """Match closest ancestor.""" 

1745 

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

1747 

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

1749 """ 

1750 Filter. 

1751 

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

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

1754 and we can take advantage of the optimization. 

1755 

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

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

1758 """ 

1759 

1760 if isinstance(iterable, bs4.Tag): 

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

1762 else: 

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

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

1765 

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

1767 """Select a single tag.""" 

1768 

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

1770 return tags[0] if tags else None 

1771 

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

1773 """Select the specified tags.""" 

1774 

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

1776 

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

1778 """Iterate the specified tags.""" 

1779 

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

1781 

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

1783 """Representation.""" 

1784 

1785 return ( 

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

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

1788 ) 

1789 

1790 __str__ = __repr__ 

1791 

1792 

1793ct.pickle_register(SoupSieve)