From 03f897c2b7f382828a7fcae24e81f9de6c3e1394 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 30 Oct 2014 23:03:46 +0100 Subject: [PATCH] Support for all possible nth-child arguments. That is, as far as I can tell based on Nokogiri's behaviour (which Oga now matches). --- lib/oga/css/parser.y | 61 ++++++++++++++++---- spec/oga/css/parser/pseudo_classes_spec.rb | 66 ++++++++++------------ 2 files changed, 79 insertions(+), 48 deletions(-) diff --git a/lib/oga/css/parser.y b/lib/oga/css/parser.y index ba393ce..741d333 100644 --- a/lib/oga/css/parser.y +++ b/lib/oga/css/parser.y @@ -271,10 +271,31 @@ rule # on_pseudo_class_nth_child() to determine what the final AST should be. nth - : T_NTH { s(:nth, s(:int, 1)) } - | T_MINUS T_NTH { s(:nth, s(:int, 1)) } - | integer T_NTH { s(:nth, val[0]) } - | integer T_NTH integer { s(:nth, val[0], val[2]) } + # n + : T_NTH { s(:nth, s(:int, 1)) } + + # -n + | T_MINUS T_NTH { s(:nth, s(:int, 1)) } + + # -n+2, -n-2 + | T_MINUS T_NTH integer { s(:nth, nil, val[2]) } + + # 2n + | integer T_NTH { s(:nth, val[0]) } + + # 2n+1, 2n-1 + | integer T_NTH integer + { + a = val[0] + b = val[2] + + # 2n-1 gets turned into 2n+1 + if b.children[0] < 0 + b = s(:int, a.children[0] - (b.children[0] % a.children[0])) + end + + s(:nth, a, b) + } ; odd @@ -358,6 +379,7 @@ end # @return [AST::Node] # def on_pseudo_class_nth_child(arg) + # literal 2, 4, etc if arg.type == :int node = s( :eq, @@ -371,18 +393,35 @@ end else step, offset = *arg - before_count = s( - :add, - s(:call, 'count', s(:axis, 'preceding-sibling', s(:test, nil, '*'))), - s(:int, 1) + count_call = s( + :call, + 'count', + s(:axis, 'preceding-sibling', s(:test, nil, '*')) ) - if offset + before_count = s(:add, count_call, s(:int, 1)) + + if step and step.children[0] >= 0 + compare = :gte + else + compare = :lte + end + + # -n-6, -n-4, etc + if !step and offset.children[0] <= 0 + node = s(:eq, count_call, s(:int, -1)) + + # 2n+2, 2n-4, etc + elsif offset + mod_val = step ? s(:int, 2) : s(:int, 1) + node = s( :and, - s(:gte, before_count, offset), - s(:eq, s(:mod, s(:sub, before_count, offset), s(:int, 2)), s(:int, 0)) + s(compare, before_count, offset), + s(:eq, s(:mod, s(:sub, before_count, offset), mod_val), s(:int, 0)) ) + + # 2n, n, -2n else node = s(:eq, s(:mod, before_count, step), s(:int, 0)) end diff --git a/spec/oga/css/parser/pseudo_classes_spec.rb b/spec/oga/css/parser/pseudo_classes_spec.rb index cb698bb..8d58280 100644 --- a/spec/oga/css/parser/pseudo_classes_spec.rb +++ b/spec/oga/css/parser/pseudo_classes_spec.rb @@ -57,56 +57,48 @@ describe Oga::CSS::Parser do ) end - example 'parse the x:nth-child(2n) pseudo class' do - parse_css('x:nth-child(2n)').should == s( - :pseudo, - s(:test, nil, 'x'), - 'nth-child', - s(:nth, s(:int, 2)) + example 'parse the x:nth-child(-n+6) pseudo class' do + parse_css('x:nth-child(-n+6)').should == parse_xpath( + 'descendant-or-self::x[((count(preceding-sibling::*) + 1) <= 6) ' \ + 'and (((count(preceding-sibling::*) + 1) - 6) mod 1) = 0]' ) end + example 'parse the x:nth-child(-n-6) pseudo class' do + parse_css('x:nth-child(-n-6)').should == parse_xpath( + 'descendant-or-self::x[count(preceding-sibling::*) = -1]' + ) + end + + example 'parse the x:nth-child(2n) pseudo class' do + parse_css('x:nth-child(2n)').should == parse_css('x:nth-child(even)') + end + example 'parse the x:nth-child(2n+1) pseudo class' do - parse_css('x:nth-child(2n+1)').should == s( - :pseudo, - s(:test, nil, 'x'), - 'nth-child', - s(:nth, s(:int, 2), s(:int, 1)) + parse_css('x:nth-child(2n+1)').should == parse_xpath( + 'descendant-or-self::x[(count(preceding-sibling::*) + 1) >= 1 ' \ + 'and (((count(preceding-sibling::*) + 1) - 1) mod 2) = 0]' ) end - example 'parse the x:nth-child(2n-1) pseudo class' do - parse_css('x:nth-child(2n-1)').should == s( - :pseudo, - s(:test, nil, 'x'), - 'nth-child', - s(:nth, s(:int, 2), s(:int, -1)) + example 'parse the x:nth-child(2n-6) pseudo class' do + parse_css('x:nth-child(2n-6)').should == parse_xpath( + 'descendant-or-self::x[(count(preceding-sibling::*) + 1) >= 2 ' \ + 'and (((count(preceding-sibling::*) + 1) - 2) mod 2) = 0]' ) end - example 'parse the x:nth-child(-2n-1) pseudo class' do - parse_css('x:nth-child(-2n-1)').should == s( - :pseudo, - s(:test, nil, 'x'), - 'nth-child', - s(:nth, s(:int, -2), s(:int, -1)) + example 'parse the x:nth-child(-2n-6) pseudo class' do + parse_css('x:nth-child(-2n-6)').should == parse_xpath( + 'descendant-or-self::x[((count(preceding-sibling::*) + 1) <= -2) ' \ + 'and (((count(preceding-sibling::*) + 1) - -2) mod 2) = 0]' ) end - example 'parse two pseudo selectors' do - parse_css('x:focus:hover').should == s( - :pseudo, - s(:pseudo, s(:test, nil, 'x'), 'focus'), - 'hover' - ) - end - - example 'parse a pseudo class with an identifier as the argument' do - parse_css('x:lang(fr)').should == s( - :pseudo, - s(:test, nil, 'x'), - 'lang', - s(:test, nil, 'fr') + example 'parse the x:nth-child(-2n+6) pseudo class' do + parse_css('x:nth-child(-2n+6)').should == parse_xpath( + 'descendant-or-self::x[((count(preceding-sibling::*) + 1) <= 6) ' \ + 'and (((count(preceding-sibling::*) + 1) - 6) mod 2) = 0]' ) end end