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 4, 2021
1 parent 52bbdd1 commit 2c15198
Show file tree
Hide file tree
Showing 3 changed files with 30 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
11 changes: 9 additions & 2 deletions cssselect/xpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,17 @@ 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:
sub_xpath.add_condition(
"name() != %s" % GenericTranslator.xpath_literal(sub_xpath.element)
)
sub2_xpath = self.xpath(negation.subselector2.parsed_tree)
return sub2_xpath.add_condition("..[%s]" % sub_xpath.condition)
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
11 changes: 5 additions & 6 deletions tests/test_cssselect.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ 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("div:not(div.foo)") == [
"Negation[Element[div]:not(Class[Element[div].foo])]"
]
Expand Down Expand Up @@ -391,10 +392,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 +469,12 @@ 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(f > e)") == "e[..[name() != 'f']]" # select e whose parent is not f
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 2c15198

Please sign in to comment.