Skip to content

Commit

Permalink
support parsing complex selector in :not()
Browse files Browse the repository at this point in the history
  • Loading branch information
annbgn committed Aug 17, 2021
1 parent 52bbdd1 commit 3c86499
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 12 deletions.
20 changes: 16 additions & 4 deletions cssselect/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
34 changes: 32 additions & 2 deletions cssselect/xpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
25 changes: 19 additions & 6 deletions tests/test_cssselect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])]"
]
Expand Down Expand Up @@ -391,10 +395,8 @@ def get_error(css):
assert get_error("> div p") == ("Expected selector, got <DELIM '>' at 0>")

# Unsupported :has() with several arguments
assert get_error(':has(a, b)') == (
"Expected an argument, got <DELIM ',' at 6>")
assert get_error(':has()') == (
"Expected selector, got <EOF at 0>")
assert get_error(":has(a, b)") == ("Expected an argument, got <DELIM ',' at 6>")
assert get_error(":has()") == ("Expected selector, got <EOF at 0>")

def test_translation(self):
def xpath(css):
Expand Down Expand Up @@ -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')]")
Expand Down

0 comments on commit 3c86499

Please sign in to comment.