diff --git a/cssselect/parser.py b/cssselect/parser.py index 9d48dc7..43d55eb 100644 --- a/cssselect/parser.py +++ b/cssselect/parser.py @@ -255,8 +255,9 @@ class Relation(object): Represents selector:has(subselector) """ - def __init__(self, selector, subselector): + def __init__(self, selector, combinator, subselector): self.selector = selector + self.combinator = combinator self.subselector = subselector def __repr__(self): @@ -267,19 +268,20 @@ def __repr__(self): ) def canonical(self): - if not self.subselector: - subsel = "*" - else: + try: subsel = self.subselector[0].canonical() + except TypeError: + subsel = self.subselector.canonical() if len(subsel) > 1: subsel = subsel.lstrip("*") return "%s:has(%s)" % (self.selector.canonical(), subsel) def specificity(self): a1, b1, c1 = self.selector.specificity() - a2 = b2 = c2 = 0 - if self.subselector: + try: a2, b2, c2 = self.subselector[-1].specificity() + except TypeError: + a2, b2, c2 = self.subselector.specificity() return a1 + a2, b1 + b2, c1 + c2 @@ -600,8 +602,8 @@ def parse_simple_selector(stream, inside_negation=False): raise SelectorSyntaxError("Expected ')', got %s" % (next,)) result = Negation(result, argument) elif ident.lower() == "has": - arguments = parse_relative_selector(stream) - result = Relation(result, arguments) + combinator, arguments = parse_relative_selector(stream) + result = Relation(result, combinator, arguments) elif ident.lower() in ("matches", "is"): selectors = parse_simple_selector_arguments(stream) result = Matching(result, selectors) @@ -631,23 +633,27 @@ def parse_arguments(stream): def parse_relative_selector(stream): - arguments = [] stream.skip_whitespace() + subselector = "" next = stream.next() + if next in [("DELIM", "+"), ("DELIM", "-"), ("DELIM", ">"), ("DELIM", "~")]: - arguments.append(next) - elif next.type in ("IDENT", "STRING", "NUMBER"): - arguments.append(Element(element=next.value)) - while 1: + combinator = next stream.skip_whitespace() next = stream.next() - if next.type in ("IDENT", "STRING", "NUMBER"): - arguments.append(Element(element=next.value)) + else: + combinator = Token("DELIM", " ", pos=0) + + while 1: + if next.type in ("IDENT", "STRING", "NUMBER") or next in [("DELIM", "."), ("DELIM", "*")]: + subselector += next.value elif next == ('DELIM', ')'): - return arguments + result = parse(subselector) + return combinator, result[0] else: raise SelectorSyntaxError( "Expected an argument, got %s" % (next,)) + next = stream.next() def parse_simple_selector_arguments(stream): diff --git a/cssselect/xpath.py b/cssselect/xpath.py index d7a2203..82c03f1 100644 --- a/cssselect/xpath.py +++ b/cssselect/xpath.py @@ -275,12 +275,9 @@ def xpath_negation(self, negation): def xpath_relation(self, relation): xpath = self.xpath(relation.selector) - combinator, *subselector = relation.subselector - if not subselector: - combinator.value = " " - right = self.xpath(combinator) - else: - right = self.xpath(subselector[0]) + combinator = relation.combinator + subselector = relation.subselector + right = self.xpath(subselector.parsed_tree) method = getattr( self, "xpath_relation_%s_combinator" % self.combinator_mapping[combinator.value], diff --git a/tests/test_cssselect.py b/tests/test_cssselect.py index 5552b78..78f2558 100644 --- a/tests/test_cssselect.py +++ b/tests/test_cssselect.py @@ -145,6 +145,8 @@ def parse_many(first, *others): 'Hash[Element[div]#foobar]'] assert parse_many('div:not(div.foo)') == [ 'Negation[Element[div]:not(Class[Element[div].foo])]'] + assert parse_many('div:has(div.foo)') == [ + 'Relation[Element[div]:has(Selector[Class[Element[div].foo]])]'] assert parse_many('div:is(.foo, #bar)') == [ 'Matching[Element[div]:is(Class[Element[*].foo], Hash[Element[*]#bar])]'] assert parse_many(':is(:hover, :visited)') == [ @@ -272,6 +274,7 @@ def specificity(css): assert specificity(":has(*)") == (0, 0, 0) assert specificity(":has(foo)") == (0, 0, 1) + assert specificity(":has(.foo)") == (0, 1, 0) assert specificity(":has(> foo)") == (0, 0, 1) assert specificity(':is(.foo, #bar)') == (1, 0, 0) @@ -313,6 +316,7 @@ def css2css(css, res=None): css2css(':not(#foo)') css2css(":has(*)") css2css(":has(foo)") + css2css(':has(*.foo)', ':has(.foo)') css2css(':is(#bar, .foo)') css2css(':is(:focused, :visited)') css2css('foo:empty') @@ -400,6 +404,12 @@ def get_error(css): ) assert get_error('> div p') == ("Expected selector, got ' at 0>") + # Unsupported :has() with several arguments + assert get_error(':has(a, b)') == ( + "Expected an argument, got ") + assert get_error(':has()') == ( + "Expected selector, got ") + def test_translation(self): def xpath(css): return _unicode(GenericTranslator().css_to_xpath(css, prefix='')) @@ -889,6 +899,7 @@ def pcss(main, *selectors, **kwargs): assert pcss('ol :Not(li[class])') == [ 'first-li', 'second-li', 'li-div', 'fifth-li', 'sixth-li', 'seventh-li'] + assert pcss('link:has(*)') == [] assert pcss("ol:has(div)") == ["first-ol"] assert pcss(':is(#first-li, #second-li)') == [ 'first-li', 'second-li']