class CSSPool::CSS::Parser

token CHARSET_SYM IMPORT_SYM STRING SEMI IDENT S COMMA LBRACE RBRACE STAR HASH
token LSQUARE RSQUARE EQUAL INCLUDES DASHMATCH LPAREN RPAREN FUNCTION GREATER PLUS
token SLASH NUMBER MINUS LENGTH PERCENTAGE ANGLE TIME FREQ URI
token IMPORTANT_SYM MEDIA_SYM NOT ONLY AND NTH_PSEUDO_CLASS
token DOCUMENT_QUERY_SYM FUNCTION_NO_QUOTE
token TILDE
token PREFIXMATCH SUFFIXMATCH SUBSTRINGMATCH
token NOT_PSEUDO_CLASS
token KEYFRAMES_SYM
token MATCHES_PSEUDO_CLASS
token NAMESPACE_SYM
token MOZ_PSEUDO_ELEMENT
token RESOLUTION
token COLON
token SUPPORTS_SYM
token OR
token VARIABLE_NAME
token CALC_SYM
token FONTFACE_SYM
token UNICODE_RANGE
token RATIO

rule
  document
    : { @handler.start_document }
      stylesheet
      { @handler.end_document }
    ;
  stylesheet
    : charset stylesheet
    | import stylesheet
    | namespace stylesheet
    | charset
    | import
    | namespace
    | body
    |
    ;
  charset
    : CHARSET_SYM STRING SEMI { @handler.charset interpret_string(val[1]), {} }
    ;
  import
    : IMPORT_SYM import_location medium SEMI {
        @handler.import_style val[2], val[1]
      }
    | IMPORT_SYM import_location SEMI {
        @handler.import_style [], val[1]
      }
    ;
  import_location
    : import_location S
    | STRING { result = Terms::String.new interpret_string val.first }
    | URI { result = Terms::URI.new interpret_uri val.first }
    ;
  namespace
    : NAMESPACE_SYM ident import_location SEMI {
        @handler.namespace val[1], val[2]
      }
    | NAMESPACE_SYM import_location SEMI {
        @handler.namespace nil, val[1]
      }
    ;
  medium
    : medium COMMA IDENT {
        result = val[0] << MediaType.new(val[2])
      }
    | IDENT {
        result = [MediaType.new(val[0])]
      }
    ;
  media_query_list
    : media_query                           { result = MediaQueryList.new([ val[0] ]) }
    | media_query_list COMMA media_query    { result = val[0] << val[2] }
    |                                       { result = MediaQueryList.new }
    ;
  media_query
    : optional_only_or_not media_type optional_and_exprs   { result = MediaQuery.new(val[0], val[1], val[2]) }
    | media_expr optional_and_exprs                        { result = MediaQuery.new(nil, val[0], val[1]) }
    ;
  optional_only_or_not
    : ONLY                { result = :only }
    | NOT                 { result = :not }
    |                     { result = nil }
    ;
  media_type
    : IDENT               { result = MediaType.new(val[0]) }
    ;
  media_expr
    : LPAREN optional_space IDENT optional_space RPAREN                            { result = MediaType.new(val[2]) }
    | LPAREN optional_space IDENT optional_space COLON optional_space expr RPAREN  { result = MediaFeature.new(val[2], val[6][0]) }
    ;
  optional_space
    : S                 { result = val[0] }
    |                   { result = nil }
    ;
  optional_and_exprs
    : optional_and_exprs AND media_expr  { result = val[0] << val[2] }
    |                                    { result = [] }
    ;
  resolution
    : RESOLUTION {
        unit = val.first.gsub(/[\s\d.]/, '')
        number = numeric(val.first)
        result = Terms::Resolution.new(number, unit)
      }
    ;
  body
    : ruleset body
    | conditional_rule body
    | keyframes_rule body
    | fontface_rule body
    | ruleset
    | conditional_rule
    | keyframes_rule
    | fontface_rule
    ;
  conditional_rule
    : media
    | document_query
    | supports
    ;
  body_in_media
    : body
    | empty_ruleset
    ;
  media
    : start_media body_in_media RBRACE { @handler.end_media val.first }
    ;
  start_media
    : MEDIA_SYM media_query_list LBRACE {
        result = val[1]
        @handler.start_media result
      }
    ;
  document_query
    : start_document_query body RBRACE { @handler.end_document_query(before_pos(val), after_pos(val)) }
    | start_document_query RBRACE { @handler.end_document_query(before_pos(val), after_pos(val)) }
    ;
  start_document_query
    : start_document_query_pos url_match_fns LBRACE {
        @handler.start_document_query(val[1], after_pos(val))
      }
    ;
  start_document_query_pos
    : DOCUMENT_QUERY_SYM {
        @handler.node_start_pos = before_pos(val)
      }
    ;
  url_match_fns
    : url_match_fn COMMA url_match_fns {
        result = [val[0], val[2]].flatten
      }
    | url_match_fn {
        result = val
      }
    ;
  url_match_fn
    : function_no_quote
    | function
    | uri
    ;
  supports
    : start_supports body RBRACE { @handler.end_supports }
    | start_supports RBRACE { @handler.end_supports }
    ;
  start_supports
    : SUPPORTS_SYM supports_condition_root LBRACE {
        @handler.start_supports val[1]
      }
    ;
  supports_condition_root
    : supports_negation { result = val.join('') }
    | supports_conjunction_or_disjunction { result = val.join('') }
    | supports_condition_in_parens { result = val.join('') }
    ;
  supports_condition
    : supports_negation { result = val.join('') }
    | supports_conjunction_or_disjunction { result = val.join('') }
    | supports_condition_in_parens { result = val.join('') }
    ;
  supports_condition_in_parens
    : LPAREN supports_condition RPAREN { result = val.join('') }
    | supports_declaration_condition { result = val.join('') }
    ;
  supports_negation
    : NOT supports_condition_in_parens { result = val.join('') }
    ;
  supports_conjunction_or_disjunction
    : supports_conjunction
    | supports_disjunction
    ;
  supports_conjunction
    : supports_condition_in_parens AND supports_condition_in_parens { result = val.join('') }
    | supports_conjunction_or_disjunction AND supports_condition_in_parens { result = val.join('') }
    ;
  supports_disjunction
    : supports_condition_in_parens OR supports_condition_in_parens { result = val.join('') }
    | supports_conjunction_or_disjunction OR supports_condition_in_parens { result = val.join('') }
    ;
  supports_declaration_condition
    : LPAREN declaration_internal RPAREN { result = val.join('') }
    | LPAREN S declaration_internal RPAREN { result = val.join('') }
  ;
  keyframes_rule
    : start_keyframes_rule keyframes_blocks RBRACE
    | start_keyframes_rule RBRACE
    ;
  start_keyframes_rule
    : KEYFRAMES_SYM IDENT LBRACE {
        @handler.start_keyframes_rule val[1]
      }
    ;
  keyframes_blocks
    : keyframes_block keyframes_blocks
    | keyframes_block
    ;
  keyframes_block
    : start_keyframes_block declarations RBRACE { @handler.end_keyframes_block }
    | start_keyframes_block RBRACE { @handler.end_keyframes_block }
    ;
  start_keyframes_block
    : keyframes_selectors LBRACE {
        @handler.start_keyframes_block val[0]
      }
    ;
  keyframes_selectors
    | keyframes_selector COMMA keyframes_selectors {
         result = val[0] + ', ' + val[2]
      }
    | keyframes_selector
    ;
  keyframes_selector
    : IDENT
    | PERCENTAGE { result = val[0].strip }
    ;
  fontface_rule
    : start_fontface_rule declarations RBRACE { @handler.end_fontface_rule }
    | start_fontface_rule RBRACE { @handler.end_fontface_rule }
    ;
  start_fontface_rule
    : FONTFACE_SYM LBRACE {
        @handler.start_fontface_rule
      }
    ;
  ruleset
    : start_selector declarations RBRACE {
        @handler.end_selector val.first
      }
    | start_selector RBRACE {
        @handler.end_selector val.first
      }
    ;
  empty_ruleset
    : optional_space {
        start = @handler.start_selector([])
        @handler.end_selector(start)
      }
    ;
  start_selector
    : S start_selector { result = val.last }
    | selectors LBRACE {
        @handler.start_selector val.first
      }
    ;
  selectors
    : selector COMMA selectors
      {
        sel = Selector.new(val.first, {})
        result = [sel].concat(val[2])
      }
    | selector
      {
        result = [Selector.new(val.first, {})]
      }
    ;
  selector
    : simple_selector combinator selector
      {
        val.flatten!
        val[2].combinator = val.delete_at 1
        result = val
      }
    | simple_selector
    ;
  combinator
    : S       { result = :s }
    | GREATER { result = :> }
    | PLUS    { result = :+ }
    | TILDE   { result = :~ }
    ;
  simple_selector
    : element_name hcap {
        selector = val.first
        selector.additional_selectors = val.last
        result = [selector]
      }
    | element_name { result = val }
    | hcap
      {
        ss = Selectors::Simple.new nil, nil
        ss.additional_selectors = val.flatten
        result = [ss]
      }
    ;
  simple_selectors
    : simple_selector COMMA simple_selectors { result = [val[0], val[2]].flatten }
    | simple_selector
    ;
  ident_with_namespace
    : IDENT { result = [interpret_identifier(val[0]), nil] }
    | IDENT '|' IDENT { result = [interpret_identifier(val[2]), interpret_identifier(val[0])] }
    | '|' IDENT { result = [interpret_identifier(val[1]), nil] }
    | STAR '|' IDENT { result = [interpret_identifier(val[2]), '*'] }
    ;
  element_name
    : ident_with_namespace { result = Selectors::Type.new val.first[0], nil, val.first[1] }
    | STAR { result = Selectors::Universal.new val.first }
    | '|' STAR { result = Selectors::Universal.new val[1] }
    | STAR '|' STAR { result = Selectors::Universal.new val[2], nil, val[0] }
    | IDENT '|' STAR { result = Selectors::Universal.new val[2], nil, interpret_identifier(val[0]) }
    ;
  hcap
    : hash        { result = val }
    | class       { result = val }
    | attrib      { result = val }
    | pseudo      { result = val }
    | hash hcap   { result = val.flatten }
    | class hcap  { result = val.flatten }
    | attrib hcap { result = val.flatten }
    | pseudo hcap { result = val.flatten }
    ;
  hash
    : HASH {
        result = Selectors::Id.new interpret_identifier val.first.sub(/^#/, '')
      }
  class
    : '.' IDENT {
        result = Selectors::Class.new interpret_identifier val.last
      }
    ;
  attrib
    : LSQUARE ident_with_namespace EQUAL IDENT RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_identifier(val[3]),
          Selectors::Attribute::EQUALS,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace EQUAL STRING RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_string(val[3]),
          Selectors::Attribute::EQUALS,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace INCLUDES STRING RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_string(val[3]),
          Selectors::Attribute::INCLUDES,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace INCLUDES IDENT RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_identifier(val[3]),
          Selectors::Attribute::INCLUDES,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace DASHMATCH IDENT RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_identifier(val[3]),
          Selectors::Attribute::DASHMATCH,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace DASHMATCH STRING RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_string(val[3]),
          Selectors::Attribute::DASHMATCH,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace PREFIXMATCH IDENT RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_identifier(val[3]),
          Selectors::Attribute::PREFIXMATCH,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace PREFIXMATCH STRING RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_string(val[3]),
          Selectors::Attribute::PREFIXMATCH,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace SUFFIXMATCH IDENT RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_identifier(val[3]),
          Selectors::Attribute::SUFFIXMATCH,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace SUFFIXMATCH STRING RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_string(val[3]),
          Selectors::Attribute::SUFFIXMATCH,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace SUBSTRINGMATCH IDENT RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_identifier(val[3]),
          Selectors::Attribute::SUBSTRINGMATCH,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace SUBSTRINGMATCH STRING RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          interpret_string(val[3]),
          Selectors::Attribute::SUBSTRINGMATCH,
          val[1][1]
        )
      }
    | LSQUARE ident_with_namespace RSQUARE {
        result = Selectors::Attribute.new(
          val[1][0],
          nil,
          Selectors::Attribute::SET,
          val[1][1]
        )
      }
    ;
  pseudo
    : COLON IDENT {
        result = Selectors::pseudo interpret_identifier(val[1])
      }
    | COLON COLON IDENT {
        result = Selectors::PseudoElement.new(
          interpret_identifier(val[2])
        )
      }
    | COLON FUNCTION RPAREN {
        result = Selectors::PseudoClass.new(
          interpret_identifier(val[1].sub(/\($/, '')),
          ''
        )
      }
    | COLON FUNCTION IDENT RPAREN {
        result = Selectors::PseudoClass.new(
          interpret_identifier(val[1].sub(/\($/, '')),
          interpret_identifier(val[2])
        )
      }
    | COLON NOT_PSEUDO_CLASS simple_selector RPAREN {
        result = Selectors::PseudoClass.new(
          'not',
          val[2].first.to_s
        )
      }
    | COLON NTH_PSEUDO_CLASS {
        result = Selectors::PseudoClass.new(
          interpret_identifier(val[1].sub(/\(.*/, '')),
          interpret_identifier(val[1].sub(/.*\(/, '').sub(/\).*/, ''))
        )
      }
    | COLON MATCHES_PSEUDO_CLASS simple_selectors RPAREN {
        result = Selectors::PseudoClass.new(
          val[1].split('(').first.strip,
          val[2].join(', ')
        )
      }
    | COLON MOZ_PSEUDO_ELEMENT optional_space any_number_of_idents optional_space RPAREN {
        result = Selectors::PseudoElement.new(
          interpret_identifier(val[1].sub(/\($/, ''))
        )
      }
    | COLON COLON MOZ_PSEUDO_ELEMENT optional_space any_number_of_idents optional_space RPAREN {
        result = Selectors::PseudoElement.new(
          interpret_identifier(val[2].sub(/\($/, ''))
        )
      }
    ;
  any_number_of_idents
    :
    | multiple_idents
    ;
  multiple_idents
    : IDENT
    | IDENT COMMA multiple_idents
    ;
  # declarations can be separated by one *or more* semicolons. semi-colons at the start or end of a ruleset are also allowed
  one_or_more_semis
    : SEMI
    | SEMI one_or_more_semis
    ;
  declarations
    : declaration one_or_more_semis declarations
    | one_or_more_semis declarations
    | declaration one_or_more_semis
    | declaration
    | one_or_more_semis
    ;
  declaration
    : declaration_internal { @handler.property val.first }
    ;
  declaration_internal
    : property COLON expr prio
      { result = Declaration.new(val.first, val[2], val[3]) }
    | property COLON S expr prio
      { result = Declaration.new(val.first, val[3], val[4]) }
    | property S COLON expr prio
      { result = Declaration.new(val.first, val[3], val[4]) }
    | property S COLON S expr prio
      { result = Declaration.new(val.first, val[4], val[5]) }
    ;
  prio
    : IMPORTANT_SYM { result = true }
    |               { result = false }
    ;
  property
    : IDENT { result = interpret_identifier val[0] }
    | STAR IDENT { result = interpret_identifier val.join }
    | VARIABLE_NAME { result = interpret_identifier val[0] }
    ;
  operator
    : COMMA
    | SLASH
    | EQUAL
    ;
  expr
    : term operator expr {
        result = [val.first, val.last].flatten
        val.last.first.operator = val[1]
      }
    | term expr { result = val.flatten }
    | term      { result = val }
    ;
  term
    : ident
    | ratio
    | numeric
    | string
    | uri
    | hexcolor
    | calc
    | function
    | resolution
    | VARIABLE_NAME
    | uranges
    ;
  function
    : function S { result = val.first }
    | FUNCTION expr RPAREN {
        name = interpret_identifier val.first.sub(/\($/, '')
        if name == 'rgb'
          result = Terms::Rgb.new(*val[1])
        else
          result = Terms::Function.new name, val[1]
        end
      }
    | FUNCTION RPAREN {
        name = interpret_identifier val.first.sub(/\($/, '')
        result = Terms::Function.new name
      }
    ;
  function_no_quote
    : function_no_quote S { result = val.first }
    | FUNCTION_NO_QUOTE {
        parts = val.first.split('(')
        name = interpret_identifier parts.first
        result = Terms::Function.new(name, [Terms::String.new(interpret_string_no_quote(parts.last))])
      }
    ;
  uranges
    : UNICODE_RANGE COMMA uranges
    | UNICODE_RANGE
    ;
  calc
    : CALC_SYM calc_sum RPAREN optional_space {
       result = Terms::Math.new(val.first.split('(').first, val[1])
      }
    ;
  # plus and minus are supposed to have whitespace around them, per http://dev.w3.org/csswg/css-values/#calc-syntax, but the numbers are eating trailing whitespace, so we inject it back in
  calc_sum
    : calc_product
    | calc_product PLUS calc_sum { val.insert(1, ' '); result = val.join('') }
    | calc_product MINUS calc_sum { val.insert(1, ' '); result = val.join('') }
    ;
  calc_product
    : calc_value
    | calc_value optional_space STAR calc_value { result = val.join('') }
    | calc_value optional_space SLASH calc_value { result = val.join('') }
    ;
  calc_value
    : numeric { result = val.join('') }
    | function { result = val.join('') } # for var() variable references
    | LPAREN calc_sum RPAREN { result = val.join('') }
    ;
  hexcolor
    : hexcolor S { result = val.first }
    | HASH { result = Terms::Hash.new val.first.sub(/^#/, '') }
    ;
  uri
    : uri S { result = val.first }
    | URI { result = Terms::URI.new interpret_uri val.first }
    ;
  string
    : string S { result = val.first }
    | STRING { result = Terms::String.new interpret_string val.first }
    ;
  numeric
    : unary_operator numeric {
        result = val[1]
        val[1].unary_operator = val.first
      }
    | NUMBER {
        result = Terms::Number.new numeric val.first
      }
    | PERCENTAGE {
        result = Terms::Number.new numeric(val.first), nil, '%'
      }
    | LENGTH {
        unit    = val.first.gsub(/[\s\d.]/, '')
        result = Terms::Number.new numeric(val.first), nil, unit
      }
    | ANGLE {
        unit    = val.first.gsub(/[\s\d.]/, '')
        result = Terms::Number.new numeric(val.first), nil, unit
      }
    | TIME {
        unit    = val.first.gsub(/[\s\d.]/, '')
        result = Terms::Number.new numeric(val.first), nil, unit
      }
    | FREQ {
        unit    = val.first.gsub(/[\s\d.]/, '')
        result = Terms::Number.new numeric(val.first), nil, unit
      }
    ;
  ratio
    : RATIO {
        result = Terms::Ratio.new(val[0], val[1])
      }
    ;
  unary_operator
    : MINUS { result = :minus }
    | PLUS  { result = :plus }
    ;
  ident
    : ident S { result = val.first }
    | IDENT { result = Terms::Ident.new interpret_identifier val.first }
    ;

---- inner

def numeric thing
  thing = thing.gsub(/[^\d.]/, '')
  Integer(thing) rescue Float(thing)
end

def interpret_identifier s
  interpret_escapes s
end

def interpret_uri s
  interpret_escapes s.match(/^url\((.*)\)$/mui)[1].strip.match(/^(['"]?)((?:\\.|.)*)\1$/mu)[2]
end

def interpret_string_no_quote s
  interpret_escapes s.match(/^(.*)\)$/mu)[1].strip.match(/^(['"]?)((?:\\.|.)*)\1$/mu)[2]
end

def interpret_string s
  interpret_escapes s.match(/^(['"])((?:\\.|.)*)\1$/mu)[2]
end

def interpret_escapes s
  token_exp = /\\(?:([0-9a-fA-F]{1,6}(?:\r\n|\s)?)|(.))/mu
  return s.gsub(token_exp) do |escape_sequence|
    if !$1.nil?
      code = $1.chomp.to_i 16
      code = 0xFFFD if code > 0x10FFFF
      next [code].pack('U')
    end
    next '' if $2 == "\n"
    next $2
  end
end

# override racc's on_error so we can have context in our error messages
def on_error(t, val, vstack)
  errcontext = (@ss.pre_match[-10..-1] || @ss.pre_match) +
                @ss.matched + @ss.post_match[0..9]
  line_number = @ss.pre_match.lines.count
  raise ParseError, sprintf("parse error on value %s (%s) " +
                            "on line %s around \"%s\"",
                            val.inspect, token_to_str(t) || '?',
                            line_number, errcontext)
end

def before_pos(val)
  # don't include leading whitespace
  return current_pos - val.last.length + val.last[/\A\s*/].size
end

def after_pos(val)
  # don't include trailing whitespace
  return current_pos - val.last[/\s*\z/].size
end

# charpos will work with multibyte strings but is not available until ruby 2
def current_pos
  @ss.respond_to?('charpos') ? @ss.charpos : @ss.pos
end