From 33c28f633b065c9612cee5314d3b0e4d58a4809d Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 11 Aug 2014 00:41:36 +0200 Subject: [PATCH] Proper namespace support for elements. This is still a bit rough on the edges but already way better than the broken setup I had before. --- lib/oga/xml/element.rb | 109 +++++++++++++++--------- lib/oga/xml/parser.y | 10 +-- spec/oga/xml/element_spec.rb | 123 ++++++++++++++++++++++----- spec/oga/xml/namespace_spec.rb | 4 +- spec/oga/xml/parser/elements_spec.rb | 32 ++++++- 5 files changed, 207 insertions(+), 71 deletions(-) diff --git a/lib/oga/xml/element.rb b/lib/oga/xml/element.rb index 9b490cd..48aae24 100644 --- a/lib/oga/xml/element.rb +++ b/lib/oga/xml/element.rb @@ -8,47 +8,48 @@ module Oga # The name of the element. # @return [String] # - # @!attribute [rw] namespace - # The namespace of the element, if any. - # @return [Oga::XML::Namespace] + # @!attribute [ww] namespace_name + # The name of the namespace. + # @return [String] # # @!attribute [rw] attributes # The attributes of the element. # @return [Array] # + # @!attribute [rw] namespaces + # The registered namespaces. + # @return [Hash] + # class Element < Node - attr_accessor :name, :namespace, :attributes + attr_accessor :name, :namespace_name, :attributes, :namespaces ## - # List of options that can be passed to the constructor and the required - # types of their values. + # The attribute prefix/namespace used for registering element namespaces. # - # @return [Hash] + # @return [String] # - OPTION_TYPES = { - :namespace => Namespace, - :attributes => Array - } + XMLNS_PREFIX = 'xmlns'.freeze ## # @param [Hash] options # # @option options [String] :name The name of the element. # - # @option options [Oga::XML::Namespace] :namespace The namespace of the - # element. + # @option options [String] :namespace_name The name of the namespace. # # @option options [Array] :attributes The attributes # of the element as an Array. # def initialize(options = {}) - validate_option_types!(options) - super - @name = options[:name] - @namespace = options[:namespace] - @attributes = options[:attributes] || [] + @name = options[:name] + @namespace_name = options[:namespace_name] + @attributes = options[:attributes] || [] + @namespaces = options[:namespaces] || {} + + link_attributes + register_namespaces_from_attributes end ## @@ -76,6 +77,15 @@ module Oga alias_method :attr, :attribute + ## + # Returns the namespace of the element. + # + # @return [Oga::XML::Namespace] + # + def namespace + return @namespace ||= available_namespaces[namespace_name] + end + ## # Returns the text of all child nodes joined together. # @@ -146,40 +156,63 @@ module Oga end ## - # Returns a node set of all the namespaces that are available to the - # current node. This includes the namespaces registered on the current - # node. + # Registers a new namespace for the current element and its child + # elements. # - # @return [Oga::XML::NodeSet] + # @param [String] name + # @param [String] uri + # @see [Oga::XML::Namespace#initialize] # - def available_namespaces + def register_namespace(name, uri) + if namespaces[name] + raise ArgumentError, "The namespace #{name.inspect} already exists" + end + namespaces[name] = Namespace.new(:name => name, :uri => uri) end ## - # Returns a node set of all the namespaces registered with the current - # node. + # Returns a Hash containing all the namespaces available to the current + # element. # - # @return [Oga::XML::NodeSet] + # @return [Hash] # - def namespaces + def available_namespaces + merged = namespaces + node = parent + while node && node.respond_to?(:namespaces) + merged = merged.merge(node.namespaces) + node = node.parent + end + + return merged end private ## - # @param [Hash] options - # @raise [TypeError] + # Registers namespaces based on any "xmlns" attributes. Once a namespace + # has been registered the corresponding attribute is removed. # - def validate_option_types!(options) - OPTION_TYPES.each do |key, type| - if options[key] and !options[key].is_a?(type) - raise( - TypeError, - "#{key.inspect} must be an instance of #{type}" - ) - end + def register_namespaces_from_attributes + self.attributes = attributes.reject do |attr| + # We're using `namespace_name` opposed to `namespace.name` as "xmlns" + # is not a registered namespace. + remove = attr.namespace_name && attr.namespace_name == XMLNS_PREFIX + + register_namespace(attr.name, attr.value) if remove + + remove + end + end + + ## + # Links all attributes to the current element. + # + def link_attributes + attributes.each do |attr| + attr.element = self end end @@ -206,7 +239,7 @@ module Oga if ns ns_matches = attr.namespace.to_s == ns - elsif name_matches + elsif name_matches and !attr.namespace ns_matches = true end diff --git a/lib/oga/xml/parser.y b/lib/oga/xml/parser.y index 003f667..0f11f1f 100644 --- a/lib/oga/xml/parser.y +++ b/lib/oga/xml/parser.y @@ -336,14 +336,10 @@ Unexpected #{name} with value #{value.inspect} on line #{@line}: # @return [Oga::XML::Element] # def on_element(namespace, name, attributes = {}) - if namespace - namespace = Namespace.new(:name => namespace) - end - element = Element.new( - :namespace => namespace, - :name => name, - :attributes => attributes + :namespace_name => namespace, + :name => name, + :attributes => attributes ) return element diff --git a/spec/oga/xml/element_spec.rb b/spec/oga/xml/element_spec.rb index cd9a353..61793f0 100644 --- a/spec/oga/xml/element_spec.rb +++ b/spec/oga/xml/element_spec.rb @@ -6,18 +6,6 @@ describe Oga::XML::Element do described_class.new(:name => 'p').name.should == 'p' end - example 'raise TypeError when the namespace is not a Namespace' do - block = lambda { described_class.new(:namespace => 'x') } - - block.should raise_error(TypeError) - end - - example 'raise TypeError when the attributes are not an Array' do - block = lambda { described_class.new(:attributes => 'foo') } - - block.should raise_error(TypeError) - end - example 'set the name via a setter' do instance = described_class.new instance.name = 'p' @@ -30,18 +18,42 @@ describe Oga::XML::Element do end end + context 'setting namespaces via attributes' do + before do + attr = Oga::XML::Attribute.new(:name => 'foo', :namespace_name => 'xmlns') + + @element = described_class.new(:attributes => [attr]) + end + + example 'register the "foo" namespace' do + @element.namespaces['foo'].is_a?(Oga::XML::Namespace).should == true + end + + example 'remove the namespace attribute from the list of attributes' do + @element.attributes.empty?.should == true + end + end + context '#attribute' do before do attributes = [ Oga::XML::Attribute.new(:name => 'key', :value => 'value'), Oga::XML::Attribute.new( - :name => 'key', - :value => 'foo', - :namespace => Oga::XML::Namespace.new(:name => 'x') + :name => 'bar', + :value => 'baz', + :namespace_name => 'x' + ), + Oga::XML::Attribute.new( + :name => 'key', + :value => 'foo', + :namespace_name => 'x' ) ] - @instance = described_class.new(:attributes => attributes) + @instance = described_class.new( + :attributes => attributes, + :namespaces => {'x' => Oga::XML::Namespace.new(:name => 'x')} + ) end example 'return an attribute with only a name' do @@ -71,6 +83,24 @@ describe Oga::XML::Element do example 'return nil for a non existing attribute' do @instance.attribute('foobar').nil?.should == true end + + example 'return nil if an attribute has a namespace that is not given' do + @instance.attribute('bar').nil?.should == true + end + end + + context '#namespace' do + before do + @namespace = Oga::XML::Namespace.new(:name => 'x') + @element = described_class.new( + :namespace_name => 'x', + :namespaces => {'x' => @namespace} + ) + end + + example 'return the namespace' do + @element.namespace.should == @namespace + end end context '#text' do @@ -116,8 +146,9 @@ describe Oga::XML::Element do example 'include the namespace if present' do instance = described_class.new( - :name => 'p', - :namespace => Oga::XML::Namespace.new(:name => 'foo') + :name => 'p', + :namespace_name => 'foo', + :namespaces => {'foo' => Oga::XML::Namespace.new(:name => 'foo')} ) instance.to_xml.should == '' @@ -164,11 +195,13 @@ describe Oga::XML::Element do example 'inspect a node with a namespace' do node = described_class.new( - :name => 'p', - :namespace => Oga::XML::Namespace.new(:name => 'x') + :name => 'p', + :namespace_name => 'x', + :namespaces => {'x' => Oga::XML::Namespace.new(:name => 'x')} ) - node.inspect.should == 'Element(name: "p" namespace: Namespace(name: "x"))' + node.inspect.should == 'Element(name: "p" ' \ + 'namespace: Namespace(name: "x" uri: nil))' end end @@ -178,7 +211,53 @@ describe Oga::XML::Element do end end + context '#register_namespace' do + before do + @element = described_class.new + + @element.register_namespace('foo', 'http://example.com') + end + + example 'return a Namespace instance' do + @element.namespaces['foo'].is_a?(Oga::XML::Namespace).should == true + end + + example 'set the name of the namespace' do + @element.namespaces['foo'].name.should == 'foo' + end + + example 'set the URI of the namespace' do + @element.namespaces['foo'].uri.should == 'http://example.com' + end + + example 'raise ArgumentError if the namespace already exists' do + block = lambda { @element.register_namespace('foo', 'bar') } + + block.should raise_error(ArgumentError) + end + end + context '#available_namespaces' do - # TODO: write me + before do + @parent = described_class.new + @child = described_class.new + + @child.node_set = Oga::XML::NodeSet.new([@child], @parent) + + @parent.register_namespace('foo', 'bar') + @child.register_namespace('baz', 'xxx') + + @parent_ns = @parent.available_namespaces + @child_ns = @child.available_namespaces + end + + example 'return the available namespaces of the child node' do + @child_ns['foo'].is_a?(Oga::XML::Namespace).should == true + @child_ns['baz'].is_a?(Oga::XML::Namespace).should == true + end + + example 'return the available namespaces of the parent node' do + @parent_ns['foo'].is_a?(Oga::XML::Namespace).should == true + end end end diff --git a/spec/oga/xml/namespace_spec.rb b/spec/oga/xml/namespace_spec.rb index 26f5f3c..58b9a75 100644 --- a/spec/oga/xml/namespace_spec.rb +++ b/spec/oga/xml/namespace_spec.rb @@ -15,9 +15,9 @@ describe Oga::XML::Namespace do context '#inspect' do example 'return the inspect value' do - ns = described_class.new(:name => 'x') + ns = described_class.new(:name => 'x', :uri => 'y') - ns.inspect.should == 'Namespace(name: "x")' + ns.inspect.should == 'Namespace(name: "x" uri: "y")' end end end diff --git a/spec/oga/xml/parser/elements_spec.rb b/spec/oga/xml/parser/elements_spec.rb index 4cbccb7..4488594 100644 --- a/spec/oga/xml/parser/elements_spec.rb +++ b/spec/oga/xml/parser/elements_spec.rb @@ -21,7 +21,7 @@ describe Oga::XML::Parser do context 'elements with namespaces' do before :all do - @element = parse('').children[0] + @element = parse('').children[0] end example 'return an Element instance' do @@ -57,7 +57,7 @@ describe Oga::XML::Parser do context 'elements with namespaced attributes' do before :all do - @element = parse('').children[0] + @element = parse('').children[0] end example 'return an Element instance' do @@ -108,4 +108,32 @@ describe Oga::XML::Parser do @element.children[1].children[0].is_a?(Oga::XML::Text).should == true end end + + context 'elements with namespace registrations' do + before :all do + document = parse('') + + @root = document.children[0] + @foo = @root.children[0] + end + + example 'return the namespaces of the node' do + @root.namespaces['a'].name.should == 'a' + @root.namespaces['a'].uri.should == '1' + end + + example 'return the namespaces of the node' do + @foo.namespaces['b'].name.should == 'b' + @foo.namespaces['b'].uri.should == '2' + end + + example 'return the available namespaces of the node' do + @root.available_namespaces['a'].name.should == 'a' + end + + example 'return the available namespaces of the node' do + @foo.available_namespaces['a'].name.should == 'a' + @foo.available_namespaces['b'].name.should == 'b' + end + end end