Skip to content

Commit ea57bc2

Browse files
author
annbgn
committed
Merge branch 'add_support_for_has' of https://github.com/annbgn/cssselect into add_support_for_has
2 parents 62f737b + 0dfbe3d commit ea57bc2

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
@@ -283,6 +283,31 @@ def specificity(self):
283283
return a1 + a2, b1 + b2, c1 + c2
284284

285285

286+
class Matching(object):
287+
"""
288+
Represents selector:is(selector_list)
289+
"""
290+
def __init__(self, selector, selector_list):
291+
self.selector = selector
292+
self.selector_list = selector_list
293+
294+
def __repr__(self):
295+
return '%s[%r:is(%s)]' % (
296+
self.__class__.__name__, self.selector, ", ".join(
297+
map(repr, self.selector_list)))
298+
299+
def canonical(self):
300+
selector_arguments = []
301+
for s in self.selector_list:
302+
selarg = s.canonical()
303+
selector_arguments.append(selarg.lstrip('*'))
304+
return '%s:is(%s)' % (self.selector.canonical(),
305+
", ".join(map(str, selector_arguments)))
306+
307+
def specificity(self):
308+
return max([x.specificity() for x in self.selector_list])
309+
310+
286311
class Attrib(object):
287312
"""
288313
Represents selector[namespace|attrib operator value]
@@ -465,6 +490,7 @@ def parse_selector_group(stream):
465490
else:
466491
break
467492

493+
468494
def parse_selector(stream):
469495
result, pseudo_element = parse_simple_selector(stream)
470496
while 1:
@@ -574,6 +600,9 @@ def parse_simple_selector(stream, inside_negation=False):
574600
elif ident.lower() == "has":
575601
arguments = parse_relative_selector(stream)
576602
result = Relation(result, arguments)
603+
elif ident.lower() in ('matches', 'is'):
604+
selectors = parse_simple_selector_arguments(stream)
605+
result = Matching(result, selectors)
577606
else:
578607
result = Function(result, ident, parse_arguments(stream))
579608
else:
@@ -619,6 +648,29 @@ def parse_relative_selector(stream):
619648
"Expected an argument, got %s" % (next,))
620649

621650

651+
def parse_simple_selector_arguments(stream):
652+
arguments = []
653+
while 1:
654+
result, pseudo_element = parse_simple_selector(stream, True)
655+
if pseudo_element:
656+
raise SelectorSyntaxError(
657+
'Got pseudo-element ::%s inside function'
658+
% (pseudo_element, ))
659+
stream.skip_whitespace()
660+
next = stream.next()
661+
if next in (('EOF', None), ('DELIM', ',')):
662+
stream.next()
663+
stream.skip_whitespace()
664+
arguments.append(result)
665+
elif next == ('DELIM', ')'):
666+
arguments.append(result)
667+
break
668+
else:
669+
raise SelectorSyntaxError(
670+
"Expected an argument, got %s" % (next,))
671+
return arguments
672+
673+
622674
def parse_attrib(selector, stream):
623675
stream.skip_whitespace()
624676
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)
@@ -307,6 +313,8 @@ def css2css(css, res=None):
307313
css2css(':not(#foo)')
308314
css2css(':has(*)')
309315
css2css(':has(foo)')
316+
css2css(':is(#bar, .foo)')
317+
css2css(':is(:focused, :visited)')
310318
css2css('foo:empty')
311319
css2css('foo::before')
312320
css2css('foo:empty::before')
@@ -380,6 +388,10 @@ def get_error(css):
380388
"Got pseudo-element ::before inside :not() at 12")
381389
assert get_error(':not(:not(a))') == (
382390
"Got nested :not()")
391+
assert get_error(':is(:before)') == (
392+
"Got pseudo-element ::before inside function")
393+
assert get_error(':is(a b)') == (
394+
"Expected an argument, got <IDENT 'b' at 6>")
383395
assert get_error(':scope > div :scope header') == (
384396
'Got immediate child pseudo-element ":scope" not at the start of a selector'
385397
)
@@ -498,7 +510,7 @@ def xpath(css):
498510
assert xpath('e:not(:nth-child(odd))') == (
499511
"e[not(count(preceding-sibling::*) mod 2 = 0)]")
500512
assert xpath('e:nOT(*)') == (
501-
"e[0]") # never matches
513+
"e[0]") # never matches
502514
assert xpath('e:has(> f)') == 'e[./f]'
503515
assert xpath('e:has(f)') == 'e[descendant::f]'
504516
assert xpath('e:has(~ f)') == 'e[following-sibling::f]'
@@ -875,6 +887,12 @@ def pcss(main, *selectors, **kwargs):
875887
'first-li', 'second-li', 'li-div',
876888
'fifth-li', 'sixth-li', 'seventh-li']
877889
assert pcss('ol:has(div)') == ['first-ol']
890+
assert pcss(':is(#first-li, #second-li)') == [
891+
'first-li', 'second-li']
892+
assert pcss('a:is(#name-anchor, #tag-anchor)') == [
893+
'name-anchor', 'tag-anchor']
894+
assert pcss(':is(.c)') == [
895+
'first-ol', 'third-li', 'fourth-li']
878896
assert pcss('ol.a.b.c > li.c:nth-child(3)') == ['third-li']
879897

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

0 commit comments

Comments
 (0)