Skip to content

Commit 90d2eee

Browse files
author
annbgn
committed
support parsing complex selector in :not()
1 parent 52bbdd1 commit 90d2eee

File tree

3 files changed

+64
-12
lines changed

3 files changed

+64
-12
lines changed

cssselect/parser.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,22 @@ class Negation(object):
238238
Represents selector:not(subselector)
239239
"""
240240

241-
def __init__(self, selector, subselector):
241+
def __init__(self, selector, subselector, combinator=None, subselector2=None):
242242
self.selector = selector
243243
self.subselector = subselector
244+
self.combinator = combinator
245+
self.subselector2 = subselector2
244246

245247
def __repr__(self):
246-
return "%s[%r:not(%r)]" % (self.__class__.__name__, self.selector, self.subselector)
248+
if self.combinator is None and self.subselector2 is None:
249+
return "%s[%r:not(%r)]" % (self.__class__.__name__, self.selector, self.subselector)
250+
return "%s[%r:not(%r %s %r)]" % (
251+
self.__class__.__name__,
252+
self.selector,
253+
self.subselector,
254+
self.combinator.value,
255+
self.subselector2.parsed_tree,
256+
)
247257

248258
def canonical(self):
249259
subsel = self.subselector.canonical()
@@ -614,9 +624,11 @@ def parse_simple_selector(stream, inside_negation=False):
614624
"Got pseudo-element ::%s inside :not() at %s"
615625
% (argument_pseudo_element, next.pos)
616626
)
627+
combinator = arguments = None
617628
if next != ("DELIM", ")"):
618-
raise SelectorSyntaxError("Expected ')', got %s" % (next,))
619-
result = Negation(result, argument)
629+
stream.skip_whitespace()
630+
combinator, arguments = parse_relative_selector(stream)
631+
result = Negation(result, argument, combinator, arguments)
620632
elif ident.lower() == "has":
621633
combinator, arguments = parse_relative_selector(stream)
622634
result = Relation(result, combinator, arguments)

cssselect/xpath.py

+29-2
Original file line numberDiff line numberDiff line change
@@ -270,10 +270,19 @@ def xpath_combinedselector(self, combined):
270270
def xpath_negation(self, negation):
271271
xpath = self.xpath(negation.selector)
272272
sub_xpath = self.xpath(negation.subselector)
273-
sub_xpath.add_name_test()
274-
if sub_xpath.condition:
273+
if negation.combinator is not None and negation.subselector2 is not None:
274+
sub2_xpath = self.xpath(negation.subselector2.parsed_tree)
275+
method = getattr(
276+
self,
277+
"xpath_negation_%s_combinator"
278+
% self.combinator_mapping[negation.combinator.value],
279+
)
280+
return method(xpath, sub_xpath, sub2_xpath)
281+
elif sub_xpath.condition:
282+
sub_xpath.add_name_test()
275283
return xpath.add_condition("not(%s)" % sub_xpath.condition)
276284
else:
285+
sub_xpath.add_name_test()
277286
return xpath.add_condition("0")
278287

279288
def xpath_relation(self, relation):
@@ -407,6 +416,24 @@ def xpath_relation_indirect_adjacent_combinator(self, left, right):
407416
"""right is a sibling after left, immediately or not; select left"""
408417
return left.join("[following-sibling::", right, closing_combiner="]")
409418

419+
def xpath_negation_descendant_combinator(self, xpath, left, right):
420+
xpath.add_condition('not(name()="%s" and ancestor::*[name()="%s"])' % (right, left))
421+
return xpath
422+
423+
def xpath_negation_child_combinator(self, xpath, left, right):
424+
xpath.add_condition("not([%s] and parent::*[%s])" % (left, right))
425+
return xpath
426+
427+
def xpath_negation_direct_adjacent_combinator(self, xpath, left, right):
428+
xpath.add_condition(
429+
"not([%s] and following-sibling::*[position()=1 and %s])" % (left, right)
430+
)
431+
return xpath
432+
433+
def xpath_negation_indirect_adjacent_combinator(self, xpath, left, right):
434+
xpath.add_condition("not([%s] and following-sibling::*[%s])" % (left, right))
435+
return xpath
436+
410437
# Function: dispatch by function/pseudo-class name
411438

412439
def xpath_nth_child_function(self, xpath, function, last=False, add_name_test=True):

tests/test_cssselect.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ def parse_many(first, *others):
145145
assert parse_many("a:lang(fr)") == ["Function[Element[a]:lang(['fr'])]"]
146146
assert parse_many('div:contains("foo")') == ["Function[Element[div]:contains(['foo'])]"]
147147
assert parse_many("div#foobar") == ["Hash[Element[div]#foobar]"]
148+
assert parse_many(":not(a > b)") == ["Negation[Element[*]:not(Element[a] > Element[b])]"]
149+
assert parse_many(":not(a + b)") == ["Negation[Element[*]:not(Element[a] + Element[b])]"]
150+
assert parse_many(":not(a ~ b)") == ["Negation[Element[*]:not(Element[a] ~ Element[b])]"]
151+
assert parse_many(":not(a b)") == ["Negation[Element[*]:not(Element[a] Element[b])]"]
148152
assert parse_many("div:not(div.foo)") == [
149153
"Negation[Element[div]:not(Class[Element[div].foo])]"
150154
]
@@ -391,10 +395,8 @@ def get_error(css):
391395
assert get_error("> div p") == ("Expected selector, got <DELIM '>' at 0>")
392396

393397
# Unsupported :has() with several arguments
394-
assert get_error(':has(a, b)') == (
395-
"Expected an argument, got <DELIM ',' at 6>")
396-
assert get_error(':has()') == (
397-
"Expected selector, got <EOF at 0>")
398+
assert get_error(":has(a, b)") == ("Expected an argument, got <DELIM ',' at 6>")
399+
assert get_error(":has()") == ("Expected selector, got <EOF at 0>")
398400

399401
def test_translation(self):
400402
def xpath(css):
@@ -470,12 +472,23 @@ def xpath(css):
470472
assert xpath("e:EmPTY") == ("e[not(*) and not(string-length())]")
471473
assert xpath("e:root") == ("e[not(parent::*)]")
472474
assert xpath("e:hover") == ("e[0]") # never matches
475+
assert xpath("*:not(a > b)") == (
476+
"*[not([a] and parent::*[b])]"
477+
) # select anything that is not b or doesn't have a parent a
478+
assert xpath("*:not(a + b)") == (
479+
"*[not([a] and following-sibling::*[position()=1 and b])]"
480+
) # select anything that is not b or doesn't have an immediate sibling a
481+
assert xpath("*:not(a ~ b)") == (
482+
"*[not([a] and following-sibling::*[b])]"
483+
) # select anything that is not b or doesn't have a sibling a
484+
assert xpath("*:not(a b)") == (
485+
'*[not(name()="b" and ancestor::*[name()="a"])]'
486+
) # select anything that is not b or doesn't have a ancestor a
473487
assert xpath("e:has(> f)") == "e[./f]"
474488
assert xpath("e:has(f)") == "e[descendant::f]"
475489
assert xpath("e:has(~ f)") == "e[following-sibling::f]"
476490
assert (
477-
xpath("e:has(+ f)")
478-
== "e[following-sibling::*[(name() = 'f') and (position() = 1)]]"
491+
xpath("e:has(+ f)") == "e[following-sibling::*[(name() = 'f') and (position() = 1)]]"
479492
)
480493
assert xpath('e:contains("foo")') == ("e[contains(., 'foo')]")
481494
assert xpath("e:ConTains(foo)") == ("e[contains(., 'foo')]")

0 commit comments

Comments
 (0)