Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/soupsieve/css_match.py: 49%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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
1"""CSS matcher."""
2from __future__ import annotations
3from datetime import datetime
4from math import isfinite
5from collections.abc import Hashable
6from . import util
7import re
8from . import css_types as ct
9import unicodedata
10import bs4
11from typing import (
12 Generator,
13 Iterator,
14 Iterable,
15 Any,
16 Callable,
17 Sequence,
18 overload,
19 Literal,
20 cast,
21 TYPE_CHECKING
22) # noqa: F401, F811
24# Empty tag pattern (whitespace okay)
25RE_NOT_EMPTY = re.compile('[^ \t\r\n\f]')
27RE_NOT_WS = re.compile('[^ \t\r\n\f]+')
29# Relationships
30REL_PARENT = ' '
31REL_CLOSE_PARENT = '>'
32REL_SIBLING = '~'
33REL_CLOSE_SIBLING = '+'
35# Relationships for :has() (forward looking)
36REL_HAS_PARENT = ': '
37REL_HAS_CLOSE_PARENT = ':>'
38REL_HAS_SIBLING = ':~'
39REL_HAS_CLOSE_SIBLING = ':+'
41# `bs4` imports this module while it is still initializing,
42# so these must only be evaluated when type checking.
43if TYPE_CHECKING: # pragma: no cover
44 MatchRequest = tuple[bs4.Tag, ct.SelectorList]
45 MatchGenerator = Generator[MatchRequest, bool, bool]
47NS_XHTML = 'http://www.w3.org/1999/xhtml'
48NS_XML = 'http://www.w3.org/XML/1998/namespace'
50DIR_FLAGS = ct.SEL_DIR_LTR | ct.SEL_DIR_RTL
51RANGES = ct.SEL_IN_RANGE | ct.SEL_OUT_OF_RANGE
53DIR_MAP = {
54 'ltr': ct.SEL_DIR_LTR,
55 'rtl': ct.SEL_DIR_RTL,
56 'auto': 0
57}
59RE_NUM = re.compile(r"^(?P<value>-?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?)$")
60RE_TIME = re.compile(r'^(?P<hour>[0-9]{2}):(?P<minutes>[0-9]{2})(?::(?P<seconds>[0-9]{2}(?:\.[0-9]{1,3})?))?$')
61RE_MONTH = re.compile(r'^(?P<year>[0-9]{4,})-(?P<month>[0-9]{2})$')
62RE_WEEK = re.compile(r'^(?P<year>[0-9]{4,})-W(?P<week>[0-9]{2})$')
63RE_DATE = re.compile(r'^(?P<year>[0-9]{4,})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})$')
64RE_DATETIME = re.compile(
65 r'^(?P<year>[0-9]{4,})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})T(?P<hour>[0-9]{2}):(?P<minutes>[0-9]{2})'
66 r'(?::(?P<seconds>[0-9]{2}(?:\.[0-9]{1,3})?))?$'
67)
68RE_WILD_STRIP = re.compile(r'(?:(?:-\*-)(?:\*(?:-|$))*|-\*$)')
70MONTHS_30 = (4, 6, 9, 11) # April, June, September, and November
71FEB = 2
72SHORT_MONTH = 30
73LONG_MONTH = 31
74FEB_MONTH = 28
75FEB_LEAP_MONTH = 29
76DAYS_IN_WEEK = 7
79def within(target: bs4.Tag, parent: bs4.Tag | bs4.BeautifulSoup, start: int, end: int | None = None) -> bool:
80 """Check if target is within data."""
82 contents = parent.contents
83 return any(contents[i] is target for i in range(start, end if end is not None else len(contents)))
86class _DocumentNav:
87 """Navigate a Beautiful Soup document."""
89 @classmethod
90 def assert_valid_input(cls, tag: Any) -> None:
91 """Check if valid input tag or document."""
93 # Fail on unexpected types.
94 if not cls.is_tag(tag):
95 raise TypeError(f"Expected a BeautifulSoup 'Tag', but instead received type {type(tag)}")
97 @staticmethod
98 def is_doc(obj: bs4.element.PageElement | None) -> bool:
99 """Is `BeautifulSoup` object."""
100 return isinstance(obj, bs4.BeautifulSoup)
102 @staticmethod
103 def is_tag(obj: bs4.element.PageElement | None) -> bool:
104 """Is tag."""
105 return isinstance(obj, bs4.Tag)
107 @staticmethod
108 def is_declaration(obj: bs4.element.PageElement | None) -> bool: # pragma: no cover
109 """Is declaration."""
110 return isinstance(obj, bs4.Declaration)
112 @staticmethod
113 def is_cdata(obj: bs4.element.PageElement | None) -> bool:
114 """Is CDATA."""
115 return isinstance(obj, bs4.CData)
117 @staticmethod
118 def is_processing_instruction(obj: bs4.element.PageElement | None) -> bool: # pragma: no cover
119 """Is processing instruction."""
120 return isinstance(obj, bs4.ProcessingInstruction)
122 @staticmethod
123 def is_navigable_string(obj: bs4.element.PageElement | None) -> bool:
124 """Is navigable string."""
125 return isinstance(obj, bs4.element.NavigableString)
127 @staticmethod
128 def is_special_string(obj: bs4.element.PageElement | None) -> bool:
129 """Is special string."""
130 return isinstance(obj, (bs4.Comment, bs4.Declaration, bs4.CData, bs4.ProcessingInstruction, bs4.Doctype))
132 @classmethod
133 def is_content_string(cls, obj: bs4.element.PageElement | None) -> bool:
134 """Check if node is content string."""
136 return cls.is_navigable_string(obj) and not cls.is_special_string(obj)
138 @staticmethod
139 def is_xml_tree(el: bs4.Tag | None) -> bool:
140 """Check if element (or document) is from a XML tree."""
142 return el is not None and bool(el._is_xml)
144 def is_iframe(self, el: bs4.Tag | None) -> bool:
145 """Check if element is an `iframe`."""
147 if el is None: # pragma: no cover
148 return False
150 return bool(
151 ((el.name if self.is_xml_tree(el) else util.lower(el.name)) == 'iframe') and
152 self.is_html_tag(el) # type: ignore[attr-defined]
153 )
155 def is_root(self, el: bs4.Tag) -> bool:
156 """
157 Return whether element is a root element.
159 We check that the element is the root of the tree (which we have already pre-calculated),
160 and we check if it is the root element under an `iframe`.
161 """
163 root = self.root and self.root is el # type: ignore[attr-defined]
164 if not root:
165 parent = self.get_parent(el)
166 root = parent is not None and self.is_html and self.is_iframe(parent) # type: ignore[attr-defined]
167 return root
169 def get_contents(self, el: bs4.Tag | None, no_iframe: bool = False) -> Iterator[bs4.element.PageElement]:
170 """Get contents or contents in reverse."""
172 if el is not None:
173 if not no_iframe or not self.is_iframe(el):
174 yield from el.contents
176 def get_tag_children(
177 self,
178 el: bs4.Tag | None,
179 start: int | None = None,
180 reverse: bool = False,
181 no_iframe: bool = False
182 ) -> Iterator[bs4.Tag]:
183 """Get tag children."""
185 return self.get_children(el, start, reverse, True, no_iframe)
187 @overload
188 def get_children(
189 self,
190 el: bs4.Tag | None,
191 start: int | None = None,
192 reverse: bool = False,
193 tags: Literal[True] = ...,
194 no_iframe: bool = False
195 ) -> Iterator[bs4.Tag]:
196 ...
198 @overload
199 def get_children(
200 self,
201 el: bs4.Tag | None,
202 start: int | None = None,
203 reverse: bool = False,
204 tags: Literal[False] = ...,
205 no_iframe: bool = False
206 ) -> Iterator[bs4.element.PageElement]:
207 ...
209 def get_children(
210 self,
211 el: bs4.Tag | None,
212 start: int | None = None,
213 reverse: bool = False,
214 tags: Literal[True] | Literal[False] = False,
215 no_iframe: bool = False
216 ) -> Iterator[bs4.element.PageElement]:
217 """Get children."""
219 if el is not None and (not no_iframe or not self.is_iframe(el)):
220 last = len(el.contents) - 1
221 if start is None:
222 index = last if reverse else 0
223 else:
224 index = start
225 end = -1 if reverse else last + 1
226 incr = -1 if reverse else 1
228 if 0 <= index <= last:
229 for i in range(index, end, incr):
230 node = el.contents[i]
231 if not tags or self.is_tag(node):
232 yield node
234 def get_tag_descendants(
235 self,
236 el: bs4.Tag | None,
237 no_iframe: bool = False
238 ) -> Iterator[bs4.Tag]:
239 """Specifically get tag descendants."""
241 yield from self.get_descendants(el, tags=True, no_iframe=no_iframe) # type: ignore[misc]
243 def get_descendants(
244 self,
245 el: bs4.Tag | None,
246 tags: bool = False,
247 no_iframe: bool = False
248 ) -> Iterator[bs4.element.PageElement]:
249 """Get descendants."""
251 if el is not None and (not no_iframe or not self.is_iframe(el)):
252 next_good = None
253 for child in el.descendants:
255 if next_good is not None:
256 if child is not next_good:
257 continue
258 next_good = None
260 if isinstance(child, bs4.Tag):
261 if no_iframe and self.is_iframe(child):
262 if child.next_sibling is not None:
263 next_good = child.next_sibling
264 else:
265 last_child: bs4.element.PageElement = child
266 while isinstance(last_child, bs4.Tag) and last_child.contents:
267 last_child = last_child.contents[-1]
268 next_good = last_child.next_element
269 yield child
270 if next_good is None:
271 break
272 # Coverage isn't seeing this even though it's executed
273 continue # pragma: no cover
274 yield child
276 elif not tags:
277 yield child
279 def get_parent(self, el: bs4.Tag | None, no_iframe: bool = False, no_doc: bool = True) -> bs4.Tag | None:
280 """Get parent."""
282 parent = el.parent if el is not None else None
283 if no_iframe and parent is not None and self.is_iframe(parent): # pragma: no cover
284 parent = None
285 if no_doc and self.is_doc(parent):
286 parent = None
287 return parent
289 @staticmethod
290 def get_tag_name(el: bs4.Tag | None) -> str | None:
291 """Get tag."""
293 return el.name if el is not None else None
295 @staticmethod
296 def get_prefix_name(el: bs4.Tag) -> str | None:
297 """Get prefix."""
299 return el.prefix
301 @staticmethod
302 def get_uri(el: bs4.Tag | None) -> str | None:
303 """Get namespace `URI`."""
305 return el.namespace if el is not None else None
307 @classmethod
308 def get_next_tag(cls, el: bs4.Tag) -> bs4.Tag | None:
309 """Get next sibling tag."""
311 return cls.get_next(el, tags=True) # type: ignore[return-value]
313 @classmethod
314 def get_next(cls, el: bs4.Tag, tags: bool = False) -> bs4.element.PageElement | None:
315 """Get next sibling tag."""
317 sibling = el.next_sibling
318 while tags and not isinstance(sibling, bs4.Tag) and sibling is not None:
319 sibling = sibling.next_sibling
321 if tags and not isinstance(sibling, bs4.Tag):
322 sibling = None
324 return sibling
326 @classmethod
327 def get_previous_tag(cls, el: bs4.Tag, tags: bool = True) -> bs4.Tag | None:
328 """Get previous sibling tag."""
330 return cls.get_previous(el, True) # type: ignore[return-value]
332 @classmethod
333 def get_previous(cls, el: bs4.Tag, tags: bool = False) -> bs4.element.PageElement | None:
334 """Get previous sibling tag."""
336 sibling = el.previous_sibling
337 while tags and not isinstance(sibling, bs4.Tag) and sibling is not None:
338 sibling = sibling.previous_sibling
340 if tags and not isinstance(sibling, bs4.Tag):
341 sibling = None
343 return sibling
345 @staticmethod
346 def has_html_ns(el: bs4.Tag | None) -> bool:
347 """
348 Check if element has an HTML namespace.
350 This is a bit different than whether a element is treated as having an HTML namespace,
351 like we do in the case of `is_html_tag`.
352 """
354 ns = getattr(el, 'namespace') if el is not None else None # noqa: B009
355 return bool(ns and ns == NS_XHTML)
357 @staticmethod
358 def split_namespace(el: bs4.Tag | None, attr_name: str) -> tuple[str | None, str | None]:
359 """Return namespace and attribute name without the prefix."""
361 if el is None: # pragma: no cover
362 return None, None
364 return getattr(attr_name, 'namespace', None), getattr(attr_name, 'name', None)
366 @classmethod
367 def get_attribute_by_name(
368 cls,
369 el: bs4.Tag,
370 name: str,
371 default: str | Sequence[str] | None = None
372 ) -> str | Sequence[str] | None:
373 """Get attribute by name."""
375 value = default
376 if el._is_xml:
377 if name in el.attrs:
378 v = el.attrs[name]
379 value = '' if v is None else v
380 else:
381 for k, v in el.attrs.items():
382 if util.lower(k) == name:
383 value = '' if v is None else v
384 break
385 return value
387 @classmethod
388 def iter_attributes(cls, el: bs4.Tag | None) -> Iterator[tuple[str, str | Sequence[str] | None]]:
389 """Iterate attributes."""
391 if el is not None:
392 for k, v in el.attrs.items():
393 yield k, '' if v is None else v
395 @classmethod
396 def get_classes(cls, el: bs4.Tag) -> Sequence[str]:
397 """Get classes."""
399 classes = cls.get_attribute_by_name(el, 'class', [])
400 if isinstance(classes, str):
401 classes = RE_NOT_WS.findall(classes)
402 return cast(Sequence[str], classes)
404 def get_text(self, el: bs4.Tag, no_iframe: bool = False) -> str:
405 """Get text."""
407 return ''.join(
408 [
409 node for node in self.get_descendants(el, no_iframe=no_iframe) # type: ignore[misc]
410 if self.is_content_string(node)
411 ]
412 )
414 def get_own_text(self, el: bs4.Tag, no_iframe: bool = False) -> list[str]:
415 """Get Own Text."""
417 return [
418 node for node in self.get_contents(el, no_iframe=no_iframe) if self.is_content_string(node) # type: ignore[misc]
419 ]
422class Inputs:
423 """Class for parsing and validating input items."""
425 @staticmethod
426 def validate_day(year: int, month: int, day: int) -> bool:
427 """Validate day."""
429 max_days = LONG_MONTH
430 if month == FEB:
431 max_days = FEB_LEAP_MONTH if ((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0) else FEB_MONTH
432 elif month in MONTHS_30:
433 max_days = SHORT_MONTH
434 return 1 <= day <= max_days
436 @staticmethod
437 def validate_week(year: int, week: int) -> bool:
438 """Validate week."""
440 # Validate an ISO week number for `year`.
441 #
442 # Per ISO 8601 rules, the last ISO week of a year is the week
443 # containing Dec 28. Using Dec 28 guarantees we obtain the
444 # correct ISO week-number for the final week of `year`, even in
445 # years where Dec 31 falls in ISO week 01 of the following year.
446 #
447 # Example: if Dec 31 is a Thursday the year's last ISO week will
448 # be week 53; if Dec 31 is a Monday and that week is counted as
449 # week 1 of the next year, Dec 28 still belongs to the final
450 # week of the current ISO year and yields the correct max week.
451 #
452 # `datetime` stops at year 9999, but the calendar repeats every 400
453 # years (a whole number of weeks), so use the matching year from 2000
454 # to 2399, as `calendar.weekday()` does.
455 max_week = datetime(2000 + year % 400, 12, 28).isocalendar()[1]
456 return 1 <= week <= max_week
458 @staticmethod
459 def validate_month(month: int) -> bool:
460 """Validate month."""
462 return 1 <= month <= 12
464 @staticmethod
465 def validate_year(year: int) -> bool:
466 """Validate year."""
468 return 1 <= year
470 @staticmethod
471 def validate_hour(hour: int) -> bool:
472 """Validate hour."""
474 return 0 <= hour <= 23
476 @staticmethod
477 def validate_minutes(minutes: int) -> bool:
478 """Validate minutes."""
480 return 0 <= minutes <= 59
482 @staticmethod
483 def validate_seconds(seconds: float) -> bool:
484 """Validate seconds."""
486 return 0 <= seconds < 60
488 @classmethod
489 def parse_value(cls, itype: str, value: str | None) -> tuple[float, ...] | None:
490 """Parse the input value."""
492 parsed: tuple[float, ...] | None = None
493 if value is None:
494 return value
495 if itype == "date":
496 m = RE_DATE.match(value)
497 if m:
498 year = int(m.group('year'), 10)
499 month = int(m.group('month'), 10)
500 day = int(m.group('day'), 10)
501 if cls.validate_year(year) and cls.validate_month(month) and cls.validate_day(year, month, day):
502 parsed = (year, month, day)
503 elif itype == "month":
504 m = RE_MONTH.match(value)
505 if m:
506 year = int(m.group('year'), 10)
507 month = int(m.group('month'), 10)
508 if cls.validate_year(year) and cls.validate_month(month):
509 parsed = (year, month)
510 elif itype == "week":
511 m = RE_WEEK.match(value)
512 if m:
513 year = int(m.group('year'), 10)
514 week = int(m.group('week'), 10)
515 if cls.validate_year(year) and cls.validate_week(year, week):
516 parsed = (year, week)
517 elif itype == "time":
518 m = RE_TIME.match(value)
519 if m:
520 hour = int(m.group('hour'), 10)
521 minutes = int(m.group('minutes'), 10)
522 seconds = float(m.group('seconds') or 0)
523 if cls.validate_hour(hour) and cls.validate_minutes(minutes) and cls.validate_seconds(seconds):
524 parsed = (hour, minutes, seconds)
525 elif itype == "datetime-local":
526 m = RE_DATETIME.match(value)
527 if m:
528 year = int(m.group('year'), 10)
529 month = int(m.group('month'), 10)
530 day = int(m.group('day'), 10)
531 hour = int(m.group('hour'), 10)
532 minutes = int(m.group('minutes'), 10)
533 seconds = float(m.group('seconds') or 0)
534 if (
535 cls.validate_year(year) and cls.validate_month(month) and cls.validate_day(year, month, day) and
536 cls.validate_hour(hour) and cls.validate_minutes(minutes) and cls.validate_seconds(seconds)
537 ):
538 parsed = (year, month, day, hour, minutes, seconds)
539 elif itype in ("number", "range"):
540 m = RE_NUM.match(value)
541 if m:
542 number = float(m.group('value'))
543 if isfinite(number):
544 parsed = (number,)
545 return parsed
548class CSSMatch(_DocumentNav):
549 """Perform CSS matching."""
551 def __init__(
552 self,
553 selectors: ct.SelectorList,
554 scope: bs4.Tag | None,
555 namespaces: ct.Namespaces | None,
556 flags: int
557 ) -> None:
558 """Initialize."""
560 self.assert_valid_input(scope)
561 self.tag = scope
562 self.cached_meta_lang: list[tuple[str, str]] = []
563 self.cached_default_forms: list[tuple[bs4.Tag, bs4.Tag]] = []
564 self.cached_indeterminate_forms: list[tuple[bs4.Tag, str, bool]] = []
565 self.selectors = selectors
566 self.namespaces: ct.Namespaces | dict[str, str] = {} if namespaces is None else namespaces
567 self.flags = flags
568 self.enable_cache = not bool(self.flags & util.NOCACHE)
569 self.iframe_restrict = False
570 self.nth_cache: dict[Hashable, list[int]] = {}
571 self.sib_cache: dict[Hashable, int] = {}
572 self.dir_cache: dict[int, int] = {}
574 # Find the root element for the whole tree
575 doc = scope
576 parent = self.get_parent(doc)
577 while parent:
578 doc = parent
579 parent = self.get_parent(doc)
580 root: bs4.Tag | None = None
581 if not self.is_doc(doc):
582 root = doc
583 else:
584 for child in self.get_tag_children(doc):
585 root = child
586 break
588 self.root = root
589 self.scope = scope if scope is not doc else root
590 self.has_html_namespace = self.has_html_ns(root)
592 # A document can be both XML and HTML (XHTML)
593 self.is_xml = self.is_xml_tree(doc)
594 self.is_html = not self.is_xml or self.has_html_namespace
596 def reset(self) -> None: # pragma: no cover
597 """Reset."""
599 self.nth_cache.clear()
600 self.sib_cache.clear()
601 self.dir_cache.clear()
603 def supports_namespaces(self) -> bool:
604 """Check if namespaces are supported in the HTML type."""
606 return self.is_xml or self.has_html_namespace
608 def get_tag_ns(self, el: bs4.Tag | None) -> str:
609 """Get tag namespace."""
611 namespace = ''
612 if el is None: # pragma: no cover
613 return namespace
615 if self.supports_namespaces():
616 ns = self.get_uri(el)
617 if ns:
618 namespace = ns
619 else:
620 namespace = NS_XHTML
621 return namespace
623 def is_html_tag(self, el: bs4.Tag | None) -> bool:
624 """Check if tag is in HTML namespace."""
626 return self.get_tag_ns(el) == NS_XHTML
628 def get_tag(self, el: bs4.Tag | None) -> str | None:
629 """Get tag."""
631 name = self.get_tag_name(el)
632 return util.lower(name) if name is not None and not self.is_xml else name
634 def get_prefix(self, el: bs4.Tag) -> str | None:
635 """Get prefix."""
637 prefix = self.get_prefix_name(el)
638 return util.lower(prefix) if prefix is not None and not self.is_xml else prefix
640 def find_bidi(self, el: bs4.Tag) -> int | None:
641 """Get directionality from element text."""
643 stack = [self.get_children(el)]
644 while stack:
645 for node in stack[-1]:
647 # Analyze child text nodes
648 if self.is_tag(node):
650 # Avoid analyzing certain elements specified in the specification.
651 direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(node, 'dir', '')), None)
652 name = self.get_tag(node)
653 if (
654 (name and name in ('bdi', 'script', 'style', 'textarea', 'iframe')) or
655 not self.is_html_tag(node) or
656 direction is not None
657 ):
658 continue # pragma: no cover
660 # Check directionality of this node's text, then resume with the remaining siblings.
661 stack.append(self.get_children(node))
662 break
664 # Skip `doctype` comments, etc.
665 if self.is_special_string(node):
666 continue
668 # Analyze text nodes for directionality.
669 for c in cast('bs4.element.NavigableString', node):
670 bidi = unicodedata.bidirectional(c)
671 if bidi in ('AL', 'R', 'L'):
672 return ct.SEL_DIR_LTR if bidi == 'L' else ct.SEL_DIR_RTL
673 else:
674 # All children have been analyzed and direction could not be determined.
675 stack.pop()
676 return None
678 def extended_language_filter(self, lang_range: str, lang_tag: str) -> bool:
679 """Filter the language tags."""
681 match = True
682 lang_range = RE_WILD_STRIP.sub('-', lang_range).lower()
683 ranges = lang_range.split('-')
684 subtags = lang_tag.lower().split('-')
685 length = len(ranges)
686 slength = len(subtags)
687 rindex = 0
688 sindex = 0
689 r = ranges[rindex]
690 s = subtags[sindex]
692 # Empty specified language should match unspecified language attributes
693 if length == 1 and slength == 1 and not r and r == s:
694 return True
696 # Primary tag needs to match
697 if (r != '*' and r != s) or (r == '*' and slength == 1 and not s):
698 match = False
700 rindex += 1
701 sindex += 1
703 # Match until we run out of ranges
704 while match and rindex < length:
705 r = ranges[rindex]
706 try:
707 s = subtags[sindex]
708 except IndexError:
709 # Ran out of subtags,
710 # but we still have ranges
711 match = False
712 continue
714 # Empty range
715 if not r:
716 match = False
717 continue
719 # Matched range
720 elif s == r:
721 rindex += 1
723 # Implicit wildcard cannot match
724 # singletons
725 elif len(s) == 1:
726 match = False
727 continue
729 # Implicitly matched, so grab next subtag
730 sindex += 1
732 return match
734 def match_attribute_name(
735 self,
736 el: bs4.Tag,
737 attr: str,
738 prefix: str | None
739 ) -> str | Sequence[str] | None:
740 """Match attribute name and return value if it exists."""
742 value = None
743 if self.supports_namespaces():
744 value = None
745 # If we have not defined namespaces, we can't very well find them, so don't bother trying.
746 if prefix:
747 ns = self.namespaces.get(prefix)
748 if ns is None and prefix != '*':
749 return None
750 else:
751 ns = None
753 for k, v in self.iter_attributes(el):
755 # Get attribute parts
756 namespace, name = self.split_namespace(el, k)
758 # Can't match a prefix attribute as we haven't specified one to match
759 # Try to match it normally as a whole `p:a` as selector may be trying `p\:a`.
760 if ns is None:
761 if (self.is_xml and attr == k) or (not self.is_xml and util.lower(attr) == util.lower(k)):
762 value = v
763 break
764 # Coverage is not finding this even though it is executed.
765 # Adding a print statement before this (and erasing coverage) causes coverage to find the line.
766 # Ignore the false positive message.
767 continue # pragma: no cover
769 # We can't match our desired prefix attribute as the attribute doesn't have a prefix
770 if namespace is None or (ns != namespace and prefix != '*'):
771 continue
773 # The attribute doesn't match.
774 if (util.lower(attr) != util.lower(name)) if not self.is_xml else (attr != name):
775 continue
777 value = v
778 break
779 else:
780 for k, v in self.iter_attributes(el):
781 if util.lower(attr) != util.lower(k):
782 continue
783 value = v
784 break
785 return value
787 def match_namespace(self, el: bs4.Tag, tag: ct.SelectorTag) -> bool:
788 """Match the namespace of the element."""
790 match = True
791 namespace = self.get_tag_ns(el)
792 default_namespace = self.namespaces.get('')
793 tag_ns = '' if tag.prefix is None else self.namespaces.get(tag.prefix)
794 # We must match the default namespace if one is not provided
795 if tag.prefix is None and (default_namespace is not None and namespace != default_namespace):
796 match = False
797 # If we specified `|tag`, we must not have a namespace.
798 elif (tag.prefix is not None and tag.prefix == '' and namespace):
799 match = False
800 # Verify prefix matches
801 elif (
802 tag.prefix and
803 tag.prefix != '*' and (tag_ns is None or namespace != tag_ns)
804 ):
805 match = False
806 return match
808 def match_attributes(self, el: bs4.Tag, attributes: tuple[ct.SelectorAttribute, ...]) -> bool:
809 """Match attributes."""
811 match = True
812 if attributes:
813 for a in attributes:
814 temp = self.match_attribute_name(el, a.attribute, a.prefix)
815 pattern = a.xml_type_pattern if self.is_xml and a.xml_type_pattern else a.pattern
816 if temp is None and a.inverse:
817 continue
818 if temp is None:
819 match = False
820 break
821 value = temp if isinstance(temp, str) else ' '.join(temp)
822 if pattern is None:
823 continue
824 elif pattern.match(value) is None:
825 match = False
826 break
827 return match
829 def match_tagname(self, el: bs4.Tag, tag: ct.SelectorTag) -> bool:
830 """Match tag name."""
832 name = (util.lower(tag.name) if not self.is_xml and tag.name is not None else tag.name)
833 return not (
834 name is not None and
835 name not in (self.get_tag(el), '*')
836 )
838 def match_tag(self, el: bs4.Tag, tag: ct.SelectorTag | None) -> bool:
839 """Match the tag."""
841 match = True
842 if tag is not None:
843 # Verify namespace
844 if not self.match_tagname(el, tag):
845 match = False
846 if match and not self.match_namespace(el, tag):
847 match = False
848 return match
850 def match_general_sibling(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator:
851 """Match general sibling combinator."""
853 found = False
855 if relation[0] is ct.Null: # pragma: no cover
856 return found
858 pkey: int | None = None
859 key: tuple[int, int] | None = None
861 # Get the parent and use the ID for the cache key
862 parent = self.get_parent(el)
863 if parent is None: # pragma: no cover
864 return found
865 pkey = id(parent)
867 # Check the cache to see if we already know where the first sibling is,
868 # and if we do, check if we are on the correct side of it.
869 # If we've previously searched and found no sibling, there is no sibling.
870 # Lastly, if this is our first time, setup the cache.
871 reverse = relation[0].rel_type == REL_HAS_SIBLING
872 start = len(parent) - 1 if reverse else 0
873 if pkey is not None:
874 key = (pkey, id(relation))
875 if key in self.sib_cache:
876 index = self.sib_cache[key]
877 if index >= 0:
878 a, b = (index + 1, start + 1) if reverse else (start, index)
879 return not within(el, parent, a, b)
880 else:
881 return False
882 self.sib_cache[key] = start
884 # Start at the furthest endpoint and walk back towards the element looking for siblings.
885 # The current element counts as a sibling, but will not cause a match.
886 passed = False
887 incr = -1 if reverse else 1
888 for child in self.get_children(parent, start=start, reverse=reverse):
889 start += incr
890 if not isinstance(child, bs4.Tag):
891 continue
893 # Flag that we are passing the element.
894 # Any siblings we find aren't valid for this element.
895 if child is el:
896 passed = True
898 # We found the furthest sibling.
899 if (yield child, relation):
900 found = True
901 break
903 # Cache the index of the sibling or mark as there being no siblings.
904 if pkey and key:
905 self.sib_cache[key] = start if found else -1
907 # If we passed the current element and then found a sibling, it doesn't count as a match.
908 if passed:
909 found = False
911 return found
913 def match_past_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator:
914 """Match past relationship."""
916 found = False
917 # I don't think this can ever happen, but it makes `mypy` happy
918 if relation[0] is ct.Null: # pragma: no cover
919 return found
921 if relation[0].rel_type == REL_PARENT:
922 parent: bs4.Tag | None = el
923 while not found and parent and (parent := self.get_parent(parent, no_iframe=self.iframe_restrict)):
924 found = parent is not None and (yield parent, relation)
925 elif relation[0].rel_type == REL_CLOSE_PARENT:
926 parent = self.get_parent(el, no_iframe=self.iframe_restrict)
927 found = parent is not None and (yield parent, relation)
928 elif relation[0].rel_type == REL_SIBLING:
929 if self.enable_cache:
930 found = yield from self.match_general_sibling(el, relation)
931 else:
932 sibling: bs4.Tag | None = el
933 while not found and sibling and (sibling := self.get_previous_tag(sibling)):
934 found = sibling is not None and (yield sibling, relation)
935 elif relation[0].rel_type == REL_CLOSE_SIBLING:
936 sibling = self.get_previous_tag(el)
937 found = sibling is not None and (yield sibling, relation)
938 return found
940 def match_future_child(
941 self,
942 parent: bs4.Tag,
943 relation: ct.SelectorList,
944 recursive: bool = False
945 ) -> MatchGenerator:
946 """Match future child."""
948 match = False
949 if recursive:
950 children: Callable[..., Iterator[bs4.Tag]] = self.get_tag_descendants
951 else:
952 children = self.get_tag_children
953 for child in children(parent, no_iframe=self.iframe_restrict):
954 if (yield child, relation):
955 match = True
956 break
957 return match
959 def match_future_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator:
960 """Match future relationship."""
962 found = False
963 # I don't think this can ever happen, but it makes `mypy` happy
964 if relation[0] is ct.Null: # pragma: no cover
965 return found
967 if relation[0].rel_type == REL_HAS_PARENT:
968 found = yield from self.match_future_child(el, relation, True)
969 elif relation[0].rel_type == REL_HAS_CLOSE_PARENT:
970 found = yield from self.match_future_child(el, relation)
971 elif relation[0].rel_type == REL_HAS_SIBLING:
972 if self.enable_cache:
973 found = yield from self.match_general_sibling(el, relation)
974 else:
975 sibling: bs4.Tag | None = el
976 while not found and sibling and (sibling := self.get_next_tag(sibling)):
977 found = yield sibling, relation
978 elif relation[0].rel_type == REL_HAS_CLOSE_SIBLING:
979 sibling = self.get_next_tag(el)
980 found = sibling is not None and (yield sibling, relation)
981 return found
983 def match_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator:
984 """Match relationship to other elements."""
986 sel = relation[0]
987 if sel is not ct.Null and sel.rel_type is not None and sel.rel_type.startswith(':'):
988 return self.match_future_relations(el, relation)
989 return self.match_past_relations(el, relation)
991 def match_id(self, el: bs4.Tag, ids: tuple[str, ...]) -> bool:
992 """Match element's ID."""
994 found = True
995 for i in ids:
996 if i != self.get_attribute_by_name(el, 'id', ''):
997 found = False
998 break
999 return found
1001 def match_classes(self, el: bs4.Tag, classes: tuple[str, ...]) -> bool:
1002 """Match element's classes."""
1004 current_classes = self.get_classes(el)
1005 found = True
1006 for c in classes:
1007 if c not in current_classes:
1008 found = False
1009 break
1010 return found
1012 def match_root(self, el: bs4.Tag) -> bool:
1013 """Match element as root."""
1015 is_root = self.is_root(el)
1016 if is_root:
1017 sibling: Any = self.get_previous(el)
1018 while is_root and sibling is not None:
1019 if (
1020 self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or
1021 self.is_cdata(sibling)
1022 ):
1023 is_root = False
1024 else:
1025 sibling = self.get_previous(sibling)
1026 if is_root:
1027 sibling = self.get_next(el)
1028 while is_root and sibling is not None:
1029 if (
1030 self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or
1031 self.is_cdata(sibling)
1032 ):
1033 is_root = False
1034 else:
1035 sibling = self.get_next(sibling)
1036 return is_root
1038 def match_scope(self, el: bs4.Tag) -> bool:
1039 """Match element as scope."""
1041 return self.scope is el
1043 def match_nth_tag_type(self, el: bs4.Tag, child: bs4.Tag) -> bool:
1044 """Match tag type for `nth` matches."""
1046 return (
1047 (self.get_tag(child) == self.get_tag(el)) and
1048 (self.get_tag_ns(child) == self.get_tag_ns(el))
1049 )
1051 def match_nth(self, el: bs4.Tag, nth: tuple[ct.SelectorNth, ...]) -> MatchGenerator:
1052 """Match `nth` elements."""
1054 key: tuple[int, int, str | None, str | None] | None = None
1055 start = rindex = 0
1056 incr = rincr = 0
1058 # Get the parent and use the ID for the cache key
1059 parent = self.get_parent(el, no_doc=False)
1060 pkey = id(parent) if self.enable_cache and parent else None
1062 # Test element against the `nth` selectors.
1063 matched = True
1064 for n in nth:
1065 matched = False
1066 last = n.last
1067 key = None
1069 # Prepare the child iterator and get the starting, real index and the relative index
1070 if pkey is not None and parent:
1071 # Get last info from the cache
1072 if pkey:
1073 key = (
1074 (pkey, id(n), self.get_tag(el), self.get_tag_ns(el))
1075 if n.of_type
1076 else (pkey, id(n), None, None)
1077 )
1079 valid = False
1080 if key and key in self.nth_cache:
1081 start, rindex = self.nth_cache[key]
1082 if within(el, parent, start):
1083 last = False
1084 rincr = -1 if n.last else 1
1085 valid = True
1087 # Start/overwrite the cache if the cache was empty or invalid
1088 if not valid:
1089 start, rindex = len(parent) - 1 if last else 0, 0
1090 self.nth_cache[key] = [start, rindex]
1091 rincr = 1
1093 incr = 1 if not last else -1
1094 children = self.get_children(parent, start=start, reverse=last)
1096 # Non-cached handling of parented element
1097 elif parent:
1098 rindex = 0
1099 start = len(parent) - 1 if last else 0
1100 rincr = incr = 1
1101 children = self.get_children(parent, start=start, reverse=last)
1103 # No parent, just evaluate the element against the selectors
1104 else:
1105 start = rindex = 0
1106 rincr = incr = 1
1107 children = iter([el])
1109 # Find index of element compared to its siblings and check the index conditions
1110 child: bs4.Tag
1111 for child in children:
1112 start += incr
1114 # We only care about tags
1115 if not self.is_tag(child):
1116 continue
1118 # Handle `of S` in `nth-child` and handle `of-type`
1119 if (
1120 (n.selectors and not (yield child, n.selectors)) or
1121 (n.of_type and not self.match_nth_tag_type(el, child))
1122 ):
1123 if child is el:
1124 # Cached "last" lookups count down from `rindex`, so it must be the
1125 # position `el` would have had, even though `el` did not match.
1126 if last:
1127 rindex += 1
1128 break
1129 continue
1131 # Test the relative index against the `nth` requirement.
1132 rindex += rincr
1133 if child is el:
1134 if n.a != 0:
1135 v = (rindex - n.b) / n.a
1136 matched = v.is_integer() and v >= 0
1137 else:
1138 matched = rindex == n.b and n.b >= 1
1139 break
1141 # "Last index" selectors evaluate first from the bottom and then evaluate
1142 # from the first found element top-down. Start will be incremented in the
1143 # wrong direction, so increment it and step over the current index.
1144 if last:
1145 start += 2
1147 # Update the cache
1148 if key:
1149 self.nth_cache[key] = [start, rindex]
1151 # If we failed to match any `nth` selectors, quit.
1152 if not matched:
1153 break
1155 return matched
1157 def match_empty(self, el: bs4.Tag) -> bool:
1158 """Check if element is empty (if requested)."""
1160 is_empty = True
1161 child: Any
1162 for child in self.get_children(el):
1163 if self.is_tag(child):
1164 is_empty = False
1165 break
1166 elif self.is_content_string(child) and RE_NOT_EMPTY.search(child):
1167 is_empty = False
1168 break
1169 return is_empty
1171 def match_subselectors(self, el: bs4.Tag, selectors: tuple[ct.SelectorList, ...]) -> MatchGenerator:
1172 """Match selectors."""
1174 match = True
1175 for sel in selectors:
1176 if not (yield el, sel):
1177 match = False
1178 return match
1180 def match_contains(self, el: bs4.Tag, contains: tuple[ct.SelectorContains, ...]) -> bool:
1181 """Match element if it contains text."""
1183 match = True
1184 content: str | None = None
1185 own_content: Sequence[str] | None = None
1186 for contain_list in contains:
1187 found = False
1188 for text in contain_list.text:
1189 if contain_list.own:
1190 if own_content is None:
1191 own_content = self.get_own_text(el, no_iframe=self.is_html)
1192 for c in own_content:
1193 if text in c:
1194 found = True
1195 break
1196 if found:
1197 break
1198 else:
1199 if content is None:
1200 content = self.get_text(el, no_iframe=self.is_html)
1201 if text in content:
1202 found = True
1203 break
1204 if not found:
1205 match = False
1206 return match
1208 def match_default(self, el: bs4.Tag) -> bool:
1209 """Match default."""
1211 match = False
1213 # Find this input's form
1214 form: bs4.Tag | None = None
1215 parent = self.get_parent(el, no_iframe=True)
1216 while parent and form is None:
1217 if self.get_tag(parent) == 'form' and self.is_html_tag(parent):
1218 form = parent
1219 else:
1220 parent = self.get_parent(parent, no_iframe=True)
1222 if form is not None:
1223 # Look in form cache to see if we've already located its default button
1224 found_form = False
1225 for f, t in self.cached_default_forms:
1226 if f is form:
1227 found_form = True
1228 if t is el:
1229 match = True
1230 break
1232 # We didn't have the form cached, so look for its default button
1233 if not found_form:
1234 for child in self.get_tag_descendants(form, no_iframe=True):
1235 name = self.get_tag(child)
1236 # Can't do nested forms (haven't figured out why we never hit this)
1237 if name == 'form': # pragma: no cover
1238 break
1239 if name in ('input', 'button'):
1240 v = self.get_attribute_by_name(child, 'type', '')
1241 if v and util.lower(v) == 'submit':
1242 self.cached_default_forms.append((form, child))
1243 if el is child:
1244 match = True
1245 break
1246 return match
1248 def match_indeterminate(self, el: bs4.Tag) -> bool:
1249 """Match default."""
1251 match = False
1252 name = cast(str, self.get_attribute_by_name(el, 'name'))
1254 def get_parent_form(el: bs4.Tag) -> bs4.Tag | None:
1255 """Find this input's form."""
1256 form = None
1257 parent = self.get_parent(el, no_iframe=True)
1258 while form is None:
1259 if self.get_tag(parent) == 'form' and self.is_html_tag(parent):
1260 form = parent
1261 break
1262 last_parent = parent
1263 parent = self.get_parent(parent, no_iframe=True)
1264 if parent is None:
1265 form = last_parent
1266 break
1267 return form
1269 form = get_parent_form(el)
1271 # Look in form cache to see if we've already evaluated that its fellow radio buttons are indeterminate
1272 if form is not None:
1273 found_form = False
1274 for f, n, i in self.cached_indeterminate_forms:
1275 if f is form and n == name:
1276 found_form = True
1277 if i is True:
1278 match = True
1279 break
1281 # We didn't have the form cached, so validate that the radio button is indeterminate
1282 if not found_form:
1283 checked = False
1284 for child in self.get_tag_descendants(form, no_iframe=True):
1285 if child is el:
1286 continue
1287 tag_name = self.get_tag(child)
1288 if tag_name == 'input':
1289 is_radio = False
1290 check = False
1291 has_name = False
1292 for k, v in self.iter_attributes(child):
1293 if util.lower(k) == 'type' and util.lower(v) == 'radio':
1294 is_radio = True
1295 elif util.lower(k) == 'name' and v == name:
1296 has_name = True
1297 elif util.lower(k) == 'checked':
1298 check = True
1299 if is_radio and check and has_name and get_parent_form(child) is form:
1300 checked = True
1301 break
1302 if checked:
1303 break
1304 if not checked:
1305 match = True
1306 self.cached_indeterminate_forms.append((form, name, match))
1308 return match
1310 def match_lang(self, el: bs4.Tag, langs: tuple[ct.SelectorLang, ...]) -> bool:
1311 """Match languages."""
1313 match = False
1314 has_ns = self.supports_namespaces()
1315 root = self.root
1316 has_html_namespace = self.has_html_namespace
1318 # Walk parents looking for `lang` (HTML) or `xml:lang` XML property.
1319 parent: bs4.Tag | None = el
1320 found_lang = None
1321 last = None
1322 while not found_lang:
1323 has_html_ns = self.has_html_ns(parent)
1324 for k, v in self.iter_attributes(parent):
1325 attr_ns, attr = self.split_namespace(parent, k)
1326 if (
1327 ((not has_ns or has_html_ns) and (util.lower(k) if not self.is_xml else k) == 'lang') or
1328 (
1329 has_ns and not has_html_ns and attr_ns == NS_XML and
1330 (util.lower(attr) if not self.is_xml and attr is not None else attr) == 'lang'
1331 )
1332 ):
1333 found_lang = v
1334 break
1335 last = parent
1336 parent = self.get_parent(parent, no_iframe=self.is_html)
1338 if parent is None:
1339 root = last
1340 has_html_namespace = self.has_html_ns(root)
1341 parent = last
1342 break
1344 # Use cached meta language.
1345 if found_lang is None and self.cached_meta_lang:
1346 for cache in self.cached_meta_lang:
1347 if root is not None and cast(str, root) is cache[0]:
1348 found_lang = cache[1]
1350 # If we couldn't find a language, and the document is HTML, look to meta to determine language.
1351 if found_lang is None and (not self.is_xml or (has_html_namespace and root and root.name == 'html')):
1352 # Find head
1353 found = False
1354 for tag in ('html', 'head'):
1355 found = False
1356 for child in self.get_tag_children(parent, no_iframe=self.is_html):
1357 if self.get_tag(child) == tag and self.is_html_tag(child):
1358 found = True
1359 parent = child
1360 break
1361 if found: # pragma: no cover
1362 break
1364 # Search meta tags
1365 if found and parent is not None:
1366 for child2 in parent:
1367 if isinstance(child2, bs4.Tag) and self.get_tag(child2) == 'meta' and self.is_html_tag(parent):
1368 c_lang = False
1369 content = None
1370 for k, v in self.iter_attributes(child2):
1371 if util.lower(k) == 'http-equiv' and util.lower(v) == 'content-language':
1372 c_lang = True
1373 if util.lower(k) == 'content':
1374 content = v
1375 if c_lang and content:
1376 found_lang = content
1377 self.cached_meta_lang.append((cast(str, root), cast(str, found_lang)))
1378 break
1379 if found_lang is not None:
1380 break
1381 if found_lang is None:
1382 self.cached_meta_lang.append((cast(str, root), ''))
1384 # If we determined a language, compare.
1385 if found_lang is not None:
1386 for patterns in langs:
1387 match = False
1388 for pattern in patterns:
1389 if self.extended_language_filter(pattern, cast(str, found_lang)):
1390 match = True
1391 if not match:
1392 break
1394 return match
1396 def match_dir(self, el: bs4.Tag | None, directionality: int) -> bool:
1397 """Check directionality."""
1399 # If we have to match both left and right, we can't match either.
1400 if directionality & ct.SEL_DIR_LTR and directionality & ct.SEL_DIR_RTL:
1401 return False
1403 # Every element visited while resolving shares the resolved direction,
1404 # so cache it to avoid walking the same ancestors again for each element.
1405 visited: list[int] = []
1406 direction = self.resolve_dir(el, visited)
1407 if self.enable_cache:
1408 for key in visited:
1409 self.dir_cache[key] = direction
1410 return direction == directionality
1412 def resolve_dir(self, el: bs4.Tag | None, visited: list[int]) -> int:
1413 """Resolve the direction of an element, returning `0` if it cannot be resolved."""
1415 while True:
1416 if el is None or not self.is_html_tag(el):
1417 return 0
1419 key = id(el)
1420 if key in self.dir_cache:
1421 return self.dir_cache[key]
1422 visited.append(key)
1424 # Element has defined direction of left to right or right to left
1425 direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(el, 'dir', '')), None)
1426 if direction not in (None, 0):
1427 return direction
1429 # Element is the document element (the root) and no direction assigned, assume left to right.
1430 is_root = self.is_root(el)
1431 if is_root and direction is None:
1432 return ct.SEL_DIR_LTR
1434 # If `input[type=telephone]` and no direction is assigned, assume left to right.
1435 name = self.get_tag(el)
1436 is_input = name == 'input'
1437 is_textarea = name == 'textarea'
1438 is_bdi = name == 'bdi'
1439 itype = util.lower(self.get_attribute_by_name(el, 'type', '')) if is_input else ''
1440 if is_input and itype == 'tel' and direction is None:
1441 return ct.SEL_DIR_LTR
1443 # Auto handling for text inputs
1444 if ((is_input and itype in ('text', 'search', 'tel', 'url', 'email')) or is_textarea) and direction == 0:
1445 if is_textarea:
1446 value = ''.join(
1447 node for node in self.get_contents(el, no_iframe=True) # type: ignore[misc]
1448 if self.is_content_string(node)
1449 )
1450 else:
1451 value = cast(str, self.get_attribute_by_name(el, 'value', ''))
1452 for c in value:
1453 bidi = unicodedata.bidirectional(c)
1454 if bidi in ('AL', 'R', 'L'):
1455 direction = ct.SEL_DIR_LTR if bidi == 'L' else ct.SEL_DIR_RTL
1456 return ct.SEL_DIR_LTR if bidi == 'L' else ct.SEL_DIR_RTL
1457 # No strong directional characters (or no value), assume left to right
1458 return ct.SEL_DIR_LTR
1460 # Auto handling for `bdi` and other non text inputs.
1461 if (is_bdi and direction is None) or direction == 0:
1462 direction = self.find_bidi(el)
1463 if direction is not None:
1464 return direction
1465 # Auto directionality could not be determined, assume left to right
1466 return ct.SEL_DIR_LTR
1468 # Match parents direction
1469 el = self.get_parent(el, no_iframe=True)
1471 def match_range(self, el: bs4.Tag, condition: int) -> bool:
1472 """
1473 Match range.
1475 Behavior is modeled after what we see in browsers. Browsers seem to evaluate
1476 if the value is out of range, and if not, it is in range. So a missing value
1477 will not evaluate out of range; therefore, value is in range. Personally, I
1478 feel like this should evaluate as neither in or out of range.
1479 """
1481 out_of_range = False
1483 itype = util.lower(self.get_attribute_by_name(el, 'type'))
1484 mn = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'min', None)))
1485 mx = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'max', None)))
1487 # There is no valid min or max, so we cannot evaluate a range
1488 if mn is None and mx is None:
1489 return False
1491 value = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'value', None)))
1492 if value is not None:
1493 if itype in ("date", "datetime-local", "month", "week", "number", "range"):
1494 if mn is not None and value < mn:
1495 out_of_range = True
1496 if not out_of_range and mx is not None and value > mx:
1497 out_of_range = True
1498 elif itype == "time":
1499 if mn is not None and mx is not None and mn > mx:
1500 # Time is periodic, so this is a reversed/discontinuous range
1501 if value < mn and value > mx:
1502 out_of_range = True
1503 else:
1504 if mn is not None and value < mn:
1505 out_of_range = True
1506 if not out_of_range and mx is not None and value > mx:
1507 out_of_range = True
1509 return not out_of_range if condition & ct.SEL_IN_RANGE else out_of_range
1511 def match_defined(self, el: bs4.Tag) -> bool:
1512 """
1513 Match defined.
1515 `:defined` is related to custom elements in a browser.
1517 - If the document is XML (not XHTML), all tags will match.
1518 - Tags that are not custom (don't have a hyphen) are marked defined.
1519 - If the tag has a prefix (without or without a namespace), it will not match.
1521 This is of course requires the parser to provide us with the proper prefix and namespace info,
1522 if it doesn't, there is nothing we can do.
1523 """
1525 name = self.get_tag(el)
1526 return (
1527 name is not None and (
1528 name.find('-') == -1 or
1529 name.find(':') != -1 or
1530 self.get_prefix(el) is not None
1531 )
1532 )
1534 def match_placeholder_shown(self, el: bs4.Tag) -> bool:
1535 """
1536 Match placeholder shown according to HTML spec.
1538 - text area should be checked if they have content. A single newline does not count as content.
1540 """
1542 match = False
1543 content = self.get_text(el)
1544 if content in ('', '\n'):
1545 match = True
1547 return match
1549 def match_selectors(self, el: bs4.Tag, selectors: ct.SelectorList) -> bool:
1550 """
1551 Check if element matches one of the selectors.
1553 Since some selectors can have complex relationships, they may need to recursively
1554 check if another relative element matches specific selectors. To avoid recursion,
1555 generators are yielded instead whenever it needs to check if a relative element
1556 satisfies specific selectors. These can be compared and the result sent back.
1557 We can check the end result of the generator once it is satisfied.
1558 """
1560 # Most matches never need a nested match, so avoid setting up the stack unless needed.
1561 # `frame` yields a `bool` when it is done.
1562 frame = self._match_selectors(el, selectors)
1563 item = next(frame)
1564 if item.__class__ is bool:
1565 return item
1567 # We need to setup a stack as a selector has sent back a match request.
1568 stack = [frame, self._match_selectors(*cast('MatchRequest', item))]
1569 result: bool | None = None
1570 while True:
1571 item = stack[-1].send(result) # type: ignore[arg-type]
1572 if item.__class__ is bool:
1573 stack.pop()
1574 if not stack:
1575 return item
1576 result = item
1577 else:
1578 stack.append(self._match_selectors(*cast('MatchRequest', item)))
1579 result = None
1581 def _match_selectors(self, el: bs4.Tag, selectors: ct.SelectorList) -> Generator[MatchRequest | bool, bool, None]:
1582 """
1583 Check if element matches one of the selectors, yielding requests to match nested selectors.
1585 Yields `MatchRequests` or affirmation as to whether a selector was matched. The match request
1586 requires a boolean to be sent back to communicate whether the match was successful.
1587 """
1589 match = False
1590 is_not = selectors.is_not
1591 is_html = selectors.is_html
1593 # Internal selector lists that use the HTML flag, will automatically get the `html` namespace.
1594 if is_html:
1595 namespaces = self.namespaces
1596 iframe_restrict = self.iframe_restrict
1597 self.namespaces = {'html': NS_XHTML}
1598 self.iframe_restrict = True
1600 if not is_html or self.is_html:
1601 for selector in selectors:
1602 match = is_not
1603 # We have a un-matchable situation (like `:focus` as you can focus an element in this environment)
1604 if selector is ct.Null:
1605 continue
1606 # Verify tag matches
1607 if not self.match_tag(el, selector.tag):
1608 continue
1609 # Verify tag is defined
1610 if selector.flags & ct.SEL_DEFINED and not self.match_defined(el):
1611 continue
1612 # Verify element is root
1613 if selector.flags & ct.SEL_ROOT and not self.match_root(el):
1614 continue
1615 # Verify element is scope
1616 if selector.flags & ct.SEL_SCOPE and not self.match_scope(el):
1617 continue
1618 # Verify element has placeholder shown
1619 if selector.flags & ct.SEL_PLACEHOLDER_SHOWN and not self.match_placeholder_shown(el):
1620 continue
1621 # Verify `nth` matches
1622 if selector.nth and not (yield from self.match_nth(el, selector.nth)):
1623 continue
1624 if selector.flags & ct.SEL_EMPTY and not self.match_empty(el):
1625 continue
1626 # Verify id matches
1627 if selector.ids and not self.match_id(el, selector.ids):
1628 continue
1629 # Verify classes match
1630 if selector.classes and not self.match_classes(el, selector.classes):
1631 continue
1632 # Verify attribute(s) match
1633 if not self.match_attributes(el, selector.attributes):
1634 continue
1635 # Verify ranges
1636 if selector.flags & RANGES and not self.match_range(el, selector.flags & RANGES):
1637 continue
1638 # Verify language patterns
1639 if selector.lang and not self.match_lang(el, selector.lang):
1640 continue
1641 # Verify pseudo selector patterns
1642 if selector.selectors and not (yield from self.match_subselectors(el, selector.selectors)):
1643 continue
1644 # Verify relationship selectors
1645 if selector.relation and not (yield from self.match_relations(el, selector.relation)):
1646 continue
1647 # Validate that the current default selector match corresponds to the first submit button in the form
1648 if selector.flags & ct.SEL_DEFAULT and not self.match_default(el):
1649 continue
1650 # Validate that the unset radio button is among radio buttons with the same name in a form that are
1651 # also not set.
1652 if selector.flags & ct.SEL_INDETERMINATE and not self.match_indeterminate(el):
1653 continue
1654 # Validate element directionality
1655 if selector.flags & DIR_FLAGS and not self.match_dir(el, selector.flags & DIR_FLAGS):
1656 continue
1657 # Validate that the tag contains the specified text.
1658 if selector.contains and not self.match_contains(el, selector.contains):
1659 continue
1660 match = not is_not
1661 break
1663 # Restore actual namespaces being used for external selector lists
1664 if is_html:
1665 self.namespaces = namespaces
1666 self.iframe_restrict = iframe_restrict
1668 yield match
1670 def select(self, limit: int = 0) -> Iterator[bs4.Tag]:
1671 """Match all tags under the targeted tag."""
1673 lim = None if limit < 1 else limit
1675 for child in self.get_tag_descendants(self.tag):
1676 if self.match(child):
1677 yield child
1678 if lim is not None:
1679 lim -= 1
1680 if lim < 1:
1681 break
1683 def closest(self) -> bs4.Tag | None:
1684 """Match closest ancestor."""
1686 current: bs4.Tag | None = self.tag
1687 closest = None
1688 while closest is None and current is not None:
1689 if self.match(current):
1690 closest = current
1691 else:
1692 current = self.get_parent(current)
1693 return closest
1695 def filter(self) -> list[bs4.Tag]: # noqa A001
1696 """Filter tag's children."""
1698 return [
1699 tag for tag in self.get_contents(self.tag)
1700 if isinstance(tag, bs4.Tag) and self.match(tag)
1701 ]
1703 def match(self, el: bs4.Tag) -> bool:
1704 """Match."""
1706 return not self.is_doc(el) and self.is_tag(el) and self.match_selectors(el, self.selectors)
1709class SoupSieve(ct.Immutable):
1710 """Compiled Soup Sieve selector matching object."""
1712 pattern: str
1713 selectors: ct.SelectorList
1714 namespaces: ct.Namespaces | None
1715 custom: dict[str, str]
1716 flags: int
1718 __slots__ = ("pattern", "selectors", "namespaces", "custom", "flags", "_hash")
1720 def __init__(
1721 self,
1722 pattern: str,
1723 selectors: ct.SelectorList,
1724 namespaces: ct.Namespaces | None,
1725 custom: ct.CustomSelectors | None,
1726 flags: int
1727 ):
1728 """Initialize."""
1730 super().__init__(
1731 pattern=pattern,
1732 selectors=selectors,
1733 namespaces=namespaces,
1734 custom=custom,
1735 flags=flags
1736 )
1738 def match(self, tag: bs4.Tag) -> bool:
1739 """Match."""
1741 return CSSMatch(self.selectors, tag, self.namespaces, self.flags).match(tag)
1743 def closest(self, tag: bs4.Tag) -> bs4.Tag | None:
1744 """Match closest ancestor."""
1746 return CSSMatch(self.selectors, tag, self.namespaces, self.flags).closest()
1748 def filter(self, iterable: Iterable[bs4.Tag]) -> list[bs4.Tag]: # noqa A001
1749 """
1750 Filter.
1752 `CSSMatch` can cache certain searches for tags of the same document,
1753 so if we are given a tag, all tags are from the same document,
1754 and we can take advantage of the optimization.
1756 Any other kind of iterable could have tags from different documents or detached tags,
1757 so for those, we use a new `CSSMatch` for each item in the iterable.
1758 """
1760 if isinstance(iterable, bs4.Tag):
1761 return CSSMatch(self.selectors, iterable, self.namespaces, self.flags).filter()
1762 else:
1763 # There is no guarantee that elements are from the same document, evaluate them separately.
1764 return [node for node in iterable if not CSSMatch.is_navigable_string(node) and self.match(node)]
1766 def select_one(self, tag: bs4.Tag) -> bs4.Tag | None:
1767 """Select a single tag."""
1769 tags = self.select(tag, limit=1)
1770 return tags[0] if tags else None
1772 def select(self, tag: bs4.Tag, limit: int = 0) -> list[bs4.Tag]:
1773 """Select the specified tags."""
1775 return list(self.iselect(tag, limit))
1777 def iselect(self, tag: bs4.Tag, limit: int = 0) -> Iterator[bs4.Tag]:
1778 """Iterate the specified tags."""
1780 yield from CSSMatch(self.selectors, tag, self.namespaces, self.flags).select(limit)
1782 def __repr__(self) -> str: # pragma: no cover
1783 """Representation."""
1785 return (
1786 f"SoupSieve(pattern={self.pattern!r}, namespaces={self.namespaces!r}, "
1787 f"custom={self.custom!r}, flags={self.flags!r})"
1788 )
1790 __str__ = __repr__
1793ct.pickle_register(SoupSieve)