Skip to content

Commit 2722ae6

Browse files
author
annbgn
committed
add suppport for [of S]? part in nth-child's arguments
1 parent ffb931c commit 2722ae6

File tree

3 files changed

+65
-13
lines changed

3 files changed

+65
-13
lines changed

cssselect/parser.py

+48-11
Original file line numberDiff line numberDiff line change
@@ -161,16 +161,18 @@ def __init__(self, name, arguments):
161161
self.arguments = arguments
162162

163163
def __repr__(self):
164-
return '%s[::%s(%r)]' % (
165-
self.__class__.__name__, self.name,
166-
[token.value for token in self.arguments])
164+
return "%s[::%s(%r)]" % (
165+
self.__class__.__name__,
166+
self.name,
167+
[token.value for token in self.arguments[0]],
168+
)
167169

168170
def argument_types(self):
169171
return [token.type for token in self.arguments]
170172

171173
def canonical(self):
172-
args = ''.join(token.css() for token in self.arguments)
173-
return '%s(%s)' % (self.name, args)
174+
args = "".join(token.css() for token in self.arguments[0])
175+
return "%s(%s)" % (self.name, args)
174176

175177
def specificity(self):
176178
a, b, c = self.selector.specificity()
@@ -182,12 +184,27 @@ class Function(object):
182184
"""
183185
Represents selector:name(expr)
184186
"""
185-
def __init__(self, selector, name, arguments):
187+
188+
def __init__(self, selector, name, arguments, of_type=None):
186189
self.selector = selector
187190
self.name = ascii_lower(name)
188191
self.arguments = arguments
189192

193+
# for css4 :nth-child(An+B of Subselector)
194+
try:
195+
self.of_type = of_type[0]
196+
except (IndexError, TypeError):
197+
self.of_type = None
198+
190199
def __repr__(self):
200+
if self.of_type:
201+
return "%s[%r:%s(%r of %s)]" % (
202+
self.__class__.__name__,
203+
self.selector,
204+
self.name,
205+
[token.value for token in self.arguments],
206+
self.of_type.__repr__(),
207+
)
191208
return '%s[%r:%s(%r)]' % (
192209
self.__class__.__name__, self.selector, self.name,
193210
[token.value for token in self.arguments])
@@ -539,7 +556,8 @@ def parse_simple_selector(stream, inside_negation=False):
539556
raise SelectorSyntaxError("Expected ')', got %s" % (next,))
540557
result = Negation(result, argument)
541558
else:
542-
result = Function(result, ident, parse_arguments(stream))
559+
arguments, of_type = parse_arguments(stream)
560+
result = Function(result, ident, arguments, of_type)
543561
else:
544562
raise SelectorSyntaxError(
545563
"Expected selector, got %s" % (peek,))
@@ -554,16 +572,33 @@ def parse_arguments(stream):
554572
while 1:
555573
stream.skip_whitespace()
556574
next = stream.next()
557-
if next.type in ('IDENT', 'STRING', 'NUMBER') or next in [
558-
('DELIM', '+'), ('DELIM', '-')]:
575+
if next == ("IDENT", "of"):
576+
stream.skip_whitespace()
577+
of_type = parse_of_type(stream)
578+
return arguments, of_type
579+
elif next.type in ("IDENT", "STRING", "NUMBER") or next in [
580+
("DELIM", "+"),
581+
("DELIM", "-"),
582+
]:
559583
arguments.append(next)
560584
elif next == ('DELIM', ')'):
561-
return arguments
585+
return arguments, None
562586
else:
563587
raise SelectorSyntaxError(
564588
"Expected an argument, got %s" % (next,))
565589

566590

591+
def parse_of_type(stream):
592+
subselector = ""
593+
while 1:
594+
next = stream.next()
595+
if next == ("DELIM", ")"):
596+
break
597+
subselector += next.value
598+
result = parse(subselector)
599+
return result
600+
601+
567602
def parse_attrib(selector, stream):
568603
stream.skip_whitespace()
569604
attrib = stream.next_ident_or_star()
@@ -620,6 +655,7 @@ def parse_series(tokens):
620655
for token in tokens:
621656
if token.type == 'STRING':
622657
raise ValueError('String tokens not allowed in series.')
658+
623659
s = ''.join(token.value for token in tokens).strip()
624660
if s == 'odd':
625661
return 2, 1
@@ -630,7 +666,7 @@ def parse_series(tokens):
630666
if 'n' not in s:
631667
# Just b
632668
return 0, int(s)
633-
a, b = s.split('n', 1)
669+
a, b = s.split("n", 1)
634670
if not a:
635671
a = 1
636672
elif a == '-' or a == '+':
@@ -641,6 +677,7 @@ def parse_series(tokens):
641677
b = 0
642678
else:
643679
b = int(b)
680+
644681
return a, b
645682

646683

cssselect/xpath.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,9 @@ def xpath_nth_child_function(self, xpath, function, last=False,
439439
# `add_name_test` boolean is inverted and somewhat counter-intuitive:
440440
#
441441
# nth_of_type() calls nth_child(add_name_test=False)
442-
if add_name_test:
442+
if function.of_type:
443+
nodetest = self.xpath(function.of_type.parsed_tree)
444+
elif add_name_test:
443445
nodetest = '*'
444446
else:
445447
nodetest = '%s' % xpath.element

tests/test_cssselect.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,19 @@ def xpath(css):
406406
"@hreflang = 'en' or starts-with(@hreflang, 'en-'))]")
407407

408408
# --- nth-* and nth-last-* -------------------------------------
409+
assert (
410+
xpath("e:nth-child(2n+1 of S)")
411+
== "e[count(preceding-sibling::S) mod 2 = 0]"
412+
)
413+
assert (
414+
xpath("e:nth-of-type(2n+1 of S)")
415+
== "e[count(preceding-sibling::S) mod 2 = 0]"
416+
)
417+
assert (
418+
xpath('e:nth-child(2n+1 of li.important)')
419+
== "e[count(preceding-sibling::li[@class and contains(concat(' ', normalize-space(@class), ' '), ' important ')]) mod 2 = 0]"
420+
)
421+
409422
assert xpath('e:nth-child(1)') == (
410423
"e[count(preceding-sibling::*) = 0]")
411424

@@ -606,7 +619,7 @@ def xpath_five_attributes_pseudo(self, xpath):
606619
# functional pseudo-element:
607620
# element's attribute by name
608621
def xpath_attr_functional_pseudo_element(self, xpath, arguments):
609-
attribute_name = arguments[0].value
622+
attribute_name = arguments[0][0].value
610623
other = XPathExpr('@%s' % attribute_name, '', )
611624
return xpath.join('/', other)
612625

0 commit comments

Comments
 (0)