Fixed processing of nested predicates.
This ensures that nested predicates and functions that depend on predicates are processed correctly.
This commit is contained in:
parent
3196050978
commit
bd31379c85
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue