diff --git a/cssselect/parser.py b/cssselect/parser.py index dd8e61a..3be064c 100644 --- a/cssselect/parser.py +++ b/cssselect/parser.py @@ -771,7 +771,7 @@ def parse_relative_selector(stream: TokenStream) -> tuple[Token, Selector]: # else it would be a descendant combinator, which is not # supported in :has() arguments. seen_whitespace = True - elif next_.type == "IDENT" or next_.is_delim(".", "*"): + elif next_.type in ("IDENT", "HASH") or next_.is_delim(".", "*"): if seen_whitespace: raise SelectorSyntaxError(f"Expected an argument, got {next_}") subselector_tokens.append(next_) diff --git a/tests/test_cssselect.py b/tests/test_cssselect.py index 3c71064..a8a0516 100644 --- a/tests/test_cssselect.py +++ b/tests/test_cssselect.py @@ -190,6 +190,12 @@ def parse_many(first: str, *others: str) -> list[str]: assert parse_many("div:has(~ div.foo)") == [ "Relation[Element[div]:has(~ Selector[Class[Element[div].foo]])]" ] + assert parse_many("div:has(#foo)") == [ + "Relation[Element[div]:has(Selector[Hash[Element[*]#foo]])]" + ] + assert parse_many("div:has(> a#foo.bar)") == [ + "Relation[Element[div]:has(> Selector[Class[Hash[Element[a]#foo].bar]])]" + ] assert parse_many("div:is(.foo, #bar)") == [ "Matching[Element[div]:is(Class[Element[*].foo], Hash[Element[*]#bar])]" ] @@ -363,6 +369,7 @@ def specificity(css: str) -> tuple[int, int, int]: assert specificity(":has(foo)") == (0, 0, 1) assert specificity(":has(.foo)") == (0, 1, 0) assert specificity(":has(> foo)") == (0, 0, 1) + assert specificity(":has(#foo)") == (1, 0, 0) assert specificity(":is(.foo, #bar)") == (1, 0, 0) assert specificity(":is(:hover, :visited)") == (0, 1, 0) @@ -428,6 +435,7 @@ def css2css(css: str, res: str | None = None) -> None: css2css(":has(~ foo)") css2css(":has(+ foo)") css2css("div:has(> div.foo)") + css2css("div:has(> #foo)") css2css(":is(#bar, .foo)") css2css(":is(:focused, :visited)") css2css(":where(:focused, :visited)") @@ -625,6 +633,7 @@ def get_error(css: str) -> str | None: assert get_error(":has()") == ("Expected selector, got ") assert get_error(":has(a b)") == ("Expected an argument, got ") assert get_error(":has(a .b)") == ("Expected an argument, got ") + assert get_error(":has(a #b)") == ("Expected an argument, got ") # '-' is not a valid relative combinator assert get_error(":has(- p)") == ("Expected an argument, got ") # Strings and numbers are not selectors @@ -745,6 +754,8 @@ def xpath(css: str) -> str: "e[./*[@class and contains(" "concat(' ', normalize-space(@class), ' '), ' bar ')]]" ) + assert xpath("e:has(> #bar)") == "e[./*[@id = 'bar']]" + assert xpath("e:has(#bar)") == "e[descendant::*[@id = 'bar']]" assert xpath("e:has(~ f.bar)") == ( "e[following-sibling::f[@class and contains(" "concat(' ', normalize-space(@class), ' '), ' bar ')]]"