diff --git a/cssselect/parser.py b/cssselect/parser.py index f1ccf98..4fbeb2f 100644 --- a/cssselect/parser.py +++ b/cssselect/parser.py @@ -238,12 +238,22 @@ class Negation(object): Represents selector:not(subselector) """ - def __init__(self, selector, subselector): + def __init__(self, selector, subselector, combinator=None, subselector2=None): self.selector = selector self.subselector = subselector + self.combinator = combinator + self.subselector2 = subselector2 def __repr__(self): - return "%s[%r:not(%r)]" % (self.__class__.__name__, self.selector, self.subselector) + if self.combinator is None and self.subselector2 is None: + return "%s[%r:not(%r)]" % (self.__class__.__name__, self.selector, self.subselector) + return "%s[%r:not(%r %s %r)]" % ( + self.__class__.__name__, + self.selector, + self.subselector, + self.combinator.value, + self.subselector2.parsed_tree, + ) def canonical(self): subsel = self.subselector.canonical() @@ -614,9 +624,11 @@ def parse_simple_selector(stream, inside_negation=False): "Got pseudo-element ::%s inside :not() at %s" % (argument_pseudo_element, next.pos) ) + combinator = arguments = None if next != ("DELIM", ")"): - raise SelectorSyntaxError("Expected ')', got %s" % (next,)) - result = Negation(result, argument) + stream.skip_whitespace() + combinator, arguments = parse_relative_selector(stream) + result = Negation(result, argument, combinator, arguments) elif ident.lower() == "has": combinator, arguments = parse_relative_selector(stream) result = Relation(result, combinator, arguments) diff --git a/cssselect/xpath.py b/cssselect/xpath.py index b9ff1d2..47cb755 100644 --- a/cssselect/xpath.py +++ b/cssselect/xpath.py @@ -270,10 +270,19 @@ def xpath_combinedselector(self, combined): def xpath_negation(self, negation): xpath = self.xpath(negation.selector) sub_xpath = self.xpath(negation.subselector) - sub_xpath.add_name_test() - if sub_xpath.condition: + if negation.combinator is not None and negation.subselector2 is not None: + sub2_xpath = self.xpath(negation.subselector2.parsed_tree) + method = getattr( + self, + "xpath_negation_%s_combinator" + % self.combinator_mapping[negation.combinator.value], + ) + return method(xpath, sub_xpath, sub2_xpath) + elif sub_xpath.condition: + sub_xpath.add_name_test() return xpath.add_condition("not(%s)" % sub_xpath.condition) else: + sub_xpath.add_name_test() return xpath.add_condition("0") def xpath_relation(self, relation): @@ -407,6 +416,27 @@ def xpath_relation_indirect_adjacent_combinator(self, left, right): """right is a sibling after left, immediately or not; select left""" return left.join("[following-sibling::", right, closing_combiner="]") + def xpath_negation_descendant_combinator(self, xpath, left, right): + xpath.add_condition('not(name()="%s" and ancestor::*[name()="%s"])' % (right, left)) + return xpath + + def xpath_negation_child_combinator(self, xpath, left, right): + xpath.add_condition('not(name()="%s" and parent::*[name()="%s"])' % (right, left)) + return xpath + + def xpath_negation_direct_adjacent_combinator(self, xpath, left, right): + xpath.add_condition( + 'not(name()="%s" and following-sibling::*[position()=1 and name()="%s"])' + % (right, left) + ) + return xpath + + def xpath_negation_indirect_adjacent_combinator(self, xpath, left, right): + xpath.add_condition( + 'not(name()="%s" and following-sibling::*[name()="%s"])' % (right, left) + ) + return xpath + # Function: dispatch by function/pseudo-class name def xpath_nth_child_function(self, xpath, function, last=False, add_name_test=True): diff --git a/tests/test_cssselect.py b/tests/test_cssselect.py index ba64f6f..5d28c3a 100644 --- a/tests/test_cssselect.py +++ b/tests/test_cssselect.py @@ -145,6 +145,10 @@ def parse_many(first, *others): assert parse_many("a:lang(fr)") == ["Function[Element[a]:lang(['fr'])]"] assert parse_many('div:contains("foo")') == ["Function[Element[div]:contains(['foo'])]"] assert parse_many("div#foobar") == ["Hash[Element[div]#foobar]"] + assert parse_many(":not(a > b)") == ["Negation[Element[*]:not(Element[a] > Element[b])]"] + assert parse_many(":not(a + b)") == ["Negation[Element[*]:not(Element[a] + Element[b])]"] + assert parse_many(":not(a ~ b)") == ["Negation[Element[*]:not(Element[a] ~ Element[b])]"] + assert parse_many(":not(a b)") == ["Negation[Element[*]:not(Element[a] Element[b])]"] assert parse_many("div:not(div.foo)") == [ "Negation[Element[div]:not(Class[Element[div].foo])]" ] @@ -391,10 +395,8 @@ 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 ") + assert get_error(":has(a, b)") == ("Expected an argument, got ") + assert get_error(":has()") == ("Expected selector, got ") def test_translation(self): def xpath(css): @@ -470,12 +472,23 @@ def xpath(css): assert xpath("e:EmPTY") == ("e[not(*) and not(string-length())]") assert xpath("e:root") == ("e[not(parent::*)]") assert xpath("e:hover") == ("e[0]") # never matches + assert xpath("*:not(a > b)") == ( + '*[not(name()="b" and parent::*[name()="a"])]' + ) # select anything that is not b or doesn't have a parent a + assert xpath("*:not(a + b)") == ( + '*[not(name()="b" and following-sibling::*[position()=1 and name()="a"])]' + ) # select anything that is not b or doesn't have an immediate sibling a + assert xpath("*:not(a ~ b)") == ( + '*[not(name()="b" and following-sibling::*[name()="a"])]' + ) # select anything that is not b or doesn't have a sibling a + assert xpath("*:not(a b)") == ( + '*[not(name()="b" and ancestor::*[name()="a"])]' + ) # select anything that is not b or doesn't have an ancestor a assert xpath("e:has(> f)") == "e[./f]" assert xpath("e:has(f)") == "e[descendant::f]" assert xpath("e:has(~ f)") == "e[following-sibling::f]" assert ( - xpath("e:has(+ f)") - == "e[following-sibling::*[(name() = 'f') and (position() = 1)]]" + xpath("e:has(+ f)") == "e[following-sibling::*[(name() = 'f') and (position() = 1)]]" ) assert xpath('e:contains("foo")') == ("e[contains(., 'foo')]") assert xpath("e:ConTains(foo)") == ("e[contains(., 'foo')]")