Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/soupsieve/css_match.py: 65%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 collections.abc import Hashable
5from . import util
6import re
7from . import css_types as ct
8import unicodedata
9import bs4
10from typing import Iterator, Iterable, Any, Callable, Sequence, Any, overload, Literal, cast # noqa: F401, F811
12# Empty tag pattern (whitespace okay)
13RE_NOT_EMPTY = re.compile('[^ \t\r\n\f]')
15RE_NOT_WS = re.compile('[^ \t\r\n\f]+')
17# Relationships
18REL_PARENT = ' '
19REL_CLOSE_PARENT = '>'
20REL_SIBLING = '~'
21REL_CLOSE_SIBLING = '+'
23# Relationships for :has() (forward looking)
24REL_HAS_PARENT = ': '
25REL_HAS_CLOSE_PARENT = ':>'
26REL_HAS_SIBLING = ':~'
27REL_HAS_CLOSE_SIBLING = ':+'
29NS_XHTML = 'http://www.w3.org/1999/xhtml'
30NS_XML = 'http://www.w3.org/XML/1998/namespace'
32DIR_FLAGS = ct.SEL_DIR_LTR | ct.SEL_DIR_RTL
33RANGES = ct.SEL_IN_RANGE | ct.SEL_OUT_OF_RANGE
35DIR_MAP = {
36 'ltr': ct.SEL_DIR_LTR,
37 'rtl': ct.SEL_DIR_RTL,
38 'auto': 0
39}
41RE_NUM = re.compile(r"^(?P<value>-?(?:[0-9]{1,}(\.[0-9]+)?|\.[0-9]+))$")
42RE_TIME = re.compile(r'^(?P<hour>[0-9]{2}):(?P<minutes>[0-9]{2})$')
43RE_MONTH = re.compile(r'^(?P<year>[0-9]{4,})-(?P<month>[0-9]{2})$')
44RE_WEEK = re.compile(r'^(?P<year>[0-9]{4,})-W(?P<week>[0-9]{2})$')
45RE_DATE = re.compile(r'^(?P<year>[0-9]{4,})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})$')
46RE_DATETIME = re.compile(
47 r'^(?P<year>[0-9]{4,})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})T(?P<hour>[0-9]{2}):(?P<minutes>[0-9]{2})$'
48)
49RE_WILD_STRIP = re.compile(r'(?:(?:-\*-)(?:\*(?:-|$))*|-\*$)')
51MONTHS_30 = (4, 6, 9, 11) # April, June, September, and November
52FEB = 2
53SHORT_MONTH = 30
54LONG_MONTH = 31
55FEB_MONTH = 28
56FEB_LEAP_MONTH = 29
57DAYS_IN_WEEK = 7
60def within(target: bs4.Tag, parent: bs4.Tag | bs4.BeautifulSoup, start: int, end: int | None = None) -> bool:
61 """Check if target is within data."""
63 contents = parent.contents
64 return any(contents[i] is target for i in range(start, end if end is not None else len(contents)))
67class _DocumentNav:
68 """Navigate a Beautiful Soup document."""
70 @classmethod
71 def assert_valid_input(cls, tag: Any) -> None:
72 """Check if valid input tag or document."""
74 # Fail on unexpected types.
75 if not cls.is_tag(tag):
76 raise TypeError(f"Expected a BeautifulSoup 'Tag', but instead received type {type(tag)}")
78 @staticmethod
79 def is_doc(obj: bs4.element.PageElement | None) -> bool:
80 """Is `BeautifulSoup` object."""
81 return isinstance(obj, bs4.BeautifulSoup)
83 @staticmethod
84 def is_tag(obj: bs4.element.PageElement | None) -> bool:
85 """Is tag."""
86 return isinstance(obj, bs4.Tag)
88 @staticmethod
89 def is_declaration(obj: bs4.element.PageElement | None) -> bool: # pragma: no cover
90 """Is declaration."""
91 return isinstance(obj, bs4.Declaration)
93 @staticmethod
94 def is_cdata(obj: bs4.element.PageElement | None) -> bool:
95 """Is CDATA."""
96 return isinstance(obj, bs4.CData)
98 @staticmethod
99 def is_processing_instruction(obj: bs4.element.PageElement | None) -> bool: # pragma: no cover
100 """Is processing instruction."""
101 return isinstance(obj, bs4.ProcessingInstruction)
103 @staticmethod
104 def is_navigable_string(obj: bs4.element.PageElement | None) -> bool:
105 """Is navigable string."""
106 return isinstance(obj, bs4.element.NavigableString)
108 @staticmethod
109 def is_special_string(obj: bs4.element.PageElement | None) -> bool:
110 """Is special string."""
111 return isinstance(obj, (bs4.Comment, bs4.Declaration, bs4.CData, bs4.ProcessingInstruction, bs4.Doctype))
113 @classmethod
114 def is_content_string(cls, obj: bs4.element.PageElement | None) -> bool:
115 """Check if node is content string."""
117 return cls.is_navigable_string(obj) and not cls.is_special_string(obj)
119 @staticmethod
120 def is_xml_tree(el: bs4.Tag | None) -> bool:
121 """Check if element (or document) is from a XML tree."""
123 return el is not None and bool(el._is_xml)
125 def is_iframe(self, el: bs4.Tag | None) -> bool:
126 """Check if element is an `iframe`."""
128 if el is None: # pragma: no cover
129 return False
131 return bool(
132 ((el.name if self.is_xml_tree(el) else util.lower(el.name)) == 'iframe') and
133 self.is_html_tag(el) # type: ignore[attr-defined]
134 )
136 def is_root(self, el: bs4.Tag) -> bool:
137 """
138 Return whether element is a root element.
140 We check that the element is the root of the tree (which we have already pre-calculated),
141 and we check if it is the root element under an `iframe`.
142 """
144 root = self.root and self.root is el # type: ignore[attr-defined]
145 if not root:
146 parent = self.get_parent(el)
147 root = parent is not None and self.is_html and self.is_iframe(parent) # type: ignore[attr-defined]
148 return root
150 def get_contents(self, el: bs4.Tag | None, no_iframe: bool = False) -> Iterator[bs4.element.PageElement]:
151 """Get contents or contents in reverse."""
153 if el is not None:
154 if not no_iframe or not self.is_iframe(el):
155 yield from el.contents
157 def get_tag_children(
158 self,
159 el: bs4.Tag | None,
160 start: int | None = None,
161 reverse: bool = False,
162 no_iframe: bool = False
163 ) -> Iterator[bs4.Tag]:
164 """Get tag children."""
166 return self.get_children(el, start, reverse, True, no_iframe)
168 @overload
169 def get_children(
170 self,
171 el: bs4.Tag | None,
172 start: int | None = None,
173 reverse: bool = False,
174 tags: Literal[True] = ...,
175 no_iframe: bool = False
176 ) -> Iterator[bs4.Tag]:
177 ...
179 @overload
180 def get_children(
181 self,
182 el: bs4.Tag | None,
183 start: int | None = None,
184 reverse: bool = False,
185 tags: Literal[False] = ...,
186 no_iframe: bool = False
187 ) -> Iterator[bs4.element.PageElement]:
188 ...
190 def get_children(
191 self,
192 el: bs4.Tag | None,
193 start: int | None = None,
194 reverse: bool = False,
195 tags: Literal[True] | Literal[False] = False,
196 no_iframe: bool = False
197 ) -> Iterator[bs4.element.PageElement]:
198 """Get children."""
200 if el is not None and (not no_iframe or not self.is_iframe(el)):
201 last = len(el.contents) - 1
202 if start is None:
203 index = last if reverse else 0
204 else:
205 index = start
206 end = -1 if reverse else last + 1
207 incr = -1 if reverse else 1
209 if 0 <= index <= last:
210 for i in range(index, end, incr):
211 node = el.contents[i]
212 if not tags or self.is_tag(node):
213 yield node
215 def get_tag_descendants(
216 self,
217 el: bs4.Tag | None,
218 no_iframe: bool = False
219 ) -> Iterator[bs4.Tag]:
220 """Specifically get tag descendants."""
222 yield from self.get_descendants(el, tags=True, no_iframe=no_iframe) # type: ignore[misc]
224 def get_descendants(
225 self,
226 el: bs4.Tag | None,
227 tags: bool = False,
228 no_iframe: bool = False
229 ) -> Iterator[bs4.element.PageElement]:
230 """Get descendants."""
232 if el is not None and (not no_iframe or not self.is_iframe(el)):
233 next_good = None
234 for child in el.descendants:
236 if next_good is not None:
237 if child is not next_good:
238 continue
239 next_good = None
241 if isinstance(child, bs4.Tag):
242 if no_iframe and self.is_iframe(child):
243 if child.next_sibling is not None:
244 next_good = child.next_sibling
245 else:
246 last_child = child # type: bs4.element.PageElement
247 while isinstance(last_child, bs4.Tag) and last_child.contents:
248 last_child = last_child.contents[-1]
249 next_good = last_child.next_element
250 yield child
251 if next_good is None:
252 break
253 # Coverage isn't seeing this even though it's executed
254 continue # pragma: no cover
255 yield child
257 elif not tags:
258 yield child
260 def get_parent(self, el: bs4.Tag | None, no_iframe: bool = False) -> bs4.Tag | None:
261 """Get parent."""
263 parent = el.parent if el is not None else None
264 if no_iframe and parent is not None and self.is_iframe(parent): # pragma: no cover
265 parent = None
266 return parent
268 @staticmethod
269 def get_tag_name(el: bs4.Tag | None) -> str | None:
270 """Get tag."""
272 return el.name if el is not None else None
274 @staticmethod
275 def get_prefix_name(el: bs4.Tag) -> str | None:
276 """Get prefix."""
278 return el.prefix
280 @staticmethod
281 def get_uri(el: bs4.Tag | None) -> str | None:
282 """Get namespace `URI`."""
284 return el.namespace if el is not None else None
286 @classmethod
287 def get_next_tag(cls, el: bs4.Tag) -> bs4.Tag | None:
288 """Get next sibling tag."""
290 return cls.get_next(el, tags=True) # type: ignore[return-value]
292 @classmethod
293 def get_next(cls, el: bs4.Tag, tags: bool = False) -> bs4.element.PageElement | None:
294 """Get next sibling tag."""
296 sibling = el.next_sibling
297 while tags and not isinstance(sibling, bs4.Tag) and sibling is not None:
298 sibling = sibling.next_sibling
300 if tags and not isinstance(sibling, bs4.Tag):
301 sibling = None
303 return sibling
305 @classmethod
306 def get_previous_tag(cls, el: bs4.Tag, tags: bool = True) -> bs4.Tag | None:
307 """Get previous sibling tag."""
309 return cls.get_previous(el, True) # type: ignore[return-value]
311 @classmethod
312 def get_previous(cls, el: bs4.Tag, tags: bool = False) -> bs4.element.PageElement | None:
313 """Get previous sibling tag."""
315 sibling = el.previous_sibling
316 while tags and not isinstance(sibling, bs4.Tag) and sibling is not None:
317 sibling = sibling.previous_sibling
319 if tags and not isinstance(sibling, bs4.Tag):
320 sibling = None
322 return sibling
324 @staticmethod
325 def has_html_ns(el: bs4.Tag | None) -> bool:
326 """
327 Check if element has an HTML namespace.
329 This is a bit different than whether a element is treated as having an HTML namespace,
330 like we do in the case of `is_html_tag`.
331 """
333 ns = getattr(el, 'namespace') if el is not None else None # noqa: B009
334 return bool(ns and ns == NS_XHTML)
336 @staticmethod
337 def split_namespace(el: bs4.Tag | None, attr_name: str) -> tuple[str | None, str | None]:
338 """Return namespace and attribute name without the prefix."""
340 if el is None: # pragma: no cover
341 return None, None
343 return getattr(attr_name, 'namespace', None), getattr(attr_name, 'name', None)
345 @classmethod
346 def get_attribute_by_name(
347 cls,
348 el: bs4.Tag,
349 name: str,
350 default: str | Sequence[str] | None = None
351 ) -> str | Sequence[str] | None:
352 """Get attribute by name."""
354 value = default
355 if el._is_xml:
356 if name in el.attrs:
357 v = el.attrs[name]
358 value = '' if v is None else v
359 else:
360 for k, v in el.attrs.items():
361 if util.lower(k) == name:
362 value = '' if v is None else v
363 break
364 return value
366 @classmethod
367 def iter_attributes(cls, el: bs4.Tag | None) -> Iterator[tuple[str, str | Sequence[str] | None]]:
368 """Iterate attributes."""
370 if el is not None:
371 for k, v in el.attrs.items():
372 yield k, '' if v is None else v
374 @classmethod
375 def get_classes(cls, el: bs4.Tag) -> Sequence[str]:
376 """Get classes."""
378 classes = cls.get_attribute_by_name(el, 'class', [])
379 if isinstance(classes, str):
380 classes = RE_NOT_WS.findall(classes)
381 return cast(Sequence[str], classes)
383 def get_text(self, el: bs4.Tag, no_iframe: bool = False) -> str:
384 """Get text."""
386 return ''.join(
387 [
388 node for node in self.get_descendants(el, no_iframe=no_iframe) # type: ignore[misc]
389 if self.is_content_string(node)
390 ]
391 )
393 def get_own_text(self, el: bs4.Tag, no_iframe: bool = False) -> list[str]:
394 """Get Own Text."""
396 return [
397 node for node in self.get_contents(el, no_iframe=no_iframe) if self.is_content_string(node) # type: ignore[misc]
398 ]
401class Inputs:
402 """Class for parsing and validating input items."""
404 @staticmethod
405 def validate_day(year: int, month: int, day: int) -> bool:
406 """Validate day."""
408 max_days = LONG_MONTH
409 if month == FEB:
410 max_days = FEB_LEAP_MONTH if ((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0) else FEB_MONTH
411 elif month in MONTHS_30:
412 max_days = SHORT_MONTH
413 return 1 <= day <= max_days
415 @staticmethod
416 def validate_week(year: int, week: int) -> bool:
417 """Validate week."""
419 # Validate an ISO week number for `year`.
420 #
421 # Per ISO 8601 rules, the last ISO week of a year is the week
422 # containing Dec 28. Using Dec 28 guarantees we obtain the
423 # correct ISO week-number for the final week of `year`, even in
424 # years where Dec 31 falls in ISO week 01 of the following year.
425 #
426 # Example: if Dec 31 is a Thursday the year's last ISO week will
427 # be week 53; if Dec 31 is a Monday and that week is counted as
428 # week 1 of the next year, Dec 28 still belongs to the final
429 # week of the current ISO year and yields the correct max week.
430 max_week = datetime(year, 12, 28).isocalendar()[1]
431 return 1 <= week <= max_week
433 @staticmethod
434 def validate_month(month: int) -> bool:
435 """Validate month."""
437 return 1 <= month <= 12
439 @staticmethod
440 def validate_year(year: int) -> bool:
441 """Validate year."""
443 return 1 <= year
445 @staticmethod
446 def validate_hour(hour: int) -> bool:
447 """Validate hour."""
449 return 0 <= hour <= 23
451 @staticmethod
452 def validate_minutes(minutes: int) -> bool:
453 """Validate minutes."""
455 return 0 <= minutes <= 59
457 @classmethod
458 def parse_value(cls, itype: str, value: str | None) -> tuple[float, ...] | None:
459 """Parse the input value."""
461 parsed = None # type: tuple[float, ...] | None
462 if value is None:
463 return value
464 if itype == "date":
465 m = RE_DATE.match(value)
466 if m:
467 year = int(m.group('year'), 10)
468 month = int(m.group('month'), 10)
469 day = int(m.group('day'), 10)
470 if cls.validate_year(year) and cls.validate_month(month) and cls.validate_day(year, month, day):
471 parsed = (year, month, day)
472 elif itype == "month":
473 m = RE_MONTH.match(value)
474 if m:
475 year = int(m.group('year'), 10)
476 month = int(m.group('month'), 10)
477 if cls.validate_year(year) and cls.validate_month(month):
478 parsed = (year, month)
479 elif itype == "week":
480 m = RE_WEEK.match(value)
481 if m:
482 year = int(m.group('year'), 10)
483 week = int(m.group('week'), 10)
484 if cls.validate_year(year) and cls.validate_week(year, week):
485 parsed = (year, week)
486 elif itype == "time":
487 m = RE_TIME.match(value)
488 if m:
489 hour = int(m.group('hour'), 10)
490 minutes = int(m.group('minutes'), 10)
491 if cls.validate_hour(hour) and cls.validate_minutes(minutes):
492 parsed = (hour, minutes)
493 elif itype == "datetime-local":
494 m = RE_DATETIME.match(value)
495 if m:
496 year = int(m.group('year'), 10)
497 month = int(m.group('month'), 10)
498 day = int(m.group('day'), 10)
499 hour = int(m.group('hour'), 10)
500 minutes = int(m.group('minutes'), 10)
501 if (
502 cls.validate_year(year) and cls.validate_month(month) and cls.validate_day(year, month, day) and
503 cls.validate_hour(hour) and cls.validate_minutes(minutes)
504 ):
505 parsed = (year, month, day, hour, minutes)
506 elif itype in ("number", "range"):
507 m = RE_NUM.match(value)
508 if m:
509 parsed = (float(m.group('value')),)
510 return parsed
513class CSSMatch(_DocumentNav):
514 """Perform CSS matching."""
516 def __init__(
517 self,
518 selectors: ct.SelectorList,
519 scope: bs4.Tag | None,
520 namespaces: ct.Namespaces | None,
521 flags: int
522 ) -> None:
523 """Initialize."""
525 self.assert_valid_input(scope)
526 self.tag = scope
527 self.cached_meta_lang = [] # type: list[tuple[str, str]]
528 self.cached_default_forms = [] # type: list[tuple[bs4.Tag, bs4.Tag]]
529 self.cached_indeterminate_forms = [] # type: list[tuple[bs4.Tag, str, bool]]
530 self.selectors = selectors
531 self.namespaces = {} if namespaces is None else namespaces # type: ct.Namespaces | dict[str, str]
532 self.flags = flags
533 self.enable_cache = not bool(self.flags & util.NOCACHE)
534 self.iframe_restrict = False
535 self.nth_cache: dict[Hashable, dict[Hashable, list[int]]] = {}
537 # Find the root element for the whole tree
538 doc = scope
539 parent = self.get_parent(doc)
540 while parent:
541 doc = parent
542 parent = self.get_parent(doc)
543 root = None # type: bs4.Tag | None
544 if not self.is_doc(doc):
545 root = doc
546 else:
547 for child in self.get_tag_children(doc):
548 root = child
549 break
551 self.root = root
552 self.scope = scope if scope is not doc else root
553 self.has_html_namespace = self.has_html_ns(root)
555 # A document can be both XML and HTML (XHTML)
556 self.is_xml = self.is_xml_tree(doc)
557 self.is_html = not self.is_xml or self.has_html_namespace
559 def reset(self) -> None: # pragma: no cover
560 """Reset."""
562 self.nth_cache.clear()
564 def supports_namespaces(self) -> bool:
565 """Check if namespaces are supported in the HTML type."""
567 return self.is_xml or self.has_html_namespace
569 def get_tag_ns(self, el: bs4.Tag | None) -> str:
570 """Get tag namespace."""
572 namespace = ''
573 if el is None: # pragma: no cover
574 return namespace
576 if self.supports_namespaces():
577 ns = self.get_uri(el)
578 if ns:
579 namespace = ns
580 else:
581 namespace = NS_XHTML
582 return namespace
584 def is_html_tag(self, el: bs4.Tag | None) -> bool:
585 """Check if tag is in HTML namespace."""
587 return self.get_tag_ns(el) == NS_XHTML
589 def get_tag(self, el: bs4.Tag | None) -> str | None:
590 """Get tag."""
592 name = self.get_tag_name(el)
593 return util.lower(name) if name is not None and not self.is_xml else name
595 def get_prefix(self, el: bs4.Tag) -> str | None:
596 """Get prefix."""
598 prefix = self.get_prefix_name(el)
599 return util.lower(prefix) if prefix is not None and not self.is_xml else prefix
601 def find_bidi(self, el: bs4.Tag) -> int | None:
602 """Get directionality from element text."""
604 for node in self.get_children(el):
606 # Analyze child text nodes
607 if self.is_tag(node):
609 # Avoid analyzing certain elements specified in the specification.
610 direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(node, 'dir', '')), None)
611 name = self.get_tag(node)
612 if (
613 (name and name in ('bdi', 'script', 'style', 'textarea', 'iframe')) or
614 not self.is_html_tag(node) or
615 direction is not None
616 ):
617 continue # pragma: no cover
619 # Check directionality of this node's text
620 value = self.find_bidi(node)
621 if value is not None:
622 return value
624 # Direction could not be determined
625 continue # pragma: no cover
627 # Skip `doctype` comments, etc.
628 if self.is_special_string(node):
629 continue
631 # Analyze text nodes for directionality.
632 for c in cast('bs4.element.NavigableString', node):
633 bidi = unicodedata.bidirectional(c)
634 if bidi in ('AL', 'R', 'L'):
635 return ct.SEL_DIR_LTR if bidi == 'L' else ct.SEL_DIR_RTL
636 return None
638 def extended_language_filter(self, lang_range: str, lang_tag: str) -> bool:
639 """Filter the language tags."""
641 match = True
642 lang_range = RE_WILD_STRIP.sub('-', lang_range).lower()
643 ranges = lang_range.split('-')
644 subtags = lang_tag.lower().split('-')
645 length = len(ranges)
646 slength = len(subtags)
647 rindex = 0
648 sindex = 0
649 r = ranges[rindex]
650 s = subtags[sindex]
652 # Empty specified language should match unspecified language attributes
653 if length == 1 and slength == 1 and not r and r == s:
654 return True
656 # Primary tag needs to match
657 if (r != '*' and r != s) or (r == '*' and slength == 1 and not s):
658 match = False
660 rindex += 1
661 sindex += 1
663 # Match until we run out of ranges
664 while match and rindex < length:
665 r = ranges[rindex]
666 try:
667 s = subtags[sindex]
668 except IndexError:
669 # Ran out of subtags,
670 # but we still have ranges
671 match = False
672 continue
674 # Empty range
675 if not r:
676 match = False
677 continue
679 # Matched range
680 elif s == r:
681 rindex += 1
683 # Implicit wildcard cannot match
684 # singletons
685 elif len(s) == 1:
686 match = False
687 continue
689 # Implicitly matched, so grab next subtag
690 sindex += 1
692 return match
694 def match_attribute_name(
695 self,
696 el: bs4.Tag,
697 attr: str,
698 prefix: str | None
699 ) -> str | Sequence[str] | None:
700 """Match attribute name and return value if it exists."""
702 value = None
703 if self.supports_namespaces():
704 value = None
705 # If we have not defined namespaces, we can't very well find them, so don't bother trying.
706 if prefix:
707 ns = self.namespaces.get(prefix)
708 if ns is None and prefix != '*':
709 return None
710 else:
711 ns = None
713 for k, v in self.iter_attributes(el):
715 # Get attribute parts
716 namespace, name = self.split_namespace(el, k)
718 # Can't match a prefix attribute as we haven't specified one to match
719 # Try to match it normally as a whole `p:a` as selector may be trying `p\:a`.
720 if ns is None:
721 if (self.is_xml and attr == k) or (not self.is_xml and util.lower(attr) == util.lower(k)):
722 value = v
723 break
724 # Coverage is not finding this even though it is executed.
725 # Adding a print statement before this (and erasing coverage) causes coverage to find the line.
726 # Ignore the false positive message.
727 continue # pragma: no cover
729 # We can't match our desired prefix attribute as the attribute doesn't have a prefix
730 if namespace is None or (ns != namespace and prefix != '*'):
731 continue
733 # The attribute doesn't match.
734 if (util.lower(attr) != util.lower(name)) if not self.is_xml else (attr != name):
735 continue
737 value = v
738 break
739 else:
740 for k, v in self.iter_attributes(el):
741 if util.lower(attr) != util.lower(k):
742 continue
743 value = v
744 break
745 return value
747 def match_namespace(self, el: bs4.Tag, tag: ct.SelectorTag) -> bool:
748 """Match the namespace of the element."""
750 match = True
751 namespace = self.get_tag_ns(el)
752 default_namespace = self.namespaces.get('')
753 tag_ns = '' if tag.prefix is None else self.namespaces.get(tag.prefix)
754 # We must match the default namespace if one is not provided
755 if tag.prefix is None and (default_namespace is not None and namespace != default_namespace):
756 match = False
757 # If we specified `|tag`, we must not have a namespace.
758 elif (tag.prefix is not None and tag.prefix == '' and namespace):
759 match = False
760 # Verify prefix matches
761 elif (
762 tag.prefix and
763 tag.prefix != '*' and (tag_ns is None or namespace != tag_ns)
764 ):
765 match = False
766 return match
768 def match_attributes(self, el: bs4.Tag, attributes: tuple[ct.SelectorAttribute, ...]) -> bool:
769 """Match attributes."""
771 match = True
772 if attributes:
773 for a in attributes:
774 temp = self.match_attribute_name(el, a.attribute, a.prefix)
775 pattern = a.xml_type_pattern if self.is_xml and a.xml_type_pattern else a.pattern
776 if temp is None:
777 match = False
778 break
779 value = temp if isinstance(temp, str) else ' '.join(temp)
780 if pattern is None:
781 continue
782 elif pattern.match(value) is None:
783 match = False
784 break
785 return match
787 def match_tagname(self, el: bs4.Tag, tag: ct.SelectorTag) -> bool:
788 """Match tag name."""
790 name = (util.lower(tag.name) if not self.is_xml and tag.name is not None else tag.name)
791 return not (
792 name is not None and
793 name not in (self.get_tag(el), '*')
794 )
796 def match_tag(self, el: bs4.Tag, tag: ct.SelectorTag | None) -> bool:
797 """Match the tag."""
799 match = True
800 if tag is not None:
801 # Verify namespace
802 if not self.match_tagname(el, tag):
803 match = False
804 if match and not self.match_namespace(el, tag):
805 match = False
806 return match
808 def match_past_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> bool:
809 """Match past relationship."""
811 found = False
812 # I don't think this can ever happen, but it makes `mypy` happy
813 if relation[0] is ct.Null: # pragma: no cover
814 return found
816 if relation[0].rel_type == REL_PARENT:
817 parent = self.get_parent(el, no_iframe=self.iframe_restrict)
818 while not found and parent:
819 found = self.match_selectors(parent, relation)
820 parent = self.get_parent(parent, no_iframe=self.iframe_restrict)
821 elif relation[0].rel_type == REL_CLOSE_PARENT:
822 parent = self.get_parent(el, no_iframe=self.iframe_restrict)
823 if parent:
824 found = self.match_selectors(parent, relation)
825 elif relation[0].rel_type == REL_SIBLING:
826 sibling = self.get_previous_tag(el)
827 while not found and sibling:
828 found = self.match_selectors(sibling, relation)
829 sibling = self.get_previous_tag(sibling)
830 elif relation[0].rel_type == REL_CLOSE_SIBLING:
831 sibling = self.get_previous_tag(el)
832 if sibling and self.is_tag(sibling):
833 found = self.match_selectors(sibling, relation)
834 return found
836 def match_future_child(self, parent: bs4.Tag, relation: ct.SelectorList, recursive: bool = False) -> bool:
837 """Match future child."""
839 match = False
840 if recursive:
841 children = self.get_tag_descendants # type: Callable[..., Iterator[bs4.Tag]]
842 else:
843 children = self.get_tag_children
844 for child in children(parent, no_iframe=self.iframe_restrict):
845 match = self.match_selectors(child, relation)
846 if match:
847 break
848 return match
850 def match_future_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> bool:
851 """Match future relationship."""
853 found = False
854 # I don't think this can ever happen, but it makes `mypy` happy
855 if relation[0] is ct.Null: # pragma: no cover
856 return found
858 if relation[0].rel_type == REL_HAS_PARENT:
859 found = self.match_future_child(el, relation, True)
860 elif relation[0].rel_type == REL_HAS_CLOSE_PARENT:
861 found = self.match_future_child(el, relation)
862 elif relation[0].rel_type == REL_HAS_SIBLING:
863 sibling = self.get_next_tag(el)
864 while not found and sibling:
865 found = self.match_selectors(sibling, relation)
866 sibling = self.get_next_tag(sibling)
867 elif relation[0].rel_type == REL_HAS_CLOSE_SIBLING:
868 sibling = self.get_next_tag(el)
869 if sibling and self.is_tag(sibling):
870 found = self.match_selectors(sibling, relation)
871 return found
873 def match_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> bool:
874 """Match relationship to other elements."""
876 found = False
878 if relation[0] is ct.Null or relation[0].rel_type is None:
879 return found
881 if relation[0].rel_type.startswith(':'):
882 found = self.match_future_relations(el, relation)
883 else:
884 found = self.match_past_relations(el, relation)
886 return found
888 def match_id(self, el: bs4.Tag, ids: tuple[str, ...]) -> bool:
889 """Match element's ID."""
891 found = True
892 for i in ids:
893 if i != self.get_attribute_by_name(el, 'id', ''):
894 found = False
895 break
896 return found
898 def match_classes(self, el: bs4.Tag, classes: tuple[str, ...]) -> bool:
899 """Match element's classes."""
901 current_classes = self.get_classes(el)
902 found = True
903 for c in classes:
904 if c not in current_classes:
905 found = False
906 break
907 return found
909 def match_root(self, el: bs4.Tag) -> bool:
910 """Match element as root."""
912 is_root = self.is_root(el)
913 if is_root:
914 sibling = self.get_previous(el) # type: Any
915 while is_root and sibling is not None:
916 if (
917 self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or
918 self.is_cdata(sibling)
919 ):
920 is_root = False
921 else:
922 sibling = self.get_previous(sibling)
923 if is_root:
924 sibling = self.get_next(el)
925 while is_root and sibling is not None:
926 if (
927 self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or
928 self.is_cdata(sibling)
929 ):
930 is_root = False
931 else:
932 sibling = self.get_next(sibling)
933 return is_root
935 def match_scope(self, el: bs4.Tag) -> bool:
936 """Match element as scope."""
938 return self.scope is el
940 def match_nth_tag_type(self, el: bs4.Tag, child: bs4.Tag) -> bool:
941 """Match tag type for `nth` matches."""
943 return (
944 (self.get_tag(child) == self.get_tag(el)) and
945 (self.get_tag_ns(child) == self.get_tag_ns(el))
946 )
948 def match_nth(self, el: bs4.Tag, nth: tuple[ct.SelectorNth, ...]) -> bool:
949 """Match `nth` elements."""
951 # `nth` selectors are evaluated against siblings under the same parent.
952 parent = self.get_parent(el) # type: bs4.Tag | None
953 pkey: tuple[str | None, int] | None = None
954 key: tuple[ct.SelectorNth, int, str | None, str | None] | None = None
955 start = rindex = 0
956 incr = rincr = 0
958 # Setup the cache by the parent, if parent a parent is present
959 if self.enable_cache and parent:
960 pkey = (parent.name, id(parent))
962 # Initialize the cache if necessary
963 if pkey not in self.nth_cache:
964 self.nth_cache[pkey] = {}
966 # Test element against the `nth` selectors.
967 matched = True
968 for n in nth:
969 matched = False
970 last = n.last
971 key = None
973 # Prepare the child iterator and get the starting, real index and the relative index
974 if pkey and parent:
975 # Get last info from the cache
976 key = (n, id(n), self.get_tag(el), self.get_tag_ns(el)) if n.of_type else (n, id(n), None, None)
977 valid = False
978 if key in self.nth_cache[pkey]:
979 start, rindex = self.nth_cache[pkey][key]
980 if within(el, parent, start):
981 last = False
982 rincr = -1 if n.last else 1
983 valid = True
985 # Start/overwrite the cache if the cache was empty or invalid
986 if not valid:
987 start, rindex = len(parent) - 1 if last else 0, 0
988 self.nth_cache[pkey][key] = [start, rindex]
989 rincr = 1
991 incr = 1 if not last else -1
992 children = self.get_children(parent, start=start, reverse=last)
994 # Non-cached handling of parented element
995 elif parent:
996 rindex = 0
997 start = len(parent) - 1 if last else 0
998 rincr = incr = 1
999 children = self.get_children(parent, start=start, reverse=last)
1001 # No parent, just evaluate the element against the selectors
1002 else:
1003 start = rindex = 0
1004 rincr = incr = 1
1005 children = iter([el])
1007 # Find index of element compared to its siblings and check the index conditions
1008 child: bs4.Tag
1009 for child in children:
1010 start += incr
1012 # We only care about tags
1013 if not self.is_tag(child):
1014 continue
1016 # Handle `of S` in `nth-child` and handle `of-type`
1017 if (
1018 (n.selectors and not self.match_selectors(child, n.selectors)) or
1019 (n.of_type and not self.match_nth_tag_type(el, child))
1020 ):
1021 if child is el:
1022 break
1023 continue
1025 # Test the relative index against the `nth` requirement.
1026 rindex += rincr
1027 if child is el:
1028 if n.a != 0:
1029 v = (rindex - n.b) / n.a
1030 matched = v.is_integer() and v >= 0
1031 else:
1032 matched = rindex == n.b and n.b >= 1
1033 break
1035 # "Last index" selectors evaluate first from the bottom and then evaluate
1036 # from the first found element top-down. Start will be incremented in the
1037 # wrong direction, so increment it and step over the current index.
1038 if last:
1039 start += 2
1041 # Update the cache
1042 if pkey and key:
1043 self.nth_cache[pkey][key] = [start, rindex]
1045 # If we failed to match any `nth` selectors, quit.
1046 if not matched:
1047 break
1049 return matched
1051 def match_empty(self, el: bs4.Tag) -> bool:
1052 """Check if element is empty (if requested)."""
1054 is_empty = True
1055 for child in self.get_children(el):
1056 if self.is_tag(child):
1057 is_empty = False
1058 break
1059 elif self.is_content_string(child) and RE_NOT_EMPTY.search(child): # type: ignore[call-overload]
1060 is_empty = False
1061 break
1062 return is_empty
1064 def match_subselectors(self, el: bs4.Tag, selectors: tuple[ct.SelectorList, ...]) -> bool:
1065 """Match selectors."""
1067 match = True
1068 for sel in selectors:
1069 if not self.match_selectors(el, sel):
1070 match = False
1071 return match
1073 def match_contains(self, el: bs4.Tag, contains: tuple[ct.SelectorContains, ...]) -> bool:
1074 """Match element if it contains text."""
1076 match = True
1077 content = None # type: str | Sequence[str] | None
1078 for contain_list in contains:
1079 if content is None:
1080 if contain_list.own:
1081 content = self.get_own_text(el, no_iframe=self.is_html)
1082 else:
1083 content = self.get_text(el, no_iframe=self.is_html)
1084 found = False
1085 for text in contain_list.text:
1086 if contain_list.own:
1087 for c in content:
1088 if text in c:
1089 found = True
1090 break
1091 if found:
1092 break
1093 else:
1094 if text in content:
1095 found = True
1096 break
1097 if not found:
1098 match = False
1099 return match
1101 def match_default(self, el: bs4.Tag) -> bool:
1102 """Match default."""
1104 match = False
1106 # Find this input's form
1107 form = None # type: bs4.Tag | None
1108 parent = self.get_parent(el, no_iframe=True)
1109 while parent and form is None:
1110 if self.get_tag(parent) == 'form' and self.is_html_tag(parent):
1111 form = parent
1112 else:
1113 parent = self.get_parent(parent, no_iframe=True)
1115 if form is not None:
1116 # Look in form cache to see if we've already located its default button
1117 found_form = False
1118 for f, t in self.cached_default_forms:
1119 if f is form:
1120 found_form = True
1121 if t is el:
1122 match = True
1123 break
1125 # We didn't have the form cached, so look for its default button
1126 if not found_form:
1127 for child in self.get_tag_descendants(form, no_iframe=True):
1128 name = self.get_tag(child)
1129 # Can't do nested forms (haven't figured out why we never hit this)
1130 if name == 'form': # pragma: no cover
1131 break
1132 if name in ('input', 'button'):
1133 v = self.get_attribute_by_name(child, 'type', '')
1134 if v and util.lower(v) == 'submit':
1135 self.cached_default_forms.append((form, child))
1136 if el is child:
1137 match = True
1138 break
1139 return match
1141 def match_indeterminate(self, el: bs4.Tag) -> bool:
1142 """Match default."""
1144 match = False
1145 name = cast(str, self.get_attribute_by_name(el, 'name'))
1147 def get_parent_form(el: bs4.Tag) -> bs4.Tag | None:
1148 """Find this input's form."""
1149 form = None
1150 parent = self.get_parent(el, no_iframe=True)
1151 while form is None:
1152 if self.get_tag(parent) == 'form' and self.is_html_tag(parent):
1153 form = parent
1154 break
1155 last_parent = parent
1156 parent = self.get_parent(parent, no_iframe=True)
1157 if parent is None:
1158 form = last_parent
1159 break
1160 return form
1162 form = get_parent_form(el)
1164 # Look in form cache to see if we've already evaluated that its fellow radio buttons are indeterminate
1165 if form is not None:
1166 found_form = False
1167 for f, n, i in self.cached_indeterminate_forms:
1168 if f is form and n == name:
1169 found_form = True
1170 if i is True:
1171 match = True
1172 break
1174 # We didn't have the form cached, so validate that the radio button is indeterminate
1175 if not found_form:
1176 checked = False
1177 for child in self.get_tag_descendants(form, no_iframe=True):
1178 if child is el:
1179 continue
1180 tag_name = self.get_tag(child)
1181 if tag_name == 'input':
1182 is_radio = False
1183 check = False
1184 has_name = False
1185 for k, v in self.iter_attributes(child):
1186 if util.lower(k) == 'type' and util.lower(v) == 'radio':
1187 is_radio = True
1188 elif util.lower(k) == 'name' and v == name:
1189 has_name = True
1190 elif util.lower(k) == 'checked':
1191 check = True
1192 if is_radio and check and has_name and get_parent_form(child) is form:
1193 checked = True
1194 break
1195 if checked:
1196 break
1197 if not checked:
1198 match = True
1199 self.cached_indeterminate_forms.append((form, name, match))
1201 return match
1203 def match_lang(self, el: bs4.Tag, langs: tuple[ct.SelectorLang, ...]) -> bool:
1204 """Match languages."""
1206 match = False
1207 has_ns = self.supports_namespaces()
1208 root = self.root
1209 has_html_namespace = self.has_html_namespace
1211 # Walk parents looking for `lang` (HTML) or `xml:lang` XML property.
1212 parent = el # type: bs4.Tag | None
1213 found_lang = None
1214 last = None
1215 while not found_lang:
1216 has_html_ns = self.has_html_ns(parent)
1217 for k, v in self.iter_attributes(parent):
1218 attr_ns, attr = self.split_namespace(parent, k)
1219 if (
1220 ((not has_ns or has_html_ns) and (util.lower(k) if not self.is_xml else k) == 'lang') or
1221 (
1222 has_ns and not has_html_ns and attr_ns == NS_XML and
1223 (util.lower(attr) if not self.is_xml and attr is not None else attr) == 'lang'
1224 )
1225 ):
1226 found_lang = v
1227 break
1228 last = parent
1229 parent = self.get_parent(parent, no_iframe=self.is_html)
1231 if parent is None:
1232 root = last
1233 has_html_namespace = self.has_html_ns(root)
1234 parent = last
1235 break
1237 # Use cached meta language.
1238 if found_lang is None and self.cached_meta_lang:
1239 for cache in self.cached_meta_lang:
1240 if root is not None and cast(str, root) is cache[0]:
1241 found_lang = cache[1]
1243 # If we couldn't find a language, and the document is HTML, look to meta to determine language.
1244 if found_lang is None and (not self.is_xml or (has_html_namespace and root and root.name == 'html')):
1245 # Find head
1246 found = False
1247 for tag in ('html', 'head'):
1248 found = False
1249 for child in self.get_tag_children(parent, no_iframe=self.is_html):
1250 if self.get_tag(child) == tag and self.is_html_tag(child):
1251 found = True
1252 parent = child
1253 break
1254 if not found: # pragma: no cover
1255 break
1257 # Search meta tags
1258 if found and parent is not None:
1259 for child2 in parent:
1260 if isinstance(child2, bs4.Tag) and self.get_tag(child2) == 'meta' and self.is_html_tag(parent):
1261 c_lang = False
1262 content = None
1263 for k, v in self.iter_attributes(child2):
1264 if util.lower(k) == 'http-equiv' and util.lower(v) == 'content-language':
1265 c_lang = True
1266 if util.lower(k) == 'content':
1267 content = v
1268 if c_lang and content:
1269 found_lang = content
1270 self.cached_meta_lang.append((cast(str, root), cast(str, found_lang)))
1271 break
1272 if found_lang is not None:
1273 break
1274 if found_lang is None:
1275 self.cached_meta_lang.append((cast(str, root), ''))
1277 # If we determined a language, compare.
1278 if found_lang is not None:
1279 for patterns in langs:
1280 match = False
1281 for pattern in patterns:
1282 if self.extended_language_filter(pattern, cast(str, found_lang)):
1283 match = True
1284 if not match:
1285 break
1287 return match
1289 def match_dir(self, el: bs4.Tag | None, directionality: int) -> bool:
1290 """Check directionality."""
1292 # If we have to match both left and right, we can't match either.
1293 if directionality & ct.SEL_DIR_LTR and directionality & ct.SEL_DIR_RTL:
1294 return False
1296 if el is None or not self.is_html_tag(el):
1297 return False
1299 # Element has defined direction of left to right or right to left
1300 direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(el, 'dir', '')), None)
1301 if direction not in (None, 0):
1302 return direction == directionality
1304 # Element is the document element (the root) and no direction assigned, assume left to right.
1305 is_root = self.is_root(el)
1306 if is_root and direction is None:
1307 return ct.SEL_DIR_LTR == directionality
1309 # If `input[type=telephone]` and no direction is assigned, assume left to right.
1310 name = self.get_tag(el)
1311 is_input = name == 'input'
1312 is_textarea = name == 'textarea'
1313 is_bdi = name == 'bdi'
1314 itype = util.lower(self.get_attribute_by_name(el, 'type', '')) if is_input else ''
1315 if is_input and itype == 'tel' and direction is None:
1316 return ct.SEL_DIR_LTR == directionality
1318 # Auto handling for text inputs
1319 if ((is_input and itype in ('text', 'search', 'tel', 'url', 'email')) or is_textarea) and direction == 0:
1320 if is_textarea:
1321 value = ''.join(node for node in self.get_contents(el, no_iframe=True) if self.is_content_string(node)) # type: ignore[misc]
1322 else:
1323 value = cast(str, self.get_attribute_by_name(el, 'value', ''))
1324 if value:
1325 for c in value:
1326 bidi = unicodedata.bidirectional(c)
1327 if bidi in ('AL', 'R', 'L'):
1328 direction = ct.SEL_DIR_LTR if bidi == 'L' else ct.SEL_DIR_RTL
1329 return direction == directionality
1330 # Assume left to right
1331 return ct.SEL_DIR_LTR == directionality
1332 elif is_root:
1333 return ct.SEL_DIR_LTR == directionality
1334 return self.match_dir(self.get_parent(el, no_iframe=True), directionality)
1336 # Auto handling for `bdi` and other non text inputs.
1337 if (is_bdi and direction is None) or direction == 0:
1338 direction = self.find_bidi(el)
1339 if direction is not None:
1340 return direction == directionality
1341 elif is_root:
1342 return ct.SEL_DIR_LTR == directionality
1343 return self.match_dir(self.get_parent(el, no_iframe=True), directionality)
1345 # Match parents direction
1346 return self.match_dir(self.get_parent(el, no_iframe=True), directionality)
1348 def match_range(self, el: bs4.Tag, condition: int) -> bool:
1349 """
1350 Match range.
1352 Behavior is modeled after what we see in browsers. Browsers seem to evaluate
1353 if the value is out of range, and if not, it is in range. So a missing value
1354 will not evaluate out of range; therefore, value is in range. Personally, I
1355 feel like this should evaluate as neither in or out of range.
1356 """
1358 out_of_range = False
1360 itype = util.lower(self.get_attribute_by_name(el, 'type'))
1361 mn = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'min', None)))
1362 mx = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'max', None)))
1364 # There is no valid min or max, so we cannot evaluate a range
1365 if mn is None and mx is None:
1366 return False
1368 value = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'value', None)))
1369 if value is not None:
1370 if itype in ("date", "datetime-local", "month", "week", "number", "range"):
1371 if mn is not None and value < mn:
1372 out_of_range = True
1373 if not out_of_range and mx is not None and value > mx:
1374 out_of_range = True
1375 elif itype == "time":
1376 if mn is not None and mx is not None and mn > mx:
1377 # Time is periodic, so this is a reversed/discontinuous range
1378 if value < mn and value > mx:
1379 out_of_range = True
1380 else:
1381 if mn is not None and value < mn:
1382 out_of_range = True
1383 if not out_of_range and mx is not None and value > mx:
1384 out_of_range = True
1386 return not out_of_range if condition & ct.SEL_IN_RANGE else out_of_range
1388 def match_defined(self, el: bs4.Tag) -> bool:
1389 """
1390 Match defined.
1392 `:defined` is related to custom elements in a browser.
1394 - If the document is XML (not XHTML), all tags will match.
1395 - Tags that are not custom (don't have a hyphen) are marked defined.
1396 - If the tag has a prefix (without or without a namespace), it will not match.
1398 This is of course requires the parser to provide us with the proper prefix and namespace info,
1399 if it doesn't, there is nothing we can do.
1400 """
1402 name = self.get_tag(el)
1403 return (
1404 name is not None and (
1405 name.find('-') == -1 or
1406 name.find(':') != -1 or
1407 self.get_prefix(el) is not None
1408 )
1409 )
1411 def match_placeholder_shown(self, el: bs4.Tag) -> bool:
1412 """
1413 Match placeholder shown according to HTML spec.
1415 - text area should be checked if they have content. A single newline does not count as content.
1417 """
1419 match = False
1420 content = self.get_text(el)
1421 if content in ('', '\n'):
1422 match = True
1424 return match
1426 def match_selectors(self, el: bs4.Tag, selectors: ct.SelectorList) -> bool:
1427 """Check if element matches one of the selectors."""
1429 match = False
1430 is_not = selectors.is_not
1431 is_html = selectors.is_html
1433 # Internal selector lists that use the HTML flag, will automatically get the `html` namespace.
1434 if is_html:
1435 namespaces = self.namespaces
1436 iframe_restrict = self.iframe_restrict
1437 self.namespaces = {'html': NS_XHTML}
1438 self.iframe_restrict = True
1440 if not is_html or self.is_html:
1441 for selector in selectors:
1442 match = is_not
1443 # We have a un-matchable situation (like `:focus` as you can focus an element in this environment)
1444 if selector is ct.Null:
1445 continue
1446 # Verify tag matches
1447 if not self.match_tag(el, selector.tag):
1448 continue
1449 # Verify tag is defined
1450 if selector.flags & ct.SEL_DEFINED and not self.match_defined(el):
1451 continue
1452 # Verify element is root
1453 if selector.flags & ct.SEL_ROOT and not self.match_root(el):
1454 continue
1455 # Verify element is scope
1456 if selector.flags & ct.SEL_SCOPE and not self.match_scope(el):
1457 continue
1458 # Verify element has placeholder shown
1459 if selector.flags & ct.SEL_PLACEHOLDER_SHOWN and not self.match_placeholder_shown(el):
1460 continue
1461 # Verify `nth` matches
1462 if selector.nth and not self.match_nth(el, selector.nth):
1463 continue
1464 if selector.flags & ct.SEL_EMPTY and not self.match_empty(el):
1465 continue
1466 # Verify id matches
1467 if selector.ids and not self.match_id(el, selector.ids):
1468 continue
1469 # Verify classes match
1470 if selector.classes and not self.match_classes(el, selector.classes):
1471 continue
1472 # Verify attribute(s) match
1473 if not self.match_attributes(el, selector.attributes):
1474 continue
1475 # Verify ranges
1476 if selector.flags & RANGES and not self.match_range(el, selector.flags & RANGES):
1477 continue
1478 # Verify language patterns
1479 if selector.lang and not self.match_lang(el, selector.lang):
1480 continue
1481 # Verify pseudo selector patterns
1482 if selector.selectors and not self.match_subselectors(el, selector.selectors):
1483 continue
1484 # Verify relationship selectors
1485 if selector.relation and not self.match_relations(el, selector.relation):
1486 continue
1487 # Validate that the current default selector match corresponds to the first submit button in the form
1488 if selector.flags & ct.SEL_DEFAULT and not self.match_default(el):
1489 continue
1490 # Validate that the unset radio button is among radio buttons with the same name in a form that are
1491 # also not set.
1492 if selector.flags & ct.SEL_INDETERMINATE and not self.match_indeterminate(el):
1493 continue
1494 # Validate element directionality
1495 if selector.flags & DIR_FLAGS and not self.match_dir(el, selector.flags & DIR_FLAGS):
1496 continue
1497 # Validate that the tag contains the specified text.
1498 if selector.contains and not self.match_contains(el, selector.contains):
1499 continue
1500 match = not is_not
1501 break
1503 # Restore actual namespaces being used for external selector lists
1504 if is_html:
1505 self.namespaces = namespaces
1506 self.iframe_restrict = iframe_restrict
1508 return match
1510 def select(self, limit: int = 0) -> Iterator[bs4.Tag]:
1511 """Match all tags under the targeted tag."""
1513 lim = None if limit < 1 else limit
1515 for child in self.get_tag_descendants(self.tag):
1516 if self.match(child):
1517 yield child
1518 if lim is not None:
1519 lim -= 1
1520 if lim < 1:
1521 break
1523 def closest(self) -> bs4.Tag | None:
1524 """Match closest ancestor."""
1526 current = self.tag # type: bs4.Tag | None
1527 closest = None
1528 while closest is None and current is not None:
1529 if self.match(current):
1530 closest = current
1531 else:
1532 current = self.get_parent(current)
1533 return closest
1535 def filter(self) -> list[bs4.Tag]: # noqa A001
1536 """Filter tag's children."""
1538 return [
1539 tag for tag in self.get_contents(self.tag)
1540 if isinstance(tag, bs4.Tag) and self.match(tag)
1541 ]
1543 def match(self, el: bs4.Tag) -> bool:
1544 """Match."""
1546 return not self.is_doc(el) and self.is_tag(el) and self.match_selectors(el, self.selectors)
1549class SoupSieve(ct.Immutable):
1550 """Compiled Soup Sieve selector matching object."""
1552 pattern: str
1553 selectors: ct.SelectorList
1554 namespaces: ct.Namespaces | None
1555 custom: dict[str, str]
1556 flags: int
1558 __slots__ = ("pattern", "selectors", "namespaces", "custom", "flags", "_hash")
1560 def __init__(
1561 self,
1562 pattern: str,
1563 selectors: ct.SelectorList,
1564 namespaces: ct.Namespaces | None,
1565 custom: ct.CustomSelectors | None,
1566 flags: int
1567 ):
1568 """Initialize."""
1570 super().__init__(
1571 pattern=pattern,
1572 selectors=selectors,
1573 namespaces=namespaces,
1574 custom=custom,
1575 flags=flags
1576 )
1578 def match(self, tag: bs4.Tag) -> bool:
1579 """Match."""
1581 return CSSMatch(self.selectors, tag, self.namespaces, self.flags).match(tag)
1583 def closest(self, tag: bs4.Tag) -> bs4.Tag | None:
1584 """Match closest ancestor."""
1586 return CSSMatch(self.selectors, tag, self.namespaces, self.flags).closest()
1588 def filter(self, iterable: Iterable[bs4.Tag]) -> list[bs4.Tag]: # noqa A001
1589 """
1590 Filter.
1592 `CSSMatch` can cache certain searches for tags of the same document,
1593 so if we are given a tag, all tags are from the same document,
1594 and we can take advantage of the optimization.
1596 Any other kind of iterable could have tags from different documents or detached tags,
1597 so for those, we use a new `CSSMatch` for each item in the iterable.
1598 """
1600 if isinstance(iterable, bs4.Tag):
1601 return CSSMatch(self.selectors, iterable, self.namespaces, self.flags).filter()
1602 else:
1603 # There is no guarantee that elements are from the same document, evaluate them separately.
1604 return [node for node in iterable if not CSSMatch.is_navigable_string(node) and self.match(node)]
1606 def select_one(self, tag: bs4.Tag) -> bs4.Tag | None:
1607 """Select a single tag."""
1609 tags = self.select(tag, limit=1)
1610 return tags[0] if tags else None
1612 def select(self, tag: bs4.Tag, limit: int = 0) -> list[bs4.Tag]:
1613 """Select the specified tags."""
1615 return list(self.iselect(tag, limit))
1617 def iselect(self, tag: bs4.Tag, limit: int = 0) -> Iterator[bs4.Tag]:
1618 """Iterate the specified tags."""
1620 yield from CSSMatch(self.selectors, tag, self.namespaces, self.flags).select(limit)
1622 def __repr__(self) -> str: # pragma: no cover
1623 """Representation."""
1625 return (
1626 f"SoupSieve(pattern={self.pattern!r}, namespaces={self.namespaces!r}, "
1627 f"custom={self.custom!r}, flags={self.flags!r})"
1628 )
1630 __str__ = __repr__
1633ct.pickle_register(SoupSieve)