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
@param [String] data The input to parse.
# File lib/oga/css/parser.rb, line 249 def initialize(data) @lexer = Lexer.new(data) end
@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
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
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
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
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
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
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
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
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
@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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Parses the input and returns the corresponding AST.
@example
parser = Oga::CSS::Parser.new('foo.bar') ast = parser.parse
@return [AST::Node]
# File lib/oga/css/parser.rb, line 290 def parse reset super end
Resets the internal state of the parser.
# File lib/oga/css/parser.rb, line 254 def reset @current_element = nil end
@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
# File lib/oga/css/parser.rb, line 620 def _rule_0(val) val[0] end
# File lib/oga/css/parser.rb, line 624 def _rule_1(val) nil end
# 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
# 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
# File lib/oga/css/parser.rb, line 722 def _rule_12(val) val end
# File lib/oga/css/parser.rb, line 726 def _rule_13(val) [nil, val[0]] end
# File lib/oga/css/parser.rb, line 730 def _rule_14(val) val[0] end
# File lib/oga/css/parser.rb, line 734 def _rule_15(val) val[0] end
# File lib/oga/css/parser.rb, line 738 def _rule_16(val) val[0] end
# File lib/oga/css/parser.rb, line 742 def _rule_17(val) on_test(*val[0]) end
# File lib/oga/css/parser.rb, line 746 def _rule_18(val) val[1] ? [val[0], val[1]] : [nil, val[0]] end
# File lib/oga/css/parser.rb, line 750 def _rule_19(val) val[1] end
# 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
# 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
# File lib/oga/css/parser.rb, line 766 def _rule_21(val) val[0] end
# File lib/oga/css/parser.rb, line 770 def _rule_22(val) val[0] end
# File lib/oga/css/parser.rb, line 774 def _rule_23(val) val[1] end
# File lib/oga/css/parser.rb, line 778 def _rule_24(val) val[0] end
# File lib/oga/css/parser.rb, line 782 def _rule_25(val) s(:axis, 'attribute', on_test(*val[0])) end
# 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
# File lib/oga/css/parser.rb, line 821 def _rule_27(val) :eq end
# File lib/oga/css/parser.rb, line 825 def _rule_28(val) :space_in end
# File lib/oga/css/parser.rb, line 829 def _rule_29(val) :starts_with end
# File lib/oga/css/parser.rb, line 640 def _rule_3(val) val[1] end
# File lib/oga/css/parser.rb, line 833 def _rule_30(val) :ends_with end
# File lib/oga/css/parser.rb, line 837 def _rule_31(val) :in end
# File lib/oga/css/parser.rb, line 841 def _rule_32(val) :hyphen_in end
# 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
# 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
# File lib/oga/css/parser.rb, line 868 def _rule_35(val) on_pseudo_class(val[0], val[1]) end
# File lib/oga/css/parser.rb, line 872 def _rule_36(val) val[1] end
# File lib/oga/css/parser.rb, line 876 def _rule_37(val) val[1] end
# File lib/oga/css/parser.rb, line 880 def _rule_38(val) val[0] end
# File lib/oga/css/parser.rb, line 884 def _rule_39(val) val[0] end
# 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
# File lib/oga/css/parser.rb, line 888 def _rule_40(val) val[0] end
# File lib/oga/css/parser.rb, line 892 def _rule_41(val) val[0] end
# File lib/oga/css/parser.rb, line 896 def _rule_42(val) s(:string, val[0]) end
# File lib/oga/css/parser.rb, line 900 def _rule_43(val) s(:int, val[0].to_i) end
# 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
# 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
# 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
# File lib/oga/css/parser.rb, line 941 def _rule_47(val) :nth end
# File lib/oga/css/parser.rb, line 945 def _rule_48(val) s(:nth, s(:int, 2), s(:int, 1)) end
# File lib/oga/css/parser.rb, line 949 def _rule_49(val) s(:nth, s(:int, 2)) end
# File lib/oga/css/parser.rb, line 660 def _rule_5(val) val[0] end
# File lib/oga/css/parser.rb, line 953 def _rule_50(val) val[0] end
# File lib/oga/css/parser.rb, line 957 def _rule_51(val) val[0] end
# File lib/oga/css/parser.rb, line 961 def _rule_52(val) val[0] end
# File lib/oga/css/parser.rb, line 965 def _rule_53(val) val[0] end
# File lib/oga/css/parser.rb, line 969 def _rule_54(val) val[0] end
# File lib/oga/css/parser.rb, line 973 def _rule_55(val) val[0] end
# File lib/oga/css/parser.rb, line 977 def _rule_56(val) val[0] end
# File lib/oga/css/parser.rb, line 981 def _rule_57(val) val[0] end
# File lib/oga/css/parser.rb, line 985 def _rule_58(val) val[0] end
# File lib/oga/css/parser.rb, line 989 def _rule_59(val) val[0] end
# 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
# File lib/oga/css/parser.rb, line 993 def _rule_60(val) val end
# File lib/oga/css/parser.rb, line 997 def _rule_61(val) val[0] end
# File lib/oga/css/parser.rb, line 1001 def _rule_62(val) val[0] end
# File lib/oga/css/parser.rb, line 1005 def _rule_63(val) val[0] end
# File lib/oga/css/parser.rb, line 1009 def _rule_64(val) val[0] end
# File lib/oga/css/parser.rb, line 1013 def _rule_65(val) val[0] end
# File lib/oga/css/parser.rb, line 682 def _rule_7(val) val[1] end
# File lib/oga/css/parser.rb, line 686 def _rule_8(val) val[0] end
# 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
@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
@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
@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
@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
@param [AST::Node] node @return [TrueClass|FalseClass]
# File lib/oga/css/parser.rb, line 558 def int_node?(node) node.type.equal?(:int) end
@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
@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
@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