diff --git a/cssselect/__init__.py b/cssselect/__init__.py index 59d62df..372ce98 100644 --- a/cssselect/__init__.py +++ b/cssselect/__init__.py @@ -3,8 +3,6 @@ ============================ This module supports selecting XML/HTML elements based on CSS selectors. -See the `CSSSelector` class for details. - :copyright: (c) 2007-2012 Ian Bicking and contributors. See AUTHORS for more details. diff --git a/cssselect/parser.py b/cssselect/parser.py index 4e97355..200f900 100644 --- a/cssselect/parser.py +++ b/cssselect/parser.py @@ -835,13 +835,7 @@ def parse_attrib(selector: Tree, stream: TokenStream) -> Attrib: def parse_series(tokens: Iterable[Token]) -> tuple[int, int]: - """ - Parses the arguments for :nth-child() and friends. - - :raises: A list of tokens - :returns: :``(a, b)`` - - """ + """Parses the arguments for :nth-child() and friends.""" for token in tokens: if token.type == "STRING": raise ValueError("String tokens not allowed in series.") @@ -1028,19 +1022,14 @@ def tokenize(s: str) -> Iterator[Token]: match = _match_ident(s, pos=pos) if match: - value = _sub_simple_escape( - _replace_simple, _sub_unicode_escape(_replace_unicode, match.group()) - ) + value = unescape_ident(match.group()) yield Token("IDENT", value, pos) pos = match.end() continue match = _match_hash(s, pos=pos) if match: - value = _sub_simple_escape( - _replace_simple, - _sub_unicode_escape(_replace_unicode, match.group()[1:]), - ) + value = unescape_ident(match.group()[1:]) yield Token("HASH", value, pos) pos = match.end() continue diff --git a/cssselect/xpath.py b/cssselect/xpath.py index c6f96ee..e0639c0 100644 --- a/cssselect/xpath.py +++ b/cssselect/xpath.py @@ -345,10 +345,14 @@ def _xpath_add_selector_list_condition( condition = "" for e in (self.xpath(selector) for selector in selector_list): if e.path: - # E.g. a :has() argument: it translates to a path, which - # cannot be embedded into a predicate of the outer expression. + # Only a combined selector (e.g. "a b") translates to a path, + # which cannot be embedded into a predicate of the outer + # expression. The parser rejects combinators in these arguments, + # so this is only reachable through a hand-built Matching or + # SpecificityAdjustment node. raise ExpressionError( - ":has() is not supported inside :is() and :where()" + "Combined selectors are not supported inside " + ":is(), :where() and :matches()" ) e.add_name_test() if not e.condition: @@ -601,7 +605,7 @@ def xpath_nth_child_function( expressions.append(f"{siblings_count} >= {b_min_1}") else: # if a<0, and (b-1)<0, no "n" satisfies this, - # this is tested above as an early exist condition + # this is tested above as an early exit condition # otherwise, expressions.append(f"{siblings_count} <= {b_min_1}") @@ -614,7 +618,7 @@ def xpath_nth_child_function( # - or: # count(***-sibling::***) - (b-1) = -n = 0, -1, -2, -3, etc., # i.e. count(***-sibling::***) <= (b-1) - # we we just did above. + # we just did above. # if abs(a) != 1: # count(***-sibling::***) - (b-1) ≡ 0 (mod a) diff --git a/docs/index.rst b/docs/index.rst index a024f20..830fdad 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -108,15 +108,25 @@ be implemented): * The ``:scope`` pseudo-class. Limitation: it can only be used at a start of a selector. -* The ``:is()``, ``:where()`` and ``:has()`` pseudo-classes. Limitation: - ``:has()`` cannot contain nested ``:has()`` or ``:not()``. +* The ``:is()`` and ``:where()`` pseudo-classes. Limitation: their arguments + are a comma-separated list of *compound selectors*; combinators are not + allowed (e.g. ``:is(a b)`` or ``:is(a > b)``). ``:not()`` and ``:scope`` are + also rejected inside them, while ``:has()`` is supported (e.g. + ``:is(:has(> a))``). +* The ``:has()`` pseudo-class. Limitation: it takes a single argument, made of + an optional leading combinator (``>``, ``+`` or ``~``) followed by one + *compound selector* built only from type, class and universal selectors + (e.g. ``:has(> a.important)``). Anything else is unsupported, e.g. an ID + (``:has(#id)``), or a selector list (``:has(a, b)``). These are non-standard extensions: * The ``:contains(text)`` pseudo-class that existed in `an early draft`_ but was then removed. * The ``!=`` attribute operator. ``[foo!=bar]`` is the same as - ``:not([foo=bar])``. + ``:not([foo=bar])``, except for an empty value: ``[foo!='']`` matches only + elements that do have a ``foo`` attribute and whose value is not empty, + while ``:not([foo=''])`` also matches elements without a ``foo`` attribute. * ``:not()`` accepts a *sequence of simple selectors*, not just single *simple selector*. For example, ``:not(a.important[rel])`` is allowed, even though the negation contains 3 *simple selectors*. diff --git a/tests/test_cssselect.py b/tests/test_cssselect.py index e2d5acf..e9daca0 100644 --- a/tests/test_cssselect.py +++ b/tests/test_cssselect.py @@ -120,10 +120,6 @@ def parse_many(first: str, *others: str) -> list[str]: ] assert parse_many("div > p") == ["CombinedSelector[Element[div] > Element[p]]"] assert parse_many("td:first") == ["Pseudo[Element[td]:first]"] - assert parse_many("td:first") == ["Pseudo[Element[td]:first]"] - assert parse_many("td :first") == [ - "CombinedSelector[Element[td] Pseudo[Element[*]:first]]" - ] assert parse_many("td :first") == [ "CombinedSelector[Element[td] Pseudo[Element[*]:first]]" ] @@ -892,7 +888,8 @@ class LowerValues(GenericTranslator): combined = parse("a b")[0].parsed_tree matching = Matching(base, [combined]) with pytest.raises( - ExpressionError, match=r"not supported inside :is\(\) and :where\(\)" + ExpressionError, + match=r"not supported inside :is\(\), :where\(\) and :matches\(\)", ): GenericTranslator().xpath(matching) @@ -1082,9 +1079,7 @@ def operator_id(selector: str) -> list[str]: def test_series(self) -> None: def series(css: str) -> tuple[int, int] | None: (selector,) = parse(f":nth-child({css})") - args = typing.cast( - "FunctionalPseudoElement", selector.parsed_tree - ).arguments + args = typing.cast("Function", selector.parsed_tree).arguments try: return parse_series(args) except ValueError: