From 2ff1f9ab4f2ed37c22dbae4e2d90979fe4fa420b Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Fri, 7 Aug 2015 17:09:10 +0200 Subject: [PATCH] XPath compiler support for a bucket of functions This includes the following functions: * boolean() * ceiling() * floor() * round() * concat() * contains() * count() --- lib/oga/xpath/compiler.rb | 164 +++++++++++++++++++++++++++++++++----- 1 file changed, 144 insertions(+), 20 deletions(-) diff --git a/lib/oga/xpath/compiler.rb b/lib/oga/xpath/compiler.rb index dd91a96..b624f29 100644 --- a/lib/oga/xpath/compiler.rb +++ b/lib/oga/xpath/compiler.rb @@ -649,31 +649,151 @@ module Oga # @return [Oga::Ruby::Node] # def on_call(ast, input, &block) - name, args = *ast + name, *args = *ast handler = name.gsub('-', '_') send(:"on_call_#{handler}", input, *args, &block) end - ## - # Processes the `true()` function call. - # # @return [Oga::Ruby::Node] - # def on_call_true(*) self.true end - ## - # Processes the `false()` function call. - # # @return [Oga::Ruby::Node] - # def on_call_false(*) self.false end + # @param [Oga::Ruby::Node] input + # @param [AST::Node] arg + # @return [Oga::Ruby::Node] + def on_call_boolean(input, arg) + arg_ast = try_match_first_node(arg, input) + call_arg = unique_literal(:call_arg) + conversion = literal(Conversion) + + call_arg.assign(arg_ast) + .followed_by(conversion.to_boolean(call_arg)) + end + + # @param [Oga::Ruby::Node] input + # @param [AST::Node] arg + # @return [Oga::Ruby::Node] + def on_call_ceiling(input, arg) + arg_ast = try_match_first_node(arg, input) + call_arg = unique_literal(:call_arg) + conversion = literal(Conversion) + + initial_assign = call_arg.assign(arg_ast) + float_assign = call_arg.assign(conversion.to_float(call_arg)) + + if_nan = call_arg.nan? + .if_true { call_arg } + .else { call_arg.ceil.to_f } + + initial_assign.followed_by(float_assign) + .followed_by(if_nan) + end + + # @param [Oga::Ruby::Node] input + # @param [AST::Node] arg + # @return [Oga::Ruby::Node] + def on_call_floor(input, arg) + arg_ast = try_match_first_node(arg, input) + call_arg = unique_literal(:call_arg) + conversion = literal(Conversion) + + initial_assign = call_arg.assign(arg_ast) + float_assign = call_arg.assign(conversion.to_float(call_arg)) + + if_nan = call_arg.nan? + .if_true { call_arg } + .else { call_arg.floor.to_f } + + initial_assign.followed_by(float_assign) + .followed_by(if_nan) + end + + # @param [Oga::Ruby::Node] input + # @param [AST::Node] arg + # @return [Oga::Ruby::Node] + def on_call_round(input, arg) + arg_ast = try_match_first_node(arg, input) + call_arg = unique_literal(:call_arg) + conversion = literal(Conversion) + + initial_assign = call_arg.assign(arg_ast) + float_assign = call_arg.assign(conversion.to_float(call_arg)) + + if_nan = call_arg.nan? + .if_true { call_arg } + .else { call_arg.round.to_f } + + initial_assign.followed_by(float_assign) + .followed_by(if_nan) + end + + # @param [Oga::Ruby::Node] input + # @param [Array] args + # @return [Oga::Ruby::Node] + def on_call_concat(input, *args) + conversion = literal(Conversion) + assigns = [] + conversions = [] + + args.each do |arg| + arg_var = unique_literal(:concat_arg) + arg_ast = try_match_first_node(arg, input) + + assigns << arg_var.assign(arg_ast) + conversions << conversion.to_string(arg_var) + end + + assigns.inject(:followed_by) + .followed_by(conversions.inject(:+)) + end + + # @param [Oga::Ruby::Node] input + # @param [AST::Node] haystack_ast + # @param [AST::Node] needle_ast + # @return [Oga::Ruby::Node] + def on_call_contains(input, haystack, needle) + haystack_lit = unique_literal(:haystack) + needle_lit = unique_literal(:needle) + conversion = literal(Conversion) + + haystack_ast = try_match_first_node(haystack, input) + needle_ast = try_match_first_node(needle, input) + + include_call = conversion.to_string(haystack_lit) + .include?(conversion.to_string(needle_lit)) + + haystack_lit.assign(haystack_ast) + .followed_by(needle_lit.assign(needle_ast)) + .followed_by(include_call) + end + + # @param [Oga::Ruby::Node] input + # @param [AST::Node] arg + # @return [Oga::Ruby::Node] + def on_call_count(input, arg) + count = unique_literal(:count) + assign = count.assign(literal('0.0')) + + unless return_nodeset?(arg) + raise TypeError, 'count() can only operate on NodeSet instances' + end + + increment = process(arg, input) do + count.assign(count + literal('1')) + end + + assign.followed_by(increment) + .followed_by(count) + end + ## # Delegates type tests to specific handlers. # @@ -818,6 +938,19 @@ module Oga end end + ## + # Tries to match the first node in a set, otherwise processes it as usual. + # + # @see [#match_first_node] + # + def try_match_first_node(ast, input, optimize_first = true) + if return_nodeset?(ast) and optimize_first + match_first_node(ast, input) + else + process(ast, input) + end + end + ## # Generates the code for an operator. # @@ -887,17 +1020,8 @@ module Oga left_var = unique_literal(:op_left) right_var = unique_literal(:op_right) - if return_nodeset?(left) and optimize_first - left_ast = match_first_node(left, input) - else - left_ast = process(left, input) - end - - if return_nodeset?(right) and optimize_first - right_ast = match_first_node(right, input) - else - right_ast = process(right, input) - end + left_ast = try_match_first_node(left, input, optimize_first) + right_ast = try_match_first_node(right, input, optimize_first) initial_assign = left_var.assign(left_ast.wrap) .followed_by(right_var.assign(right_ast.wrap))