diff --git a/lib/oga/xml/querying.rb b/lib/oga/xml/querying.rb index 1eee81c..80691b0 100644 --- a/lib/oga/xml/querying.rb +++ b/lib/oga/xml/querying.rb @@ -10,6 +10,7 @@ module Oga # document = Oga.parse_xml <<-EOF # # Alice + # Bob # # EOF # @@ -25,15 +26,23 @@ module Oga # # document.xpath('people/person[@age = $age]', 'age' => 25) # + # Using namespace aliases: + # + # namespaces = {'example' => 'http://example.net'} + # document.xpath('people/example:person', namespaces: namespaces) + # # @param [String] expression The XPath expression to run. # # @param [Hash] variables Variables to bind. The keys of this Hash should # be String values. # + # @param [Hash] namespaces Namespace aliases. The keys of this Hash should + # be String values. + # # @return [Oga::XML::NodeSet] - def xpath(expression, variables = {}) + def xpath(expression, variables = {}, namespaces: nil) ast = XPath::Parser.parse_with_cache(expression) - block = XPath::Compiler.compile_with_cache(ast) + block = XPath::Compiler.compile_with_cache(ast, namespaces: namespaces) block.call(self, variables) end diff --git a/lib/oga/xpath/compiler.rb b/lib/oga/xpath/compiler.rb index ec711b3..d407fdf 100644 --- a/lib/oga/xpath/compiler.rb +++ b/lib/oga/xpath/compiler.rb @@ -42,12 +42,16 @@ module Oga # Compiles and caches an AST. # # @see [#compile] - def self.compile_with_cache(ast) - CACHE.get_or_set(ast) { new.compile(ast) } + def self.compile_with_cache(ast, namespaces: nil) + cache_key = namespaces ? [ast, namespaces] : ast + CACHE.get_or_set(cache_key) { new(namespaces: namespaces).compile(ast) } end - def initialize + # @param [Hash] namespaces + def initialize(namespaces: nil) reset + + @namespaces = namespaces end # Resets the internal state. @@ -1385,7 +1389,18 @@ module Oga end if ns and ns != STAR - ns_match = input.namespace_name.eq(string(ns)) + if @namespaces + ns_uri = @namespaces[ns] + ns_match = + if ns_uri + input.namespace.and(input.namespace.uri.eq(string(ns_uri))) + else + self.false + end + else + ns_match = input.namespace_name.eq(string(ns)) + end + condition = condition ? condition.and(ns_match) : ns_match end diff --git a/spec/oga/xml/querying_spec.rb b/spec/oga/xml/querying_spec.rb index e0400e1..df20813 100644 --- a/spec/oga/xml/querying_spec.rb +++ b/spec/oga/xml/querying_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Oga::XML::Querying do before do @document = parse('foo') + @document2 = parse('bar') + @namespaces = { + "n" => "y" + } end describe '#xpath' do @@ -17,6 +21,10 @@ describe Oga::XML::Querying do it 'evaluates an expression using a variable' do expect(@document.xpath('$number', 'number' => 10)).to eq(10) end + + it 'respects custom namespace aliases' do + expect(@document2.xpath('a/n:b', namespaces: @namespaces)[0].text).to eq('bar') + end end describe '#at_xpath' do @@ -31,6 +39,10 @@ describe Oga::XML::Querying do it 'evaluates an expression using a variable' do expect(@document.at_xpath('$number', 'number' => 10)).to eq(10) end + + it 'respects custom namespace aliases' do + expect(@document2.at_xpath('a/n:b', namespaces: @namespaces).text).to eq('bar') + end end describe '#css' do diff --git a/spec/oga/xpath/compiler/namespace_aliases_spec.rb b/spec/oga/xpath/compiler/namespace_aliases_spec.rb new file mode 100644 index 0000000..98726b7 --- /dev/null +++ b/spec/oga/xpath/compiler/namespace_aliases_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Oga::XPath::Compiler do + before do + @document = parse('') + @root = @document.children[0] + @a = @root.children[0] + @b = @root.children[1] + @attr = @b.attributes[0] + @namespaces = { + "n" => "y" + } + end + + describe 'with custom namespace aliases' do + it 'uses aliases when querying an element' do + expect(evaluate_xpath(@document, 'root/n:a', namespaces: @namespaces)).to eq(node_set(@a)) + end + + it "doesn't use namespaces in XPath expression when querying an element" do + expect(evaluate_xpath(@document, 'root/x:a', namespaces: @namespaces)).to eq(node_set) + end + + it 'uses aliases when querying an attribute' do + expect(evaluate_xpath(@document, 'root/b/@n:num', namespaces: @namespaces)).to eq(node_set(@attr)) + end + + it "doesn't use namespaces in XPath expression when querying an attribute" do + expect(evaluate_xpath(@document, 'root/b/@x:num', namespaces: @namespaces)).to eq(node_set) + end + + it 'uses aliases when querying an element with a namespaced attribute' do + expect(evaluate_xpath(@document, 'root/b[@n:num]', namespaces: @namespaces)).to eq(node_set(@b)) + end + + it "doesn't use namespaces in XPath expression when querying an element with a namespaced attribute" do + expect(evaluate_xpath(@document, 'root/b[@x:num]', namespaces: @namespaces)).to eq(node_set) + end + end +end diff --git a/spec/support/evaluation_helpers.rb b/spec/support/evaluation_helpers.rb index 5bcbb55..f006d8f 100644 --- a/spec/support/evaluation_helpers.rb +++ b/spec/support/evaluation_helpers.rb @@ -5,9 +5,9 @@ module Oga # @param [String] xpath # @return [Oga::XML::NodeSet] # - def evaluate_xpath(document, xpath = self.class.description) + def evaluate_xpath(document, xpath = self.class.description, namespaces: nil) ast = parse_xpath(xpath) - compiler = Oga::XPath::Compiler.new + compiler = Oga::XPath::Compiler.new(namespaces: namespaces) block = compiler.compile(ast) block.call(document)