Fixed processing of nested predicates.

This ensures that nested predicates and functions that depend on predicates are
processed correctly.
This commit is contained in:
Yorick Peterse 2014-09-03 20:56:07 +02:00
parent 3196050978
commit bd31379c85
2 changed files with 51 additions and 97 deletions

View File

@ -11,19 +11,19 @@ module Oga
# for more information). It is however perfectly fine to use multiple # for more information). It is however perfectly fine to use multiple
# separated instances as this class does not use a thread global state. # separated instances as this class does not use a thread global state.
# #
# ## Node Stack # ## Node Set Stack
# #
# This class uses an internal stack of XML nodes. This stack is used for # This class uses an internal stack of XML node sets. This stack is used for
# certain XPath functions that require access to the current node being # functions that require access to the set of nodes a predicate belongs to.
# processed in a predicate. An example of such a function is `position()`. # An example of such a function is `position()`.
# #
# An alternative to a stack would be to pass the current node as arguments # An alternative would be to pass the node sets a predicate belongs to as an
# to the various `on_*` methods. The problematic part of this approach is # extra argument to the various `on_*` methods. The problematic part of
# that it requires every method to take and pass along the argument. It's # this approach is that it requires every method to take and pass along the
# far too easy to make mistakes in such a setup and as such I've chosen to # argument. It's far too easy to make mistakes in such a setup and as such
# use an internal stack instead. # I've chosen to use an internal stack instead.
# #
# See {#with_node} and {#current_node} for more information. # See {#with_node_set} and {#current_node_set} for more information.
# #
# ## Set Indices # ## Set Indices
# #
@ -66,8 +66,8 @@ module Oga
# #
def initialize(document, variables = {}) def initialize(document, variables = {})
@document = document @document = document
@nodes = [[]]
@variables = variables @variables = variables
@node_sets = []
end end
## ##
@ -147,15 +147,13 @@ module Oga
def on_path(ast_node, context) def on_path(ast_node, context)
nodes = XML::NodeSet.new nodes = XML::NodeSet.new
with_node_stack do ast_node.children.each do |test|
ast_node.children.each do |test| nodes = process(test, context)
nodes = process(test, context)
if nodes.empty? if nodes.empty?
break break
else else
context = nodes context = nodes
end
end end
end end
@ -174,34 +172,31 @@ module Oga
nodes = XML::NodeSet.new nodes = XML::NodeSet.new
predicate = ast_node.children[2] predicate = ast_node.children[2]
context.each do |xml_node| context.each_with_index do |xml_node, index|
nodes << xml_node if node_matches?(xml_node, ast_node) next unless node_matches?(xml_node, ast_node)
end
# Filter the nodes based on the predicate. if predicate
if predicate
new_nodes = XML::NodeSet.new
nodes.each_with_index do |current, index|
xpath_index = index + 1 xpath_index = index + 1
retval = with_node(current) { process(predicate, nodes) }
retval = with_node_set(context) do
process(predicate, XML::NodeSet.new([xml_node]))
end
# Numeric values are used as node set indexes. # Numeric values are used as node set indexes.
if retval.is_a?(Numeric) if retval.is_a?(Numeric)
new_nodes << current if retval.to_i == xpath_index nodes << xml_node if retval.to_i == xpath_index
# Node sets, strings, booleans, etc # Node sets, strings, booleans, etc
elsif retval elsif retval
# Empty strings and node sets evaluate to false.
if retval.respond_to?(:empty?) and retval.empty? if retval.respond_to?(:empty?) and retval.empty?
next next
end end
new_nodes << current nodes << xml_node
end end
else
nodes << xml_node
end end
nodes = new_nodes
end end
return nodes return nodes
@ -492,12 +487,8 @@ module Oga
def on_axis_self(ast_node, context) def on_axis_self(ast_node, context)
nodes = XML::NodeSet.new nodes = XML::NodeSet.new
if current_node context.each do |context_node|
nodes << current_node if node_matches?(current_node, ast_node) nodes << context_node if node_matches?(context_node, ast_node)
else
context.each do |context_node|
nodes << context_node if node_matches?(context_node, ast_node)
end
end end
return nodes return nodes
@ -883,7 +874,7 @@ module Oga
# #
def on_call_last(context) def on_call_last(context)
# XPath uses indexes 1 to N instead of 0 to N. # XPath uses indexes 1 to N instead of 0 to N.
return context.length.to_f return current_node_set.length.to_f
end end
## ##
@ -894,7 +885,7 @@ module Oga
# @return [Float] # @return [Float]
# #
def on_call_position(context) def on_call_position(context)
index = context.index(current_node) + 1 index = current_node_set.index(context.first) + 1
return index.to_f return index.to_f
end end
@ -1044,7 +1035,7 @@ module Oga
convert = convert[0] convert = convert[0]
end end
else else
convert = current_node convert = context.first
end end
if convert.respond_to?(:text) if convert.respond_to?(:text)
@ -1087,7 +1078,7 @@ module Oga
convert = exp_retval convert = exp_retval
end end
else else
convert = current_node.text convert = context.first.text
end end
return to_float(convert) return to_float(convert)
@ -1395,7 +1386,7 @@ module Oga
# #
def on_call_lang(context, language) def on_call_lang(context, language)
lang_str = on_call_string(context, language) lang_str = on_call_string(context, language)
node = current_node node = context.first
while node.respond_to?(:attribute) while node.respond_to?(:attribute)
found = node.attribute('xml:lang') found = node.attribute('xml:lang')
@ -1560,7 +1551,7 @@ module Oga
raise TypeError, 'only node sets can be used as arguments' raise TypeError, 'only node sets can be used as arguments'
end end
else else
node = current_node node = context.first
end end
return node return node
@ -1717,65 +1708,31 @@ module Oga
end end
## ##
# Stores the node in the node stack, yields the block and removes the node # Stores the specified node set and yields the supplied block. The return
# from the stack. # value of this method is whatever the block returned.
#
# This method is mainly intended to be used when processing predicates.
# Expressions inside a predicate might need access to the node on which
# the predicate is performed.
#
# This method's return value is the same as whatever the block returned.
# #
# @example # @example
# some_node_set.each do |node| # retval = with_node_set(context) do
# result = with_node(node) { process(...) } # process(....)
# end # end
# #
# @param [Oga::XML::Node] node # @param [Oga::XML::NodeSet] nodes
# @return [Mixed]
# #
def with_node(node) def with_node_set(nodes)
node_stack << node @node_sets << nodes
retval = yield retval = yield
node_stack.pop @node_sets.pop
return retval return retval
end end
## ##
# Adds a new stack for storing context nodes and yields the supplied # @return [Oga::XML::NodeSet]
# block.
# #
# The return value of this method is whatever the supplied block returns. def current_node_set
# return @node_sets.last
# @return [Mixed]
#
def with_node_stack
@nodes << []
retval = yield
@nodes.pop
return retval
end
##
# Returns the current node that's being processed.
#
# @return [Oga::XML::Node]
#
def current_node
return node_stack.last
end
##
# @return [Array]
#
def node_stack
return @nodes.last
end end
end # Evaluator end # Evaluator
end # XPath end # XPath

View File

@ -8,14 +8,15 @@ describe Oga::XPath::Evaluator do
context '#function_node' do context '#function_node' do
before do before do
@context_set = Oga::XML::NodeSet.new([@document]) @document = parse('<root><a>Hello</a></root>')
@context_set = @document.children
end end
example 'return the first node in the expression' do example 'return the first node in the expression' do
exp = s(:axis, 'child', s(:test, nil, 'a')) exp = s(:axis, 'child', s(:test, nil, 'a'))
node = @evaluator.function_node(@context_set, exp) node = @evaluator.function_node(@context_set, exp)
node.should == @document.children[0] node.should == @context_set[0].children[0]
end end
example 'raise a TypeError if the expression did not return a node set' do example 'raise a TypeError if the expression did not return a node set' do
@ -26,11 +27,7 @@ describe Oga::XPath::Evaluator do
end end
example 'use the current context node if the expression is empty' do example 'use the current context node if the expression is empty' do
a_node = @document.children[0] @evaluator.function_node(@context_set).should == @context_set[0]
@evaluator.stub(:current_node).and_return(a_node)
@evaluator.function_node(@context_set).should == a_node
end end
end end