class Oga::CSS::Parser

AST parser for CSS expressions.

This parser does not build a CSS specific AST, instead it directly produces an XPath AST. This removes the need to transform the AST or generate corresponding XPath expressions as a String.

Similar to {Oga::XPath::Parser} this parser only takes String instances as input.

@api private

Constants

CACHE

@return [Oga::LRU]

CONFIG

Public Class Methods

new(data) click to toggle source

@param [String] data The input to parse.

# File lib/oga/css/parser.rb, line 249
def initialize(data)
  @lexer = Lexer.new(data)
end
parse_with_cache(data) click to toggle source

@param [String] data @return [AST::Node]

# File lib/oga/css/parser.rb, line 244
def self.parse_with_cache(data)
  CACHE.get_or_set(data) { new(data).parse }
end

Public Instance Methods

current_element() click to toggle source

Returns the node test for the current element.

@return [AST::Node]

# File lib/oga/css/parser.rb, line 279
def current_element
  @current_element ||= s(:test, nil, '*')
end
each_token() { |args| ... } click to toggle source

Yields the next token from the lexer.

@yieldparam [Array]

# File lib/oga/css/parser.rb, line 268
def each_token
  @lexer.advance do |*args|
    yield args
  end

  yield [-1, -1]
end
on_op_ends_with(attr, value) click to toggle source

Generates the AST for the `$=` operator.

@param [AST::Node] attr @param [AST::Node] value @return [AST::Node]

# File lib/oga/css/parser.rb, line 465
def on_op_ends_with(attr, value)
  s(
    :eq,
    s(
      :call,
      'substring',
      attr,
      s(
        :add,
        s(
          :sub,
          s(:call, 'string-length', attr),
          s(:call, 'string-length', value)
        ),
        s(:int, 1)
      ),
      s(:call, 'string-length', value)
    ),
    value
  )
end
on_op_eq(attr, value) click to toggle source

Generates the AST for the `=` operator.

@param [AST::Node] attr @param [AST::Node] value @return [AST::Node]

# File lib/oga/css/parser.rb, line 433
def on_op_eq(attr, value)
  s(:eq, attr, value)
end
on_op_hyphen_in(attr, value) click to toggle source

Generates the AST for the `|=` operator.

@param [AST::Node] attr @param [AST::Node] value @return [AST::Node]

# File lib/oga/css/parser.rb, line 501
def on_op_hyphen_in(attr, value)
  s(
    :or,
    s(:eq, attr, value),
    s(
      :call,
      'starts-with',
      attr,
      s(:call, 'concat', value, s(:string, '-'))
    )
  )
end
on_op_in(attr, value) click to toggle source

Generates the AST for the `*=` operator.

@param [AST::Node] attr @param [AST::Node] value @return [AST::Node]

# File lib/oga/css/parser.rb, line 492
def on_op_in(attr, value)
  s(:call, 'contains', attr, value)
end
on_op_space_in(attr, value) click to toggle source

Generates the AST for the `~=` operator.

@param [AST::Node] attr @param [AST::Node] value @return [AST::Node]

# File lib/oga/css/parser.rb, line 442
def on_op_space_in(attr, value)
  s(
    :call,
    'contains',
    s(:call, 'concat', s(:string, ' '), attr, s(:string, ' ')),
    s(:call, 'concat', s(:string, ' '), value, s(:string, ' '))
  )
end
on_op_starts_with(attr, value) click to toggle source

Generates the AST for the `^=` operator.

@param [AST::Node] attr @param [AST::Node] value @return [AST::Node]

# File lib/oga/css/parser.rb, line 456
def on_op_starts_with(attr, value)
  s(:call, 'starts-with', attr, value)
end
on_pseudo_class(name, arg = nil) click to toggle source

@param [String] name @param [AST::Node] arg @return [AST::Node]

# File lib/oga/css/parser.rb, line 308
def on_pseudo_class(name, arg = nil)
  handler = "on_pseudo_class_#{name.gsub('-', '_')}"

  arg ? send(handler, arg) : send(handler)
end
on_pseudo_class_empty() click to toggle source

Generates the AST for the `:empty` selector.

@return [AST::Node]

# File lib/oga/css/parser.rb, line 406
def on_pseudo_class_empty
  s(:call, 'not', s(:axis, 'child', s(:type_test, 'node')))
end
on_pseudo_class_first_child() click to toggle source

Generates the AST for the `:first-child` selector.

@return [AST::Node]

# File lib/oga/css/parser.rb, line 364
def on_pseudo_class_first_child
  generate_no_siblings('preceding-sibling')
end
on_pseudo_class_first_of_type() click to toggle source

Generates the AST for the `:first-of-type` selector.

@return [AST::Node]

# File lib/oga/css/parser.rb, line 378
def on_pseudo_class_first_of_type
  generate_no_siblings('preceding-sibling', current_element)
end
on_pseudo_class_last_child() click to toggle source

Generates the AST for the `:last-child` selector.

@return [AST::Node]

# File lib/oga/css/parser.rb, line 371
def on_pseudo_class_last_child
  generate_no_siblings('following-sibling')
end
on_pseudo_class_last_of_type() click to toggle source

Generates the AST for the `:last-of-type` selector.

@return [AST::Node]

# File lib/oga/css/parser.rb, line 385
def on_pseudo_class_last_of_type
  generate_no_siblings('following-sibling', current_element)
end
on_pseudo_class_not(arg) click to toggle source

Generates the AST for the `:not` selector.

@param [AST::Node] arg @return [AST::Node]

# File lib/oga/css/parser.rb, line 414
def on_pseudo_class_not(arg)
  # Unpacks (axis "descendant" (test nil "x")) into just (test nil "x") as
  # in this case we want to wrap the (test) node in a (axis "self") node.
  if arg.type == :axis
    arg = s(:axis, 'self', arg.children[1])

  # Unpack (predicate (eq ...)) into just (eq)
  elsif arg.type == :predicate
    arg = arg.children[1]
  end

  s(:call, 'not', arg)
end
on_pseudo_class_nth(arg) click to toggle source

Generates the AST for the `nth` pseudo class.

@param [AST::Node] arg @return [AST::Node]

# File lib/oga/css/parser.rb, line 357
def on_pseudo_class_nth(arg)
  s(:eq, s(:call, 'position'), arg)
end
on_pseudo_class_nth_child(arg) click to toggle source

Generates the AST for the `nth-child` pseudo class.

@param [AST::Node] arg @return [AST::Node]

# File lib/oga/css/parser.rb, line 325
def on_pseudo_class_nth_child(arg)
  generate_nth_child('preceding-sibling', arg)
end
on_pseudo_class_nth_last_child(arg) click to toggle source

Generates the AST for the `nth-last-child` pseudo class.

@param [AST::Node] arg @return [AST::Node]

# File lib/oga/css/parser.rb, line 333
def on_pseudo_class_nth_last_child(arg)
  generate_nth_child('following-sibling', arg)
end
on_pseudo_class_nth_last_of_type(arg) click to toggle source

Generates the AST for the `nth-last-of-type` pseudo class.

@param [AST::Node] arg @return [AST::Node]

# File lib/oga/css/parser.rb, line 349
def on_pseudo_class_nth_last_of_type(arg)
  generate_nth_child('following-sibling', arg, current_element)
end
on_pseudo_class_nth_of_type(arg) click to toggle source

Generates the AST for the `nth-of-type` pseudo class.

@param [AST::Node] arg @return [AST::Node]

# File lib/oga/css/parser.rb, line 341
def on_pseudo_class_nth_of_type(arg)
  generate_nth_child('preceding-sibling', arg, current_element)
end
on_pseudo_class_only_child() click to toggle source

Generates the AST for the `:only-child` selector.

@return [AST::Node]

# File lib/oga/css/parser.rb, line 392
def on_pseudo_class_only_child
  s(:and, on_pseudo_class_first_child, on_pseudo_class_last_child)
end
on_pseudo_class_only_of_type() click to toggle source

Generates the AST for the `:only-of-type` selector.

@return [AST::Node]

# File lib/oga/css/parser.rb, line 399
def on_pseudo_class_only_of_type
  s(:and, on_pseudo_class_first_of_type, on_pseudo_class_last_of_type)
end
on_pseudo_class_root() click to toggle source

Generates the AST for the `root` pseudo class.

@return [AST::Node]

# File lib/oga/css/parser.rb, line 317
def on_pseudo_class_root
  s(:call, 'not', s(:axis, 'parent', s(:test, nil, '*')))
end
on_test(namespace, name) click to toggle source

Generates the AST for a node test.

@param [String] namespace @param [String] name @return [AST::Node]

# File lib/oga/css/parser.rb, line 301
def on_test(namespace, name)
  @current_element = s(:test, namespace, name)
end
parse() click to toggle source

Parses the input and returns the corresponding AST.

@example

parser = Oga::CSS::Parser.new('foo.bar')
ast    = parser.parse

@return [AST::Node]

Calls superclass method
# File lib/oga/css/parser.rb, line 290
def parse
  reset

  super
end
reset() click to toggle source

Resets the internal state of the parser.

# File lib/oga/css/parser.rb, line 254
def reset
  @current_element = nil
end
s(type, *children) click to toggle source

@param [Symbol] type @param [Array] children @return [AST::Node]

# File lib/oga/css/parser.rb, line 261
def s(type, *children)
  AST::Node.new(type, children)
end

Private Instance Methods

_rule_0(val) click to toggle source
# File lib/oga/css/parser.rb, line 620
def _rule_0(val)
  val[0]
end
_rule_1(val) click to toggle source
# File lib/oga/css/parser.rb, line 624
def _rule_1(val)
   nil 
end
_rule_10(val) click to toggle source
# File lib/oga/css/parser.rb, line 699
def _rule_10(val)
  
    test, preds = val[1]
    more        = val[2]

    s(
      :predicate,
      s(:axis, 'following-sibling', on_test(nil, '*')),
      s(:int, 1),
      generate_axis('self', test, preds, more)
    )
  
end
_rule_11(val) click to toggle source
# File lib/oga/css/parser.rb, line 713
def _rule_11(val)
  
    test, preds = val[1]
    more        = val[2]

    generate_axis('following-sibling', test, preds, more)
  
end
_rule_12(val) click to toggle source
# File lib/oga/css/parser.rb, line 722
def _rule_12(val)
  val
end
_rule_13(val) click to toggle source
# File lib/oga/css/parser.rb, line 726
def _rule_13(val)
   [nil, val[0]] 
end
_rule_14(val) click to toggle source
# File lib/oga/css/parser.rb, line 730
def _rule_14(val)
  val[0]
end
_rule_15(val) click to toggle source
# File lib/oga/css/parser.rb, line 734
def _rule_15(val)
  val[0]
end
_rule_16(val) click to toggle source
# File lib/oga/css/parser.rb, line 738
def _rule_16(val)
  val[0]
end
_rule_17(val) click to toggle source
# File lib/oga/css/parser.rb, line 742
def _rule_17(val)
   on_test(*val[0]) 
end
_rule_18(val) click to toggle source
# File lib/oga/css/parser.rb, line 746
def _rule_18(val)
   val[1] ? [val[0], val[1]] : [nil, val[0]] 
end
_rule_19(val) click to toggle source
# File lib/oga/css/parser.rb, line 750
def _rule_19(val)
   val[1] 
end
_rule_2(val) click to toggle source
# File lib/oga/css/parser.rb, line 628
def _rule_2(val)
  
    query = val[0]

    val[1].each do |chunk|
      query = s(:pipe, query, chunk)
    end

    query
  
end
_rule_20(val) click to toggle source
# File lib/oga/css/parser.rb, line 754
def _rule_20(val)
  
    ret = val[0]

    val[1].each do |pred|
      ret = s(:and, ret, pred)
    end

    ret
  
end
_rule_21(val) click to toggle source
# File lib/oga/css/parser.rb, line 766
def _rule_21(val)
  val[0]
end
_rule_22(val) click to toggle source
# File lib/oga/css/parser.rb, line 770
def _rule_22(val)
  val[0]
end
_rule_23(val) click to toggle source
# File lib/oga/css/parser.rb, line 774
def _rule_23(val)
   val[1] 
end
_rule_24(val) click to toggle source
# File lib/oga/css/parser.rb, line 778
def _rule_24(val)
  val[0]
end
_rule_25(val) click to toggle source
# File lib/oga/css/parser.rb, line 782
def _rule_25(val)
   s(:axis, 'attribute', on_test(*val[0])) 
end
_rule_26(val) click to toggle source
# File lib/oga/css/parser.rb, line 786
def _rule_26(val)
  
    op_type = val[1] ? val[1][0] : nil

    case op_type
    # a="b"
    when :eq
      on_op_eq(val[0], val[1][1])

    # a~="b"
    when :space_in
      on_op_space_in(val[0], val[1][1])

    # a^="b"
    when :starts_with
      on_op_starts_with(val[0], val[1][1])

    # a$="b"
    when :ends_with
      on_op_ends_with(val[0], val[1][1])

    # a*="b"
    when :in
      on_op_in(val[0], val[1][1])

    # a|="b"
    when :hyphen_in
      on_op_hyphen_in(val[0], val[1][1])

    else
      val[0]
    end
  
end
_rule_27(val) click to toggle source
# File lib/oga/css/parser.rb, line 821
def _rule_27(val)
   :eq 
end
_rule_28(val) click to toggle source
# File lib/oga/css/parser.rb, line 825
def _rule_28(val)
   :space_in 
end
_rule_29(val) click to toggle source
# File lib/oga/css/parser.rb, line 829
def _rule_29(val)
   :starts_with 
end
_rule_3(val) click to toggle source
# File lib/oga/css/parser.rb, line 640
def _rule_3(val)
   val[1] 
end
_rule_30(val) click to toggle source
# File lib/oga/css/parser.rb, line 833
def _rule_30(val)
   :ends_with 
end
_rule_31(val) click to toggle source
# File lib/oga/css/parser.rb, line 837
def _rule_31(val)
   :in 
end
_rule_32(val) click to toggle source
# File lib/oga/css/parser.rb, line 841
def _rule_32(val)
   :hyphen_in 
end
_rule_33(val) click to toggle source
# File lib/oga/css/parser.rb, line 845
def _rule_33(val)
  
    axis = s(:axis, 'attribute', s(:test, nil, 'class'))

    s(
      :call,
      'contains',
      s(:call, 'concat', s(:string, ' '), axis, s(:string, ' ')),
      s(:string, " #{val[1]} ")
    )
  
end
_rule_34(val) click to toggle source
# File lib/oga/css/parser.rb, line 858
def _rule_34(val)
  
    s(
      :eq,
      s(:axis, 'attribute', s(:test, nil, 'id')),
      s(:string, val[1])
    )
  
end
_rule_35(val) click to toggle source
# File lib/oga/css/parser.rb, line 868
def _rule_35(val)
   on_pseudo_class(val[0], val[1]) 
end
_rule_36(val) click to toggle source
# File lib/oga/css/parser.rb, line 872
def _rule_36(val)
   val[1] 
end
_rule_37(val) click to toggle source
# File lib/oga/css/parser.rb, line 876
def _rule_37(val)
   val[1] 
end
_rule_38(val) click to toggle source
# File lib/oga/css/parser.rb, line 880
def _rule_38(val)
  val[0]
end
_rule_39(val) click to toggle source
# File lib/oga/css/parser.rb, line 884
def _rule_39(val)
  val[0]
end
_rule_4(val) click to toggle source
# File lib/oga/css/parser.rb, line 644
def _rule_4(val)
  
    node  = s(:axis, 'descendant', val[0])
    preds = val[1]
    more  = val[2]

    if preds
      node = s(:predicate, node, preds)
    end

    node = add_child(node, more) if more

    node
  
end
_rule_40(val) click to toggle source
# File lib/oga/css/parser.rb, line 888
def _rule_40(val)
  val[0]
end
_rule_41(val) click to toggle source
# File lib/oga/css/parser.rb, line 892
def _rule_41(val)
  val[0]
end
_rule_42(val) click to toggle source
# File lib/oga/css/parser.rb, line 896
def _rule_42(val)
   s(:string, val[0]) 
end
_rule_43(val) click to toggle source
# File lib/oga/css/parser.rb, line 900
def _rule_43(val)
   s(:int, val[0].to_i) 
end
_rule_44(val) click to toggle source
# File lib/oga/css/parser.rb, line 904
def _rule_44(val)
  
    val[1] ? s(:nth, s(:int, 1), val[1]) : s(:nth, s(:int, 1))
  
end
_rule_45(val) click to toggle source
# File lib/oga/css/parser.rb, line 910
def _rule_45(val)
  
    val[2] ? s(:nth, s(:int, -1), val[2]) : s(:nth, s(:int, 1))
  
end
_rule_46(val) click to toggle source
# File lib/oga/css/parser.rb, line 916
def _rule_46(val)
  
    # 2n+1
    if val[1] and val[2]
      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)

    # 2n
    elsif val[1]
      s(:nth, val[0])

    # 2
    else
      val[0]
    end
  
end
_rule_47(val) click to toggle source
# File lib/oga/css/parser.rb, line 941
def _rule_47(val)
   :nth 
end
_rule_48(val) click to toggle source
# File lib/oga/css/parser.rb, line 945
def _rule_48(val)
   s(:nth, s(:int, 2), s(:int, 1)) 
end
_rule_49(val) click to toggle source
# File lib/oga/css/parser.rb, line 949
def _rule_49(val)
   s(:nth, s(:int, 2)) 
end
_rule_5(val) click to toggle source
# File lib/oga/css/parser.rb, line 660
def _rule_5(val)
  val[0]
end
_rule_50(val) click to toggle source
# File lib/oga/css/parser.rb, line 953
def _rule_50(val)
  val[0]
end
_rule_51(val) click to toggle source
# File lib/oga/css/parser.rb, line 957
def _rule_51(val)
  val[0]
end
_rule_52(val) click to toggle source
# File lib/oga/css/parser.rb, line 961
def _rule_52(val)
  val[0]
end
_rule_53(val) click to toggle source
# File lib/oga/css/parser.rb, line 965
def _rule_53(val)
  val[0]
end
_rule_54(val) click to toggle source
# File lib/oga/css/parser.rb, line 969
def _rule_54(val)
  val[0]
end
_rule_55(val) click to toggle source
# File lib/oga/css/parser.rb, line 973
def _rule_55(val)
  val[0]
end
_rule_56(val) click to toggle source
# File lib/oga/css/parser.rb, line 977
def _rule_56(val)
  val[0]
end
_rule_57(val) click to toggle source
# File lib/oga/css/parser.rb, line 981
def _rule_57(val)
  val[0]
end
_rule_58(val) click to toggle source
# File lib/oga/css/parser.rb, line 985
def _rule_58(val)
  val[0]
end
_rule_59(val) click to toggle source
# File lib/oga/css/parser.rb, line 989
def _rule_59(val)
  val[0]
end
_rule_6(val) click to toggle source
# File lib/oga/css/parser.rb, line 664
def _rule_6(val)
  
    node  = s(:axis, 'descendant', on_test(nil, '*'))
    preds = val[0]
    more  = val[1]

    if preds
      node = s(:predicate, node, preds)
    else
      node = s(:predicate, node)
    end

    node = add_child(node, more) if more

    node
  
end
_rule_60(val) click to toggle source
# File lib/oga/css/parser.rb, line 993
def _rule_60(val)
  val
end
_rule_61(val) click to toggle source
# File lib/oga/css/parser.rb, line 997
def _rule_61(val)
  val[0]
end
_rule_62(val) click to toggle source
# File lib/oga/css/parser.rb, line 1001
def _rule_62(val)
  val[0]
end
_rule_63(val) click to toggle source
# File lib/oga/css/parser.rb, line 1005
def _rule_63(val)
  val[0]
end
_rule_64(val) click to toggle source
# File lib/oga/css/parser.rb, line 1009
def _rule_64(val)
  val[0]
end
_rule_65(val) click to toggle source
# File lib/oga/css/parser.rb, line 1013
def _rule_65(val)
  val[0]
end
_rule_7(val) click to toggle source
# File lib/oga/css/parser.rb, line 682
def _rule_7(val)
   val[1] 
end
_rule_8(val) click to toggle source
# File lib/oga/css/parser.rb, line 686
def _rule_8(val)
  val[0]
end
_rule_9(val) click to toggle source
# File lib/oga/css/parser.rb, line 690
def _rule_9(val)
  
    test, preds = val[1]
    more        = val[2]

    generate_axis('child', test, preds, more)
  
end
add_child(node, child) click to toggle source

@param [AST::Node] node @param [AST::Node] child @return [AST::Node]

# File lib/oga/css/parser.rb, line 616
def add_child(node, child)
  node.updated(nil, node.children + [child])
end
generate_axis(name, test = nil, predicates = nil, more = nil) click to toggle source

@param [String] name @param [AST::Node] test @param [AST::Node] predicates @param [AST::Node] more @return [AST::Node]

# File lib/oga/css/parser.rb, line 597
def generate_axis(name, test = nil, predicates = nil, more = nil)
  if test
    node = s(:axis, name, test)
  else
    node = s(:axis, name, on_test(nil, '*'))
  end

  if predicates
    node = s(:predicate, node, predicates)
  end

  node = add_child(node, more) if more

  node
end
generate_no_siblings(axis, test = s(:test, nil, '*')) click to toggle source

@param [String] axis @param [AST::Node] test @return [AST::Node]

# File lib/oga/css/parser.rb, line 552
def generate_no_siblings(axis, test = s(:test, nil, '*'))
  s(:eq, s(:call, 'count', s(:axis, axis, test)), s(:int, 0))
end
generate_nth_child(count_axis, arg, count_test = s(:test, nil, '*')) click to toggle source

@param [String] count_axis @param [AST::Node] arg @param [AST::Node] count_test @return [AST::Node]

# File lib/oga/css/parser.rb, line 520
def generate_nth_child(count_axis, arg, count_test = s(:test, nil, '*'))
  count_call = s(:call, 'count', s(:axis, count_axis, count_test))

 # literal 2, 4, etc
  if int_node?(arg)
    node = s(:eq, count_call, s(:int, arg.children[0] - 1))
  else
    step, offset = *arg
    before_count = s(:add, count_call, s(:int, 1))
    compare      = step_comparison(step)

    # 2n+2, 2n-4, etc
    if offset
      mod_val = step_modulo_value(step)
      node    = s(
        :and,
        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
  end

  node
end
int_node?(node) click to toggle source

@param [AST::Node] node @return [TrueClass|FalseClass]

# File lib/oga/css/parser.rb, line 558
def int_node?(node)
  node.type.equal?(:int)
end
non_positive_number?(node) click to toggle source

@param [AST::Node] node @return [TrueClass|FalseClass]

# File lib/oga/css/parser.rb, line 564
def non_positive_number?(node)
  node.children[0] <= 0
end
step_comparison(node) click to toggle source

@param [AST::Node] node @return [Symbol]

# File lib/oga/css/parser.rb, line 570
def step_comparison(node)
  node.children[0] >= 0 ? :gte : :lte
end
step_modulo_value(step) click to toggle source

@param [AST::Node] step @return [AST::Node]

# File lib/oga/css/parser.rb, line 576
def step_modulo_value(step)
  # -2n
  if step and non_positive_number?(step)
    mod_val = s(:int, -step.children[0])

  # 2n
  elsif step
    mod_val = step

  else
    mod_val = s(:int, 1)
  end

  mod_val
end