class Nokogiri::CSS::Parser

token FUNCTION INCLUDES DASHMATCH LBRACE HASH PLUS GREATER S STRING IDENT
token COMMA NUMBER PREFIXMATCH SUFFIXMATCH SUBSTRINGMATCH TILDE NOT_EQUAL
token SLASH DOUBLESLASH NOT EQUAL RPAREN LSQUARE RSQUARE HAS

rule
  selector
    : selector COMMA simple_selector_1toN {
        result = [val.first, val.last].flatten
      }
    | prefixless_combinator_selector { result = val.flatten }
    | optional_S simple_selector_1toN { result = [val.last].flatten }
    ;
  combinator
    : PLUS { result = :DIRECT_ADJACENT_SELECTOR }
    | GREATER { result = :CHILD_SELECTOR }
    | TILDE { result = :FOLLOWING_SELECTOR }
    | DOUBLESLASH { result = :DESCENDANT_SELECTOR }
    | SLASH { result = :CHILD_SELECTOR }
    ;
  simple_selector
    : element_name hcap_0toN {
        result =  if val[1].nil?
                    val.first
                  else
                    Node.new(:CONDITIONAL_SELECTOR, [val.first, val[1]])
                  end
      }
    | function
    | function pseudo {
        result = Node.new(:CONDITIONAL_SELECTOR, val)
      }
    | function attrib {
        result = Node.new(:CONDITIONAL_SELECTOR, val)
      }
    | hcap_1toN {
        result = Node.new(:CONDITIONAL_SELECTOR,
          [Node.new(:ELEMENT_NAME, ['*']), val.first]
        )
      }
    ;
  prefixless_combinator_selector
    : combinator simple_selector_1toN {
        result = Node.new(val.first, [nil, val.last])
      }
    ;
  simple_selector_1toN
    : simple_selector combinator simple_selector_1toN {
        result = Node.new(val[1], [val.first, val.last])
      }
    | simple_selector S simple_selector_1toN {
        result = Node.new(:DESCENDANT_SELECTOR, [val.first, val.last])
      }
    | simple_selector
    ;
  class
    : '.' IDENT { result = Node.new(:CLASS_CONDITION, [unescape_css_identifier(val[1])]) }
    ;
  element_name
    : namespaced_ident
    | '*' { result = Node.new(:ELEMENT_NAME, val) }
    ;
  namespaced_ident
    : namespace '|' IDENT {
        result = Node.new(:ELEMENT_NAME,
          [[val.first, val.last].compact.join(':')]
        )
      }
    | IDENT {
        name = @namespaces.key?('xmlns') ? "xmlns:#{val.first}" : val.first
        result = Node.new(:ELEMENT_NAME, [name])
      }
    ;
  namespace
    : IDENT { result = val[0] }
    |
    ;
  attrib
    : LSQUARE attrib_name attrib_val_0or1 RSQUARE {
        result = Node.new(:ATTRIBUTE_CONDITION,
          [val[1]] + (val[2] || [])
        )
      }
    | LSQUARE function attrib_val_0or1 RSQUARE {
        result = Node.new(:ATTRIBUTE_CONDITION,
          [val[1]] + (val[2] || [])
        )
      }
    | LSQUARE NUMBER RSQUARE {
        # non-standard, from hpricot
        result = Node.new(:PSEUDO_CLASS,
          [Node.new(:FUNCTION, ['nth-child(', val[1]])]
        )
      }
    ;
  attrib_name
    : namespace '|' IDENT {
        result = Node.new(:ELEMENT_NAME,
          [[val.first, val.last].compact.join(':')]
        )
      }
    | IDENT {
        # Default namespace is not applied to attributes.
        # So we don't add prefix "xmlns:" as in namespaced_ident.
        result = Node.new(:ELEMENT_NAME, [val.first])
      }
    ;
  function
    : FUNCTION RPAREN {
        result = Node.new(:FUNCTION, [val.first.strip])
      }
    | FUNCTION expr RPAREN {
        result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten)
      }
    | FUNCTION nth RPAREN {
        result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten)
      }
    | NOT expr RPAREN {
        result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten)
      }
    | HAS selector RPAREN {
        result = Node.new(:FUNCTION, [val.first.strip, val[1]].flatten)
      }
    ;
  expr
    : NUMBER COMMA expr { result = [val.first, val.last] }
    | STRING COMMA expr { result = [val.first, val.last] }
    | IDENT COMMA expr { result = [val.first, val.last] }
    | NUMBER
    | STRING
    | IDENT                             # even, odd
      {
        case val[0]
        when 'even'
          result = Node.new(:NTH, ['2','n','+','0'])
        when 'odd'
          result = Node.new(:NTH, ['2','n','+','1'])
        when 'n'
          result = Node.new(:NTH, ['1','n','+','0'])
        else
          # non-standard to support custom functions:
          # assert_xpath("//a[foo(., @href)]", @parser.parse('a:foo(@href)'))
          # assert_xpath("//a[foo(., @a, b)]", @parser.parse('a:foo(@a, b)'))
          # assert_xpath("//a[foo(., a, 10)]", @parser.parse('a:foo(a, 10)'))
          result = val
        end
      }
    ;
  nth
    : NUMBER IDENT PLUS NUMBER          # 5n+3 -5n+3
      {
        if val[1] == 'n'
          result = Node.new(:NTH, val)
        else
          raise Racc::ParseError, "parse error on IDENT '#{val[1]}'"
        end
      }
    | IDENT PLUS NUMBER {               # n+3, -n+3
        if val[0] == 'n'
          val.unshift("1")
          result = Node.new(:NTH, val)
        elsif val[0] == '-n'
          val[0] = 'n'
          val.unshift("-1")
          result = Node.new(:NTH, val)
        else
          raise Racc::ParseError, "parse error on IDENT '#{val[1]}'"
        end
      }
    | NUMBER IDENT {                    # 5n, -5n, 10n-1
        n = val[1]
        if n[0, 2] == 'n-'
          val[1] = 'n'
          val << "-"
          # b is contained in n as n is the string "n-b"
          val << n[2, n.size]
          result = Node.new(:NTH, val)
        elsif n == 'n'
          val << "+"
          val << "0"
          result = Node.new(:NTH, val)
        else
          raise Racc::ParseError, "parse error on IDENT '#{val[1]}'"
        end
      }
    ;
  pseudo
    : ':' function {
        result = Node.new(:PSEUDO_CLASS, [val[1]])
      }
    | ':' IDENT { result = Node.new(:PSEUDO_CLASS, [val[1]]) }
    ;
  hcap_0toN
    : hcap_1toN
    |
    ;
  hcap_1toN
    : attribute_id hcap_1toN {
        result = Node.new(:COMBINATOR, val)
      }
    | class hcap_1toN {
        result = Node.new(:COMBINATOR, val)
      }
    | attrib hcap_1toN {
        result = Node.new(:COMBINATOR, val)
      }
    | pseudo hcap_1toN {
        result = Node.new(:COMBINATOR, val)
      }
    | negation hcap_1toN {
        result = Node.new(:COMBINATOR, val)
      }
    | attribute_id
    | class
    | attrib
    | pseudo
    | negation
    ;
  attribute_id
    : HASH { result = Node.new(:ID, [unescape_css_identifier(val.first)]) }
    ;
  attrib_val_0or1
    : eql_incl_dash IDENT { result = [val.first, unescape_css_identifier(val[1])] }
    | eql_incl_dash STRING { result = [val.first, unescape_css_string(val[1])] }
    | eql_incl_dash NUMBER { result = [val.first, val[1]] }
    |
    ;
  eql_incl_dash
    : EQUAL           { result = :equal }
    | PREFIXMATCH     { result = :prefix_match }
    | SUFFIXMATCH     { result = :suffix_match }
    | SUBSTRINGMATCH  { result = :substring_match }
    | NOT_EQUAL       { result = :not_equal }
    | INCLUDES        { result = :includes }
    | DASHMATCH       { result = :dash_match }
    ;
  negation
    : NOT negation_arg RPAREN {
        result = Node.new(:NOT, [val[1]])
      }
    ;
  negation_arg
    : element_name
    | element_name hcap_1toN
    | hcap_1toN
    ;
  optional_S
    : S
    |
    ;
end

---- header

require 'nokogiri/css/parser_extras'

---- inner

def unescape_css_identifier(identifier)
  identifier.gsub(/\\(?:([^0-9a-fA-F])|([0-9a-fA-F]{1,6})\s?)/){ |m| $1 || [$2.hex].pack('U') }
end

def unescape_css_string(str)
  str.gsub(/\\(?:([^0-9a-fA-F])|([0-9a-fA-F]{1,6})\s?)/) do |m|
    if $1=="\n"
      ''
    else
      $1 || [$2.hex].pack('U')
    end
  end
end
