Skip to content

Commit

Permalink
add test, expand :has() to accept more complex arguments, remove usel…
Browse files Browse the repository at this point in the history
…ess ifs
  • Loading branch information
annbgn committed Jul 26, 2021
1 parent de1836a commit 41a0f7f
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 22 deletions.
38 changes: 22 additions & 16 deletions cssselect/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
9 changes: 3 additions & 6 deletions cssselect/xpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
11 changes: 11 additions & 0 deletions tests/test_cssselect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)') == [
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -400,6 +404,12 @@ 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>")

def test_translation(self):
def xpath(css):
return _unicode(GenericTranslator().css_to_xpath(css, prefix=''))
Expand Down Expand Up @@ -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']
Expand Down

0 comments on commit 41a0f7f

Please sign in to comment.