diff --git a/lib/oga/xpath/evaluator.rb b/lib/oga/xpath/evaluator.rb
index df1eef7..12f871f 100644
--- a/lib/oga/xpath/evaluator.rb
+++ b/lib/oga/xpath/evaluator.rb
@@ -99,6 +99,8 @@ module Oga
def on_absolute_path(ast_node, context)
if @document.respond_to?(:root_node)
context = XML::NodeSet.new([@document.root_node])
+ else
+ context = XML::NodeSet.new([@document])
end
return on_path(ast_node, context)
@@ -651,6 +653,46 @@ module Oga
return retval.length.to_f
end
+ ##
+ # Processes the `id()` function call.
+ #
+ # The XPath specification states that this function's behaviour should be
+ # controlled by a DTD. If a DTD were to specify that the ID attribute for
+ # a certain element would be "foo" then this function should use said
+ # attribute.
+ #
+ # Oga does not support DTD parsing/evaluation and as such always uses the
+ # "id" attribute.
+ #
+ # This function searches the entire document for a matching node,
+ # regardless of the current position.
+ #
+ # @param [Oga::XML::NodeSet] context
+ # @param [Oga::XPath::Node] expression
+ # @return [Oga::XML::NodeSet]
+ #
+ def on_call_id(context, expression)
+ id = process(expression, context)
+ nodes = XML::NodeSet.new
+
+ # Based on Nokogiri's/libxml behaviour it appears that when using a node
+ # set the text of the set is used as the ID.
+ id = id.is_a?(XML::NodeSet) ? id.text : id.to_s
+ ids = id.split(' ')
+
+ @document.each_node do |node|
+ next unless node.is_a?(XML::Element)
+
+ attr = node.attribute('id')
+
+ if attr and ids.include?(attr.value)
+ nodes << node
+ end
+ end
+
+ return nodes
+ end
+
##
# Processes an `(int)` node. This method simply returns the value as a
# Float.
@@ -663,6 +705,17 @@ module Oga
return ast_node.children[0].to_f
end
+ ##
+ # Processes a `(string)` node.
+ #
+ # @param [Oga::XPath::Node] ast_node
+ # @param [Oga::XML::NodeSet] context
+ # @return [String]
+ #
+ def on_string(ast_node, context)
+ return ast_node.children[0]
+ end
+
##
# Returns a node set containing all the child nodes of the given set of
# nodes.
diff --git a/spec/oga/xpath/evaluator/calls/id_spec.rb b/spec/oga/xpath/evaluator/calls/id_spec.rb
new file mode 100644
index 0000000..7f17b75
--- /dev/null
+++ b/spec/oga/xpath/evaluator/calls/id_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Oga::XPath::Evaluator do
+ context 'id() function' do
+ before do
+ @document = parse('a1')
+ @first_a = @document.children[0].children[0]
+ @second_a = @document.children[0].children[1]
+ @evaluator = described_class.new(@document)
+ end
+
+ context 'using a single string ID' do
+ before do
+ @set = @evaluator.evaluate('id("a1")')
+ end
+
+ it_behaves_like :node_set, :length => 1
+
+ example 'return the first node' do
+ @set[0].should == @first_a
+ end
+ end
+
+ context 'using multiple string IDs' do
+ before do
+ @set = @evaluator.evaluate('id("a1 a2")')
+ end
+
+ it_behaves_like :node_set, :length => 2
+
+ example 'return the first node' do
+ @set[0].should == @first_a
+ end
+
+ example 'return the second node' do
+ @set[1].should == @second_a
+ end
+ end
+
+ context 'using a node set' do
+ before do
+ @set = @evaluator.evaluate('id(root/a[2])')
+ end
+
+ it_behaves_like :node_set, :length => 1
+
+ example 'return the first node' do
+ @set[0].should == @first_a
+ end
+ end
+ end
+end
diff --git a/spec/oga/xpath/evaluator/types/string_spec.rb b/spec/oga/xpath/evaluator/types/string_spec.rb
new file mode 100644
index 0000000..f310350
--- /dev/null
+++ b/spec/oga/xpath/evaluator/types/string_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe Oga::XPath::Evaluator do
+ context 'string types' do
+ before do
+ document = parse('')
+ evaluator = described_class.new(document)
+ @string = evaluator.evaluate('"foo"')
+ end
+
+ example 'return the literal string' do
+ @string.should == 'foo'
+ end
+ end
+end