Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/soupsieve/css_match.py: 17%
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 bisect import bisect_right
4from datetime import datetime
5from math import isfinite
6from collections.abc import Hashable
7from . import bidi
8from . import util
9import re
10from . import css_types as ct
11import bs4
12from typing import (
13 Generator,
14 Iterator,
15 Iterable,
16 Any,
17 Callable,
18 Sequence,
19 overload,
20 Literal,
21 NamedTuple,
22 cast,
23 TYPE_CHECKING
24) # noqa: F401, F811
26# Empty tag pattern (whitespace okay)
27RE_NOT_EMPTY = re.compile('[^ \t\r\n\f]')
29RE_NOT_WS = re.compile('[^ \t\r\n\f]+')
31# Relationships
32REL_PARENT = ' '
33REL_CLOSE_PARENT = '>'
34REL_SIBLING = '~'
35REL_CLOSE_SIBLING = '+'
37# Relationships for :has() (forward looking)
38REL_HAS_PARENT = ': '
39REL_HAS_CLOSE_PARENT = ':>'
40REL_HAS_SIBLING = ':~'
41REL_HAS_CLOSE_SIBLING = ':+'
43# `bs4` imports this module while it is still initializing,
44# so these must only be evaluated when type checking.
45if TYPE_CHECKING: # pragma: no cover
46 MatchRequest = tuple[bs4.Tag, ct.SelectorList]
47 MatchGenerator = Generator[MatchRequest, bool, bool]
49NS_XHTML = 'http://www.w3.org/1999/xhtml'
50NS_XML = 'http://www.w3.org/XML/1998/namespace'
51NS_SVG = 'http://www.w3.org/2000/svg'
53# Values of `type` that put an `input` in a state that does not use its value for `dir="auto"`.
54# A missing or invalid value is the Text state, which does.
55NON_AUTO_DIR_INPUT_TYPES = frozenset((
56 'date', 'month', 'week', 'time', 'datetime-local', 'number', 'range', 'color', 'checkbox',
57 'radio', 'file', 'image'
58))
60# HTML elements whose text is ignored when resolving `dir="auto"` on an ancestor.
61AUTO_DIR_IGNORED_ELEMENTS = frozenset(('bdi', 'script', 'style', 'textarea'))
63DIR_FLAGS = ct.SEL_DIR_LTR | ct.SEL_DIR_RTL
64RANGES = ct.SEL_IN_RANGE | ct.SEL_OUT_OF_RANGE
66DIR_MAP = {
67 'ltr': ct.SEL_DIR_LTR,
68 'rtl': ct.SEL_DIR_RTL,
69 'auto': 0
70}
72RE_NUM = re.compile(r"^(?P<value>-?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?)\Z")
73RE_TIME = re.compile(r'^(?P<hour>[0-9]{2}):(?P<minutes>[0-9]{2})(?::(?P<seconds>[0-9]{2}(?:\.[0-9]{1,3})?))?\Z')
74RE_MONTH = re.compile(r'^(?P<year>[0-9]{4,})-(?P<month>[0-9]{2})\Z')
75RE_WEEK = re.compile(r'^(?P<year>[0-9]{4,})-W(?P<week>[0-9]{2})\Z')
76RE_DATE = re.compile(r'^(?P<year>[0-9]{4,})-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})\Z')
77RE_DATETIME = re.compile(
78 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})'
79 r'(?::(?P<seconds>[0-9]{2}(?:\.[0-9]{1,3})?))?\Z'
80)
81RE_SIZE = re.compile(r'[ \t\r\n\f]*\+?(?P<value>[0-9]+)')
82# BCP 47 syntax for an extended language range (RFC 4647) and a language tag (RFC 5646), as browsers check it:
83# subtags of 1 to 8 ASCII letters and digits, the first only letters, and `*` subtags only in a range.
84# Subtags are not checked against the registry.
85RE_LANG_RANGE = re.compile(r'(?:[a-zA-Z]{1,8}|\*)(?:-(?:[a-zA-Z0-9]{1,8}|\*))*')
86RE_LANG_TAG = re.compile(r'[a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*')
88MONTHS_30 = (4, 6, 9, 11) # April, June, September, and November
89FEB = 2
90SHORT_MONTH = 30
91LONG_MONTH = 31
92FEB_MONTH = 28
93FEB_LEAP_MONTH = 29
94DAYS_IN_WEEK = 7
97class TreeForms(NamedTuple):
98 """The form information of a tree. Elements are tracked by `id()` as `bs4` hashes them by their content."""
100 # The form owner of each `input` and `button`
101 owners: dict[int, bs4.Tag | None]
102 # The default button of each form
103 defaults: dict[int, bs4.Tag]
104 # The last radio button with a `checked` attribute of each radio button group, by form owner and name
105 checked: dict[tuple[int, str], bs4.Tag]
108class _DocumentNav:
109 """Navigate a Beautiful Soup document."""
111 @classmethod
112 def assert_valid_input(cls, tag: Any) -> None:
113 """Check if valid input tag or document."""
115 # Fail on unexpected types.
116 if not cls.is_tag(tag):
117 raise TypeError(f"Expected a BeautifulSoup 'Tag', but instead received type {type(tag)}")
119 @staticmethod
120 def is_doc(obj: bs4.element.PageElement | None) -> bool:
121 """Is `BeautifulSoup` object."""
122 return isinstance(obj, bs4.BeautifulSoup)
124 @staticmethod
125 def is_tag(obj: bs4.element.PageElement | None) -> bool:
126 """Is tag."""
127 return isinstance(obj, bs4.Tag)
129 @staticmethod
130 def is_declaration(obj: bs4.element.PageElement | None) -> bool: # pragma: no cover
131 """Is declaration."""
132 return isinstance(obj, bs4.Declaration)
134 @staticmethod
135 def is_cdata(obj: bs4.element.PageElement | None) -> bool:
136 """Is CDATA."""
137 return isinstance(obj, bs4.CData)
139 @staticmethod
140 def is_processing_instruction(obj: bs4.element.PageElement | None) -> bool: # pragma: no cover
141 """Is processing instruction."""
142 return isinstance(obj, bs4.ProcessingInstruction)
144 @staticmethod
145 def is_navigable_string(obj: bs4.element.PageElement | None) -> bool:
146 """Is navigable string."""
147 return isinstance(obj, bs4.element.NavigableString)
149 @staticmethod
150 def is_special_string(obj: bs4.element.PageElement | None) -> bool:
151 """Is special string."""
152 return isinstance(obj, (bs4.Comment, bs4.Declaration, bs4.CData, bs4.ProcessingInstruction, bs4.Doctype))
154 @classmethod
155 def is_content_string(cls, obj: bs4.element.PageElement | None) -> bool:
156 """Check if node is content string."""
158 return cls.is_navigable_string(obj) and not cls.is_special_string(obj)
160 @staticmethod
161 def is_xml_tree(el: bs4.Tag | None) -> bool:
162 """Check if element (or document) is from a XML tree."""
164 return el is not None and bool(el._is_xml)
166 def is_template(self, el: bs4.Tag) -> bool:
167 """
168 Check if element is an HTML `template`.
170 The contents of a `template` are not part of the document, so navigation stops at one in both directions.
171 This is called for every element that is navigated, so avoid normalizing names that can't match.
172 """
174 if len(el.name) != 8:
175 return False
177 return bool(
178 ((el.name if self.is_xml_tree(el) else util.lower(el.name)) == 'template') and
179 self.is_html_tag(el) # type: ignore[attr-defined]
180 )
182 def is_root(self, el: bs4.Tag) -> bool:
183 """Return whether element is the root of the tree (which we have already pre-calculated)."""
185 return bool(self.root and self.root is el) # type: ignore[attr-defined]
187 def has_contents(self, el: bs4.Tag | None, template_contents: bool) -> bool:
188 """Check if the contents of an element should be navigated."""
190 return el is not None and (template_contents or not self.is_template(el))
192 def get_contents(
193 self,
194 el: bs4.Tag | None,
195 template_contents: bool = False
196 ) -> Iterator[bs4.element.PageElement]:
197 """Get contents or contents in reverse."""
199 if self.has_contents(el, template_contents):
200 yield from cast('bs4.Tag', el).contents
202 def get_tag_children(
203 self,
204 el: bs4.Tag | None,
205 start: int | None = None,
206 reverse: bool = False
207 ) -> Iterator[bs4.Tag]:
208 """Get tag children."""
210 return self.get_children(el, start, reverse, True)
212 @overload
213 def get_children(
214 self,
215 el: bs4.Tag | None,
216 start: int | None = None,
217 reverse: bool = False,
218 tags: Literal[True] = ...,
219 template_contents: bool = False
220 ) -> Iterator[bs4.Tag]:
221 ...
223 @overload
224 def get_children(
225 self,
226 el: bs4.Tag | None,
227 start: int | None = None,
228 reverse: bool = False,
229 tags: Literal[False] = ...,
230 template_contents: bool = False
231 ) -> Iterator[bs4.element.PageElement]:
232 ...
234 def get_children(
235 self,
236 el: bs4.Tag | None,
237 start: int | None = None,
238 reverse: bool = False,
239 tags: Literal[True] | Literal[False] = False,
240 template_contents: bool = False
241 ) -> Iterator[bs4.element.PageElement]:
242 """Get children."""
244 if el is not None and self.has_contents(el, template_contents):
245 last = len(el.contents) - 1
246 if start is None:
247 index = last if reverse else 0
248 else:
249 index = start
250 end = -1 if reverse else last + 1
251 incr = -1 if reverse else 1
253 if 0 <= index <= last:
254 for i in range(index, end, incr):
255 node = el.contents[i]
256 if not tags or self.is_tag(node):
257 yield node
259 def get_tag_descendants(
260 self,
261 el: bs4.Tag | None,
262 template_contents: bool = False
263 ) -> Iterator[bs4.Tag]:
264 """Specifically get tag descendants."""
266 yield from self.get_descendants( # type: ignore[misc]
267 el, tags=True, template_contents=template_contents
268 )
270 def get_descendants(
271 self,
272 el: bs4.Tag | None,
273 tags: bool = False,
274 template_contents: bool = False
275 ) -> Iterator[bs4.element.PageElement]:
276 """
277 Get descendants.
279 `template_contents` only allows entering `el`, any `template` found within it is still not entered.
280 """
282 if el is not None and self.has_contents(el, template_contents):
283 next_good = None
284 for child in el.descendants:
286 if next_good is not None:
287 if child is not next_good:
288 continue
289 next_good = None
291 if isinstance(child, bs4.Tag):
292 if self.is_template(child):
293 if child.next_sibling is not None:
294 next_good = child.next_sibling
295 else:
296 last_child: bs4.element.PageElement = child
297 while isinstance(last_child, bs4.Tag) and last_child.contents:
298 last_child = last_child.contents[-1]
299 next_good = last_child.next_element
300 yield child
301 if next_good is None:
302 break
303 # Coverage isn't seeing this even though it's executed
304 continue # pragma: no cover
305 yield child
307 elif not tags:
308 yield child
310 def get_parent(
311 self,
312 el: bs4.Tag | None,
313 no_doc: bool = True,
314 no_template: bool = True
315 ) -> bs4.Tag | None:
316 """Get parent."""
318 parent = el.parent if el is not None else None
319 if no_template and parent is not None and self.is_template(parent):
320 parent = None
321 if no_doc and self.is_doc(parent):
322 parent = None
323 return parent
325 @staticmethod
326 def get_tag_name(el: bs4.Tag | None) -> str | None:
327 """Get tag."""
329 return el.name if el is not None else None
331 @staticmethod
332 def get_prefix_name(el: bs4.Tag) -> str | None:
333 """Get prefix."""
335 return el.prefix
337 @staticmethod
338 def get_uri(el: bs4.Tag | None) -> str | None:
339 """Get namespace `URI`."""
341 return el.namespace if el is not None else None
343 @classmethod
344 def get_next_tag(cls, el: bs4.Tag) -> bs4.Tag | None:
345 """Get next sibling tag."""
347 return cls.get_next(el, tags=True) # type: ignore[return-value]
349 @classmethod
350 def get_next(cls, el: bs4.Tag, tags: bool = False) -> bs4.element.PageElement | None:
351 """Get next sibling tag."""
353 sibling = el.next_sibling
354 while tags and not isinstance(sibling, bs4.Tag) and sibling is not None:
355 sibling = sibling.next_sibling
357 if tags and not isinstance(sibling, bs4.Tag):
358 sibling = None
360 return sibling
362 @classmethod
363 def get_previous_tag(cls, el: bs4.Tag, tags: bool = True) -> bs4.Tag | None:
364 """Get previous sibling tag."""
366 return cls.get_previous(el, True) # type: ignore[return-value]
368 @classmethod
369 def get_previous(cls, el: bs4.Tag, tags: bool = False) -> bs4.element.PageElement | None:
370 """Get previous sibling tag."""
372 sibling = el.previous_sibling
373 while tags and not isinstance(sibling, bs4.Tag) and sibling is not None:
374 sibling = sibling.previous_sibling
376 if tags and not isinstance(sibling, bs4.Tag):
377 sibling = None
379 return sibling
381 @staticmethod
382 def has_html_ns(el: bs4.Tag | None) -> bool:
383 """
384 Check if element has an HTML namespace.
386 This is a bit different than whether a element is treated as having an HTML namespace,
387 like we do in the case of `is_html_tag`.
388 """
390 ns = getattr(el, 'namespace') if el is not None else None # noqa: B009
391 return bool(ns and ns == NS_XHTML)
393 @staticmethod
394 def split_namespace(el: bs4.Tag | None, attr_name: str) -> tuple[str | None, str | None]:
395 """Return namespace and attribute name without the prefix."""
397 if el is None: # pragma: no cover
398 return None, None
400 return getattr(attr_name, 'namespace', None), getattr(attr_name, 'name', None)
402 @classmethod
403 def get_attribute_by_name(
404 cls,
405 el: bs4.Tag,
406 name: str,
407 default: str | Sequence[str] | None = None
408 ) -> str | Sequence[str] | None:
409 """Get attribute by name."""
411 value = default
412 if el._is_xml:
413 if name in el.attrs:
414 v = el.attrs[name]
415 value = '' if v is None else v
416 else:
417 for k, v in el.attrs.items():
418 if util.lower(k) == name:
419 value = '' if v is None else v
420 break
421 return value
423 @classmethod
424 def iter_attributes(cls, el: bs4.Tag | None) -> Iterator[tuple[str, str | Sequence[str] | None]]:
425 """Iterate attributes."""
427 if el is not None:
428 for k, v in el.attrs.items():
429 yield k, '' if v is None else v
431 @classmethod
432 def get_classes(cls, el: bs4.Tag) -> Sequence[str]:
433 """Get classes."""
435 classes = cls.get_attribute_by_name(el, 'class', [])
436 if isinstance(classes, str):
437 classes = RE_NOT_WS.findall(classes)
438 return cast(Sequence[str], classes)
440 def get_text(self, el: bs4.Tag) -> str:
441 """Get text."""
443 return ''.join(
444 [
445 node for node in self.get_descendants(el) # type: ignore[misc]
446 if self.is_content_string(node)
447 ]
448 )
450 def get_own_text(self, el: bs4.Tag) -> list[str]:
451 """Get Own Text."""
453 return [
454 node for node in self.get_contents(el) if self.is_content_string(node) # type: ignore[misc]
455 ]
458class Inputs:
459 """Class for parsing and validating input items."""
461 @staticmethod
462 def validate_day(year: int, month: int, day: int) -> bool:
463 """Validate day."""
465 max_days = LONG_MONTH
466 if month == FEB:
467 max_days = FEB_LEAP_MONTH if ((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0) else FEB_MONTH
468 elif month in MONTHS_30:
469 max_days = SHORT_MONTH
470 return 1 <= day <= max_days
472 @staticmethod
473 def validate_week(year: int, week: int) -> bool:
474 """Validate week."""
476 # Validate an ISO week number for `year`.
477 #
478 # Per ISO 8601 rules, the last ISO week of a year is the week
479 # containing Dec 28. Using Dec 28 guarantees we obtain the
480 # correct ISO week-number for the final week of `year`, even in
481 # years where Dec 31 falls in ISO week 01 of the following year.
482 #
483 # Example: if Dec 31 is a Thursday the year's last ISO week will
484 # be week 53; if Dec 31 is a Monday and that week is counted as
485 # week 1 of the next year, Dec 28 still belongs to the final
486 # week of the current ISO year and yields the correct max week.
487 #
488 # `datetime` stops at year 9999, but the calendar repeats every 400
489 # years (a whole number of weeks), so use the matching year from 2000
490 # to 2399, as `calendar.weekday()` does.
491 max_week = datetime(2000 + year % 400, 12, 28).isocalendar()[1]
492 return 1 <= week <= max_week
494 @staticmethod
495 def validate_month(month: int) -> bool:
496 """Validate month."""
498 return 1 <= month <= 12
500 @staticmethod
501 def validate_year(year: int) -> bool:
502 """Validate year."""
504 return 1 <= year
506 @staticmethod
507 def parse_year(year: str) -> int:
508 """
509 Parse year.
511 A year is four or more digits with no upper limit, but `int()` refuses digit strings longer than
512 `sys.get_int_max_str_digits()`, so treat a year that long as invalid.
513 """
515 try:
516 return int(year, 10)
517 except ValueError:
518 return 0
520 @staticmethod
521 def validate_hour(hour: int) -> bool:
522 """Validate hour."""
524 return 0 <= hour <= 23
526 @staticmethod
527 def validate_minutes(minutes: int) -> bool:
528 """Validate minutes."""
530 return 0 <= minutes <= 59
532 @staticmethod
533 def validate_seconds(seconds: float) -> bool:
534 """Validate seconds."""
536 return 0 <= seconds < 60
538 @classmethod
539 def parse_value(cls, itype: str, value: str | None) -> tuple[float, ...] | None:
540 """Parse the input value."""
542 parsed: tuple[float, ...] | None = None
543 if value is None:
544 return value
545 if itype == "date":
546 m = RE_DATE.match(value)
547 if m:
548 year = cls.parse_year(m.group('year'))
549 month = int(m.group('month'), 10)
550 day = int(m.group('day'), 10)
551 if cls.validate_year(year) and cls.validate_month(month) and cls.validate_day(year, month, day):
552 parsed = (year, month, day)
553 elif itype == "month":
554 m = RE_MONTH.match(value)
555 if m:
556 year = cls.parse_year(m.group('year'))
557 month = int(m.group('month'), 10)
558 if cls.validate_year(year) and cls.validate_month(month):
559 parsed = (year, month)
560 elif itype == "week":
561 m = RE_WEEK.match(value)
562 if m:
563 year = cls.parse_year(m.group('year'))
564 week = int(m.group('week'), 10)
565 if cls.validate_year(year) and cls.validate_week(year, week):
566 parsed = (year, week)
567 elif itype == "time":
568 m = RE_TIME.match(value)
569 if m:
570 hour = int(m.group('hour'), 10)
571 minutes = int(m.group('minutes'), 10)
572 seconds = float(m.group('seconds') or 0)
573 if cls.validate_hour(hour) and cls.validate_minutes(minutes) and cls.validate_seconds(seconds):
574 parsed = (hour, minutes, seconds)
575 elif itype == "datetime-local":
576 m = RE_DATETIME.match(value)
577 if m:
578 year = cls.parse_year(m.group('year'))
579 month = int(m.group('month'), 10)
580 day = int(m.group('day'), 10)
581 hour = int(m.group('hour'), 10)
582 minutes = int(m.group('minutes'), 10)
583 seconds = float(m.group('seconds') or 0)
584 if (
585 cls.validate_year(year) and cls.validate_month(month) and cls.validate_day(year, month, day) and
586 cls.validate_hour(hour) and cls.validate_minutes(minutes) and cls.validate_seconds(seconds)
587 ):
588 parsed = (year, month, day, hour, minutes, seconds)
589 elif itype in ("number", "range"):
590 m = RE_NUM.match(value)
591 if m:
592 number = float(m.group('value'))
593 if isfinite(number):
594 parsed = (number,)
595 return parsed
598class CSSMatch(_DocumentNav):
599 """Perform CSS matching."""
601 def __init__(
602 self,
603 selectors: ct.SelectorList,
604 scope: bs4.Tag | None,
605 namespaces: ct.Namespaces | None,
606 flags: int
607 ) -> None:
608 """Initialize."""
610 self.assert_valid_input(scope)
611 self.tag = scope
612 self.cached_meta_lang: list[tuple[str, str]] = []
613 self.cached_forms: dict[int, TreeForms] = {}
614 self.cached_options: dict[int, dict[int, bool]] = {}
615 self.selectors = selectors
616 self.namespaces: ct.Namespaces | dict[str, str] = {} if namespaces is None else namespaces
617 self.flags = flags
618 self.enable_cache = not bool(self.flags & util.NOCACHE)
619 self.nth_cache: dict[Hashable, list[int]] = {}
620 self.sib_cache: dict[Hashable, int] = {}
621 self.anc_cache: dict[tuple[int, int], bool] = {}
622 self.desc_cache: dict[tuple[int, int], bool] = {}
623 self.dir_cache: dict[int, int] = {}
624 self.lang_cache: dict[int, str] = {}
625 self.edit_cache: dict[int, bool] = {}
626 self.tree_cache: dict[int, bs4.Tag] = {}
627 self.select_cache: dict[int, bs4.Tag | None] = {}
628 self.index_cache: dict[int, dict[int, int]] = {}
630 # Find the root element for the whole tree
631 doc = scope
632 parent = self.get_parent(doc, no_template=False)
633 while parent:
634 doc = parent
635 parent = self.get_parent(doc, no_template=False)
636 root: bs4.Tag | None = None
637 if not self.is_doc(doc):
638 root = doc
639 else:
640 for child in self.get_tag_children(doc):
641 root = child
642 break
644 self.root = root
645 self.scope = scope if scope is not doc else root
646 self.has_html_namespace = self.has_html_ns(root)
648 # Parsers either record a namespace for every element or for none. The root
649 # alone can't be expected to be HTML as a non-HTML element may have been detached.
650 self.has_namespaces = bool(self.get_uri(root))
652 # A document can be both XML and HTML (XHTML)
653 self.is_xml = self.is_xml_tree(doc)
654 self.is_html = not self.is_xml or self.has_html_namespace
656 def reset(self) -> None: # pragma: no cover
657 """Reset."""
659 self.nth_cache.clear()
660 self.sib_cache.clear()
661 self.anc_cache.clear()
662 self.desc_cache.clear()
663 self.dir_cache.clear()
664 self.lang_cache.clear()
665 self.edit_cache.clear()
666 self.tree_cache.clear()
667 self.select_cache.clear()
668 self.index_cache.clear()
670 def supports_namespaces(self) -> bool:
671 """Check if namespaces are supported in the HTML type."""
673 return self.is_xml or self.has_namespaces
675 def get_tag_ns(self, el: bs4.Tag | None) -> str:
676 """Get tag namespace."""
678 namespace = ''
679 if el is None: # pragma: no cover
680 return namespace
682 if self.supports_namespaces():
683 ns = self.get_uri(el)
684 if ns:
685 namespace = ns
686 else:
687 namespace = NS_XHTML
688 return namespace
690 def is_html_tag(self, el: bs4.Tag | None) -> bool:
691 """Check if tag is in HTML namespace."""
693 return self.get_tag_ns(el) == NS_XHTML
695 def get_tag(self, el: bs4.Tag | None) -> str | None:
696 """Get tag."""
698 name = self.get_tag_name(el)
699 return util.lower(name) if name is not None and not self.is_xml else name
701 def get_prefix(self, el: bs4.Tag) -> str | None:
702 """Get prefix."""
704 prefix = self.get_prefix_name(el)
705 return util.lower(prefix) if prefix is not None and not self.is_xml else prefix
707 @staticmethod
708 def find_text_dir(text: str) -> int | None:
709 """Get directionality from the first character of the text with a strong direction."""
711 for c in text:
712 direction = bidi.DIRECTIONS[bisect_right(bidi.STARTS, ord(c)) - 1]
713 if direction != 'N':
714 return ct.SEL_DIR_LTR if direction == 'L' else ct.SEL_DIR_RTL
715 return None
717 def find_bidi(self, el: bs4.Tag) -> int | None:
718 """Get directionality from element text."""
720 stack = [self.get_children(el)]
721 while stack:
722 for node in stack[-1]:
724 # Analyze child text nodes
725 if self.is_tag(node):
727 # Avoid analyzing certain elements specified in the specification. This only applies
728 # to HTML elements: the text of others (SVG, MathML, etc.) is analyzed like any other.
729 if self.is_html_tag(node) and (
730 self.get_tag(node) in AUTO_DIR_IGNORED_ELEMENTS or
731 DIR_MAP.get(util.lower(self.get_attribute_by_name(node, 'dir', '')), None) is not None
732 ):
733 continue # pragma: no cover
735 # Check directionality of this node's text, then resume with the remaining siblings.
736 stack.append(self.get_children(node))
737 break
739 # Skip `doctype` comments, etc.
740 if self.is_special_string(node):
741 continue
743 # Analyze text nodes for directionality.
744 direction = self.find_text_dir(cast('bs4.element.NavigableString', node))
745 if direction is not None:
746 return direction
747 else:
748 # All children have been analyzed and direction could not be determined.
749 stack.pop()
750 return None
752 def extended_language_filter(self, lang_range: str, lang_tag: str) -> bool:
753 """
754 Filter the language tags.
756 `lang_tag` is a well-formed language tag, or an empty string if the language is untagged.
757 """
759 # Only an empty range matches an untagged language, and a range that is not well-formed matches nothing.
760 if not lang_range or not lang_tag:
761 return not lang_range and not lang_tag
762 if RE_LANG_RANGE.fullmatch(lang_range) is None:
763 return False
765 match = True
766 # Wildcards after the first subtag are ignored (RFC 4647 section 3.3.2, step 3.A), as subtags in between are
767 # already skipped implicitly. So `en-*` is the same as `en` and `de-*-DE` the same as `de-DE`.
768 first, *rest = lang_range.lower().split('-')
769 ranges = [first, *(r for r in rest if r != '*')]
770 subtags = lang_tag.lower().split('-')
771 length = len(ranges)
772 rindex = 0
773 sindex = 0
774 r = ranges[rindex]
775 s = subtags[sindex]
777 # Primary tag needs to match
778 if r != '*' and r != s:
779 match = False
781 rindex += 1
782 sindex += 1
784 # Match until we run out of ranges
785 while match and rindex < length:
786 r = ranges[rindex]
787 try:
788 s = subtags[sindex]
789 except IndexError:
790 # Ran out of subtags,
791 # but we still have ranges
792 match = False
793 continue
795 # Matched range
796 if s == r:
797 rindex += 1
799 # Implicit wildcard cannot match
800 # singletons
801 elif len(s) == 1:
802 match = False
803 continue
805 # Implicitly matched, so grab next subtag
806 sindex += 1
808 return match
810 def match_attribute_name(
811 self,
812 el: bs4.Tag,
813 attr: str,
814 prefix: str | None
815 ) -> str | Sequence[str] | None:
816 """Match attribute name and return value if it exists."""
818 value = None
819 if self.supports_namespaces():
820 value = None
821 # If we have not defined namespaces, we can't very well find them, so don't bother trying.
822 if prefix:
823 ns = self.namespaces.get(prefix)
824 if ns is None and prefix != '*':
825 return None
826 else:
827 ns = None
829 for k, v in self.iter_attributes(el):
831 # Get attribute parts
832 namespace, name = self.split_namespace(el, k)
834 # Can't match a prefix attribute as we haven't specified one to match
835 # Try to match it normally as a whole `p:a` as selector may be trying `p\:a`.
836 if ns is None:
837 if (self.is_xml and attr == k) or (not self.is_xml and util.lower(attr) == util.lower(k)):
838 value = v
839 break
840 # Coverage is not finding this even though it is executed.
841 # Adding a print statement before this (and erasing coverage) causes coverage to find the line.
842 # Ignore the false positive message.
843 continue # pragma: no cover
845 # We can't match our desired prefix attribute as the attribute doesn't have a prefix
846 if namespace is None or (ns != namespace and prefix != '*'):
847 continue
849 # The attribute doesn't match.
850 if (util.lower(attr) != util.lower(name)) if not self.is_xml else (attr != name):
851 continue
853 value = v
854 break
855 else:
856 for k, v in self.iter_attributes(el):
857 if util.lower(attr) != util.lower(k):
858 continue
859 value = v
860 break
861 return value
863 def match_namespace(self, el: bs4.Tag, tag: ct.SelectorTag) -> bool:
864 """Match the namespace of the element."""
866 match = True
867 namespace = self.get_tag_ns(el)
868 default_namespace = self.namespaces.get('')
869 tag_ns = '' if tag.prefix is None else self.namespaces.get(tag.prefix)
870 # We must match the default namespace if one is not provided
871 if tag.prefix is None and (default_namespace is not None and namespace != default_namespace):
872 match = False
873 # If we specified `|tag`, we must not have a namespace.
874 elif (tag.prefix is not None and tag.prefix == '' and namespace):
875 match = False
876 # Verify prefix matches
877 elif (
878 tag.prefix and
879 tag.prefix != '*' and (tag_ns is None or namespace != tag_ns)
880 ):
881 match = False
882 return match
884 def match_attributes(self, el: bs4.Tag, attributes: tuple[ct.SelectorAttribute, ...]) -> bool:
885 """Match attributes."""
887 match = True
888 if attributes:
889 for a in attributes:
890 temp = self.match_attribute_name(el, a.attribute, a.prefix)
891 # The case insensitive pattern only applies to HTML elements in HTML documents
892 pattern: re.Pattern[str] | None
893 if a.xml_type_pattern and (self.is_xml or not self.is_html_tag(el)):
894 pattern = a.xml_type_pattern
895 ignore_case = False
896 else:
897 pattern = a.pattern
898 ignore_case = a.ignore_case
899 if temp is None and a.inverse:
900 continue
901 if temp is None:
902 match = False
903 break
904 if pattern is None:
905 continue
906 value = temp if isinstance(temp, str) else ' '.join(temp)
907 # A case insensitive pattern is compiled for the value converted to ASCII lowercase.
908 if ignore_case:
909 value = util.ascii_lower(value)
910 if pattern.search(value) is None:
911 match = False
912 break
913 return match
915 def match_tagname(self, el: bs4.Tag, tag: ct.SelectorTag) -> bool:
916 """Match tag name."""
918 name = (util.lower(tag.name) if not self.is_xml and tag.name is not None else tag.name)
919 return not (
920 name is not None and
921 name not in (self.get_tag(el), '*')
922 )
924 def match_tag(self, el: bs4.Tag, tag: ct.SelectorTag | None) -> bool:
925 """Match the tag."""
927 match = True
928 if tag is not None:
929 # Verify namespace
930 if not self.match_tagname(el, tag):
931 match = False
932 if match and not self.match_namespace(el, tag):
933 match = False
934 return match
936 def within(self, target: bs4.Tag, parent: bs4.Tag | bs4.BeautifulSoup, start: int, end: int | None = None) -> bool:
937 """
938 Check if target is within the parent's contents from `start` up to `end`.
940 Searching the contents for every target is quadratic on parents with many children,
941 so cache the index of each child the first time a parent is checked.
942 """
944 key = id(parent)
945 if key not in self.index_cache:
946 self.index_cache[key] = {id(child): i for i, child in enumerate(parent.contents)}
947 index = self.index_cache[key].get(id(target), -1)
948 return start <= index < (end if end is not None else len(parent.contents))
950 def match_general_sibling(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator:
951 """Match general sibling combinator."""
953 found = False
955 if relation[0] is ct.Null: # pragma: no cover
956 return found
958 pkey: int | None = None
959 key: tuple[int, int] | None = None
961 # Get the parent and use the ID for the cache key. Siblings do not need a parent element:
962 # they can be top level elements in the document or the top level contents of a `template`.
963 # Only an element that has been detached has no parent, and it has no siblings.
964 parent = self.get_parent(el, no_doc=False, no_template=False)
965 if parent is None:
966 return found
967 pkey = id(parent)
969 # Check the cache to see if we already know where the first sibling is,
970 # and if we do, check if we are on the correct side of it.
971 # If we've previously searched and found no sibling, there is no sibling.
972 # Lastly, if this is our first time, setup the cache.
973 reverse = relation[0].rel_type == REL_HAS_SIBLING
974 start = len(parent) - 1 if reverse else 0
975 if pkey is not None:
976 key = (pkey, id(relation))
977 if key in self.sib_cache:
978 index = self.sib_cache[key]
979 if index >= 0:
980 a, b = (index + 1, start + 1) if reverse else (start, index)
981 return not self.within(el, parent, a, b)
982 else:
983 return False
984 self.sib_cache[key] = start
986 # Start at the furthest endpoint and walk back towards the element looking for siblings.
987 # The current element counts as a sibling, but will not cause a match.
988 passed = False
989 incr = -1 if reverse else 1
990 for child in self.get_children(parent, start=start, reverse=reverse, template_contents=True):
991 start += incr
992 if not isinstance(child, bs4.Tag):
993 continue
995 # Flag that we are passing the element.
996 # Any siblings we find aren't valid for this element.
997 if child is el:
998 passed = True
1000 # We found the furthest sibling.
1001 if (yield child, relation):
1002 found = True
1003 break
1005 # Cache the index of the sibling or mark as there being no siblings.
1006 if pkey and key:
1007 self.sib_cache[key] = start if found else -1
1009 # If we passed the current element and then found a sibling, it doesn't count as a match.
1010 if passed:
1011 found = False
1013 return found
1015 def match_ancestor(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator:
1016 """
1017 Match descendant combinator.
1019 A selector with several descendant combinators tries every ancestor for each one, which is
1020 a polynomial of the depth with a degree of the number of combinators. Every element visited
1021 on the walk shares the result, so cache whether each one is or has a matching ancestor.
1022 """
1024 found = False
1025 visited: list[tuple[int, int]] = []
1026 parent: bs4.Tag | None = el
1027 while not found and parent and (parent := self.get_parent(parent)):
1028 key = (id(parent), id(relation))
1029 if key in self.anc_cache:
1030 found = self.anc_cache[key]
1031 break
1032 visited.append(key)
1033 found = yield parent, relation
1035 for key in visited:
1036 self.anc_cache[key] = found
1038 return found
1040 def match_past_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator:
1041 """Match past relationship."""
1043 found = False
1044 # I don't think this can ever happen, but it makes `mypy` happy
1045 if relation[0] is ct.Null: # pragma: no cover
1046 return found
1048 if relation[0].rel_type == REL_PARENT:
1049 if self.enable_cache:
1050 found = yield from self.match_ancestor(el, relation)
1051 else:
1052 parent: bs4.Tag | None = el
1053 while not found and parent and (parent := self.get_parent(parent)):
1054 found = parent is not None and (yield parent, relation)
1055 elif relation[0].rel_type == REL_CLOSE_PARENT:
1056 parent = self.get_parent(el)
1057 if parent is None:
1058 pass
1059 elif self.enable_cache:
1060 # Siblings share the parent, so cache whether it matches to avoid evaluating it again for every child.
1061 # This is inlined, instead of going through `match_ancestor`, as an extra generator is costly on
1062 # documents where most parents have a single child and the cache rarely gets a hit.
1063 key = (id(parent), id(relation))
1064 cached = self.anc_cache.get(key)
1065 if cached is None:
1066 cached = self.anc_cache[key] = yield parent, relation
1067 found = cached
1068 else:
1069 found = yield parent, relation
1070 elif relation[0].rel_type == REL_SIBLING:
1071 if self.enable_cache:
1072 found = yield from self.match_general_sibling(el, relation)
1073 else:
1074 sibling: bs4.Tag | None = el
1075 while not found and sibling and (sibling := self.get_previous_tag(sibling)):
1076 found = sibling is not None and (yield sibling, relation)
1077 elif relation[0].rel_type == REL_CLOSE_SIBLING:
1078 sibling = self.get_previous_tag(el)
1079 found = sibling is not None and (yield sibling, relation)
1080 return found
1082 def match_future_child(
1083 self,
1084 parent: bs4.Tag,
1085 relation: ct.SelectorList,
1086 recursive: bool = False
1087 ) -> MatchGenerator:
1088 """Match future child."""
1090 match = False
1091 if recursive:
1092 children: Callable[..., Iterator[bs4.Tag]] = self.get_tag_descendants
1093 else:
1094 children = self.get_tag_children
1095 for child in children(parent):
1096 if (yield child, relation):
1097 match = True
1098 break
1099 return match
1101 def match_descendant(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator:
1102 """
1103 Match descendant relation of `:has()`.
1105 Every element matched by `:has()` searches all of its descendants, which is quadratic in the depth
1106 of the document. Every element visited on the search shares the result, so cache whether each one
1107 is or has a matching descendant: an element searched without a match is cached along with all of
1108 its descendants, and when there is a match, the element and all of those it is nested in are cached.
1109 """
1111 found = False
1112 # Elements whose contents are being searched, outermost first, and elements searched without a match
1113 searching: list[tuple[int, int]] = []
1114 searched: list[tuple[int, int]] = []
1115 stack = [self.get_tag_children(el)]
1116 while stack and not found:
1117 for child in stack[-1]:
1118 key = (id(child), id(relation))
1119 if key in self.desc_cache:
1120 if self.desc_cache[key]:
1121 found = True
1122 break
1123 continue
1124 searching.append(key)
1125 if (yield child, relation):
1126 found = True
1127 else:
1128 stack.append(self.get_tag_children(child))
1129 break
1130 else:
1131 stack.pop()
1132 if searching:
1133 searched.append(searching.pop())
1135 for key in searching:
1136 self.desc_cache[key] = True
1137 for key in searched:
1138 self.desc_cache[key] = False
1140 return found
1142 def match_future_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator:
1143 """Match future relationship."""
1145 found = False
1146 # I don't think this can ever happen, but it makes `mypy` happy
1147 if relation[0] is ct.Null: # pragma: no cover
1148 return found
1150 if relation[0].rel_type == REL_HAS_PARENT:
1151 if self.enable_cache:
1152 found = yield from self.match_descendant(el, relation)
1153 else:
1154 found = yield from self.match_future_child(el, relation, True)
1155 elif relation[0].rel_type == REL_HAS_CLOSE_PARENT:
1156 found = yield from self.match_future_child(el, relation)
1157 elif relation[0].rel_type == REL_HAS_SIBLING:
1158 if self.enable_cache:
1159 found = yield from self.match_general_sibling(el, relation)
1160 else:
1161 sibling: bs4.Tag | None = el
1162 while not found and sibling and (sibling := self.get_next_tag(sibling)):
1163 found = yield sibling, relation
1164 elif relation[0].rel_type == REL_HAS_CLOSE_SIBLING:
1165 sibling = self.get_next_tag(el)
1166 found = sibling is not None and (yield sibling, relation)
1167 return found
1169 def match_relations(self, el: bs4.Tag, relation: ct.SelectorList) -> MatchGenerator:
1170 """Match relationship to other elements."""
1172 sel = relation[0]
1173 if sel is not ct.Null and sel.rel_type is not None and sel.rel_type.startswith(':'):
1174 return self.match_future_relations(el, relation)
1175 return self.match_past_relations(el, relation)
1177 def match_id(self, el: bs4.Tag, ids: tuple[str, ...]) -> bool:
1178 """Match element's ID."""
1180 found = True
1181 for i in ids:
1182 if i != self.get_attribute_by_name(el, 'id', ''):
1183 found = False
1184 break
1185 return found
1187 def match_classes(self, el: bs4.Tag, classes: tuple[str, ...]) -> bool:
1188 """Match element's classes."""
1190 current_classes = self.get_classes(el)
1191 found = True
1192 for c in classes:
1193 if c not in current_classes:
1194 found = False
1195 break
1196 return found
1198 def match_root(self, el: bs4.Tag) -> bool:
1199 """Match element as root."""
1201 is_root = self.is_root(el)
1202 if is_root:
1203 sibling: Any = self.get_previous(el)
1204 while is_root and sibling is not None:
1205 if (
1206 self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or
1207 self.is_cdata(sibling)
1208 ):
1209 is_root = False
1210 else:
1211 sibling = self.get_previous(sibling)
1212 if is_root:
1213 sibling = self.get_next(el)
1214 while is_root and sibling is not None:
1215 if (
1216 self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or
1217 self.is_cdata(sibling)
1218 ):
1219 is_root = False
1220 else:
1221 sibling = self.get_next(sibling)
1222 return is_root
1224 def match_scope(self, el: bs4.Tag) -> bool:
1225 """Match element as scope."""
1227 return self.scope is el
1229 def match_nth_tag_type(self, el: bs4.Tag, child: bs4.Tag) -> bool:
1230 """Match tag type for `nth` matches."""
1232 return (
1233 (self.get_tag(child) == self.get_tag(el)) and
1234 (self.get_tag_ns(child) == self.get_tag_ns(el))
1235 )
1237 def match_nth(self, el: bs4.Tag, nth: tuple[ct.SelectorNth, ...]) -> MatchGenerator:
1238 """Match `nth` elements."""
1240 key: tuple[int, int, str | None, str | None] | None = None
1241 start = rindex = 0
1242 incr = rincr = 0
1244 # Get the parent and use the ID for the cache key. The top level
1245 # contents of a `template` have no parent element, but they do have siblings.
1246 parent = self.get_parent(el, no_doc=False, no_template=False)
1247 pkey = id(parent) if self.enable_cache and parent else None
1249 # Test element against the `nth` selectors.
1250 matched = True
1251 for n in nth:
1252 matched = False
1253 last = n.last
1254 key = None
1256 # Prepare the child iterator and get the starting, real index and the relative index
1257 if pkey is not None and parent:
1258 # Get last info from the cache
1259 if pkey:
1260 key = (
1261 (pkey, id(n), self.get_tag(el), self.get_tag_ns(el))
1262 if n.of_type
1263 else (pkey, id(n), None, None)
1264 )
1266 valid = False
1267 if key and key in self.nth_cache:
1268 start, rindex = self.nth_cache[key]
1269 if self.within(el, parent, start):
1270 last = False
1271 rincr = -1 if n.last else 1
1272 valid = True
1274 # Start/overwrite the cache if the cache was empty or invalid
1275 if not valid:
1276 start, rindex = len(parent) - 1 if last else 0, 0
1277 self.nth_cache[key] = [start, rindex]
1278 rincr = 1
1280 incr = 1 if not last else -1
1281 children = self.get_children(parent, start=start, reverse=last, template_contents=True)
1283 # Non-cached handling of parented element
1284 elif parent:
1285 rindex = 0
1286 start = len(parent) - 1 if last else 0
1287 rincr = incr = 1
1288 children = self.get_children(parent, start=start, reverse=last, template_contents=True)
1290 # No parent, just evaluate the element against the selectors
1291 else:
1292 start = rindex = 0
1293 rincr = incr = 1
1294 children = iter([el])
1296 # Find index of element compared to its siblings and check the index conditions
1297 child: bs4.Tag
1298 for child in children:
1299 start += incr
1301 # We only care about tags
1302 if not self.is_tag(child):
1303 continue
1305 # Handle `of S` in `nth-child` and handle `of-type`
1306 if (
1307 (n.selectors and not (yield child, n.selectors)) or
1308 (n.of_type and not self.match_nth_tag_type(el, child))
1309 ):
1310 if child is el:
1311 # Cached "last" lookups count down from `rindex`, so it must be the
1312 # position `el` would have had, even though `el` did not match.
1313 if last:
1314 rindex += 1
1315 break
1316 continue
1318 # Test the relative index against the `nth` requirement.
1319 rindex += rincr
1320 if child is el:
1321 if n.a != 0:
1322 v = (rindex - n.b) / n.a
1323 matched = v.is_integer() and v >= 0
1324 else:
1325 matched = rindex == n.b and n.b >= 1
1326 break
1328 # "Last index" selectors evaluate first from the bottom and then evaluate
1329 # from the first found element top-down. Start will be incremented in the
1330 # wrong direction, so increment it and step over the current index.
1331 if last:
1332 start += 2
1334 # Update the cache
1335 if key:
1336 self.nth_cache[key] = [start, rindex]
1338 # If we failed to match any `nth` selectors, quit.
1339 if not matched:
1340 break
1342 return matched
1344 def match_empty(self, el: bs4.Tag) -> bool:
1345 """Check if element is empty (if requested)."""
1347 is_empty = True
1348 child: Any
1349 for child in self.get_children(el):
1350 if self.is_tag(child):
1351 is_empty = False
1352 break
1353 elif self.is_content_string(child) and RE_NOT_EMPTY.search(child):
1354 is_empty = False
1355 break
1356 return is_empty
1358 def match_subselectors(self, el: bs4.Tag, selectors: tuple[ct.SelectorList, ...]) -> MatchGenerator:
1359 """Match selectors."""
1361 match = True
1362 for sel in selectors:
1363 if not (yield el, sel):
1364 match = False
1365 return match
1367 def match_contains(self, el: bs4.Tag, contains: tuple[ct.SelectorContains, ...]) -> bool:
1368 """Match element if it contains text."""
1370 match = True
1371 content: str | None = None
1372 own_content: Sequence[str] | None = None
1373 for contain_list in contains:
1374 found = False
1375 for text in contain_list.text:
1376 if contain_list.own:
1377 if own_content is None:
1378 own_content = self.get_own_text(el)
1379 for c in own_content:
1380 if text in c:
1381 found = True
1382 break
1383 if found:
1384 break
1385 else:
1386 if content is None:
1387 content = self.get_text(el)
1388 if text in content:
1389 found = True
1390 break
1391 if not found:
1392 match = False
1393 return match
1395 def get_tree(self, el: bs4.Tag) -> bs4.Tag:
1396 """Get the top of the tree that an element is in: its document or `template`."""
1398 # Every element visited shares the top of the tree, so cache it to avoid walking the same ancestors again.
1399 visited: list[int] = []
1400 top = el
1401 parent = self.get_parent(top, no_doc=False, no_template=False)
1402 while parent is not None:
1403 key = id(top)
1404 if key in self.tree_cache:
1405 top = self.tree_cache[key]
1406 break
1407 visited.append(key)
1408 top = parent
1409 if self.is_template(top):
1410 break
1411 parent = self.get_parent(top, no_doc=False, no_template=False)
1412 if self.enable_cache:
1413 for key in visited:
1414 self.tree_cache[key] = top
1415 return top
1417 def is_submit_button(self, el: bs4.Tag) -> bool:
1418 """Check if an `input` or `button` is a submit button."""
1420 v = util.lower(cast(str, self.get_attribute_by_name(el, 'type', '')))
1421 if self.get_tag(el) == 'input':
1422 return v in ('submit', 'image')
1424 # A button with a missing or invalid `type` submits, unless it invokes a command or is
1425 # a child of a `select`.
1426 return v == 'submit' or (
1427 v not in ('button', 'reset') and
1428 self.get_attribute_by_name(el, 'command') is None and
1429 self.get_attribute_by_name(el, 'commandfor') is None and
1430 self.get_tag(self.get_parent(el)) != 'select'
1431 )
1433 def get_forms(self, tree: bs4.Tag) -> TreeForms:
1434 """
1435 Get the form information of a tree.
1437 A form owner is the form that the `form` attribute refers to, if there is one, otherwise the nearest
1438 ancestor form. The attribute is only used by elements connected to a document, which the contents of
1439 a `template` are not. A `template` holds its own tree, so it is not entered.
1441 This requires a pass through the whole tree as the `form` attribute can refer to any form, so it is
1442 cached instead of searching the tree again for every element.
1444 A form in a form only exists in XML and in a `template`. Anywhere else, it can only have come from a
1445 parser that doesn't follow the HTML specification. A browser's parser would have ignored the nested
1446 form and taken its end tag for that of the parent, so what it holds belongs to the parent, and
1447 what follows it does not.
1448 """
1450 key = id(tree)
1451 forms = self.cached_forms.get(key)
1452 if forms is not None:
1453 return forms
1455 ids: dict[str, bs4.Tag] = {}
1456 controls: list[tuple[bs4.Tag, bs4.Tag | None]] = []
1457 closed: set[int] = set()
1458 nesting = self.is_xml or self.is_template(tree)
1459 stack: list[tuple[bs4.Tag | None, bs4.Tag | None]] = [
1460 (el, None) for el in self.get_children(tree, reverse=True, tags=True, template_contents=True)
1461 ]
1462 while stack:
1463 el, form = stack.pop()
1465 # The end of a nested form, which ends the form it is in
1466 if el is None:
1467 closed.add(id(form))
1468 continue
1470 if id(form) in closed:
1471 form = None
1472 name = self.get_tag(el) if self.is_html_tag(el) else None
1473 if name == 'form' and form is not None and not nesting:
1474 stack.append((None, form))
1475 else:
1476 el_id = cast(str, self.get_attribute_by_name(el, 'id', ''))
1477 if el_id:
1478 ids.setdefault(el_id, el)
1479 if name == 'form':
1480 form = el
1481 elif name in ('input', 'button'):
1482 controls.append((el, form))
1483 stack.extend((child, form) for child in self.get_tag_children(el, reverse=True))
1485 forms = TreeForms({}, {}, {})
1486 connected = not self.is_template(tree)
1487 for el, form in controls:
1488 form_id = self.get_attribute_by_name(el, 'form')
1489 if connected and form_id is not None:
1490 form = ids.get(cast(str, form_id))
1491 if form is not None and not (self.get_tag(form) == 'form' and self.is_html_tag(form)):
1492 form = None
1493 forms.owners[id(el)] = form
1495 # Controls are in tree order, so the first submit button of a form is its default button,
1496 # and the last radio button with `checked` in a radio button group is the one left checked.
1497 radio_name = cast(str, self.get_attribute_by_name(el, 'name', ''))
1498 if form is not None and self.is_submit_button(el):
1499 forms.defaults.setdefault(id(form), el)
1500 elif (
1501 radio_name and
1502 self.get_attribute_by_name(el, 'checked') is not None and
1503 util.lower(cast(str, self.get_attribute_by_name(el, 'type', ''))) == 'radio'
1504 ):
1505 forms.checked[(id(form), radio_name)] = el
1507 self.cached_forms[key] = forms
1508 return forms
1510 def match_default(self, el: bs4.Tag) -> bool:
1511 """Match the default button of a form, which is the first submit button in the tree that it owns."""
1513 forms = self.get_forms(self.get_tree(el))
1514 form = forms.owners.get(id(el))
1515 return form is not None and forms.defaults.get(id(form)) is el
1517 def get_radio_group(self, el: bs4.Tag, forms: TreeForms) -> tuple[int, str]:
1518 """Get the key of the radio button group of a radio button with a name."""
1520 return id(forms.owners.get(id(el))), cast(str, self.get_attribute_by_name(el, 'name'))
1522 def match_indeterminate(self, el: bs4.Tag) -> bool:
1523 """Match a radio button in a radio button group that has no checked radio buttons."""
1525 forms = self.get_forms(self.get_tree(el))
1526 return self.get_radio_group(el, forms) not in forms.checked
1528 def match_checked_radio(self, el: bs4.Tag) -> bool:
1529 """
1530 Match a radio button with a `checked` attribute if it is still checked.
1532 A radio button that is checked unchecks the others in its group when it is added to a document, so
1533 only the last one is left checked. The contents of a `template` are never added to a document,
1534 so all of them stay checked.
1535 """
1537 if not self.get_attribute_by_name(el, 'name'):
1538 return True
1540 tree = self.get_tree(el)
1541 if self.is_template(tree):
1542 return True
1544 forms = self.get_forms(tree)
1545 return forms.checked.get(self.get_radio_group(el, forms)) is el
1547 def get_options(self, select: bs4.Tag) -> dict[int, bool]:
1548 """
1549 Get the selectedness of each option in the list of options of a `select`.
1551 An option that is not in a `select` with `multiple` is left selected if it is the last one with
1552 a `selected` attribute, and if there are none, the first enabled option is selected when
1553 the `select` shows a single option.
1555 Other than in XML, a nested `option`, `optgroup`, or `select` can only have come from a parser that
1556 doesn't follow the HTML specification. A browser's parser would have ended the first one, so
1557 an `option` or `optgroup` is treated as a sibling, and a `select` ends the list of options.
1558 """
1560 key = id(select)
1561 selectedness = self.cached_options.get(key)
1562 if selectedness is not None:
1563 return selectedness
1565 options: list[tuple[bs4.Tag, bool]] = []
1566 stack: list[tuple[bs4.Tag, bool | None]] = [
1567 (el, None) for el in self.get_tag_children(select, reverse=True)
1568 ]
1569 while stack:
1570 el, group = stack.pop()
1571 name = self.get_tag(el) if self.is_html_tag(el) else None
1572 if name == 'select' and not self.is_xml:
1573 break
1574 if name == 'option':
1575 options.append((el, bool(group)))
1576 if name in ('select', 'hr', 'datalist') or (
1577 self.is_xml and (name == 'option' or (name == 'optgroup' and group is not None))
1578 ):
1579 continue
1580 if name == 'optgroup':
1581 group = self.get_attribute_by_name(el, 'disabled') is not None
1582 stack.extend((child, group) for child in self.get_tag_children(el, reverse=True))
1584 marked = [el for el, _ in options if self.get_attribute_by_name(el, 'selected') is not None]
1585 if self.get_attribute_by_name(select, 'multiple') is None:
1586 size = RE_SIZE.match(cast(str, self.get_attribute_by_name(select, 'size', '')))
1587 if marked:
1588 marked = marked[-1:]
1589 elif size is None or size.group('value').lstrip('0') in ('', '1'):
1590 marked = [
1591 el for el, disabled in options
1592 if not disabled and self.get_attribute_by_name(el, 'disabled') is None
1593 ][:1]
1595 selected = {id(el) for el in marked}
1596 selectedness = {id(el): id(el) in selected for el, _ in options}
1597 self.cached_options[key] = selectedness
1598 return selectedness
1600 def match_checked_option(self, el: bs4.Tag) -> bool:
1601 """Match a selected option. Without a `select`, an option is only selected by its `selected` attribute."""
1603 # Every element visited shares the `select`, so cache it to avoid walking the same ancestors again.
1604 visited: list[int] = []
1605 node = el
1606 while True:
1607 key = id(node)
1608 if key in self.select_cache:
1609 parent = self.select_cache[key]
1610 break
1611 visited.append(key)
1612 parent = self.get_parent(node)
1613 if parent is None or (self.get_tag(parent) == 'select' and self.is_html_tag(parent)):
1614 break
1615 node = parent
1616 if self.enable_cache:
1617 for key in visited:
1618 self.select_cache[key] = parent
1620 selected = self.get_options(parent).get(id(el)) if parent is not None else None
1621 return self.get_attribute_by_name(el, 'selected') is not None if selected is None else selected
1623 def match_lang(self, el: bs4.Tag, langs: tuple[ct.SelectorLang, ...]) -> bool:
1624 """Match languages."""
1626 # Every element visited while resolving shares the language,
1627 # so cache it to avoid walking the same ancestors again for each element.
1628 visited: list[int] = []
1629 found_lang = self.resolve_lang(el, visited)
1630 if self.enable_cache:
1631 for key in visited:
1632 self.lang_cache[key] = found_lang
1634 # A language tag that is not well-formed matches nothing, not even `:lang('')` or `:lang('*')`.
1635 if found_lang and RE_LANG_TAG.fullmatch(found_lang) is None:
1636 return False
1638 match = False
1639 for patterns in langs:
1640 match = False
1641 for pattern in patterns:
1642 if self.extended_language_filter(pattern, found_lang):
1643 match = True
1644 if not match:
1645 break
1647 return match
1649 def resolve_lang(self, el: bs4.Tag, visited: list[int]) -> str:
1650 """Resolve the language of an element, which is an empty string if it is unknown."""
1652 has_ns = self.supports_namespaces()
1653 root = self.root
1654 has_html_namespace = self.has_html_namespace
1656 # Walk parents looking for `lang` (HTML) or `xml:lang` XML property.
1657 parent: bs4.Tag | None = el
1658 found_lang = None
1659 last = None
1660 while found_lang is None:
1661 key = id(parent)
1662 if key in self.lang_cache:
1663 return self.lang_cache[key]
1664 visited.append(key)
1665 has_html_ns = self.has_html_ns(parent)
1666 # SVG elements also use a `lang` attribute in no namespace, but `xml:lang` takes precedence.
1667 has_svg_ns = has_ns and getattr(parent, 'namespace', None) == NS_SVG
1668 svg_lang = None
1669 for k, v in self.iter_attributes(parent):
1670 attr_ns, attr = self.split_namespace(parent, k)
1671 if (
1672 ((not has_ns or has_html_ns) and (util.lower(k) if not self.is_xml else k) == 'lang') or
1673 (
1674 has_ns and not has_html_ns and attr_ns == NS_XML and
1675 (util.lower(attr) if not self.is_xml and attr is not None else attr) == 'lang'
1676 )
1677 ):
1678 found_lang = v
1679 break
1680 if has_svg_ns and attr_ns is None and (util.lower(k) if not self.is_xml else k) == 'lang':
1681 svg_lang = v
1682 if found_lang is None:
1683 found_lang = svg_lang
1684 last = parent
1685 parent = self.get_parent(parent)
1687 if parent is None:
1688 root = last
1689 has_html_namespace = self.has_html_ns(root)
1690 parent = last
1691 break
1693 # Use cached meta language.
1694 if found_lang is None and self.cached_meta_lang:
1695 for cache in self.cached_meta_lang:
1696 if root is not None and cast(str, root) is cache[0]:
1697 return cache[1]
1699 # If we couldn't find a language, and the document is HTML, look to meta to determine language.
1700 if found_lang is None and (not self.is_xml or (has_html_namespace and root and root.name == 'html')):
1701 # Find head
1702 found = False
1703 for tag in ('html', 'head'):
1704 found = False
1705 for child in self.get_tag_children(parent):
1706 if self.get_tag(child) == tag and self.is_html_tag(child):
1707 found = True
1708 parent = child
1709 break
1710 if found: # pragma: no cover
1711 break
1713 # Search meta tags
1714 if found and parent is not None:
1715 for child2 in parent:
1716 if isinstance(child2, bs4.Tag) and self.get_tag(child2) == 'meta' and self.is_html_tag(parent):
1717 c_lang = False
1718 content = None
1719 for k, v in self.iter_attributes(child2):
1720 if util.lower(k) == 'http-equiv' and util.lower(v) == 'content-language':
1721 c_lang = True
1722 if util.lower(k) == 'content':
1723 content = v
1724 if c_lang and content:
1725 found_lang = content
1726 self.cached_meta_lang.append((cast(str, root), cast(str, found_lang)))
1727 break
1728 if found_lang is not None:
1729 break
1730 if found_lang is None:
1731 self.cached_meta_lang.append((cast(str, root), ''))
1733 # Without any language information, the language is unknown, which is an empty language tag.
1734 return '' if found_lang is None else cast(str, found_lang)
1736 def match_dir(self, el: bs4.Tag, directionality: int) -> bool:
1737 """Check directionality."""
1739 # Direction is only supported in HTML documents.
1740 if not self.is_html:
1741 return False
1743 # If we have to match both left and right, we can't match either.
1744 if directionality & ct.SEL_DIR_LTR and directionality & ct.SEL_DIR_RTL:
1745 return False
1747 # Every element visited while resolving shares the resolved direction,
1748 # so cache it to avoid walking the same ancestors again for each element.
1749 visited: list[int] = []
1750 direction = self.resolve_dir(el, visited)
1751 if self.enable_cache:
1752 for key in visited:
1753 self.dir_cache[key] = direction
1754 return direction == directionality
1756 def resolve_dir(self, el: bs4.Tag, visited: list[int]) -> int:
1757 """Resolve the direction of an element."""
1759 while True:
1760 key = id(el)
1761 if key in self.dir_cache:
1762 return self.dir_cache[key]
1763 visited.append(key)
1765 # `dir` is only defined for HTML elements, so other elements (SVG, MathML, etc.)
1766 # always inherit the direction of their parent.
1767 if self.is_html_tag(el):
1769 # Element has defined direction of left to right or right to left
1770 direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(el, 'dir', '')), None)
1771 if direction not in (None, 0):
1772 return direction
1774 # If `input[type=telephone]` and no direction is assigned, assume left to right.
1775 name = self.get_tag(el)
1776 is_input = name == 'input'
1777 is_textarea = name == 'textarea'
1778 is_bdi = name == 'bdi'
1779 itype = util.lower(self.get_attribute_by_name(el, 'type', '')) if is_input else ''
1780 if is_input and itype == 'tel' and direction is None:
1781 return ct.SEL_DIR_LTR
1783 # Auto handling for inputs whose value is used
1784 if ((is_input and itype not in NON_AUTO_DIR_INPUT_TYPES) or is_textarea) and direction == 0:
1785 if is_textarea:
1786 value = ''.join(
1787 node for node in self.get_contents(el) # type: ignore[misc]
1788 if self.is_content_string(node)
1789 )
1790 else:
1791 value = cast(str, self.get_attribute_by_name(el, 'value', ''))
1792 direction = self.find_text_dir(value)
1793 # No strong directional characters (or no value), assume left to right
1794 return direction if direction is not None else ct.SEL_DIR_LTR
1796 # Auto handling for `bdi` and other non text inputs.
1797 if (is_bdi and direction is None) or direction == 0:
1798 direction = self.find_bidi(el)
1799 if direction is not None:
1800 return direction
1801 # Auto directionality could not be determined, assume left to right
1802 return ct.SEL_DIR_LTR
1804 # Match parents direction. There may be no parent element, and not just for the root:
1805 # a fragment can have many top level elements, and a `template` has its own contents.
1806 # Without one, assume left to right.
1807 parent = self.get_parent(el)
1808 if parent is None:
1809 return ct.SEL_DIR_LTR
1810 el = parent
1812 def match_range(self, el: bs4.Tag, condition: int) -> bool:
1813 """
1814 Match range.
1816 Behavior is modeled after what we see in browsers. Browsers seem to evaluate
1817 if the value is out of range, and if not, it is in range. So a missing value
1818 will not evaluate out of range; therefore, value is in range. Personally, I
1819 feel like this should evaluate as neither in or out of range.
1820 """
1822 out_of_range = False
1824 itype = util.lower(cast(str, self.get_attribute_by_name(el, 'type', '')))
1825 mn = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'min', None)))
1826 mx = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'max', None)))
1828 # There is no valid min or max, so we cannot evaluate a range
1829 if mn is None and mx is None:
1830 return False
1832 value = Inputs.parse_value(itype, cast(str, self.get_attribute_by_name(el, 'value', None)))
1833 if value is not None:
1834 if itype in ("date", "datetime-local", "month", "week", "number", "range"):
1835 if mn is not None and value < mn:
1836 out_of_range = True
1837 if not out_of_range and mx is not None and value > mx:
1838 out_of_range = True
1839 elif itype == "time":
1840 if mn is not None and mx is not None and mn > mx:
1841 # Time is periodic, so this is a reversed/discontinuous range
1842 if value < mn and value > mx:
1843 out_of_range = True
1844 else:
1845 if mn is not None and value < mn:
1846 out_of_range = True
1847 if not out_of_range and mx is not None and value > mx:
1848 out_of_range = True
1850 return not out_of_range if condition & ct.SEL_IN_RANGE else out_of_range
1852 def match_defined(self, el: bs4.Tag) -> bool:
1853 """
1854 Match defined.
1856 `:defined` is related to custom elements in a browser.
1858 - If the document is XML (not XHTML), no tags will match.
1859 - Tags that are not custom (don't have a hyphen) are marked defined.
1860 - If the tag has a prefix (without or without a namespace), it will not match.
1861 - HTML elements with an `is` attribute (customized built-in elements) are created as undefined,
1862 like custom tags, so they don't match.
1864 This is of course requires the parser to provide us with the proper prefix and namespace info,
1865 if it doesn't, there is nothing we can do.
1866 """
1868 name = self.get_tag(el)
1869 return (
1870 self.is_html and name is not None and (
1871 (name.find('-') == -1 and not self.is_customized_builtin(el)) or
1872 name.find(':') != -1 or
1873 self.get_prefix(el) is not None
1874 )
1875 )
1877 def is_customized_builtin(self, el: bs4.Tag) -> bool:
1878 """Check if an HTML element has an `is` attribute, which makes it a customized built-in element."""
1880 return self.is_html_tag(el) and self.get_attribute_by_name(el, 'is') is not None
1882 def match_placeholder_shown(self, el: bs4.Tag) -> bool:
1883 """
1884 Match placeholder shown according to HTML spec.
1886 - text area should be checked if they have content. A single newline does not count as content.
1888 """
1890 match = False
1891 content = self.get_text(el)
1892 if content in ('', '\n'):
1893 match = True
1895 return match
1897 def match_editable(self, el: bs4.Tag) -> bool:
1898 """
1899 Match editing hosts and the elements that are editable inside them.
1901 The nearest `contenteditable` attribute with a valid state decides: an empty value, `true` and
1902 `plaintext-only` make the element editable, and `false` does not. Any other value inherits
1903 the state of the parent. The attribute only applies to HTML elements, others (SVG, MathML,
1904 etc.) always inherit the state of their parent.
1905 """
1907 # Every element visited shares the state, so cache it to avoid walking the same ancestors again.
1908 visited: list[int] = []
1909 editable = False
1910 node = el # type: bs4.Tag | None
1911 while node is not None:
1912 key = id(node)
1913 if key in self.edit_cache:
1914 editable = self.edit_cache[key]
1915 break
1916 visited.append(key)
1917 state = self.get_attribute_by_name(node, 'contenteditable') if self.is_html_tag(node) else None
1918 if isinstance(state, str):
1919 state = util.lower(state)
1920 if state in ('', 'true', 'plaintext-only'):
1921 editable = True
1922 break
1923 if state == 'false':
1924 break
1925 node = self.get_parent(node)
1926 if self.enable_cache:
1927 for key in visited:
1928 self.edit_cache[key] = editable
1929 return editable
1931 def match_selectors(self, el: bs4.Tag, selectors: ct.SelectorList) -> bool:
1932 """
1933 Check if element matches one of the selectors.
1935 Since some selectors can have complex relationships, they may need to recursively
1936 check if another relative element matches specific selectors. To avoid recursion,
1937 generators are yielded instead whenever it needs to check if a relative element
1938 satisfies specific selectors. These can be compared and the result sent back.
1939 We can check the end result of the generator once it is satisfied.
1940 """
1942 # Most matches never need a nested match, so avoid setting up the stack unless needed.
1943 # `frame` yields a `bool` when it is done.
1944 frame = self._match_selectors(el, selectors)
1945 item = next(frame)
1946 if item.__class__ is bool:
1947 return item
1949 # We need to setup a stack as a selector has sent back a match request.
1950 stack = [frame, self._match_selectors(*cast('MatchRequest', item))]
1951 result: bool | None = None
1952 while True:
1953 item = stack[-1].send(result) # type: ignore[arg-type]
1954 if item.__class__ is bool:
1955 stack.pop()
1956 if not stack:
1957 return item
1958 result = item
1959 else:
1960 stack.append(self._match_selectors(*cast('MatchRequest', item)))
1961 result = None
1963 def _match_selectors(self, el: bs4.Tag, selectors: ct.SelectorList) -> Generator[MatchRequest | bool, bool, None]:
1964 """
1965 Check if element matches one of the selectors, yielding requests to match nested selectors.
1967 Yields `MatchRequests` or affirmation as to whether a selector was matched. The match request
1968 requires a boolean to be sent back to communicate whether the match was successful.
1969 """
1971 match = False
1972 is_not = selectors.is_not
1973 is_html = selectors.is_html
1975 # Internal selector lists that use the HTML flag, will automatically get the `html` namespace.
1976 if is_html:
1977 namespaces = self.namespaces
1978 self.namespaces = {'html': NS_XHTML}
1980 if not is_html or self.is_html:
1981 for selector in selectors:
1982 match = is_not
1983 # We have a un-matchable situation (like `:focus` as you can focus an element in this environment)
1984 if selector is ct.Null:
1985 continue
1986 # Verify tag matches
1987 if not self.match_tag(el, selector.tag):
1988 continue
1989 # Verify tag is defined
1990 if selector.flags & ct.SEL_DEFINED and not self.match_defined(el):
1991 continue
1992 # Verify element is root
1993 if selector.flags & ct.SEL_ROOT and not self.match_root(el):
1994 continue
1995 # Verify element is scope
1996 if selector.flags & ct.SEL_SCOPE and not self.match_scope(el):
1997 continue
1998 # Verify element has placeholder shown
1999 if selector.flags & ct.SEL_PLACEHOLDER_SHOWN and not self.match_placeholder_shown(el):
2000 continue
2001 # Verify element is an editing host or editable
2002 if selector.flags & ct.SEL_EDITABLE and not self.match_editable(el):
2003 continue
2004 # Verify `nth` matches
2005 if selector.nth and not (yield from self.match_nth(el, selector.nth)):
2006 continue
2007 if selector.flags & ct.SEL_EMPTY and not self.match_empty(el):
2008 continue
2009 # Verify id matches
2010 if selector.ids and not self.match_id(el, selector.ids):
2011 continue
2012 # Verify classes match
2013 if selector.classes and not self.match_classes(el, selector.classes):
2014 continue
2015 # Verify attribute(s) match
2016 if not self.match_attributes(el, selector.attributes):
2017 continue
2018 # Verify ranges
2019 if selector.flags & RANGES and not self.match_range(el, selector.flags & RANGES):
2020 continue
2021 # Verify language patterns
2022 if selector.lang and not self.match_lang(el, selector.lang):
2023 continue
2024 # Verify pseudo selector patterns
2025 if selector.selectors and not (yield from self.match_subselectors(el, selector.selectors)):
2026 continue
2027 # Verify relationship selectors
2028 if selector.relation and not (yield from self.match_relations(el, selector.relation)):
2029 continue
2030 # Validate that the current default selector match corresponds to the first submit button in the form
2031 if selector.flags & ct.SEL_DEFAULT and not self.match_default(el):
2032 continue
2033 # Validate that the unset radio button is among radio buttons with the same name in a form that are
2034 # also not set.
2035 if selector.flags & ct.SEL_INDETERMINATE and not self.match_indeterminate(el):
2036 continue
2037 # Validate that the radio button is left checked in its group or that the option is selected.
2038 if selector.flags & ct.SEL_CHECKED and not (
2039 self.match_checked_option(el) if self.get_tag(el) == 'option' else self.match_checked_radio(el)
2040 ):
2041 continue
2042 # Validate element directionality
2043 if selector.flags & DIR_FLAGS and not self.match_dir(el, selector.flags & DIR_FLAGS):
2044 continue
2045 # Validate that the tag contains the specified text.
2046 if selector.contains and not self.match_contains(el, selector.contains):
2047 continue
2048 match = not is_not
2049 break
2051 # Restore actual namespaces being used for external selector lists
2052 if is_html:
2053 self.namespaces = namespaces
2055 yield match
2057 def select(self, limit: int = 0) -> Iterator[bs4.Tag]:
2058 """Match all tags under the targeted tag."""
2060 lim = None if limit < 1 else limit
2062 # Selecting from a `template` is how its contents are reached, but only for that `template`.
2063 for child in self.get_tag_descendants(self.tag, template_contents=True):
2064 if self.match(child):
2065 yield child
2066 if lim is not None:
2067 lim -= 1
2068 if lim < 1:
2069 break
2071 def closest(self) -> bs4.Tag | None:
2072 """Match closest ancestor."""
2074 current: bs4.Tag | None = self.tag
2075 closest = None
2076 while closest is None and current is not None:
2077 if self.match(current):
2078 closest = current
2079 else:
2080 current = self.get_parent(current)
2081 return closest
2083 def filter(self) -> list[bs4.Tag]: # noqa A001
2084 """Filter tag's children."""
2086 return [
2087 tag for tag in self.get_contents(self.tag, template_contents=True)
2088 if isinstance(tag, bs4.Tag) and self.match(tag)
2089 ]
2091 def match(self, el: bs4.Tag) -> bool:
2092 """Match."""
2094 return not self.is_doc(el) and self.is_tag(el) and self.match_selectors(el, self.selectors)
2097class SoupSieve(ct.Immutable):
2098 """Compiled Soup Sieve selector matching object."""
2100 pattern: str
2101 selectors: ct.SelectorList
2102 namespaces: ct.Namespaces | None
2103 custom: dict[str, str]
2104 flags: int
2106 __slots__ = ("pattern", "selectors", "namespaces", "custom", "flags", "_hash")
2108 def __init__(
2109 self,
2110 pattern: str,
2111 selectors: ct.SelectorList,
2112 namespaces: ct.Namespaces | None,
2113 custom: ct.CustomSelectors | None,
2114 flags: int
2115 ):
2116 """Initialize."""
2118 super().__init__(
2119 pattern=pattern,
2120 selectors=selectors,
2121 namespaces=namespaces,
2122 custom=custom,
2123 flags=flags
2124 )
2126 def match(self, tag: bs4.Tag) -> bool:
2127 """Match."""
2129 return CSSMatch(self.selectors, tag, self.namespaces, self.flags).match(tag)
2131 def closest(self, tag: bs4.Tag) -> bs4.Tag | None:
2132 """Match closest ancestor."""
2134 return CSSMatch(self.selectors, tag, self.namespaces, self.flags).closest()
2136 def filter(self, iterable: Iterable[bs4.Tag]) -> list[bs4.Tag]: # noqa A001
2137 """
2138 Filter.
2140 `CSSMatch` can cache certain searches for tags of the same document,
2141 so if we are given a tag, all tags are from the same document,
2142 and we can take advantage of the optimization.
2144 Any other kind of iterable could have tags from different documents or detached tags,
2145 so for those, we use a new `CSSMatch` for each item in the iterable.
2146 """
2148 if isinstance(iterable, bs4.Tag):
2149 return CSSMatch(self.selectors, iterable, self.namespaces, self.flags).filter()
2150 else:
2151 # There is no guarantee that elements are from the same document, evaluate them separately.
2152 return [node for node in iterable if not CSSMatch.is_navigable_string(node) and self.match(node)]
2154 def select_one(self, tag: bs4.Tag) -> bs4.Tag | None:
2155 """Select a single tag."""
2157 tags = self.select(tag, limit=1)
2158 return tags[0] if tags else None
2160 def select(self, tag: bs4.Tag, limit: int = 0) -> list[bs4.Tag]:
2161 """Select the specified tags."""
2163 return list(self.iselect(tag, limit))
2165 def iselect(self, tag: bs4.Tag, limit: int = 0) -> Iterator[bs4.Tag]:
2166 """Iterate the specified tags."""
2168 yield from CSSMatch(self.selectors, tag, self.namespaces, self.flags).select(limit)
2170 def __repr__(self) -> str: # pragma: no cover
2171 """Representation."""
2173 return (
2174 f"SoupSieve(pattern={self.pattern!r}, namespaces={self.namespaces!r}, "
2175 f"custom={self.custom!r}, flags={self.flags!r})"
2176 )
2178 __str__ = __repr__
2181ct.pickle_register(SoupSieve)