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

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

945 statements  

1"""CSS matcher.""" 

2from __future__ import annotations 

3from datetime import datetime 

4from . import util 

5import re 

6from . import css_types as ct 

7import unicodedata 

8import bs4 

9from typing import Iterator, Iterable, Any, Callable, Sequence, Any, cast # noqa: F401, F811 

10 

11# Empty tag pattern (whitespace okay) 

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

13 

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

15 

16# Relationships 

17REL_PARENT = ' ' 

18REL_CLOSE_PARENT = '>' 

19REL_SIBLING = '~' 

20REL_CLOSE_SIBLING = '+' 

21 

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

23REL_HAS_PARENT = ': ' 

24REL_HAS_CLOSE_PARENT = ':>' 

25REL_HAS_SIBLING = ':~' 

26REL_HAS_CLOSE_SIBLING = ':+' 

27 

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

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

30 

31DIR_FLAGS = ct.SEL_DIR_LTR | ct.SEL_DIR_RTL 

32RANGES = ct.SEL_IN_RANGE | ct.SEL_OUT_OF_RANGE 

33 

34DIR_MAP = { 

35 'ltr': ct.SEL_DIR_LTR, 

36 'rtl': ct.SEL_DIR_RTL, 

37 'auto': 0 

38} 

39 

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

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

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

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

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

45RE_DATETIME = re.compile( 

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

47) 

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

49 

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

51FEB = 2 

52SHORT_MONTH = 30 

53LONG_MONTH = 31 

54FEB_MONTH = 28 

55FEB_LEAP_MONTH = 29 

56DAYS_IN_WEEK = 7 

57 

58 

59class _FakeParent: 

60 """ 

61 Fake parent class. 

62 

63 When we have a fragment with no `BeautifulSoup` document object, 

64 we can't evaluate `nth` selectors properly. Create a temporary 

65 fake parent so we can traverse the root element as a child. 

66 """ 

67 

68 def __init__(self, element: bs4.Tag) -> None: 

69 """Initialize.""" 

70 

71 self.contents = [element] 

72 

73 def __len__(self) -> int: 

74 """Length.""" 

75 

76 return len(self.contents) 

77 

78 

79class _DocumentNav: 

80 """Navigate a Beautiful Soup document.""" 

81 

82 @classmethod 

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

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

85 

86 # Fail on unexpected types. 

87 if not cls.is_tag(tag): 

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

89 

90 @staticmethod 

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

92 """Is `BeautifulSoup` object.""" 

93 return isinstance(obj, bs4.BeautifulSoup) 

94 

95 @staticmethod 

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

97 """Is tag.""" 

98 return isinstance(obj, bs4.Tag) 

99 

100 @staticmethod 

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

102 """Is declaration.""" 

103 return isinstance(obj, bs4.Declaration) 

104 

105 @staticmethod 

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

107 """Is CDATA.""" 

108 return isinstance(obj, bs4.CData) 

109 

110 @staticmethod 

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

112 """Is processing instruction.""" 

113 return isinstance(obj, bs4.ProcessingInstruction) 

114 

115 @staticmethod 

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

117 """Is navigable string.""" 

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

119 

120 @staticmethod 

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

122 """Is special string.""" 

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

124 

125 @classmethod 

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

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

128 

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

130 

131 @staticmethod 

132 def create_fake_parent(el: bs4.Tag) -> _FakeParent: 

133 """Create fake parent for a given element.""" 

134 

135 return _FakeParent(el) 

136 

137 @staticmethod 

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

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

140 

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

142 

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

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

145 

146 if el is None: # pragma: no cover 

147 return False 

148 

149 return bool( 

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

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

152 ) 

153 

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

155 """ 

156 Return whether element is a root element. 

157 

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

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

160 """ 

161 

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

163 if not root: 

164 parent = self.get_parent(el) 

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

166 return root 

167 

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

169 """Get contents or contents in reverse.""" 

170 

171 if el is not None: 

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

173 yield from el.contents 

174 

175 def get_tag_children( 

176 self, 

177 el: bs4.Tag | None, 

178 start: int | None = None, 

179 reverse: bool = False, 

180 no_iframe: bool = False 

181 ) -> Iterator[bs4.Tag]: 

182 """Get tag children.""" 

183 

184 return self.get_children(el, start, reverse, True, no_iframe) # type: ignore[return-value] 

185 

186 def get_children( 

187 self, 

188 el: bs4.Tag | None, 

189 start: int | None = None, 

190 reverse: bool = False, 

191 tags: bool = False, 

192 no_iframe: bool = False 

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

194 """Get children.""" 

195 

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

197 last = len(el.contents) - 1 

198 if start is None: 

199 index = last if reverse else 0 

200 else: 

201 index = start 

202 end = -1 if reverse else last + 1 

203 incr = -1 if reverse else 1 

204 

205 if 0 <= index <= last: 

206 while index != end: 

207 node = el.contents[index] 

208 index += incr 

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

210 yield node 

211 

212 def get_tag_descendants( 

213 self, 

214 el: bs4.Tag | None, 

215 no_iframe: bool = False 

216 ) -> Iterator[bs4.Tag]: 

217 """Specifically get tag descendants.""" 

218 

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

220 

221 def get_descendants( 

222 self, 

223 el: bs4.Tag | None, 

224 tags: bool = False, 

225 no_iframe: bool = False 

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

227 """Get descendants.""" 

228 

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

230 next_good = None 

231 for child in el.descendants: 

232 

233 if next_good is not None: 

234 if child is not next_good: 

235 continue 

236 next_good = None 

237 

238 if isinstance(child, bs4.Tag): 

239 if no_iframe and self.is_iframe(child): 

240 if child.next_sibling is not None: 

241 next_good = child.next_sibling 

242 else: 

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

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

245 last_child = last_child.contents[-1] 

246 next_good = last_child.next_element 

247 yield child 

248 if next_good is None: 

249 break 

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

251 continue # pragma: no cover 

252 yield child 

253 

254 elif not tags: 

255 yield child 

256 

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

258 """Get parent.""" 

259 

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

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

262 parent = None 

263 return parent 

264 

265 @staticmethod 

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

267 """Get tag.""" 

268 

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

270 

271 @staticmethod 

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

273 """Get prefix.""" 

274 

275 return el.prefix 

276 

277 @staticmethod 

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

279 """Get namespace `URI`.""" 

280 

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

282 

283 @classmethod 

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

285 """Get next sibling tag.""" 

286 

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

288 

289 @classmethod 

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

291 """Get next sibling tag.""" 

292 

293 sibling = el.next_sibling 

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

295 sibling = sibling.next_sibling 

296 

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

298 sibling = None 

299 

300 return sibling 

301 

302 @classmethod 

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

304 """Get previous sibling tag.""" 

305 

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

307 

308 @classmethod 

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

310 """Get previous sibling tag.""" 

311 

312 sibling = el.previous_sibling 

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

314 sibling = sibling.previous_sibling 

315 

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

317 sibling = None 

318 

319 return sibling 

320 

321 @staticmethod 

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

323 """ 

324 Check if element has an HTML namespace. 

325 

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

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

328 """ 

329 

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

331 return bool(ns and ns == NS_XHTML) 

332 

333 @staticmethod 

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

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

336 

337 if el is None: # pragma: no cover 

338 return None, None 

339 

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

341 

342 @classmethod 

343 def normalize_value(cls, value: Any) -> str | Sequence[str]: 

344 """Normalize the value to be a string or list of strings.""" 

345 

346 # Treat `None` as empty string. 

347 if value is None: 

348 return '' 

349 

350 # Pass through strings 

351 if (isinstance(value, str)): 

352 return value 

353 

354 # If it's a byte string, convert it to Unicode, treating it as UTF-8. 

355 if isinstance(value, bytes): 

356 return value.decode("utf8") 

357 

358 # BeautifulSoup supports sequences of attribute values, so make sure the children are strings. 

359 if isinstance(value, Sequence): 

360 new_value = [] 

361 for v in value: 

362 if not isinstance(v, (str, bytes)) and isinstance(v, Sequence): 

363 # This is most certainly a user error and will crash and burn later. 

364 # To keep things working, we'll do what we do with all objects, 

365 # And convert them to strings. 

366 new_value.append(str(v)) 

367 else: 

368 # Convert the child to a string 

369 new_value.append(cast(str, cls.normalize_value(v))) 

370 return new_value 

371 

372 # Try and make anything else a string 

373 return str(value) 

374 

375 @classmethod 

376 def get_attribute_by_name( 

377 cls, 

378 el: bs4.Tag, 

379 name: str, 

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

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

382 """Get attribute by name.""" 

383 

384 value = default 

385 if el._is_xml: 

386 try: 

387 value = cls.normalize_value(el.attrs[name]) 

388 except KeyError: 

389 pass 

390 else: 

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

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

393 value = cls.normalize_value(v) 

394 break 

395 return value 

396 

397 @classmethod 

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

399 """Iterate attributes.""" 

400 

401 if el is not None: 

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

403 yield k, cls.normalize_value(v) 

404 

405 @classmethod 

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

407 """Get classes.""" 

408 

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

410 if isinstance(classes, str): 

411 classes = RE_NOT_WS.findall(classes) 

412 return cast(Sequence[str], classes) 

413 

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

415 """Get text.""" 

416 

417 return ''.join( 

418 [ 

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

420 if self.is_content_string(node) 

421 ] 

422 ) 

423 

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

425 """Get Own Text.""" 

426 

427 return [ 

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

429 ] 

430 

431 

432class Inputs: 

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

434 

435 @staticmethod 

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

437 """Validate day.""" 

438 

439 max_days = LONG_MONTH 

440 if month == FEB: 

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

442 elif month in MONTHS_30: 

443 max_days = SHORT_MONTH 

444 return 1 <= day <= max_days 

445 

446 @staticmethod 

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

448 """Validate week.""" 

449 

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

451 # 

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

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

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

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

456 # 

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

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

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

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

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

462 return 1 <= week <= max_week 

463 

464 @staticmethod 

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

466 """Validate month.""" 

467 

468 return 1 <= month <= 12 

469 

470 @staticmethod 

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

472 """Validate year.""" 

473 

474 return 1 <= year 

475 

476 @staticmethod 

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

478 """Validate hour.""" 

479 

480 return 0 <= hour <= 23 

481 

482 @staticmethod 

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

484 """Validate minutes.""" 

485 

486 return 0 <= minutes <= 59 

487 

488 @classmethod 

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

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

491 

492 parsed = None # type: tuple[float, ...] | 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 if cls.validate_hour(hour) and cls.validate_minutes(minutes): 

523 parsed = (hour, minutes) 

524 elif itype == "datetime-local": 

525 m = RE_DATETIME.match(value) 

526 if m: 

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

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

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

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

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

532 if ( 

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

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

535 ): 

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

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

538 m = RE_NUM.match(value) 

539 if m: 

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

541 return parsed 

542 

543 

544class CSSMatch(_DocumentNav): 

545 """Perform CSS matching.""" 

546 

547 def __init__( 

548 self, 

549 selectors: ct.SelectorList, 

550 scope: bs4.Tag | None, 

551 namespaces: ct.Namespaces | None, 

552 flags: int 

553 ) -> None: 

554 """Initialize.""" 

555 

556 self.assert_valid_input(scope) 

557 self.tag = scope 

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

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

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

561 self.selectors = selectors 

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

563 self.flags = flags 

564 self.iframe_restrict = False 

565 

566 # Find the root element for the whole tree 

567 doc = scope 

568 parent = self.get_parent(doc) 

569 while parent: 

570 doc = parent 

571 parent = self.get_parent(doc) 

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

573 if not self.is_doc(doc): 

574 root = doc 

575 else: 

576 for child in self.get_tag_children(doc): 

577 root = child 

578 break 

579 

580 self.root = root 

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

582 self.has_html_namespace = self.has_html_ns(root) 

583 

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

585 self.is_xml = self.is_xml_tree(doc) 

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

587 

588 def supports_namespaces(self) -> bool: 

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

590 

591 return self.is_xml or self.has_html_namespace 

592 

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

594 """Get tag namespace.""" 

595 

596 namespace = '' 

597 if el is None: # pragma: no cover 

598 return namespace 

599 

600 if self.supports_namespaces(): 

601 ns = self.get_uri(el) 

602 if ns: 

603 namespace = ns 

604 else: 

605 namespace = NS_XHTML 

606 return namespace 

607 

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

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

610 

611 return self.get_tag_ns(el) == NS_XHTML 

612 

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

614 """Get tag.""" 

615 

616 name = self.get_tag_name(el) 

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

618 

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

620 """Get prefix.""" 

621 

622 prefix = self.get_prefix_name(el) 

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

624 

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

626 """Get directionality from element text.""" 

627 

628 for node in self.get_children(el): 

629 

630 # Analyze child text nodes 

631 if self.is_tag(node): 

632 

633 # Avoid analyzing certain elements specified in the specification. 

634 direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(node, 'dir', '')), None) # type: ignore[arg-type] 

635 name = self.get_tag(node) # type: ignore[arg-type] 

636 if ( 

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

638 not self.is_html_tag(node) or # type: ignore[arg-type] 

639 direction is not None 

640 ): 

641 continue # pragma: no cover 

642 

643 # Check directionality of this node's text 

644 value = self.find_bidi(node) # type: ignore[arg-type] 

645 if value is not None: 

646 return value 

647 

648 # Direction could not be determined 

649 continue # pragma: no cover 

650 

651 # Skip `doctype` comments, etc. 

652 if self.is_special_string(node): 

653 continue 

654 

655 # Analyze text nodes for directionality. 

656 for c in node: # type: ignore[attr-defined] 

657 bidi = unicodedata.bidirectional(c) 

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

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

660 return None 

661 

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

663 """Filter the language tags.""" 

664 

665 match = True 

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

667 ranges = lang_range.split('-') 

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

669 length = len(ranges) 

670 slength = len(subtags) 

671 rindex = 0 

672 sindex = 0 

673 r = ranges[rindex] 

674 s = subtags[sindex] 

675 

676 # Empty specified language should match unspecified language attributes 

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

678 return True 

679 

680 # Primary tag needs to match 

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

682 match = False 

683 

684 rindex += 1 

685 sindex += 1 

686 

687 # Match until we run out of ranges 

688 while match and rindex < length: 

689 r = ranges[rindex] 

690 try: 

691 s = subtags[sindex] 

692 except IndexError: 

693 # Ran out of subtags, 

694 # but we still have ranges 

695 match = False 

696 continue 

697 

698 # Empty range 

699 if not r: 

700 match = False 

701 continue 

702 

703 # Matched range 

704 elif s == r: 

705 rindex += 1 

706 

707 # Implicit wildcard cannot match 

708 # singletons 

709 elif len(s) == 1: 

710 match = False 

711 continue 

712 

713 # Implicitly matched, so grab next subtag 

714 sindex += 1 

715 

716 return match 

717 

718 def match_attribute_name( 

719 self, 

720 el: bs4.Tag, 

721 attr: str, 

722 prefix: str | None 

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

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

725 

726 value = None 

727 if self.supports_namespaces(): 

728 value = None 

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

730 if prefix: 

731 ns = self.namespaces.get(prefix) 

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

733 return None 

734 else: 

735 ns = None 

736 

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

738 

739 # Get attribute parts 

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

741 

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

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

744 if ns is None: 

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

746 value = v 

747 break 

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

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

750 # Ignore the false positive message. 

751 continue # pragma: no cover 

752 

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

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

755 continue 

756 

757 # The attribute doesn't match. 

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

759 continue 

760 

761 value = v 

762 break 

763 else: 

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

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

766 continue 

767 value = v 

768 break 

769 return value 

770 

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

772 """Match the namespace of the element.""" 

773 

774 match = True 

775 namespace = self.get_tag_ns(el) 

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

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

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

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

780 match = False 

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

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

783 match = False 

784 # Verify prefix matches 

785 elif ( 

786 tag.prefix and 

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

788 ): 

789 match = False 

790 return match 

791 

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

793 """Match attributes.""" 

794 

795 match = True 

796 if attributes: 

797 for a in attributes: 

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

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

800 if temp is None: 

801 match = False 

802 break 

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

804 if pattern is None: 

805 continue 

806 elif pattern.match(value) is None: 

807 match = False 

808 break 

809 return match 

810 

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

812 """Match tag name.""" 

813 

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

815 return not ( 

816 name is not None and 

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

818 ) 

819 

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

821 """Match the tag.""" 

822 

823 match = True 

824 if tag is not None: 

825 # Verify namespace 

826 if not self.match_namespace(el, tag): 

827 match = False 

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

829 match = False 

830 return match 

831 

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

833 """Match past relationship.""" 

834 

835 found = False 

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

837 if isinstance(relation[0], ct.SelectorNull): # pragma: no cover 

838 return found 

839 

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

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

842 while not found and parent: 

843 found = self.match_selectors(parent, relation) 

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

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

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

847 if parent: 

848 found = self.match_selectors(parent, relation) 

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

850 sibling = self.get_previous_tag(el) 

851 while not found and sibling: 

852 found = self.match_selectors(sibling, relation) 

853 sibling = self.get_previous_tag(sibling) 

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

855 sibling = self.get_previous_tag(el) 

856 if sibling and self.is_tag(sibling): 

857 found = self.match_selectors(sibling, relation) 

858 return found 

859 

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

861 """Match future child.""" 

862 

863 match = False 

864 if recursive: 

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

866 else: 

867 children = self.get_tag_children 

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

869 match = self.match_selectors(child, relation) 

870 if match: 

871 break 

872 return match 

873 

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

875 """Match future relationship.""" 

876 

877 found = False 

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

879 if isinstance(relation[0], ct.SelectorNull): # pragma: no cover 

880 return found 

881 

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

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

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

885 found = self.match_future_child(el, relation) 

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

887 sibling = self.get_next_tag(el) 

888 while not found and sibling: 

889 found = self.match_selectors(sibling, relation) 

890 sibling = self.get_next_tag(sibling) 

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

892 sibling = self.get_next_tag(el) 

893 if sibling and self.is_tag(sibling): 

894 found = self.match_selectors(sibling, relation) 

895 return found 

896 

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

898 """Match relationship to other elements.""" 

899 

900 found = False 

901 

902 if isinstance(relation[0], ct.SelectorNull) or relation[0].rel_type is None: 

903 return found 

904 

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

906 found = self.match_future_relations(el, relation) 

907 else: 

908 found = self.match_past_relations(el, relation) 

909 

910 return found 

911 

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

913 """Match element's ID.""" 

914 

915 found = True 

916 for i in ids: 

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

918 found = False 

919 break 

920 return found 

921 

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

923 """Match element's classes.""" 

924 

925 current_classes = self.get_classes(el) 

926 found = True 

927 for c in classes: 

928 if c not in current_classes: 

929 found = False 

930 break 

931 return found 

932 

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

934 """Match element as root.""" 

935 

936 is_root = self.is_root(el) 

937 if is_root: 

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

939 while is_root and sibling is not None: 

940 if ( 

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

942 self.is_cdata(sibling) 

943 ): 

944 is_root = False 

945 else: 

946 sibling = self.get_previous(sibling) 

947 if is_root: 

948 sibling = self.get_next(el) 

949 while is_root and sibling is not None: 

950 if ( 

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

952 self.is_cdata(sibling) 

953 ): 

954 is_root = False 

955 else: 

956 sibling = self.get_next(sibling) 

957 return is_root 

958 

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

960 """Match element as scope.""" 

961 

962 return self.scope is el 

963 

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

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

966 

967 return ( 

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

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

970 ) 

971 

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

973 """Match `nth` elements.""" 

974 

975 matched = True 

976 

977 for n in nth: 

978 matched = False 

979 if n.selectors and not self.match_selectors(el, n.selectors): 

980 break 

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

982 if parent is None: 

983 parent = cast('bs4.Tag', self.create_fake_parent(el)) 

984 last = n.last 

985 last_index = len(parent) - 1 

986 index = last_index if last else 0 

987 relative_index = 0 

988 a = n.a 

989 b = n.b 

990 var = n.n 

991 count = 0 

992 count_incr = 1 

993 factor = -1 if last else 1 

994 idx = last_idx = a * count + b if var else a 

995 

996 # We can only adjust bounds within a variable index 

997 if var: 

998 # Find the count `n` that yields the smallest in-bounds index 

999 # (>= 1), then set the increment direction so that the index 

1000 # ascends from there as the evaluation loop below walks children. 

1001 if a > 0: 

1002 # Ascending sequence: smallest n with a * n + b >= 1. 

1003 count = 0 if b >= 1 else -(-(1 - b) // a) 

1004 elif a < 0: 

1005 # Descending sequence: largest n with a * n + b >= 1, then 

1006 # walk n back down so the index increases. 

1007 count = (b - 1) // -a if b >= 1 else 0 

1008 count_incr = -1 

1009 idx = last_idx = a * count + b 

1010 

1011 # Evaluate elements while our calculated nth index is still in range 

1012 while 1 <= idx <= last_index + 1: 

1013 child = None # type: bs4.element.PageElement | None 

1014 # Evaluate while our child index is still in range. 

1015 for child in self.get_children(parent, start=index, reverse=factor < 0): 

1016 index += factor 

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

1018 continue 

1019 # Handle `of S` in `nth-child` 

1020 if n.selectors and not self.match_selectors(child, n.selectors): 

1021 continue 

1022 # Handle `of-type` 

1023 if n.of_type and not self.match_nth_tag_type(el, child): 

1024 continue 

1025 relative_index += 1 

1026 if relative_index == idx: 

1027 if child is el: 

1028 matched = True 

1029 else: 

1030 break 

1031 if child is el: 

1032 break 

1033 if child is el: 

1034 break 

1035 last_idx = idx 

1036 count += count_incr 

1037 if count < 0: 

1038 # Count is counting down and has now ventured into invalid territory. 

1039 break 

1040 idx = a * count + b if var else a 

1041 if last_idx == idx: 

1042 break 

1043 if not matched: 

1044 break 

1045 return matched 

1046 

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

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

1049 

1050 is_empty = True 

1051 for child in self.get_children(el): 

1052 if self.is_tag(child): 

1053 is_empty = False 

1054 break 

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

1056 is_empty = False 

1057 break 

1058 return is_empty 

1059 

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

1061 """Match selectors.""" 

1062 

1063 match = True 

1064 for sel in selectors: 

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

1066 match = False 

1067 return match 

1068 

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

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

1071 

1072 match = True 

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

1074 for contain_list in contains: 

1075 if content is None: 

1076 if contain_list.own: 

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

1078 else: 

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

1080 found = False 

1081 for text in contain_list.text: 

1082 if contain_list.own: 

1083 for c in content: 

1084 if text in c: 

1085 found = True 

1086 break 

1087 if found: 

1088 break 

1089 else: 

1090 if text in content: 

1091 found = True 

1092 break 

1093 if not found: 

1094 match = False 

1095 return match 

1096 

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

1098 """Match default.""" 

1099 

1100 match = False 

1101 

1102 # Find this input's form 

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

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

1105 while parent and form is None: 

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

1107 form = parent 

1108 else: 

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

1110 

1111 if form is not None: 

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

1113 found_form = False 

1114 for f, t in self.cached_default_forms: 

1115 if f is form: 

1116 found_form = True 

1117 if t is el: 

1118 match = True 

1119 break 

1120 

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

1122 if not found_form: 

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

1124 name = self.get_tag(child) 

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

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

1127 break 

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

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

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

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

1132 if el is child: 

1133 match = True 

1134 break 

1135 return match 

1136 

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

1138 """Match default.""" 

1139 

1140 match = False 

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

1142 

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

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

1145 form = None 

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

1147 while form is None: 

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

1149 form = parent 

1150 break 

1151 last_parent = parent 

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

1153 if parent is None: 

1154 form = last_parent 

1155 break 

1156 return form 

1157 

1158 form = get_parent_form(el) 

1159 

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

1161 if form is not None: 

1162 found_form = False 

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

1164 if f is form and n == name: 

1165 found_form = True 

1166 if i is True: 

1167 match = True 

1168 break 

1169 

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

1171 if not found_form: 

1172 checked = False 

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

1174 if child is el: 

1175 continue 

1176 tag_name = self.get_tag(child) 

1177 if tag_name == 'input': 

1178 is_radio = False 

1179 check = False 

1180 has_name = False 

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

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

1183 is_radio = True 

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

1185 has_name = True 

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

1187 check = True 

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

1189 checked = True 

1190 break 

1191 if checked: 

1192 break 

1193 if not checked: 

1194 match = True 

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

1196 

1197 return match 

1198 

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

1200 """Match languages.""" 

1201 

1202 match = False 

1203 has_ns = self.supports_namespaces() 

1204 root = self.root 

1205 has_html_namespace = self.has_html_namespace 

1206 

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

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

1209 found_lang = None 

1210 last = None 

1211 while not found_lang: 

1212 has_html_ns = self.has_html_ns(parent) 

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

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

1215 if ( 

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

1217 ( 

1218 has_ns and not has_html_ns and attr_ns == NS_XML and 

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

1220 ) 

1221 ): 

1222 found_lang = v 

1223 break 

1224 last = parent 

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

1226 

1227 if parent is None: 

1228 root = last 

1229 has_html_namespace = self.has_html_ns(root) 

1230 parent = last 

1231 break 

1232 

1233 # Use cached meta language. 

1234 if found_lang is None and self.cached_meta_lang: 

1235 for cache in self.cached_meta_lang: 

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

1237 found_lang = cache[1] 

1238 

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

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

1241 # Find head 

1242 found = False 

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

1244 found = False 

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

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

1247 found = True 

1248 parent = child 

1249 break 

1250 if not found: # pragma: no cover 

1251 break 

1252 

1253 # Search meta tags 

1254 if found and parent is not None: 

1255 for child2 in parent: 

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

1257 c_lang = False 

1258 content = None 

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

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

1261 c_lang = True 

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

1263 content = v 

1264 if c_lang and content: 

1265 found_lang = content 

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

1267 break 

1268 if found_lang is not None: 

1269 break 

1270 if found_lang is None: 

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

1272 

1273 # If we determined a language, compare. 

1274 if found_lang is not None: 

1275 for patterns in langs: 

1276 match = False 

1277 for pattern in patterns: 

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

1279 match = True 

1280 if not match: 

1281 break 

1282 

1283 return match 

1284 

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

1286 """Check directionality.""" 

1287 

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

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

1290 return False 

1291 

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

1293 return False 

1294 

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

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

1297 if direction not in (None, 0): 

1298 return direction == directionality 

1299 

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

1301 is_root = self.is_root(el) 

1302 if is_root and direction is None: 

1303 return ct.SEL_DIR_LTR == directionality 

1304 

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

1306 name = self.get_tag(el) 

1307 is_input = name == 'input' 

1308 is_textarea = name == 'textarea' 

1309 is_bdi = name == 'bdi' 

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

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

1312 return ct.SEL_DIR_LTR == directionality 

1313 

1314 # Auto handling for text inputs 

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

1316 if is_textarea: 

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

1318 else: 

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

1320 if value: 

1321 for c in value: 

1322 bidi = unicodedata.bidirectional(c) 

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

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

1325 return direction == directionality 

1326 # Assume left to right 

1327 return ct.SEL_DIR_LTR == directionality 

1328 elif is_root: 

1329 return ct.SEL_DIR_LTR == directionality 

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

1331 

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

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

1334 direction = self.find_bidi(el) 

1335 if direction is not None: 

1336 return direction == directionality 

1337 elif is_root: 

1338 return ct.SEL_DIR_LTR == directionality 

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

1340 

1341 # Match parents direction 

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

1343 

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

1345 """ 

1346 Match range. 

1347 

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

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

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

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

1352 """ 

1353 

1354 out_of_range = False 

1355 

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

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

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

1359 

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

1361 if mn is None and mx is None: 

1362 return False 

1363 

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

1365 if value is not None: 

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

1367 if mn is not None and value < mn: 

1368 out_of_range = True 

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

1370 out_of_range = True 

1371 elif itype == "time": 

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

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

1374 if value < mn and value > mx: 

1375 out_of_range = True 

1376 else: 

1377 if mn is not None and value < mn: 

1378 out_of_range = True 

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

1380 out_of_range = True 

1381 

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

1383 

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

1385 """ 

1386 Match defined. 

1387 

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

1389 

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

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

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

1393 

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

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

1396 """ 

1397 

1398 name = self.get_tag(el) 

1399 return ( 

1400 name is not None and ( 

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

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

1403 self.get_prefix(el) is not None 

1404 ) 

1405 ) 

1406 

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

1408 """ 

1409 Match placeholder shown according to HTML spec. 

1410 

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

1412 

1413 """ 

1414 

1415 match = False 

1416 content = self.get_text(el) 

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

1418 match = True 

1419 

1420 return match 

1421 

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

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

1424 

1425 match = False 

1426 is_not = selectors.is_not 

1427 is_html = selectors.is_html 

1428 

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

1430 if is_html: 

1431 namespaces = self.namespaces 

1432 iframe_restrict = self.iframe_restrict 

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

1434 self.iframe_restrict = True 

1435 

1436 if not is_html or self.is_html: 

1437 for selector in selectors: 

1438 match = is_not 

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

1440 if isinstance(selector, ct.SelectorNull): 

1441 continue 

1442 # Verify tag matches 

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

1444 continue 

1445 # Verify tag is defined 

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

1447 continue 

1448 # Verify element is root 

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

1450 continue 

1451 # Verify element is scope 

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

1453 continue 

1454 # Verify element has placeholder shown 

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

1456 continue 

1457 # Verify `nth` matches 

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

1459 continue 

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

1461 continue 

1462 # Verify id matches 

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

1464 continue 

1465 # Verify classes match 

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

1467 continue 

1468 # Verify attribute(s) match 

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

1470 continue 

1471 # Verify ranges 

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

1473 continue 

1474 # Verify language patterns 

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

1476 continue 

1477 # Verify pseudo selector patterns 

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

1479 continue 

1480 # Verify relationship selectors 

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

1482 continue 

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

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

1485 continue 

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

1487 # also not set. 

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

1489 continue 

1490 # Validate element directionality 

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

1492 continue 

1493 # Validate that the tag contains the specified text. 

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

1495 continue 

1496 match = not is_not 

1497 break 

1498 

1499 # Restore actual namespaces being used for external selector lists 

1500 if is_html: 

1501 self.namespaces = namespaces 

1502 self.iframe_restrict = iframe_restrict 

1503 

1504 return match 

1505 

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

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

1508 

1509 lim = None if limit < 1 else limit 

1510 

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

1512 if self.match(child): 

1513 yield child 

1514 if lim is not None: 

1515 lim -= 1 

1516 if lim < 1: 

1517 break 

1518 

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

1520 """Match closest ancestor.""" 

1521 

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

1523 closest = None 

1524 while closest is None and current is not None: 

1525 if self.match(current): 

1526 closest = current 

1527 else: 

1528 current = self.get_parent(current) 

1529 return closest 

1530 

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

1532 """Filter tag's children.""" 

1533 

1534 return [ 

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

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

1537 ] 

1538 

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

1540 """Match.""" 

1541 

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

1543 

1544 

1545class SoupSieve(ct.Immutable): 

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

1547 

1548 pattern: str 

1549 selectors: ct.SelectorList 

1550 namespaces: ct.Namespaces | None 

1551 custom: dict[str, str] 

1552 flags: int 

1553 

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

1555 

1556 def __init__( 

1557 self, 

1558 pattern: str, 

1559 selectors: ct.SelectorList, 

1560 namespaces: ct.Namespaces | None, 

1561 custom: ct.CustomSelectors | None, 

1562 flags: int 

1563 ): 

1564 """Initialize.""" 

1565 

1566 super().__init__( 

1567 pattern=pattern, 

1568 selectors=selectors, 

1569 namespaces=namespaces, 

1570 custom=custom, 

1571 flags=flags 

1572 ) 

1573 

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

1575 """Match.""" 

1576 

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

1578 

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

1580 """Match closest ancestor.""" 

1581 

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

1583 

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

1585 """ 

1586 Filter. 

1587 

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

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

1590 and we can take advantage of the optimization. 

1591 

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

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

1594 """ 

1595 

1596 if isinstance(iterable, bs4.Tag): 

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

1598 else: 

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

1600 

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

1602 """Select a single tag.""" 

1603 

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

1605 return tags[0] if tags else None 

1606 

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

1608 """Select the specified tags.""" 

1609 

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

1611 

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

1613 """Iterate the specified tags.""" 

1614 

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

1616 

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

1618 """Representation.""" 

1619 

1620 return ( 

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

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

1623 ) 

1624 

1625 __str__ = __repr__ 

1626 

1627 

1628ct.pickle_register(SoupSieve)