XPath compiler support for predicates

This currently supports index predicates (e.g. "foo[10]") and predicates
using paths (e.g. foo[bar/baz]). The usage of boolean operators and more
complex expressions has not yet been tested as these are not yet
supported in the first place.
This commit is contained in:
Yorick Peterse 2015-07-14 00:08:26 +02:00
parent 7fdf8d7460
commit 9925b2a9c9
1 changed files with 106 additions and 10 deletions

View File

@ -177,8 +177,72 @@ module Oga
def on_predicate(ast, input, &block)
test, predicate = *ast
process(test, input) do |node|
process(predicate, node).if_true(&block)
if xpath_number?(predicate)
index_predicate(test, predicate, input, &block)
else
expression_predicate(test, predicate, input, &block)
end
end
##
# Processes an index predicate such as `foo[10]`.
#
# @param [AST::Node] test
# @param [AST::Node] predicate
# @param [Oga::Ruby::Node] input
# @return [Oga::Ruby::Node]
#
def index_predicate(test, predicate, input)
int1 = literal('1')
index = on_int(predicate)
index_var = literal('index')
inner = process(test, input) do |matched_test_node|
index_var.eq(index).if_true { yield matched_test_node }
.followed_by(index_var.assign(index_var + int1))
end
index_var.assign(int1).followed_by(inner)
end
##
# Processes a predicate using an expression.
#
# This method generates Ruby code that roughly looks like the following:
#
# if catch :predicate_matched do
# node.children.each do |node|
#
# if some_condition_that_matches_a_predicate
# throw :predicate_matched, true
# end
#
# nil
# end
#
# matched.push(node)
# end
#
# @param [AST::Node] test
# @param [AST::Node] predicate
# @param [Oga::Ruby::Node] input
# @return [Oga::Ruby::Node]
#
def expression_predicate(test, predicate, input)
catch_arg = symbol(:predicate_matched)
process(test, input) do |matched_test_node|
catch_block = send_message('catch', catch_arg).add_block do
inner = process(predicate, matched_test_node) do
send_message('throw', catch_arg, literal('true'))
end
# Ensure that the "catch" only returns a value when "throw" is
# actually invoked.
inner.followed_by(literal('nil'))
end
catch_block.if_true { yield matched_test_node }
end
end
@ -225,7 +289,7 @@ module Oga
# @param [Oga::Ruby::Node] input
# @return [Oga::Ruby::Node]
#
def on_string(ast, input)
def on_string(ast, *)
string(ast.children[0])
end
@ -236,7 +300,18 @@ module Oga
# @param [Oga::Ruby::Node] input
# @return [Oga::Ruby::Node]
#
def on_int(ast, input)
def on_int(ast, *)
literal(ast.children[0].to_i.to_s)
end
##
# Processes a float.
#
# @param [AST::Node] ast
# @param [Oga::Ruby::Node] input
# @return [Oga::Ruby::Node]
#
def on_float(ast, *)
literal(ast.children[0].to_s)
end
@ -251,12 +326,8 @@ module Oga
vars = variables_literal
name = ast.children[0]
raise_call = Ruby::Node.new(
:send,
[nil, 'raise', string("Undefined XPath variable: #{name}")]
)
variables_literal.and(variables_literal[string(name)]).or(raise_call)
variables_literal.and(variables_literal[string(name)])
.or(send_message('raise', string("Undefined XPath variable: #{name}")))
end
private
@ -277,6 +348,23 @@ module Oga
Ruby::Node.new(:string, [value.to_s])
end
##
# @param [String] value
# @return [Oga::Ruby::Node]
#
def symbol(value)
Ruby::Node.new(:symbol, [value.to_sym])
end
##
# @param [String] name
# @param [Array] args
# @return [Oga::Ruby::Node]
#
def send_message(name, *args)
Ruby::Node.new(:send, [nil, name, *args])
end
##
# @param [Oga::Ruby::Node] node
# @return [Oga::Ruby::Node]
@ -299,6 +387,14 @@ module Oga
def variables_literal
literal('variables')
end
##
# @param [AST::Node] ast
# @return [TrueClass|FalseClass]
#
def xpath_number?(ast)
ast.type == :int || ast.type == :float
end
end # Compiler
end # XPath
end # Oga