diff --git a/cssselect/parser.py b/cssselect/parser.py index a63246b..3567725 100644 --- a/cssselect/parser.py +++ b/cssselect/parser.py @@ -638,10 +638,16 @@ def parse_simple_selector( stream.next() namespace = "*" if stream.peek() == ("DELIM", "|"): + if peek.type == "IDENT" and namespace == "*": + # An escaped "*" (e.g. "\*|f") is a prefix named "*", + # not the namespace wildcard, and no XPath prefix can be "*". + raise SelectorSyntaxError(f"Expected namespace prefix, got {peek}") stream.next() element = stream.next_ident_or_star() else: - element = None if namespace == "*" else namespace + # An escaped "*" (e.g. "\*") is a type selector for elements + # named "*", not the universal selector. + element = namespace if peek.type == "IDENT" else None namespace = None else: element = namespace = None @@ -863,6 +869,11 @@ def parse_attrib(selector: Tree, stream: TokenStream) -> Attrib: stream.next() op = "|=" else: + if attrib == "*": + # An escaped "*" (e.g. "[\*|attr]"), not the wildcard. + raise SelectorSyntaxError( + f"Expected namespace prefix, got {stream.used[-2]}" + ) namespace = "*" if attrib is None else attrib attrib = stream.next_ident() op = None diff --git a/tests/test_cssselect.py b/tests/test_cssselect.py index 07bf58b..276c593 100644 --- a/tests/test_cssselect.py +++ b/tests/test_cssselect.py @@ -532,6 +532,12 @@ def get_error(css: str) -> str | None: assert get_error("div > ") == ("Expected selector, got ") assert get_error(" > div") == ("Expected selector, got ' at 2>") assert get_error("foo|#bar") == ("Expected ident or '*', got ") + assert get_error(r"\*|foo") == ( + "Expected namespace prefix, got " + ) + assert get_error(r"[\2a|foo]") == ( + "Expected namespace prefix, got " + ) assert get_error(".foo|bar") == ("Expected selector, got ") assert get_error("#bar|foo") == ("Expected selector, got ") assert get_error("[baz]|foo") == ("Expected selector, got ") @@ -965,6 +971,11 @@ def xpath(css: str) -> str: assert xpath(r"div\a ") == "*[name() = 'div\n']" assert xpath(r"[href\a ]") == "*[attribute::*[name() = 'href\n']]" assert xpath(r"ns\a |div") == "*[name() = 'ns\n:div']" + # An escaped "*" is a name, not the universal selector or wildcard + assert xpath(r"\*") == "*[name() = '*']" + assert xpath(r"\2a") == "*[name() = '*']" + assert xpath(r"e > \*") == "e/*[name() = '*']" + assert xpath(r"[\*]") == "*[attribute::*[name() = '*']]" # Escaped identifiers survive inside :has() arguments assert xpath(r"e:has(di\[v)") == "e[descendant::*[name() = 'di[v']]"