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

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

935 statements  

1"""CSS matcher.""" 

2from __future__ import annotations 

3from datetime import datetime 

4from collections.abc import Hashable 

5from . import util 

6import re 

7from . import css_types as ct 

8import unicodedata 

9import bs4 

10from typing import Iterator, Iterable, Any, Callable, Sequence, Any, overload, Literal, cast # noqa: F401, F811 

11 

12# Empty tag pattern (whitespace okay) 

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

14 

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

16 

17# Relationships 

18REL_PARENT = ' ' 

19REL_CLOSE_PARENT = '>' 

20REL_SIBLING = '~' 

21REL_CLOSE_SIBLING = '+' 

22 

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

24REL_HAS_PARENT = ': ' 

25REL_HAS_CLOSE_PARENT = ':>' 

26REL_HAS_SIBLING = ':~' 

27REL_HAS_CLOSE_SIBLING = ':+' 

28 

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

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

31 

32DIR_FLAGS = ct.SEL_DIR_LTR | ct.SEL_DIR_RTL 

33RANGES = ct.SEL_IN_RANGE | ct.SEL_OUT_OF_RANGE 

34 

35DIR_MAP = { 

36 'ltr': ct.SEL_DIR_LTR, 

37 'rtl': ct.SEL_DIR_RTL, 

38 'auto': 0 

39} 

40 

41RE_NUM = re.compile(r"^(?P<value>-?(?:[0-9]{1,}(\.[0-9]+)?|\.[0-9]+))$") 

42RE_TIME = re.compile(r'^(?P<hour>[0-9]{2}):(?P<minutes>[0-9]{2})$') 

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

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

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

46RE_DATETIME = re.compile( 

47 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})$' 

48) 

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

50 

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

52FEB = 2 

53SHORT_MONTH = 30 

54LONG_MONTH = 31 

55FEB_MONTH = 28 

56FEB_LEAP_MONTH = 29 

57DAYS_IN_WEEK = 7 

58 

59 

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

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

62 

63 contents = parent.contents 

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

65 

66 

67class _DocumentNav: 

68 """Navigate a Beautiful Soup document.""" 

69 

70 @classmethod 

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

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

73 

74 # Fail on unexpected types. 

75 if not cls.is_tag(tag): 

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

77 

78 @staticmethod 

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

80 """Is `BeautifulSoup` object.""" 

81 return isinstance(obj, bs4.BeautifulSoup) 

82 

83 @staticmethod 

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

85 """Is tag.""" 

86 return isinstance(obj, bs4.Tag) 

87 

88 @staticmethod 

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

90 """Is declaration.""" 

91 return isinstance(obj, bs4.Declaration) 

92 

93 @staticmethod 

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

95 """Is CDATA.""" 

96 return isinstance(obj, bs4.CData) 

97 

98 @staticmethod 

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

100 """Is processing instruction.""" 

101 return isinstance(obj, bs4.ProcessingInstruction) 

102 

103 @staticmethod 

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

105 """Is navigable string.""" 

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

107 

108 @staticmethod 

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

110 """Is special string.""" 

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

112 

113 @classmethod 

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

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

116 

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

118 

119 @staticmethod 

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

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

122 

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

124 

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

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

127 

128 if el is None: # pragma: no cover 

129 return False 

130 

131 return bool( 

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

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

134 ) 

135 

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

137 """ 

138 Return whether element is a root element. 

139 

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

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

142 """ 

143 

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

145 if not root: 

146 parent = self.get_parent(el) 

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

148 return root 

149 

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

151 """Get contents or contents in reverse.""" 

152 

153 if el is not None: 

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

155 yield from el.contents 

156 

157 def get_tag_children( 

158 self, 

159 el: bs4.Tag | None, 

160 start: int | None = None, 

161 reverse: bool = False, 

162 no_iframe: bool = False 

163 ) -> Iterator[bs4.Tag]: 

164 """Get tag children.""" 

165 

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

167 

168 @overload 

169 def get_children( 

170 self, 

171 el: bs4.Tag | None, 

172 start: int | None = None, 

173 reverse: bool = False, 

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

175 no_iframe: bool = False 

176 ) -> Iterator[bs4.Tag]: 

177 ... 

178 

179 @overload 

180 def get_children( 

181 self, 

182 el: bs4.Tag | None, 

183 start: int | None = None, 

184 reverse: bool = False, 

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

186 no_iframe: bool = False 

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

188 ... 

189 

190 def get_children( 

191 self, 

192 el: bs4.Tag | None, 

193 start: int | None = None, 

194 reverse: bool = False, 

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

196 no_iframe: bool = False 

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

198 """Get children.""" 

199 

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

201 last = len(el.contents) - 1 

202 if start is None: 

203 index = last if reverse else 0 

204 else: 

205 index = start 

206 end = -1 if reverse else last + 1 

207 incr = -1 if reverse else 1 

208 

209 if 0 <= index <= last: 

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

211 node = el.contents[i] 

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

213 yield node 

214 

215 def get_tag_descendants( 

216 self, 

217 el: bs4.Tag | None, 

218 no_iframe: bool = False 

219 ) -> Iterator[bs4.Tag]: 

220 """Specifically get tag descendants.""" 

221 

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

223 

224 def get_descendants( 

225 self, 

226 el: bs4.Tag | None, 

227 tags: bool = False, 

228 no_iframe: bool = False 

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

230 """Get descendants.""" 

231 

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

233 next_good = None 

234 for child in el.descendants: 

235 

236 if next_good is not None: 

237 if child is not next_good: 

238 continue 

239 next_good = None 

240 

241 if isinstance(child, bs4.Tag): 

242 if no_iframe and self.is_iframe(child): 

243 if child.next_sibling is not None: 

244 next_good = child.next_sibling 

245 else: 

246 last_child = child # type: bs4.element.PageElement 

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

248 last_child = last_child.contents[-1] 

249 next_good = last_child.next_element 

250 yield child 

251 if next_good is None: 

252 break 

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

254 continue # pragma: no cover 

255 yield child 

256 

257 elif not tags: 

258 yield child 

259 

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

261 """Get parent.""" 

262 

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

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

265 parent = None 

266 return parent 

267 

268 @staticmethod 

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

270 """Get tag.""" 

271 

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

273 

274 @staticmethod 

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

276 """Get prefix.""" 

277 

278 return el.prefix 

279 

280 @staticmethod 

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

282 """Get namespace `URI`.""" 

283 

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

285 

286 @classmethod 

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

288 """Get next sibling tag.""" 

289 

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

291 

292 @classmethod 

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

294 """Get next sibling tag.""" 

295 

296 sibling = el.next_sibling 

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

298 sibling = sibling.next_sibling 

299 

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

301 sibling = None 

302 

303 return sibling 

304 

305 @classmethod 

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

307 """Get previous sibling tag.""" 

308 

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

310 

311 @classmethod 

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

313 """Get previous sibling tag.""" 

314 

315 sibling = el.previous_sibling 

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

317 sibling = sibling.previous_sibling 

318 

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

320 sibling = None 

321 

322 return sibling 

323 

324 @staticmethod 

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

326 """ 

327 Check if element has an HTML namespace. 

328 

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

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

331 """ 

332 

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

334 return bool(ns and ns == NS_XHTML) 

335 

336 @staticmethod 

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

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

339 

340 if el is None: # pragma: no cover 

341 return None, None 

342 

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

344 

345 @classmethod 

346 def get_attribute_by_name( 

347 cls, 

348 el: bs4.Tag, 

349 name: str, 

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

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

352 """Get attribute by name.""" 

353 

354 value = default 

355 if el._is_xml: 

356 if name in el.attrs: 

357 v = el.attrs[name] 

358 value = '' if v is None else v 

359 else: 

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

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

362 value = '' if v is None else v 

363 break 

364 return value 

365 

366 @classmethod 

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

368 """Iterate attributes.""" 

369 

370 if el is not None: 

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

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

373 

374 @classmethod 

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

376 """Get classes.""" 

377 

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

379 if isinstance(classes, str): 

380 classes = RE_NOT_WS.findall(classes) 

381 return cast(Sequence[str], classes) 

382 

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

384 """Get text.""" 

385 

386 return ''.join( 

387 [ 

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

389 if self.is_content_string(node) 

390 ] 

391 ) 

392 

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

394 """Get Own Text.""" 

395 

396 return [ 

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

398 ] 

399 

400 

401class Inputs: 

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

403 

404 @staticmethod 

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

406 """Validate day.""" 

407 

408 max_days = LONG_MONTH 

409 if month == FEB: 

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

411 elif month in MONTHS_30: 

412 max_days = SHORT_MONTH 

413 return 1 <= day <= max_days 

414 

415 @staticmethod 

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

417 """Validate week.""" 

418 

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

420 # 

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

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

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

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

425 # 

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

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

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

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

430 max_week = datetime(year, 12, 28).isocalendar()[1] 

431 return 1 <= week <= max_week 

432 

433 @staticmethod 

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

435 """Validate month.""" 

436 

437 return 1 <= month <= 12 

438 

439 @staticmethod 

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

441 """Validate year.""" 

442 

443 return 1 <= year 

444 

445 @staticmethod 

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

447 """Validate hour.""" 

448 

449 return 0 <= hour <= 23 

450 

451 @staticmethod 

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

453 """Validate minutes.""" 

454 

455 return 0 <= minutes <= 59 

456 

457 @classmethod 

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

459 """Parse the input value.""" 

460 

461 parsed = None # type: tuple[float, ...] | None 

462 if value is None: 

463 return value 

464 if itype == "date": 

465 m = RE_DATE.match(value) 

466 if m: 

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

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

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

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

471 parsed = (year, month, day) 

472 elif itype == "month": 

473 m = RE_MONTH.match(value) 

474 if m: 

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

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

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

478 parsed = (year, month) 

479 elif itype == "week": 

480 m = RE_WEEK.match(value) 

481 if m: 

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

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

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

485 parsed = (year, week) 

486 elif itype == "time": 

487 m = RE_TIME.match(value) 

488 if m: 

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

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

491 if cls.validate_hour(hour) and cls.validate_minutes(minutes): 

492 parsed = (hour, minutes) 

493 elif itype == "datetime-local": 

494 m = RE_DATETIME.match(value) 

495 if m: 

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

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

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

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

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

501 if ( 

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

503 cls.validate_hour(hour) and cls.validate_minutes(minutes) 

504 ): 

505 parsed = (year, month, day, hour, minutes) 

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

507 m = RE_NUM.match(value) 

508 if m: 

509 parsed = (float(m.group('value')),) 

510 return parsed 

511 

512 

513class CSSMatch(_DocumentNav): 

514 """Perform CSS matching.""" 

515 

516 def __init__( 

517 self, 

518 selectors: ct.SelectorList, 

519 scope: bs4.Tag | None, 

520 namespaces: ct.Namespaces | None, 

521 flags: int 

522 ) -> None: 

523 """Initialize.""" 

524 

525 self.assert_valid_input(scope) 

526 self.tag = scope 

527 self.cached_meta_lang = [] # type: list[tuple[str, str]] 

528 self.cached_default_forms = [] # type: list[tuple[bs4.Tag, bs4.Tag]] 

529 self.cached_indeterminate_forms = [] # type: list[tuple[bs4.Tag, str, bool]] 

530 self.selectors = selectors 

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

532 self.flags = flags 

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

534 self.iframe_restrict = False 

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

536 

537 # Find the root element for the whole tree 

538 doc = scope 

539 parent = self.get_parent(doc) 

540 while parent: 

541 doc = parent 

542 parent = self.get_parent(doc) 

543 root = None # type: bs4.Tag | None 

544 if not self.is_doc(doc): 

545 root = doc 

546 else: 

547 for child in self.get_tag_children(doc): 

548 root = child 

549 break 

550 

551 self.root = root 

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

553 self.has_html_namespace = self.has_html_ns(root) 

554 

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

556 self.is_xml = self.is_xml_tree(doc) 

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

558 

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

560 """Reset.""" 

561 

562 self.nth_cache.clear() 

563 

564 def supports_namespaces(self) -> bool: 

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

566 

567 return self.is_xml or self.has_html_namespace 

568 

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

570 """Get tag namespace.""" 

571 

572 namespace = '' 

573 if el is None: # pragma: no cover 

574 return namespace 

575 

576 if self.supports_namespaces(): 

577 ns = self.get_uri(el) 

578 if ns: 

579 namespace = ns 

580 else: 

581 namespace = NS_XHTML 

582 return namespace 

583 

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

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

586 

587 return self.get_tag_ns(el) == NS_XHTML 

588 

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

590 """Get tag.""" 

591 

592 name = self.get_tag_name(el) 

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

594 

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

596 """Get prefix.""" 

597 

598 prefix = self.get_prefix_name(el) 

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

600 

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

602 """Get directionality from element text.""" 

603 

604 for node in self.get_children(el): 

605 

606 # Analyze child text nodes 

607 if self.is_tag(node): 

608 

609 # Avoid analyzing certain elements specified in the specification. 

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

611 name = self.get_tag(node) 

612 if ( 

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

614 not self.is_html_tag(node) or 

615 direction is not None 

616 ): 

617 continue # pragma: no cover 

618 

619 # Check directionality of this node's text 

620 value = self.find_bidi(node) 

621 if value is not None: 

622 return value 

623 

624 # Direction could not be determined 

625 continue # pragma: no cover 

626 

627 # Skip `doctype` comments, etc. 

628 if self.is_special_string(node): 

629 continue 

630 

631 # Analyze text nodes for directionality. 

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

633 bidi = unicodedata.bidirectional(c) 

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

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

636 return None 

637 

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

639 """Filter the language tags.""" 

640 

641 match = True 

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

643 ranges = lang_range.split('-') 

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

645 length = len(ranges) 

646 slength = len(subtags) 

647 rindex = 0 

648 sindex = 0 

649 r = ranges[rindex] 

650 s = subtags[sindex] 

651 

652 # Empty specified language should match unspecified language attributes 

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

654 return True 

655 

656 # Primary tag needs to match 

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

658 match = False 

659 

660 rindex += 1 

661 sindex += 1 

662 

663 # Match until we run out of ranges 

664 while match and rindex < length: 

665 r = ranges[rindex] 

666 try: 

667 s = subtags[sindex] 

668 except IndexError: 

669 # Ran out of subtags, 

670 # but we still have ranges 

671 match = False 

672 continue 

673 

674 # Empty range 

675 if not r: 

676 match = False 

677 continue 

678 

679 # Matched range 

680 elif s == r: 

681 rindex += 1 

682 

683 # Implicit wildcard cannot match 

684 # singletons 

685 elif len(s) == 1: 

686 match = False 

687 continue 

688 

689 # Implicitly matched, so grab next subtag 

690 sindex += 1 

691 

692 return match 

693 

694 def match_attribute_name( 

695 self, 

696 el: bs4.Tag, 

697 attr: str, 

698 prefix: str | None 

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

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

701 

702 value = None 

703 if self.supports_namespaces(): 

704 value = None 

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

706 if prefix: 

707 ns = self.namespaces.get(prefix) 

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

709 return None 

710 else: 

711 ns = None 

712 

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

714 

715 # Get attribute parts 

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

717 

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

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

720 if ns is None: 

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

722 value = v 

723 break 

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

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

726 # Ignore the false positive message. 

727 continue # pragma: no cover 

728 

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

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

731 continue 

732 

733 # The attribute doesn't match. 

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

735 continue 

736 

737 value = v 

738 break 

739 else: 

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

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

742 continue 

743 value = v 

744 break 

745 return value 

746 

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

748 """Match the namespace of the element.""" 

749 

750 match = True 

751 namespace = self.get_tag_ns(el) 

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

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

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

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

756 match = False 

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

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

759 match = False 

760 # Verify prefix matches 

761 elif ( 

762 tag.prefix and 

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

764 ): 

765 match = False 

766 return match 

767 

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

769 """Match attributes.""" 

770 

771 match = True 

772 if attributes: 

773 for a in attributes: 

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

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

776 if temp is None: 

777 match = False 

778 break 

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

780 if pattern is None: 

781 continue 

782 elif pattern.match(value) is None: 

783 match = False 

784 break 

785 return match 

786 

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

788 """Match tag name.""" 

789 

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

791 return not ( 

792 name is not None and 

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

794 ) 

795 

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

797 """Match the tag.""" 

798 

799 match = True 

800 if tag is not None: 

801 # Verify namespace 

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

803 match = False 

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

805 match = False 

806 return match 

807 

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

809 """Match past relationship.""" 

810 

811 found = False 

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

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

814 return found 

815 

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

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

818 while not found and parent: 

819 found = self.match_selectors(parent, relation) 

820 parent = self.get_parent(parent, no_iframe=self.iframe_restrict) 

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

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

823 if parent: 

824 found = self.match_selectors(parent, relation) 

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

826 sibling = self.get_previous_tag(el) 

827 while not found and sibling: 

828 found = self.match_selectors(sibling, relation) 

829 sibling = self.get_previous_tag(sibling) 

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

831 sibling = self.get_previous_tag(el) 

832 if sibling and self.is_tag(sibling): 

833 found = self.match_selectors(sibling, relation) 

834 return found 

835 

836 def match_future_child(self, parent: bs4.Tag, relation: ct.SelectorList, recursive: bool = False) -> bool: 

837 """Match future child.""" 

838 

839 match = False 

840 if recursive: 

841 children = self.get_tag_descendants # type: Callable[..., Iterator[bs4.Tag]] 

842 else: 

843 children = self.get_tag_children 

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

845 match = self.match_selectors(child, relation) 

846 if match: 

847 break 

848 return match 

849 

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

851 """Match future relationship.""" 

852 

853 found = False 

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

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

856 return found 

857 

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

859 found = self.match_future_child(el, relation, True) 

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

861 found = self.match_future_child(el, relation) 

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

863 sibling = self.get_next_tag(el) 

864 while not found and sibling: 

865 found = self.match_selectors(sibling, relation) 

866 sibling = self.get_next_tag(sibling) 

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

868 sibling = self.get_next_tag(el) 

869 if sibling and self.is_tag(sibling): 

870 found = self.match_selectors(sibling, relation) 

871 return found 

872 

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

874 """Match relationship to other elements.""" 

875 

876 found = False 

877 

878 if relation[0] is ct.Null or relation[0].rel_type is None: 

879 return found 

880 

881 if relation[0].rel_type.startswith(':'): 

882 found = self.match_future_relations(el, relation) 

883 else: 

884 found = self.match_past_relations(el, relation) 

885 

886 return found 

887 

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

889 """Match element's ID.""" 

890 

891 found = True 

892 for i in ids: 

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

894 found = False 

895 break 

896 return found 

897 

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

899 """Match element's classes.""" 

900 

901 current_classes = self.get_classes(el) 

902 found = True 

903 for c in classes: 

904 if c not in current_classes: 

905 found = False 

906 break 

907 return found 

908 

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

910 """Match element as root.""" 

911 

912 is_root = self.is_root(el) 

913 if is_root: 

914 sibling = self.get_previous(el) # type: Any 

915 while is_root and sibling is not None: 

916 if ( 

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

918 self.is_cdata(sibling) 

919 ): 

920 is_root = False 

921 else: 

922 sibling = self.get_previous(sibling) 

923 if is_root: 

924 sibling = self.get_next(el) 

925 while is_root and sibling is not None: 

926 if ( 

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

928 self.is_cdata(sibling) 

929 ): 

930 is_root = False 

931 else: 

932 sibling = self.get_next(sibling) 

933 return is_root 

934 

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

936 """Match element as scope.""" 

937 

938 return self.scope is el 

939 

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

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

942 

943 return ( 

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

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

946 ) 

947 

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

949 """Match `nth` elements.""" 

950 

951 # `nth` selectors are evaluated against siblings under the same parent. 

952 parent = self.get_parent(el) # type: bs4.Tag | None 

953 pkey: tuple[str | None, int] | None = None 

954 key: tuple[ct.SelectorNth, int, str | None, str | None] | None = None 

955 start = rindex = 0 

956 incr = rincr = 0 

957 

958 # Setup the cache by the parent, if parent a parent is present 

959 if self.enable_cache and parent: 

960 pkey = (parent.name, id(parent)) 

961 

962 # Initialize the cache if necessary 

963 if pkey not in self.nth_cache: 

964 self.nth_cache[pkey] = {} 

965 

966 # Test element against the `nth` selectors. 

967 matched = True 

968 for n in nth: 

969 matched = False 

970 last = n.last 

971 key = None 

972 

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

974 if pkey and parent: 

975 # Get last info from the cache 

976 key = (n, id(n), self.get_tag(el), self.get_tag_ns(el)) if n.of_type else (n, id(n), None, None) 

977 valid = False 

978 if key in self.nth_cache[pkey]: 

979 start, rindex = self.nth_cache[pkey][key] 

980 if within(el, parent, start): 

981 last = False 

982 rincr = -1 if n.last else 1 

983 valid = True 

984 

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

986 if not valid: 

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

988 self.nth_cache[pkey][key] = [start, rindex] 

989 rincr = 1 

990 

991 incr = 1 if not last else -1 

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

993 

994 # Non-cached handling of parented element 

995 elif parent: 

996 rindex = 0 

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

998 rincr = incr = 1 

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

1000 

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

1002 else: 

1003 start = rindex = 0 

1004 rincr = incr = 1 

1005 children = iter([el]) 

1006 

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

1008 child: bs4.Tag 

1009 for child in children: 

1010 start += incr 

1011 

1012 # We only care about tags 

1013 if not self.is_tag(child): 

1014 continue 

1015 

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

1017 if ( 

1018 (n.selectors and not self.match_selectors(child, n.selectors)) or 

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

1020 ): 

1021 if child is el: 

1022 break 

1023 continue 

1024 

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

1026 rindex += rincr 

1027 if child is el: 

1028 if n.a != 0: 

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

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

1031 else: 

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

1033 break 

1034 

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

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

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

1038 if last: 

1039 start += 2 

1040 

1041 # Update the cache 

1042 if pkey and key: 

1043 self.nth_cache[pkey][key] = [start, rindex] 

1044 

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

1046 if not matched: 

1047 break 

1048 

1049 return matched 

1050 

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

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

1053 

1054 is_empty = True 

1055 for child in self.get_children(el): 

1056 if self.is_tag(child): 

1057 is_empty = False 

1058 break 

1059 elif self.is_content_string(child) and RE_NOT_EMPTY.search(child): # type: ignore[call-overload] 

1060 is_empty = False 

1061 break 

1062 return is_empty 

1063 

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

1065 """Match selectors.""" 

1066 

1067 match = True 

1068 for sel in selectors: 

1069 if not self.match_selectors(el, sel): 

1070 match = False 

1071 return match 

1072 

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

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

1075 

1076 match = True 

1077 content = None # type: str | Sequence[str] | None 

1078 for contain_list in contains: 

1079 if content is None: 

1080 if contain_list.own: 

1081 content = self.get_own_text(el, no_iframe=self.is_html) 

1082 else: 

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

1084 found = False 

1085 for text in contain_list.text: 

1086 if contain_list.own: 

1087 for c in content: 

1088 if text in c: 

1089 found = True 

1090 break 

1091 if found: 

1092 break 

1093 else: 

1094 if text in content: 

1095 found = True 

1096 break 

1097 if not found: 

1098 match = False 

1099 return match 

1100 

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

1102 """Match default.""" 

1103 

1104 match = False 

1105 

1106 # Find this input's form 

1107 form = None # type: bs4.Tag | None 

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

1109 while parent and form is None: 

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

1111 form = parent 

1112 else: 

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

1114 

1115 if form is not None: 

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

1117 found_form = False 

1118 for f, t in self.cached_default_forms: 

1119 if f is form: 

1120 found_form = True 

1121 if t is el: 

1122 match = True 

1123 break 

1124 

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

1126 if not found_form: 

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

1128 name = self.get_tag(child) 

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

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

1131 break 

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

1133 v = self.get_attribute_by_name(child, 'type', '') 

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

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

1136 if el is child: 

1137 match = True 

1138 break 

1139 return match 

1140 

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

1142 """Match default.""" 

1143 

1144 match = False 

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

1146 

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

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

1149 form = None 

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

1151 while form is None: 

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

1153 form = parent 

1154 break 

1155 last_parent = parent 

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

1157 if parent is None: 

1158 form = last_parent 

1159 break 

1160 return form 

1161 

1162 form = get_parent_form(el) 

1163 

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

1165 if form is not None: 

1166 found_form = False 

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

1168 if f is form and n == name: 

1169 found_form = True 

1170 if i is True: 

1171 match = True 

1172 break 

1173 

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

1175 if not found_form: 

1176 checked = False 

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

1178 if child is el: 

1179 continue 

1180 tag_name = self.get_tag(child) 

1181 if tag_name == 'input': 

1182 is_radio = False 

1183 check = False 

1184 has_name = False 

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

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

1187 is_radio = True 

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

1189 has_name = True 

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

1191 check = True 

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

1193 checked = True 

1194 break 

1195 if checked: 

1196 break 

1197 if not checked: 

1198 match = True 

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

1200 

1201 return match 

1202 

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

1204 """Match languages.""" 

1205 

1206 match = False 

1207 has_ns = self.supports_namespaces() 

1208 root = self.root 

1209 has_html_namespace = self.has_html_namespace 

1210 

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

1212 parent = el # type: bs4.Tag | None 

1213 found_lang = None 

1214 last = None 

1215 while not found_lang: 

1216 has_html_ns = self.has_html_ns(parent) 

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

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

1219 if ( 

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

1221 ( 

1222 has_ns and not has_html_ns and attr_ns == NS_XML and 

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

1224 ) 

1225 ): 

1226 found_lang = v 

1227 break 

1228 last = parent 

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

1230 

1231 if parent is None: 

1232 root = last 

1233 has_html_namespace = self.has_html_ns(root) 

1234 parent = last 

1235 break 

1236 

1237 # Use cached meta language. 

1238 if found_lang is None and self.cached_meta_lang: 

1239 for cache in self.cached_meta_lang: 

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

1241 found_lang = cache[1] 

1242 

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

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

1245 # Find head 

1246 found = False 

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

1248 found = False 

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

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

1251 found = True 

1252 parent = child 

1253 break 

1254 if not found: # pragma: no cover 

1255 break 

1256 

1257 # Search meta tags 

1258 if found and parent is not None: 

1259 for child2 in parent: 

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

1261 c_lang = False 

1262 content = None 

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

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

1265 c_lang = True 

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

1267 content = v 

1268 if c_lang and content: 

1269 found_lang = content 

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

1271 break 

1272 if found_lang is not None: 

1273 break 

1274 if found_lang is None: 

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

1276 

1277 # If we determined a language, compare. 

1278 if found_lang is not None: 

1279 for patterns in langs: 

1280 match = False 

1281 for pattern in patterns: 

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

1283 match = True 

1284 if not match: 

1285 break 

1286 

1287 return match 

1288 

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

1290 """Check directionality.""" 

1291 

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

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

1294 return False 

1295 

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

1297 return False 

1298 

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

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

1301 if direction not in (None, 0): 

1302 return direction == directionality 

1303 

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

1305 is_root = self.is_root(el) 

1306 if is_root and direction is None: 

1307 return ct.SEL_DIR_LTR == directionality 

1308 

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

1310 name = self.get_tag(el) 

1311 is_input = name == 'input' 

1312 is_textarea = name == 'textarea' 

1313 is_bdi = name == 'bdi' 

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

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

1316 return ct.SEL_DIR_LTR == directionality 

1317 

1318 # Auto handling for text inputs 

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

1320 if is_textarea: 

1321 value = ''.join(node for node in self.get_contents(el, no_iframe=True) if self.is_content_string(node)) # type: ignore[misc] 

1322 else: 

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

1324 if value: 

1325 for c in value: 

1326 bidi = unicodedata.bidirectional(c) 

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

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

1329 return direction == directionality 

1330 # Assume left to right 

1331 return ct.SEL_DIR_LTR == directionality 

1332 elif is_root: 

1333 return ct.SEL_DIR_LTR == directionality 

1334 return self.match_dir(self.get_parent(el, no_iframe=True), directionality) 

1335 

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

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

1338 direction = self.find_bidi(el) 

1339 if direction is not None: 

1340 return direction == directionality 

1341 elif is_root: 

1342 return ct.SEL_DIR_LTR == directionality 

1343 return self.match_dir(self.get_parent(el, no_iframe=True), directionality) 

1344 

1345 # Match parents direction 

1346 return self.match_dir(self.get_parent(el, no_iframe=True), directionality) 

1347 

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

1349 """ 

1350 Match range. 

1351 

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

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

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

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

1356 """ 

1357 

1358 out_of_range = False 

1359 

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

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

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

1363 

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

1365 if mn is None and mx is None: 

1366 return False 

1367 

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

1369 if value is not None: 

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

1371 if mn is not None and value < mn: 

1372 out_of_range = True 

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

1374 out_of_range = True 

1375 elif itype == "time": 

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

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

1378 if value < mn and value > mx: 

1379 out_of_range = True 

1380 else: 

1381 if mn is not None and value < mn: 

1382 out_of_range = True 

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

1384 out_of_range = True 

1385 

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

1387 

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

1389 """ 

1390 Match defined. 

1391 

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

1393 

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

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

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

1397 

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

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

1400 """ 

1401 

1402 name = self.get_tag(el) 

1403 return ( 

1404 name is not None and ( 

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

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

1407 self.get_prefix(el) is not None 

1408 ) 

1409 ) 

1410 

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

1412 """ 

1413 Match placeholder shown according to HTML spec. 

1414 

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

1416 

1417 """ 

1418 

1419 match = False 

1420 content = self.get_text(el) 

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

1422 match = True 

1423 

1424 return match 

1425 

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

1427 """Check if element matches one of the selectors.""" 

1428 

1429 match = False 

1430 is_not = selectors.is_not 

1431 is_html = selectors.is_html 

1432 

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

1434 if is_html: 

1435 namespaces = self.namespaces 

1436 iframe_restrict = self.iframe_restrict 

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

1438 self.iframe_restrict = True 

1439 

1440 if not is_html or self.is_html: 

1441 for selector in selectors: 

1442 match = is_not 

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

1444 if selector is ct.Null: 

1445 continue 

1446 # Verify tag matches 

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

1448 continue 

1449 # Verify tag is defined 

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

1451 continue 

1452 # Verify element is root 

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

1454 continue 

1455 # Verify element is scope 

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

1457 continue 

1458 # Verify element has placeholder shown 

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

1460 continue 

1461 # Verify `nth` matches 

1462 if selector.nth and not self.match_nth(el, selector.nth): 

1463 continue 

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

1465 continue 

1466 # Verify id matches 

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

1468 continue 

1469 # Verify classes match 

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

1471 continue 

1472 # Verify attribute(s) match 

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

1474 continue 

1475 # Verify ranges 

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

1477 continue 

1478 # Verify language patterns 

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

1480 continue 

1481 # Verify pseudo selector patterns 

1482 if selector.selectors and not self.match_subselectors(el, selector.selectors): 

1483 continue 

1484 # Verify relationship selectors 

1485 if selector.relation and not self.match_relations(el, selector.relation): 

1486 continue 

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

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

1489 continue 

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

1491 # also not set. 

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

1493 continue 

1494 # Validate element directionality 

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

1496 continue 

1497 # Validate that the tag contains the specified text. 

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

1499 continue 

1500 match = not is_not 

1501 break 

1502 

1503 # Restore actual namespaces being used for external selector lists 

1504 if is_html: 

1505 self.namespaces = namespaces 

1506 self.iframe_restrict = iframe_restrict 

1507 

1508 return match 

1509 

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

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

1512 

1513 lim = None if limit < 1 else limit 

1514 

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

1516 if self.match(child): 

1517 yield child 

1518 if lim is not None: 

1519 lim -= 1 

1520 if lim < 1: 

1521 break 

1522 

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

1524 """Match closest ancestor.""" 

1525 

1526 current = self.tag # type: bs4.Tag | None 

1527 closest = None 

1528 while closest is None and current is not None: 

1529 if self.match(current): 

1530 closest = current 

1531 else: 

1532 current = self.get_parent(current) 

1533 return closest 

1534 

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

1536 """Filter tag's children.""" 

1537 

1538 return [ 

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

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

1541 ] 

1542 

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

1544 """Match.""" 

1545 

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

1547 

1548 

1549class SoupSieve(ct.Immutable): 

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

1551 

1552 pattern: str 

1553 selectors: ct.SelectorList 

1554 namespaces: ct.Namespaces | None 

1555 custom: dict[str, str] 

1556 flags: int 

1557 

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

1559 

1560 def __init__( 

1561 self, 

1562 pattern: str, 

1563 selectors: ct.SelectorList, 

1564 namespaces: ct.Namespaces | None, 

1565 custom: ct.CustomSelectors | None, 

1566 flags: int 

1567 ): 

1568 """Initialize.""" 

1569 

1570 super().__init__( 

1571 pattern=pattern, 

1572 selectors=selectors, 

1573 namespaces=namespaces, 

1574 custom=custom, 

1575 flags=flags 

1576 ) 

1577 

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

1579 """Match.""" 

1580 

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

1582 

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

1584 """Match closest ancestor.""" 

1585 

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

1587 

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

1589 """ 

1590 Filter. 

1591 

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

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

1594 and we can take advantage of the optimization. 

1595 

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

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

1598 """ 

1599 

1600 if isinstance(iterable, bs4.Tag): 

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

1602 else: 

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

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

1605 

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

1607 """Select a single tag.""" 

1608 

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

1610 return tags[0] if tags else None 

1611 

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

1613 """Select the specified tags.""" 

1614 

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

1616 

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

1618 """Iterate the specified tags.""" 

1619 

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

1621 

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

1623 """Representation.""" 

1624 

1625 return ( 

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

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

1628 ) 

1629 

1630 __str__ = __repr__ 

1631 

1632 

1633ct.pickle_register(SoupSieve)