Add support for XPath namespace aliases

This fixes https://gitlab.com/yorickpeterse/oga/issues/176
This commit is contained in:
KitaitiMakoto 2019-11-29 14:21:45 +00:00 committed by Yorick Peterse
parent da9721cb34
commit 977bd594c8
5 changed files with 84 additions and 8 deletions

View File

@ -10,6 +10,7 @@ module Oga
# document = Oga.parse_xml <<-EOF
# <people>
# <person age="25">Alice</person>
# <ns:person xmlns:ns="http://example.net">Bob</ns:person>
# </people>
# 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

View File

@ -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
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

View File

@ -3,6 +3,10 @@ require 'spec_helper'
describe Oga::XML::Querying do
before do
@document = parse('<a>foo</a>')
@document2 = parse('<a xmlns:x="y"><x:b>bar</x:b></a>')
@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

View File

@ -0,0 +1,40 @@
require 'spec_helper'
describe Oga::XPath::Compiler do
before do
@document = parse('<root xmlns:x="y"><x:a></x:a><b x:num="10"></b></root>')
@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

View File

@ -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)