mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-15 05:23:59 +01:00
update Soupsieve
This commit is contained in:
+121
-142
@@ -1,4 +1,5 @@
|
||||
"""CSS selector parser."""
|
||||
from __future__ import annotations
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from . import util
|
||||
@@ -6,7 +7,7 @@ from . import css_match as cm
|
||||
from . import css_types as ct
|
||||
from .util import SelectorSyntaxError
|
||||
import warnings
|
||||
from typing import Optional, Dict, Match, Tuple, Type, Any, List, Union, Iterator, cast
|
||||
from typing import Match, Any, Iterator, cast
|
||||
|
||||
UNICODE_REPLACEMENT_CHAR = 0xFFFD
|
||||
|
||||
@@ -91,94 +92,81 @@ PSEUDO_SUPPORTED = PSEUDO_SIMPLE | PSEUDO_SIMPLE_NO_MATCH | PSEUDO_COMPLEX | PSE
|
||||
# Sub-patterns parts
|
||||
# Whitespace
|
||||
NEWLINE = r'(?:\r\n|(?!\r\n)[\n\f\r])'
|
||||
WS = r'(?:[ \t]|{})'.format(NEWLINE)
|
||||
WS = fr'(?:[ \t]|{NEWLINE})'
|
||||
# Comments
|
||||
COMMENTS = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
|
||||
# Whitespace with comments included
|
||||
WSC = r'(?:{ws}|{comments})'.format(ws=WS, comments=COMMENTS)
|
||||
WSC = fr'(?:{WS}|{COMMENTS})'
|
||||
# CSS escapes
|
||||
CSS_ESCAPES = r'(?:\\(?:[a-f0-9]{{1,6}}{ws}?|[^\r\n\f]|$))'.format(ws=WS)
|
||||
CSS_STRING_ESCAPES = r'(?:\\(?:[a-f0-9]{{1,6}}{ws}?|[^\r\n\f]|$|{nl}))'.format(ws=WS, nl=NEWLINE)
|
||||
CSS_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}{WS}?|[^\r\n\f]|$))'
|
||||
CSS_STRING_ESCAPES = fr'(?:\\(?:[a-f0-9]{{1,6}}{WS}?|[^\r\n\f]|$|{NEWLINE}))'
|
||||
# CSS Identifier
|
||||
IDENTIFIER = r'''
|
||||
(?:(?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{esc})+|--)
|
||||
(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{esc})*)
|
||||
'''.format(esc=CSS_ESCAPES)
|
||||
IDENTIFIER = fr'''
|
||||
(?:(?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})+|--)
|
||||
(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})*)
|
||||
'''
|
||||
# `nth` content
|
||||
NTH = r'(?:[-+])?(?:[0-9]+n?|n)(?:(?<=n){ws}*(?:[-+]){ws}*(?:[0-9]+))?'.format(ws=WSC)
|
||||
NTH = fr'(?:[-+])?(?:[0-9]+n?|n)(?:(?<=n){WSC}*(?:[-+]){WSC}*(?:[0-9]+))?'
|
||||
# Value: quoted string or identifier
|
||||
VALUE = r'''
|
||||
(?:"(?:\\(?:.|{nl})|[^\\"\r\n\f]+)*?"|'(?:\\(?:.|{nl})|[^\\'\r\n\f]+)*?'|{ident}+)
|
||||
'''.format(nl=NEWLINE, ident=IDENTIFIER)
|
||||
VALUE = fr'''(?:"(?:\\(?:.|{NEWLINE})|[^\\"\r\n\f]+)*?"|'(?:\\(?:.|{NEWLINE})|[^\\'\r\n\f]+)*?'|{IDENTIFIER}+)'''
|
||||
# Attribute value comparison. `!=` is handled special as it is non-standard.
|
||||
ATTR = r'''
|
||||
(?:{ws}*(?P<cmp>[!~^|*$]?=){ws}*(?P<value>{value})(?:{ws}+(?P<case>[is]))?)?{ws}*\]
|
||||
'''.format(ws=WSC, value=VALUE)
|
||||
ATTR = fr'(?:{WSC}*(?P<cmp>[!~^|*$]?=){WSC}*(?P<value>{VALUE})(?:{WSC}*(?P<case>[is]))?)?{WSC}*\]'
|
||||
|
||||
# Selector patterns
|
||||
# IDs (`#id`)
|
||||
PAT_ID = r'\#{ident}'.format(ident=IDENTIFIER)
|
||||
PAT_ID = fr'\#{IDENTIFIER}'
|
||||
# Classes (`.class`)
|
||||
PAT_CLASS = r'\.{ident}'.format(ident=IDENTIFIER)
|
||||
PAT_CLASS = fr'\.{IDENTIFIER}'
|
||||
# Prefix:Tag (`prefix|tag`)
|
||||
PAT_TAG = r'(?P<tag_ns>(?:{ident}|\*)?\|)?(?P<tag_name>{ident}|\*)'.format(ident=IDENTIFIER)
|
||||
PAT_TAG = fr'(?P<tag_ns>(?:{IDENTIFIER}|\*)?\|)?(?P<tag_name>{IDENTIFIER}|\*)'
|
||||
# Attributes (`[attr]`, `[attr=value]`, etc.)
|
||||
PAT_ATTR = r'''
|
||||
\[{ws}*(?P<attr_ns>(?:{ident}|\*)?\|)?(?P<attr_name>{ident}){attr}
|
||||
'''.format(ws=WSC, ident=IDENTIFIER, attr=ATTR)
|
||||
PAT_ATTR = fr'\[{WSC}*(?P<attr_ns>(?:{IDENTIFIER}|\*)?\|)?(?P<attr_name>{IDENTIFIER}){ATTR}'
|
||||
# Pseudo class (`:pseudo-class`, `:pseudo-class(`)
|
||||
PAT_PSEUDO_CLASS = r'(?P<name>:{ident})(?P<open>\({ws}*)?'.format(ws=WSC, ident=IDENTIFIER)
|
||||
PAT_PSEUDO_CLASS = fr'(?P<name>:{IDENTIFIER})(?P<open>\({WSC}*)?'
|
||||
# Pseudo class special patterns. Matches `:pseudo-class(` for special case pseudo classes.
|
||||
PAT_PSEUDO_CLASS_SPECIAL = r'(?P<name>:{ident})(?P<open>\({ws}*)'.format(ws=WSC, ident=IDENTIFIER)
|
||||
PAT_PSEUDO_CLASS_SPECIAL = fr'(?P<name>:{IDENTIFIER})(?P<open>\({WSC}*)'
|
||||
# Custom pseudo class (`:--custom-pseudo`)
|
||||
PAT_PSEUDO_CLASS_CUSTOM = r'(?P<name>:(?=--){ident})'.format(ident=IDENTIFIER)
|
||||
PAT_PSEUDO_CLASS_CUSTOM = fr'(?P<name>:(?=--){IDENTIFIER})'
|
||||
# Nesting ampersand selector. Matches `&`
|
||||
PAT_AMP = r'&'
|
||||
# Closing pseudo group (`)`)
|
||||
PAT_PSEUDO_CLOSE = r'{ws}*\)'.format(ws=WSC)
|
||||
PAT_PSEUDO_CLOSE = fr'{WSC}*\)'
|
||||
# Pseudo element (`::pseudo-element`)
|
||||
PAT_PSEUDO_ELEMENT = r':{}'.format(PAT_PSEUDO_CLASS)
|
||||
PAT_PSEUDO_ELEMENT = fr':{PAT_PSEUDO_CLASS}'
|
||||
# At rule (`@page`, etc.) (not supported)
|
||||
PAT_AT_RULE = r'@P{ident}'.format(ident=IDENTIFIER)
|
||||
PAT_AT_RULE = fr'@P{IDENTIFIER}'
|
||||
# Pseudo class `nth-child` (`:nth-child(an+b [of S]?)`, `:first-child`, etc.)
|
||||
PAT_PSEUDO_NTH_CHILD = r'''
|
||||
(?P<pseudo_nth_child>{name}
|
||||
(?P<nth_child>{nth}|even|odd))(?:{wsc}*\)|(?P<of>{comments}*{ws}{wsc}*of{comments}*{ws}{wsc}*))
|
||||
'''.format(name=PAT_PSEUDO_CLASS_SPECIAL, wsc=WSC, comments=COMMENTS, ws=WS, nth=NTH)
|
||||
PAT_PSEUDO_NTH_CHILD = fr'''
|
||||
(?P<pseudo_nth_child>{PAT_PSEUDO_CLASS_SPECIAL}
|
||||
(?P<nth_child>{NTH}|even|odd))(?:{WSC}*\)|(?P<of>{COMMENTS}*{WS}{WSC}*of{COMMENTS}*{WS}{WSC}*))
|
||||
'''
|
||||
# Pseudo class `nth-of-type` (`:nth-of-type(an+b)`, `:first-of-type`, etc.)
|
||||
PAT_PSEUDO_NTH_TYPE = r'''
|
||||
(?P<pseudo_nth_type>{name}
|
||||
(?P<nth_type>{nth}|even|odd)){ws}*\)
|
||||
'''.format(name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, nth=NTH)
|
||||
PAT_PSEUDO_NTH_TYPE = fr'''
|
||||
(?P<pseudo_nth_type>{PAT_PSEUDO_CLASS_SPECIAL}
|
||||
(?P<nth_type>{NTH}|even|odd)){WSC}*\)
|
||||
'''
|
||||
# Pseudo class language (`:lang("*-de", en)`)
|
||||
PAT_PSEUDO_LANG = r'{name}(?P<values>{value}(?:{ws}*,{ws}*{value})*){ws}*\)'.format(
|
||||
name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, value=VALUE
|
||||
)
|
||||
PAT_PSEUDO_LANG = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<values>{VALUE}(?:{WSC}*,{WSC}*{VALUE})*){WSC}*\)'
|
||||
# Pseudo class direction (`:dir(ltr)`)
|
||||
PAT_PSEUDO_DIR = r'{name}(?P<dir>ltr|rtl){ws}*\)'.format(name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC)
|
||||
PAT_PSEUDO_DIR = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<dir>ltr|rtl){WSC}*\)'
|
||||
# Combining characters (`>`, `~`, ` `, `+`, `,`)
|
||||
PAT_COMBINE = r'{wsc}*?(?P<relation>[,+>~]|{ws}(?![,+>~])){wsc}*'.format(ws=WS, wsc=WSC)
|
||||
PAT_COMBINE = fr'{WSC}*?(?P<relation>[,+>~]|{WS}(?![,+>~])){WSC}*'
|
||||
# Extra: Contains (`:contains(text)`)
|
||||
PAT_PSEUDO_CONTAINS = r'{name}(?P<values>{value}(?:{ws}*,{ws}*{value})*){ws}*\)'.format(
|
||||
name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, value=VALUE
|
||||
)
|
||||
PAT_PSEUDO_CONTAINS = fr'{PAT_PSEUDO_CLASS_SPECIAL}(?P<values>{VALUE}(?:{WSC}*,{WSC}*{VALUE})*){WSC}*\)'
|
||||
|
||||
# Regular expressions
|
||||
# CSS escape pattern
|
||||
RE_CSS_ESC = re.compile(r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\[^\r\n\f])|(\\$))'.format(ws=WSC), re.I)
|
||||
RE_CSS_STR_ESC = re.compile(
|
||||
r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\[^\r\n\f])|(\\$)|(\\{nl}))'.format(ws=WS, nl=NEWLINE), re.I
|
||||
)
|
||||
RE_CSS_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}{WSC}?)|(\\[^\r\n\f])|(\\$))', re.I)
|
||||
RE_CSS_STR_ESC = re.compile(fr'(?:(\\[a-f0-9]{{1,6}}{WS}?)|(\\[^\r\n\f])|(\\$)|(\\{NEWLINE}))', re.I)
|
||||
# Pattern to break up `nth` specifiers
|
||||
RE_NTH = re.compile(
|
||||
r'(?P<s1>[-+])?(?P<a>[0-9]+n?|n)(?:(?<=n){ws}*(?P<s2>[-+]){ws}*(?P<b>[0-9]+))?'.format(ws=WSC),
|
||||
re.I
|
||||
)
|
||||
RE_NTH = re.compile(fr'(?P<s1>[-+])?(?P<a>[0-9]+n?|n)(?:(?<=n){WSC}*(?P<s2>[-+]){WSC}*(?P<b>[0-9]+))?', re.I)
|
||||
# Pattern to iterate multiple values.
|
||||
RE_VALUES = re.compile(r'(?:(?P<value>{value})|(?P<split>{ws}*,{ws}*))'.format(ws=WSC, value=VALUE), re.X)
|
||||
RE_VALUES = re.compile(fr'(?:(?P<value>{VALUE})|(?P<split>{WSC}*,{WSC}*))', re.X)
|
||||
# Whitespace checks
|
||||
RE_WS = re.compile(WS)
|
||||
RE_WS_BEGIN = re.compile('^{}*'.format(WSC))
|
||||
RE_WS_END = re.compile('{}*$'.format(WSC))
|
||||
RE_CUSTOM = re.compile(r'^{}$'.format(PAT_PSEUDO_CLASS_CUSTOM), re.X)
|
||||
RE_WS_BEGIN = re.compile(fr'^{WSC}*')
|
||||
RE_WS_END = re.compile(fr'{WSC}*$')
|
||||
RE_CUSTOM = re.compile(fr'^{PAT_PSEUDO_CLASS_CUSTOM}$', re.X)
|
||||
|
||||
# Constants
|
||||
# List split token
|
||||
@@ -206,8 +194,8 @@ _MAXCACHE = 500
|
||||
@lru_cache(maxsize=_MAXCACHE)
|
||||
def _cached_css_compile(
|
||||
pattern: str,
|
||||
namespaces: Optional[ct.Namespaces],
|
||||
custom: Optional[ct.CustomSelectors],
|
||||
namespaces: ct.Namespaces | None,
|
||||
custom: ct.CustomSelectors | None,
|
||||
flags: int
|
||||
) -> cm.SoupSieve:
|
||||
"""Cached CSS compile."""
|
||||
@@ -232,7 +220,7 @@ def _purge_cache() -> None:
|
||||
_cached_css_compile.cache_clear()
|
||||
|
||||
|
||||
def process_custom(custom: Optional[ct.CustomSelectors]) -> Dict[str, Union[str, ct.SelectorList]]:
|
||||
def process_custom(custom: ct.CustomSelectors | None) -> dict[str, str | ct.SelectorList]:
|
||||
"""Process custom."""
|
||||
|
||||
custom_selectors = {}
|
||||
@@ -240,9 +228,9 @@ def process_custom(custom: Optional[ct.CustomSelectors]) -> Dict[str, Union[str,
|
||||
for key, value in custom.items():
|
||||
name = util.lower(key)
|
||||
if RE_CUSTOM.match(name) is None:
|
||||
raise SelectorSyntaxError("The name '{}' is not a valid custom pseudo-class name".format(name))
|
||||
raise SelectorSyntaxError(f"The name '{name}' is not a valid custom pseudo-class name")
|
||||
if name in custom_selectors:
|
||||
raise KeyError("The custom selector '{}' has already been registered".format(name))
|
||||
raise KeyError(f"The custom selector '{name}' has already been registered")
|
||||
custom_selectors[css_unescape(name)] = value
|
||||
return custom_selectors
|
||||
|
||||
@@ -282,23 +270,23 @@ def escape(ident: str) -> str:
|
||||
start_dash = length > 0 and ident[0] == '-'
|
||||
if length == 1 and start_dash:
|
||||
# Need to escape identifier that is a single `-` with no other characters
|
||||
string.append('\\{}'.format(ident))
|
||||
string.append(f'\\{ident}')
|
||||
else:
|
||||
for index, c in enumerate(ident):
|
||||
codepoint = ord(c)
|
||||
if codepoint == 0x00:
|
||||
string.append('\ufffd')
|
||||
elif (0x01 <= codepoint <= 0x1F) or codepoint == 0x7F:
|
||||
string.append('\\{:x} '.format(codepoint))
|
||||
string.append(f'\\{codepoint:x} ')
|
||||
elif (index == 0 or (start_dash and index == 1)) and (0x30 <= codepoint <= 0x39):
|
||||
string.append('\\{:x} '.format(codepoint))
|
||||
string.append(f'\\{codepoint:x} ')
|
||||
elif (
|
||||
codepoint in (0x2D, 0x5F) or codepoint >= 0x80 or (0x30 <= codepoint <= 0x39) or
|
||||
(0x30 <= codepoint <= 0x39) or (0x41 <= codepoint <= 0x5A) or (0x61 <= codepoint <= 0x7A)
|
||||
):
|
||||
string.append(c)
|
||||
else:
|
||||
string.append('\\{}'.format(c))
|
||||
string.append(f'\\{c}')
|
||||
return ''.join(string)
|
||||
|
||||
|
||||
@@ -316,7 +304,7 @@ class SelectorPattern:
|
||||
|
||||
return self.name
|
||||
|
||||
def match(self, selector: str, index: int, flags: int) -> Optional[Match[str]]:
|
||||
def match(self, selector: str, index: int, flags: int) -> Match[str] | None:
|
||||
"""Match the selector."""
|
||||
|
||||
return self.re_pattern.match(selector, index)
|
||||
@@ -325,7 +313,7 @@ class SelectorPattern:
|
||||
class SpecialPseudoPattern(SelectorPattern):
|
||||
"""Selector pattern."""
|
||||
|
||||
def __init__(self, patterns: Tuple[Tuple[str, Tuple[str, ...], str, Type[SelectorPattern]], ...]) -> None:
|
||||
def __init__(self, patterns: tuple[tuple[str, tuple[str, ...], str, type[SelectorPattern]], ...]) -> None:
|
||||
"""Initialize."""
|
||||
|
||||
self.patterns = {}
|
||||
@@ -335,7 +323,7 @@ class SpecialPseudoPattern(SelectorPattern):
|
||||
for pseudo in p[1]:
|
||||
self.patterns[pseudo] = pattern
|
||||
|
||||
self.matched_name = None # type: Optional[SelectorPattern]
|
||||
self.matched_name = None # type: SelectorPattern | None
|
||||
self.re_pseudo_name = re.compile(PAT_PSEUDO_CLASS_SPECIAL, re.I | re.X | re.U)
|
||||
|
||||
def get_name(self) -> str:
|
||||
@@ -343,7 +331,7 @@ class SpecialPseudoPattern(SelectorPattern):
|
||||
|
||||
return '' if self.matched_name is None else self.matched_name.get_name()
|
||||
|
||||
def match(self, selector: str, index: int, flags: int) -> Optional[Match[str]]:
|
||||
def match(self, selector: str, index: int, flags: int) -> Match[str] | None:
|
||||
"""Match the selector."""
|
||||
|
||||
pseudo = None
|
||||
@@ -371,20 +359,20 @@ class _Selector:
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
"""Initialize."""
|
||||
|
||||
self.tag = kwargs.get('tag', None) # type: Optional[ct.SelectorTag]
|
||||
self.ids = kwargs.get('ids', []) # type: List[str]
|
||||
self.classes = kwargs.get('classes', []) # type: List[str]
|
||||
self.attributes = kwargs.get('attributes', []) # type: List[ct.SelectorAttribute]
|
||||
self.nth = kwargs.get('nth', []) # type: List[ct.SelectorNth]
|
||||
self.selectors = kwargs.get('selectors', []) # type: List[ct.SelectorList]
|
||||
self.relations = kwargs.get('relations', []) # type: List[_Selector]
|
||||
self.rel_type = kwargs.get('rel_type', None) # type: Optional[str]
|
||||
self.contains = kwargs.get('contains', []) # type: List[ct.SelectorContains]
|
||||
self.lang = kwargs.get('lang', []) # type: List[ct.SelectorLang]
|
||||
self.tag = kwargs.get('tag', None) # type: ct.SelectorTag | None
|
||||
self.ids = kwargs.get('ids', []) # type: list[str]
|
||||
self.classes = kwargs.get('classes', []) # type: list[str]
|
||||
self.attributes = kwargs.get('attributes', []) # type: list[ct.SelectorAttribute]
|
||||
self.nth = kwargs.get('nth', []) # type: list[ct.SelectorNth]
|
||||
self.selectors = kwargs.get('selectors', []) # type: list[ct.SelectorList]
|
||||
self.relations = kwargs.get('relations', []) # type: list[_Selector]
|
||||
self.rel_type = kwargs.get('rel_type', None) # type: str | None
|
||||
self.contains = kwargs.get('contains', []) # type: list[ct.SelectorContains]
|
||||
self.lang = kwargs.get('lang', []) # type: list[ct.SelectorLang]
|
||||
self.flags = kwargs.get('flags', 0) # type: int
|
||||
self.no_match = kwargs.get('no_match', False) # type: bool
|
||||
|
||||
def _freeze_relations(self, relations: List['_Selector']) -> ct.SelectorList:
|
||||
def _freeze_relations(self, relations: list[_Selector]) -> ct.SelectorList:
|
||||
"""Freeze relation."""
|
||||
|
||||
if relations:
|
||||
@@ -394,7 +382,7 @@ class _Selector:
|
||||
else:
|
||||
return ct.SelectorList()
|
||||
|
||||
def freeze(self) -> Union[ct.Selector, ct.SelectorNull]:
|
||||
def freeze(self) -> ct.Selector | ct.SelectorNull:
|
||||
"""Freeze self."""
|
||||
|
||||
if self.no_match:
|
||||
@@ -418,11 +406,10 @@ class _Selector:
|
||||
"""String representation."""
|
||||
|
||||
return (
|
||||
'_Selector(tag={!r}, ids={!r}, classes={!r}, attributes={!r}, nth={!r}, selectors={!r}, '
|
||||
'relations={!r}, rel_type={!r}, contains={!r}, lang={!r}, flags={!r}, no_match={!r})'
|
||||
).format(
|
||||
self.tag, self.ids, self.classes, self.attributes, self.nth, self.selectors,
|
||||
self.relations, self.rel_type, self.contains, self.lang, self.flags, self.no_match
|
||||
f'_Selector(tag={self.tag!r}, ids={self.ids!r}, classes={self.classes!r}, attributes={self.attributes!r}, '
|
||||
f'nth={self.nth!r}, selectors={self.selectors!r}, relations={self.relations!r}, '
|
||||
f'rel_type={self.rel_type!r}, contains={self.contains!r}, lang={self.lang!r}, flags={self.flags!r}, '
|
||||
f'no_match={self.no_match!r})'
|
||||
)
|
||||
|
||||
__repr__ = __str__
|
||||
@@ -450,6 +437,7 @@ class CSSParser:
|
||||
SelectorPattern("pseudo_class_custom", PAT_PSEUDO_CLASS_CUSTOM),
|
||||
SelectorPattern("pseudo_class", PAT_PSEUDO_CLASS),
|
||||
SelectorPattern("pseudo_element", PAT_PSEUDO_ELEMENT),
|
||||
SelectorPattern("amp", PAT_AMP),
|
||||
SelectorPattern("at_rule", PAT_AT_RULE),
|
||||
SelectorPattern("id", PAT_ID),
|
||||
SelectorPattern("class", PAT_CLASS),
|
||||
@@ -461,7 +449,7 @@ class CSSParser:
|
||||
def __init__(
|
||||
self,
|
||||
selector: str,
|
||||
custom: Optional[Dict[str, Union[str, ct.SelectorList]]] = None,
|
||||
custom: dict[str, str | ct.SelectorList] | None = None,
|
||||
flags: int = 0
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
@@ -562,7 +550,7 @@ class CSSParser:
|
||||
selector = self.custom.get(pseudo)
|
||||
if selector is None:
|
||||
raise SelectorSyntaxError(
|
||||
"Undefined custom selector '{}' found at postion {}".format(pseudo, m.end(0)),
|
||||
f"Undefined custom selector '{pseudo}' found at position {m.end(0)}",
|
||||
self.pattern,
|
||||
m.end(0)
|
||||
)
|
||||
@@ -583,9 +571,9 @@ class CSSParser:
|
||||
sel: _Selector,
|
||||
m: Match[str],
|
||||
has_selector: bool,
|
||||
iselector: Iterator[Tuple[str, Match[str]]],
|
||||
iselector: Iterator[tuple[str, Match[str]]],
|
||||
is_html: bool
|
||||
) -> Tuple[bool, bool]:
|
||||
) -> tuple[bool, bool]:
|
||||
"""Parse pseudo class."""
|
||||
|
||||
complex_pseudo = False
|
||||
@@ -662,13 +650,16 @@ class CSSParser:
|
||||
has_selector = True
|
||||
elif pseudo in PSEUDO_SUPPORTED:
|
||||
raise SelectorSyntaxError(
|
||||
"Invalid syntax for pseudo class '{}'".format(pseudo),
|
||||
f"Invalid syntax for pseudo class '{pseudo}'",
|
||||
self.pattern,
|
||||
m.start(0)
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"'{}' pseudo-class is not implemented at this time".format(pseudo)
|
||||
raise SelectorSyntaxError(
|
||||
f"'{pseudo}' was detected as a pseudo-class and is either unsupported or invalid. "
|
||||
"If the syntax was not intended to be recognized as a pseudo-class, please escape the colon.",
|
||||
self.pattern,
|
||||
m.start(0)
|
||||
)
|
||||
|
||||
return has_selector, is_html
|
||||
@@ -678,7 +669,7 @@ class CSSParser:
|
||||
sel: _Selector,
|
||||
m: Match[str],
|
||||
has_selector: bool,
|
||||
iselector: Iterator[Tuple[str, Match[str]]]
|
||||
iselector: Iterator[tuple[str, Match[str]]]
|
||||
) -> bool:
|
||||
"""Parse `nth` pseudo."""
|
||||
|
||||
@@ -743,7 +734,7 @@ class CSSParser:
|
||||
sel: _Selector,
|
||||
name: str,
|
||||
has_selector: bool,
|
||||
iselector: Iterator[Tuple[str, Match[str]]],
|
||||
iselector: Iterator[tuple[str, Match[str]]],
|
||||
index: int
|
||||
) -> bool:
|
||||
"""Parse pseudo with opening bracket."""
|
||||
@@ -752,7 +743,7 @@ class CSSParser:
|
||||
if name == ':not':
|
||||
flags |= FLG_NOT
|
||||
elif name == ':has':
|
||||
flags |= FLG_RELATIVE | FLG_FORGIVE
|
||||
flags |= FLG_RELATIVE
|
||||
elif name in (':where', ':is'):
|
||||
flags |= FLG_FORGIVE
|
||||
|
||||
@@ -766,21 +757,16 @@ class CSSParser:
|
||||
sel: _Selector,
|
||||
m: Match[str],
|
||||
has_selector: bool,
|
||||
selectors: List[_Selector],
|
||||
selectors: list[_Selector],
|
||||
rel_type: str,
|
||||
index: int
|
||||
) -> Tuple[bool, _Selector, str]:
|
||||
) -> tuple[bool, _Selector, str]:
|
||||
"""Parse combinator tokens."""
|
||||
|
||||
combinator = m.group('relation').strip()
|
||||
if not combinator:
|
||||
combinator = WS_COMBINATOR
|
||||
if combinator == COMMA_COMBINATOR:
|
||||
if not has_selector:
|
||||
# If we've not captured any selector parts, the comma is either at the beginning of the pattern
|
||||
# or following another comma, both of which are unexpected. But shouldn't fail the pseudo-class.
|
||||
sel.no_match = True
|
||||
|
||||
sel.rel_type = rel_type
|
||||
selectors[-1].relations.append(sel)
|
||||
rel_type = ":" + WS_COMBINATOR
|
||||
@@ -797,7 +783,7 @@ class CSSParser:
|
||||
# multiple non-whitespace combinators. So if the current combinator is not a whitespace,
|
||||
# then we've hit the multiple combinator case, so we should fail.
|
||||
raise SelectorSyntaxError(
|
||||
'The multiple combinators at position {}'.format(index),
|
||||
f'The multiple combinators at position {index}',
|
||||
self.pattern,
|
||||
index
|
||||
)
|
||||
@@ -814,12 +800,12 @@ class CSSParser:
|
||||
sel: _Selector,
|
||||
m: Match[str],
|
||||
has_selector: bool,
|
||||
selectors: List[_Selector],
|
||||
relations: List[_Selector],
|
||||
selectors: list[_Selector],
|
||||
relations: list[_Selector],
|
||||
is_pseudo: bool,
|
||||
is_forgive: bool,
|
||||
index: int
|
||||
) -> Tuple[bool, _Selector]:
|
||||
) -> tuple[bool, _Selector]:
|
||||
"""Parse combinator tokens."""
|
||||
|
||||
combinator = m.group('relation').strip()
|
||||
@@ -828,7 +814,7 @@ class CSSParser:
|
||||
if not has_selector:
|
||||
if not is_forgive or combinator != COMMA_COMBINATOR:
|
||||
raise SelectorSyntaxError(
|
||||
"The combinator '{}' at postion {}, must have a selector before it".format(combinator, index),
|
||||
f"The combinator '{combinator}' at position {index}, must have a selector before it",
|
||||
self.pattern,
|
||||
index
|
||||
)
|
||||
@@ -873,7 +859,7 @@ class CSSParser:
|
||||
|
||||
pseudo = util.lower(css_unescape(m.group('name')))
|
||||
if pseudo == ":contains":
|
||||
warnings.warn(
|
||||
warnings.warn( # noqa: B028
|
||||
"The pseudo class ':contains' is deprecated, ':-soup-contains' should be used moving forward.",
|
||||
FutureWarning
|
||||
)
|
||||
@@ -924,7 +910,7 @@ class CSSParser:
|
||||
|
||||
def parse_selectors(
|
||||
self,
|
||||
iselector: Iterator[Tuple[str, Match[str]]],
|
||||
iselector: Iterator[tuple[str, Match[str]]],
|
||||
index: int = 0,
|
||||
flags: int = 0
|
||||
) -> ct.SelectorList:
|
||||
@@ -935,7 +921,7 @@ class CSSParser:
|
||||
selectors = []
|
||||
has_selector = False
|
||||
closed = False
|
||||
relations = [] # type: List[_Selector]
|
||||
relations = [] # type: list[_Selector]
|
||||
rel_type = ":" + WS_COMBINATOR
|
||||
|
||||
# Setup various flags
|
||||
@@ -986,13 +972,16 @@ class CSSParser:
|
||||
|
||||
# Handle parts
|
||||
if key == "at_rule":
|
||||
raise NotImplementedError("At-rules found at position {}".format(m.start(0)))
|
||||
raise NotImplementedError(f"At-rules found at position {m.start(0)}")
|
||||
elif key == "amp":
|
||||
sel.flags |= ct.SEL_SCOPE
|
||||
has_selector = True
|
||||
elif key == 'pseudo_class_custom':
|
||||
has_selector = self.parse_pseudo_class_custom(sel, m, has_selector)
|
||||
elif key == 'pseudo_class':
|
||||
has_selector, is_html = self.parse_pseudo_class(sel, m, has_selector, iselector, is_html)
|
||||
elif key == 'pseudo_element':
|
||||
raise NotImplementedError("Pseudo-element found at position {}".format(m.start(0)))
|
||||
raise NotImplementedError(f"Pseudo-element found at position {m.start(0)}")
|
||||
elif key == 'pseudo_contains':
|
||||
has_selector = self.parse_pseudo_contains(sel, m, has_selector)
|
||||
elif key in ('pseudo_nth_type', 'pseudo_nth_child'):
|
||||
@@ -1007,7 +996,7 @@ class CSSParser:
|
||||
if not has_selector:
|
||||
if not is_forgive:
|
||||
raise SelectorSyntaxError(
|
||||
"Expected a selector at postion {}".format(m.start(0)),
|
||||
f"Expected a selector at position {m.start(0)}",
|
||||
self.pattern,
|
||||
m.start(0)
|
||||
)
|
||||
@@ -1017,7 +1006,7 @@ class CSSParser:
|
||||
break
|
||||
else:
|
||||
raise SelectorSyntaxError(
|
||||
"Unmatched pseudo-class close at postion {}".format(m.start(0)),
|
||||
f"Unmatched pseudo-class close at position {m.start(0)}",
|
||||
self.pattern,
|
||||
m.start(0)
|
||||
)
|
||||
@@ -1035,7 +1024,7 @@ class CSSParser:
|
||||
elif key == 'tag':
|
||||
if has_selector:
|
||||
raise SelectorSyntaxError(
|
||||
"Tag name found at position {} instead of at the start".format(m.start(0)),
|
||||
f"Tag name found at position {m.start(0)} instead of at the start",
|
||||
self.pattern,
|
||||
m.start(0)
|
||||
)
|
||||
@@ -1050,7 +1039,7 @@ class CSSParser:
|
||||
# Handle selectors that are not closed
|
||||
if is_open and not closed:
|
||||
raise SelectorSyntaxError(
|
||||
"Unclosed pseudo-class at position {}".format(index),
|
||||
f"Unclosed pseudo-class at position {index}",
|
||||
self.pattern,
|
||||
index
|
||||
)
|
||||
@@ -1069,28 +1058,18 @@ class CSSParser:
|
||||
selectors.append(sel)
|
||||
|
||||
# Forgive empty slots in pseudo-classes that have lists (and are forgiving)
|
||||
elif is_forgive:
|
||||
if is_relative:
|
||||
# Handle relative selectors pseudo-classes with empty slots like `:has()`
|
||||
if selectors and selectors[-1].rel_type is None and rel_type == ': ':
|
||||
sel.rel_type = rel_type
|
||||
sel.no_match = True
|
||||
selectors[-1].relations.append(sel)
|
||||
has_selector = True
|
||||
else:
|
||||
# Handle normal pseudo-classes with empty slots
|
||||
if not selectors or not relations:
|
||||
# Others like `:is()` etc.
|
||||
sel.no_match = True
|
||||
del relations[:]
|
||||
selectors.append(sel)
|
||||
has_selector = True
|
||||
elif is_forgive and (not selectors or not relations):
|
||||
# Handle normal pseudo-classes with empty slots like `:is()` etc.
|
||||
sel.no_match = True
|
||||
del relations[:]
|
||||
selectors.append(sel)
|
||||
has_selector = True
|
||||
|
||||
if not has_selector:
|
||||
# We will always need to finish a selector when `:has()` is used as it leads with combining.
|
||||
# May apply to others as well.
|
||||
raise SelectorSyntaxError(
|
||||
'Expected a selector at position {}'.format(index),
|
||||
f'Expected a selector at position {index}',
|
||||
self.pattern,
|
||||
index
|
||||
)
|
||||
@@ -1112,7 +1091,7 @@ class CSSParser:
|
||||
# Return selector list
|
||||
return ct.SelectorList([s.freeze() for s in selectors], is_not, is_html)
|
||||
|
||||
def selector_iter(self, pattern: str) -> Iterator[Tuple[str, Match[str]]]:
|
||||
def selector_iter(self, pattern: str) -> Iterator[tuple[str, Match[str]]]:
|
||||
"""Iterate selector tokens."""
|
||||
|
||||
# Ignore whitespace and comments at start and end of pattern
|
||||
@@ -1122,7 +1101,7 @@ class CSSParser:
|
||||
end = (m.start(0) - 1) if m else (len(pattern) - 1)
|
||||
|
||||
if self.debug: # pragma: no cover
|
||||
print('## PARSING: {!r}'.format(pattern))
|
||||
print(f'## PARSING: {pattern!r}')
|
||||
while index <= end:
|
||||
m = None
|
||||
for v in self.css_tokens:
|
||||
@@ -1130,7 +1109,7 @@ class CSSParser:
|
||||
if m:
|
||||
name = v.get_name()
|
||||
if self.debug: # pragma: no cover
|
||||
print("TOKEN: '{}' --> {!r} at position {}".format(name, m.group(0), m.start(0)))
|
||||
print(f"TOKEN: '{name}' --> {m.group(0)!r} at position {m.start(0)}")
|
||||
index = m.end(0)
|
||||
yield name, m
|
||||
break
|
||||
@@ -1140,15 +1119,15 @@ class CSSParser:
|
||||
# throw an exception mentioning that the known selector type is in error;
|
||||
# otherwise, report the invalid character.
|
||||
if c == '[':
|
||||
msg = "Malformed attribute selector at position {}".format(index)
|
||||
msg = f"Malformed attribute selector at position {index}"
|
||||
elif c == '.':
|
||||
msg = "Malformed class selector at position {}".format(index)
|
||||
msg = f"Malformed class selector at position {index}"
|
||||
elif c == '#':
|
||||
msg = "Malformed id selector at position {}".format(index)
|
||||
msg = f"Malformed id selector at position {index}"
|
||||
elif c == ':':
|
||||
msg = "Malformed pseudo-class selector at position {}".format(index)
|
||||
msg = f"Malformed pseudo-class selector at position {index}"
|
||||
else:
|
||||
msg = "Invalid character {!r} position {}".format(c, index)
|
||||
msg = f"Invalid character {c!r} position {index}"
|
||||
raise SelectorSyntaxError(msg, self.pattern, index)
|
||||
if self.debug: # pragma: no cover
|
||||
print('## END PARSING')
|
||||
|
||||
Reference in New Issue
Block a user