Skip to content

Commit 599cbb5

Browse files
authored
Add matches-any pseudo-class: ':is()' (#109)
1 parent 577ca9c commit 599cbb5

File tree

3 files changed

+81
-2
lines changed

3 files changed

+81
-2
lines changed

cssselect/parser.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,30 @@ def specificity(self):
250250
return a1 + a2, b1 + b2, c1 + c2
251251

252252

253+
class Matching(object):
254+
"""
255+
Represents selector:is(selector_list)
256+
"""
257+
def __init__(self, selector, selector_list):
258+
self.selector = selector
259+
self.selector_list = selector_list
260+
261+
def __repr__(self):
262+
return '%s[%r:is(%s)]' % (
263+
self.__class__.__name__, self.selector, ", ".join(
264+
map(repr, self.selector_list)))
265+
266+
def canonical(self):
267+
selector_arguments = []
268+
for s in self.selector_list:
269+
selarg = s.canonical()
270+
selector_arguments.append(selarg.lstrip('*'))
271+
return '%s:is(%s)' % (self.selector.canonical(),
272+
", ".join(map(str, selector_arguments)))
273+
274+
def specificity(self):
275+
return max([x.specificity() for x in self.selector_list])
276+
253277
class Attrib(object):
254278
"""
255279
Represents selector[namespace|attrib operator value]
@@ -432,6 +456,7 @@ def parse_selector_group(stream):
432456
else:
433457
break
434458

459+
435460
def parse_selector(stream):
436461
result, pseudo_element = parse_simple_selector(stream)
437462
while 1:
@@ -538,6 +563,9 @@ def parse_simple_selector(stream, inside_negation=False):
538563
if next != ('DELIM', ')'):
539564
raise SelectorSyntaxError("Expected ')', got %s" % (next,))
540565
result = Negation(result, argument)
566+
elif ident.lower() in ('matches', 'is'):
567+
selectors = parse_simple_selector_arguments(stream)
568+
result = Matching(result, selectors)
541569
else:
542570
result = Function(result, ident, parse_arguments(stream))
543571
else:
@@ -564,6 +592,29 @@ def parse_arguments(stream):
564592
"Expected an argument, got %s" % (next,))
565593

566594

595+
def parse_simple_selector_arguments(stream):
596+
arguments = []
597+
while 1:
598+
result, pseudo_element = parse_simple_selector(stream, True)
599+
if pseudo_element:
600+
raise SelectorSyntaxError(
601+
'Got pseudo-element ::%s inside function'
602+
% (pseudo_element, ))
603+
stream.skip_whitespace()
604+
next = stream.next()
605+
if next in (('EOF', None), ('DELIM', ',')):
606+
stream.next()
607+
stream.skip_whitespace()
608+
arguments.append(result)
609+
elif next == ('DELIM', ')'):
610+
arguments.append(result)
611+
break
612+
else:
613+
raise SelectorSyntaxError(
614+
"Expected an argument, got %s" % (next,))
615+
return arguments
616+
617+
567618
def parse_attrib(selector, stream):
568619
stream.skip_whitespace()
569620
attrib = stream.next_ident_or_star()

cssselect/xpath.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ def __str__(self):
5454
def __repr__(self):
5555
return '%s[%s]' % (self.__class__.__name__, self)
5656

57-
def add_condition(self, condition):
57+
def add_condition(self, condition, conjuction='and'):
5858
if self.condition:
59-
self.condition = '(%s) and (%s)' % (self.condition, condition)
59+
self.condition = '(%s) %s (%s)' % (self.condition, conjuction, condition)
6060
else:
6161
self.condition = condition
6262
return self
@@ -272,6 +272,15 @@ def xpath_negation(self, negation):
272272
else:
273273
return xpath.add_condition('0')
274274

275+
def xpath_matching(self, matching):
276+
xpath = self.xpath(matching.selector)
277+
exprs = [self.xpath(selector) for selector in matching.selector_list]
278+
for e in exprs:
279+
e.add_name_test()
280+
if e.condition:
281+
xpath.add_condition(e.condition, 'or')
282+
return xpath
283+
275284
def xpath_function(self, function):
276285
"""Translate a functional pseudo-class."""
277286
method = 'xpath_%s_function' % function.name.replace('-', '_')

tests/test_cssselect.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ def parse_many(first, *others):
145145
'Hash[Element[div]#foobar]']
146146
assert parse_many('div:not(div.foo)') == [
147147
'Negation[Element[div]:not(Class[Element[div].foo])]']
148+
assert parse_many('div:is(.foo, #bar)') == [
149+
'Matching[Element[div]:is(Class[Element[*].foo], Hash[Element[*]#bar])]']
150+
assert parse_many(':is(:hover, :visited)') == [
151+
'Matching[Element[*]:is(Pseudo[Element[*]:hover], Pseudo[Element[*]:visited])]']
148152
assert parse_many('td ~ th') == [
149153
'CombinedSelector[Element[td] ~ Element[th]]']
150154
assert parse_many(':scope > foo') == [
@@ -266,6 +270,9 @@ def specificity(css):
266270
assert specificity(':not(:empty)') == (0, 1, 0)
267271
assert specificity(':not(#foo)') == (1, 0, 0)
268272

273+
assert specificity(':is(.foo, #bar)') == (1, 0, 0)
274+
assert specificity(':is(:hover, :visited)') == (0, 1, 0)
275+
269276
assert specificity('foo:empty') == (0, 1, 1)
270277
assert specificity('foo:before') == (0, 0, 2)
271278
assert specificity('foo::before') == (0, 0, 2)
@@ -300,6 +307,8 @@ def css2css(css, res=None):
300307
css2css(':not(*[foo])', ':not([foo])')
301308
css2css(':not(:empty)')
302309
css2css(':not(#foo)')
310+
css2css(':is(#bar, .foo)')
311+
css2css(':is(:focused, :visited)')
303312
css2css('foo:empty')
304313
css2css('foo::before')
305314
css2css('foo:empty::before')
@@ -373,6 +382,10 @@ def get_error(css):
373382
"Got pseudo-element ::before inside :not() at 12")
374383
assert get_error(':not(:not(a))') == (
375384
"Got nested :not()")
385+
assert get_error(':is(:before)') == (
386+
"Got pseudo-element ::before inside function")
387+
assert get_error(':is(a b)') == (
388+
"Expected an argument, got <IDENT 'b' at 6>")
376389
assert get_error(':scope > div :scope header') == (
377390
'Got immediate child pseudo-element ":scope" not at the start of a selector'
378391
)
@@ -863,6 +876,12 @@ def pcss(main, *selectors, **kwargs):
863876
assert pcss('ol :Not(li[class])') == [
864877
'first-li', 'second-li', 'li-div',
865878
'fifth-li', 'sixth-li', 'seventh-li']
879+
assert pcss(':is(#first-li, #second-li)') == [
880+
'first-li', 'second-li']
881+
assert pcss('a:is(#name-anchor, #tag-anchor)') == [
882+
'name-anchor', 'tag-anchor']
883+
assert pcss(':is(.c)') == [
884+
'first-ol', 'third-li', 'fourth-li']
866885
assert pcss('ol.a.b.c > li.c:nth-child(3)') == ['third-li']
867886

868887
# Invalid characters in XPath element names, should not crash

0 commit comments

Comments
 (0)