Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions cssselect/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,30 @@ def specificity(self):
return a1 + a2, b1 + b2, c1 + c2


class Matching(object):
"""
Represents selector:is(selector_list)
"""
def __init__(self, selector, selector_list):
self.selector = selector
self.selector_list = selector_list

def __repr__(self):
return '%s[%r:is(%s)]' % (
self.__class__.__name__, self.selector, ", ".join(
map(repr, self.selector_list)))

def canonical(self):
selector_arguments = []
for s in self.selector_list:
selarg = s.canonical()
selector_arguments.append(selarg.lstrip('*'))
return '%s:is(%s)' % (self.selector.canonical(),
", ".join(map(str, selector_arguments)))

def specificity(self):
return max([x.specificity() for x in self.selector_list])

class Attrib(object):
"""
Represents selector[namespace|attrib operator value]
Expand Down Expand Up @@ -432,6 +456,7 @@ def parse_selector_group(stream):
else:
break


def parse_selector(stream):
result, pseudo_element = parse_simple_selector(stream)
while 1:
Expand Down Expand Up @@ -538,6 +563,9 @@ def parse_simple_selector(stream, inside_negation=False):
if next != ('DELIM', ')'):
raise SelectorSyntaxError("Expected ')', got %s" % (next,))
result = Negation(result, argument)
elif ident.lower() in ('matches', 'is'):
selectors = parse_simple_selector_arguments(stream)
result = Matching(result, selectors)
else:
result = Function(result, ident, parse_arguments(stream))
else:
Expand All @@ -564,6 +592,29 @@ def parse_arguments(stream):
"Expected an argument, got %s" % (next,))


def parse_simple_selector_arguments(stream):
arguments = []
while 1:
result, pseudo_element = parse_simple_selector(stream, True)
if pseudo_element:
raise SelectorSyntaxError(
'Got pseudo-element ::%s inside function'
% (pseudo_element, ))
stream.skip_whitespace()
next = stream.next()
if next in (('EOF', None), ('DELIM', ',')):
stream.next()
stream.skip_whitespace()
arguments.append(result)
elif next == ('DELIM', ')'):
arguments.append(result)
break
else:
raise SelectorSyntaxError(
"Expected an argument, got %s" % (next,))
return arguments


def parse_attrib(selector, stream):
stream.skip_whitespace()
attrib = stream.next_ident_or_star()
Expand Down
13 changes: 11 additions & 2 deletions cssselect/xpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ def __str__(self):
def __repr__(self):
return '%s[%s]' % (self.__class__.__name__, self)

def add_condition(self, condition):
def add_condition(self, condition, conjuction='and'):
if self.condition:
self.condition = '(%s) and (%s)' % (self.condition, condition)
self.condition = '(%s) %s (%s)' % (self.condition, conjuction, condition)
else:
self.condition = condition
return self
Expand Down Expand Up @@ -272,6 +272,15 @@ def xpath_negation(self, negation):
else:
return xpath.add_condition('0')

def xpath_matching(self, matching):
xpath = self.xpath(matching.selector)
exprs = [self.xpath(selector) for selector in matching.selector_list]
for e in exprs:
e.add_name_test()
if e.condition:
xpath.add_condition(e.condition, 'or')
return xpath

def xpath_function(self, function):
"""Translate a functional pseudo-class."""
method = 'xpath_%s_function' % function.name.replace('-', '_')
Expand Down
19 changes: 19 additions & 0 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):
'Hash[Element[div]#foobar]']
assert parse_many('div:not(div.foo)') == [
'Negation[Element[div]:not(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)') == [
'Matching[Element[*]:is(Pseudo[Element[*]:hover], Pseudo[Element[*]:visited])]']
assert parse_many('td ~ th') == [
'CombinedSelector[Element[td] ~ Element[th]]']
assert parse_many(':scope > foo') == [
Expand Down Expand Up @@ -266,6 +270,9 @@ def specificity(css):
assert specificity(':not(:empty)') == (0, 1, 0)
assert specificity(':not(#foo)') == (1, 0, 0)

assert specificity(':is(.foo, #bar)') == (1, 0, 0)
assert specificity(':is(:hover, :visited)') == (0, 1, 0)

assert specificity('foo:empty') == (0, 1, 1)
assert specificity('foo:before') == (0, 0, 2)
assert specificity('foo::before') == (0, 0, 2)
Expand Down Expand Up @@ -300,6 +307,8 @@ def css2css(css, res=None):
css2css(':not(*[foo])', ':not([foo])')
css2css(':not(:empty)')
css2css(':not(#foo)')
css2css(':is(#bar, .foo)')
css2css(':is(:focused, :visited)')
css2css('foo:empty')
css2css('foo::before')
css2css('foo:empty::before')
Expand Down Expand Up @@ -373,6 +382,10 @@ def get_error(css):
"Got pseudo-element ::before inside :not() at 12")
assert get_error(':not(:not(a))') == (
"Got nested :not()")
assert get_error(':is(:before)') == (
"Got pseudo-element ::before inside function")
assert get_error(':is(a b)') == (
"Expected an argument, got <IDENT 'b' at 6>")
assert get_error(':scope > div :scope header') == (
'Got immediate child pseudo-element ":scope" not at the start of a selector'
)
Expand Down Expand Up @@ -863,6 +876,12 @@ 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(':is(#first-li, #second-li)') == [
'first-li', 'second-li']
assert pcss('a:is(#name-anchor, #tag-anchor)') == [
'name-anchor', 'tag-anchor']
assert pcss(':is(.c)') == [
'first-ol', 'third-li', 'fourth-li']
assert pcss('ol.a.b.c > li.c:nth-child(3)') == ['third-li']

# Invalid characters in XPath element names, should not crash
Expand Down