Skip to content

Commit 0dfbe3d

Browse files
committed
Merge remote-tracking branch 'upstream/master' into add_support_for_has
2 parents d0f4116 + 599cbb5 commit 0dfbe3d

File tree

3 files changed

+82
-3
lines changed

3 files changed

+82
-3
lines changed

cssselect/parser.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,31 @@ def specificity(self):
280280
return a1 + a2, b1 + b2, c1 + c2
281281

282282

283+
class Matching(object):
284+
"""
285+
Represents selector:is(selector_list)
286+
"""
287+
def __init__(self, selector, selector_list):
288+
self.selector = selector
289+
self.selector_list = selector_list
290+
291+
def __repr__(self):
292+
return '%s[%r:is(%s)]' % (
293+
self.__class__.__name__, self.selector, ", ".join(
294+
map(repr, self.selector_list)))
295+
296+
def canonical(self):
297+
selector_arguments = []
298+
for s in self.selector_list:
299+
selarg = s.canonical()
300+
selector_arguments.append(selarg.lstrip('*'))
301+
return '%s:is(%s)' % (self.selector.canonical(),
302+
", ".join(map(str, selector_arguments)))
303+
304+
def specificity(self):
305+
return max([x.specificity() for x in self.selector_list])
306+
307+
283308
class Attrib(object):
284309
"""
285310
Represents selector[namespace|attrib operator value]
@@ -462,6 +487,7 @@ def parse_selector_group(stream):
462487
else:
463488
break
464489

490+
465491
def parse_selector(stream):
466492
result, pseudo_element = parse_simple_selector(stream)
467493
while 1:
@@ -571,6 +597,9 @@ def parse_simple_selector(stream, inside_negation=False):
571597
elif ident.lower() == "has":
572598
arguments = parse_relative_selector(stream)
573599
result = Relation(result, arguments)
600+
elif ident.lower() in ('matches', 'is'):
601+
selectors = parse_simple_selector_arguments(stream)
602+
result = Matching(result, selectors)
574603
else:
575604
result = Function(result, ident, parse_arguments(stream))
576605
else:
@@ -616,6 +645,29 @@ def parse_relative_selector(stream):
616645
"Expected an argument, got %s" % (next,))
617646

618647

648+
def parse_simple_selector_arguments(stream):
649+
arguments = []
650+
while 1:
651+
result, pseudo_element = parse_simple_selector(stream, True)
652+
if pseudo_element:
653+
raise SelectorSyntaxError(
654+
'Got pseudo-element ::%s inside function'
655+
% (pseudo_element, ))
656+
stream.skip_whitespace()
657+
next = stream.next()
658+
if next in (('EOF', None), ('DELIM', ',')):
659+
stream.next()
660+
stream.skip_whitespace()
661+
arguments.append(result)
662+
elif next == ('DELIM', ')'):
663+
arguments.append(result)
664+
break
665+
else:
666+
raise SelectorSyntaxError(
667+
"Expected an argument, got %s" % (next,))
668+
return arguments
669+
670+
619671
def parse_attrib(selector, stream):
620672
stream.skip_whitespace()
621673
attrib = stream.next_ident_or_star()

cssselect/xpath.py

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

58-
def add_condition(self, condition):
58+
def add_condition(self, condition, conjuction='and'):
5959
if self.condition:
60-
self.condition = '(%s) and (%s)' % (self.condition, condition)
60+
self.condition = '(%s) %s (%s)' % (self.condition, conjuction, condition)
6161
else:
6262
self.condition = condition
6363
return self
@@ -289,6 +289,15 @@ def xpath_relation(self, relation):
289289
)
290290
return method(xpath, right)
291291

292+
def xpath_matching(self, matching):
293+
xpath = self.xpath(matching.selector)
294+
exprs = [self.xpath(selector) for selector in matching.selector_list]
295+
for e in exprs:
296+
e.add_name_test()
297+
if e.condition:
298+
xpath.add_condition(e.condition, 'or')
299+
return xpath
300+
292301
def xpath_function(self, function):
293302
"""Translate a functional pseudo-class."""
294303
method = 'xpath_%s_function' % function.name.replace('-', '_')

tests/test_cssselect.py

Lines changed: 19 additions & 1 deletion
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') == [
@@ -270,6 +274,8 @@ def specificity(css):
270274
assert specificity(':has(foo)') == (0, 0, 1)
271275
assert specificity(':has(> foo)') == (0, 0, 1)
272276

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

274280
assert specificity('foo:empty') == (0, 1, 1)
275281
assert specificity('foo:before') == (0, 0, 2)
@@ -311,6 +317,8 @@ def css2css(css, res=None):
311317
# css2css(':has(*[foo])', ':has([foo])')
312318
# css2css(':has(:empty)')
313319
# css2css(':has(#foo)')
320+
css2css(':is(#bar, .foo)')
321+
css2css(':is(:focused, :visited)')
314322
css2css('foo:empty')
315323
css2css('foo::before')
316324
css2css('foo:empty::before')
@@ -384,6 +392,10 @@ def get_error(css):
384392
"Got pseudo-element ::before inside :not() at 12")
385393
assert get_error(':not(:not(a))') == (
386394
"Got nested :not()")
395+
assert get_error(':is(:before)') == (
396+
"Got pseudo-element ::before inside function")
397+
assert get_error(':is(a b)') == (
398+
"Expected an argument, got <IDENT 'b' at 6>")
387399
assert get_error(':scope > div :scope header') == (
388400
'Got immediate child pseudo-element ":scope" not at the start of a selector'
389401
)
@@ -502,7 +514,7 @@ def xpath(css):
502514
assert xpath('e:not(:nth-child(odd))') == (
503515
"e[not(count(preceding-sibling::*) mod 2 = 0)]")
504516
assert xpath('e:nOT(*)') == (
505-
"e[0]") # never matches
517+
"e[0]") # never matches
506518
assert xpath('e:has(> f)') == 'e[./f]'
507519
assert xpath('e:has(f)') == 'e/descendant-or-self::f/ancestor-or-self::e'
508520
assert xpath('e:has(~ f)') == 'e/following-sibling::f/preceding-sibling::e'
@@ -881,6 +893,12 @@ def pcss(main, *selectors, **kwargs):
881893
# assert pcss('link:has(*)') == []
882894
# assert pcss('link:has([href])') == ['link-href']
883895
# assert pcss('ol:has(div)') == ['first-ol']
896+
assert pcss(':is(#first-li, #second-li)') == [
897+
'first-li', 'second-li']
898+
assert pcss('a:is(#name-anchor, #tag-anchor)') == [
899+
'name-anchor', 'tag-anchor']
900+
assert pcss(':is(.c)') == [
901+
'first-ol', 'third-li', 'fourth-li']
884902
assert pcss('ol.a.b.c > li.c:nth-child(3)') == ['third-li']
885903

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

0 commit comments

Comments
 (0)