diff --git a/cssselect/xpath.py b/cssselect/xpath.py index b0913ab..49e60ce 100644 --- a/cssselect/xpath.py +++ b/cssselect/xpath.py @@ -379,37 +379,81 @@ def xpath_nth_child_function(self, xpath, function, last=False, if add_name_test: xpath.add_name_test() xpath.add_star_prefix() - if a == 0: - if last: - b = 'last() - %s' % b - return xpath.add_condition('position() = %s' % b) + # non-last + # -------- + # position() = an+b + # -> position() - b = an + # + # if a < 0: + # position() - b <= 0 + # -> position() <= b + # + # last + # ---- + # last() - position() = an+b -1 + # -> last() - position() - b +1 = an + # + # if a < 0: + # last() - position() - b +1 <= 0 + # -> position() >= last() - b +1 + # + # -b +1 = -(b-1) if last: - # FIXME: I'm not sure if this is right - a = -a - b = -b + b = b - 1 if b > 0: b_neg = str(-b) else: b_neg = '+%s' % (-b) + if a == 0: + if last: + # http://www.w3.org/TR/selectors/#nth-last-child-pseudo + # The :nth-last-child(an+b) pseudo-class notation represents + # an element that has an+b-1 siblings after it in the document tree + # + # last() - position() = an+b-1 + # -> position() = last() -b +1 (for a==0) + # + if b == 0: + b = 'last()' + else: + b = 'last() %s' % b_neg + return xpath.add_condition('position() = %s' % b) if a != 1: - expr = ['(position() %s) mod %s = 0' % (b_neg, a)] + # last() - position() - b +1 = an + if last: + left = 'last() - position()' + # position() - b = an + else: + left = 'position()' + if b != 0: + left = '%s %s' % (left, b_neg) + if last or b != 0: + left = '(%s)' % left + expr = ['%s mod %s = 0' % (left, a)] else: expr = [] - if b >= 0: - expr.append('position() >= %s' % b) - elif b < 0 and last: - expr.append('position() < (last() %s)' % b) + if last: + if b == 0: + right = 'last()' + else: + right = 'last() %s' % b_neg + if a > 0: + expr.append('(position() <= %s)' % right) + else: + expr.append('(position() >= %s)' % right) + else: + # position() > 0 so if b < 0, then position() > b + # also, position() >= 1 always + if b > 1: + if a > 0: + expr.append('position() >= %s' % b) + else: + expr.append('position() <= %s' % b) + expr = ' and '.join(expr) if expr: xpath.add_condition(expr) return xpath - # FIXME: handle an+b, odd, even - # an+b means every-a, plus b, e.g., 2n+1 means odd - # 0n+b means b - # n+0 means a=1, i.e., all elements - # an means every a elements, i.e., 2n means even - # -n means -1n - # -1n+6 means elements 6 and previous def xpath_nth_last_child_function(self, xpath, function): return self.xpath_nth_child_function(xpath, function, last=True) diff --git a/tests/test_cssselect.py b/tests/test_cssselect.py index 567e3c5..2638ed6 100644 --- a/tests/test_cssselect.py +++ b/tests/test_cssselect.py @@ -336,19 +336,36 @@ def xpath(css): "@hreflang = 'en' or starts-with(@hreflang, 'en-'))]") assert xpath('e:nth-child(1)') == ( "*/*[name() = 'e' and (position() = 1)]") + assert xpath('e:nth-child(3n+2)') == ( + "*/*[name() = 'e' and ((position() -2) mod 3 = 0 and position() >= 2)]") + assert xpath('e:nth-child(3n-2)') == ( + "*/*[name() = 'e' and ((position() +2) mod 3 = 0)]") + assert xpath('e:nth-child(-n+6)') == ( + "*/*[name() = 'e' and ((position() -6) mod -1 = 0 and position() <= 6)]") assert xpath('e:nth-last-child(1)') == ( - "*/*[name() = 'e' and (position() = last() - 1)]") + "*/*[name() = 'e' and (position() = last())]") + assert xpath('e:nth-last-child(2n)') == ( + "*/*[name() = 'e' and (" + "(last() - position() +1) mod 2 = 0 and (position() <= last() +1))]") + assert xpath('e:nth-last-child(2n+1)') == ( + "*/*[name() = 'e' and (" + "(last() - position()) mod 2 = 0 and (position() <= last()))]") assert xpath('e:nth-last-child(2n+2)') == ( "*/*[name() = 'e' and (" - "(position() +2) mod -2 = 0 and position() < (last() -2))]") + "(last() - position() -1) mod 2 = 0 and (position() <= last() -1))]") + assert xpath('e:nth-last-child(3n+1)') == ( + "*/*[name() = 'e' and (" + "(last() - position()) mod 3 = 0 and (position() <= last()))]") + # represents the two last e elements + assert xpath('e:nth-last-child(-n+2)') == ( + "*/*[name() = 'e' and (" + "(last() - position() -1) mod -1 = 0 and (position() >= last() -1))]") assert xpath('e:nth-of-type(1)') == ( "*/e[position() = 1]") assert xpath('e:nth-last-of-type(1)') == ( - "*/e[position() = last() - 1]") - assert xpath('e:nth-last-of-type(1)') == ( - "*/e[position() = last() - 1]") + "*/e[position() = last()]") assert xpath('div e:nth-last-of-type(1) .aclass') == ( - "div/descendant-or-self::*/e[position() = last() - 1]" + "div/descendant-or-self::*/e[position() = last()]" "/descendant-or-self::*/*[@class and contains(" "concat(' ', normalize-space(@class), ' '), ' aclass ')]") assert xpath('e:first-child') == ( @@ -381,7 +398,7 @@ def xpath(css): assert xpath('e#myid') == ( "e[@id = 'myid']") assert xpath('e:not(:nth-child(odd))') == ( - "e[not((position() -1) mod 2 = 0 and position() >= 1)]") + "e[not((position() -1) mod 2 = 0)]") assert xpath('e:nOT(*)') == ( "e[0]") # never matches assert xpath('e f') == ( @@ -640,19 +657,25 @@ def pcss(main, *selectors, **kwargs): assert pcss('li:nth-child(+2n+1)', 'li:nth-child(odd)') == [ 'first-li', 'third-li', 'fifth-li', 'seventh-li'] assert pcss('li:nth-child(2n+4)') == ['fourth-li', 'sixth-li'] - # FIXME: I'm not 100% sure this is right: assert pcss('li:nth-child(3n+1)') == [ 'first-li', 'fourth-li', 'seventh-li'] - assert pcss('li:nth-last-child(0)') == [ - 'seventh-li'] + assert pcss('li:nth-child(-n+3)') == [ + 'first-li', 'second-li', 'third-li'] + assert pcss('li:nth-child(-2n+4)') == ['second-li', 'fourth-li'] + assert pcss('li:nth-last-child(0)') == [] + assert pcss('li:nth-last-child(1)') == ['seventh-li'] assert pcss('li:nth-last-child(2n)', 'li:nth-last-child(even)') == [ 'second-li', 'fourth-li', 'sixth-li'] - assert pcss('li:nth-last-child(2n+2)') == ['second-li', 'fourth-li'] + assert pcss('li:nth-last-child(2n+1)') == [ + 'first-li', 'third-li', 'fifth-li', 'seventh-li'] + assert pcss('li:nth-last-child(2n+2)') == [ + 'second-li', 'fourth-li', 'sixth-li'] + assert pcss('li:nth-last-child(3n+1)') == [ + 'first-li', 'fourth-li', 'seventh-li'] assert pcss('ol:first-of-type') == ['first-ol'] assert pcss('ol:nth-child(1)') == [] assert pcss('ol:nth-of-type(2)') == ['second-ol'] - # FIXME: like above', '(1) or (2)? - assert pcss('ol:nth-last-of-type(1)') == ['first-ol'] + assert pcss('ol:nth-last-of-type(1)') == ['second-ol'] assert pcss('span:only-child') == ['foobar-span'] assert pcss('li div:only-child') == ['li-div'] assert pcss('div *:only-child') == ['li-div', 'foobar-span']