Use XML::Attribute for element attributes.

Instead of using a raw Hash Oga now uses the XML::Attribute class for storing
information about element attributes.

Attributes are stored as an Array of XML::Attribute instances. This allows the
attributes to be more easily modified. If they were stored as a Hash you'd not
only have to update the attributes themselves but also the Hash that contains
them.

While using an Array has a slight runtime cost in most cases the amount of
attributes is small enough that this doesn't really pose a problem. If webscale
performance is desired at some point in the future Oga could most likely cache
the lookup of an attribute. This however is something for the future.
This commit is contained in:
Yorick Peterse 2014-07-20 07:29:37 +02:00
parent ad4d7a4744
commit d5569ead0b
9 changed files with 161 additions and 42 deletions

View File

@ -38,7 +38,6 @@
newline = '\n' | '\r\n';
whitespace = [ \t];
identifier = [a-zA-Z0-9\-_]+;
attribute = [a-zA-Z0-9\-_:]+;
# Comments
#
@ -144,7 +143,7 @@
};
# Attributes and their values (e.g. version="1.0").
attribute => {
identifier => {
callback("on_attribute", data, encoding, ts, te);
};
@ -187,8 +186,12 @@
callback_simple("advance_line");
};
# Attribute names.
attribute => {
# Attribute names and namespaces.
identifier ':' => {
callback("on_attribute_ns", data, encoding, ts, te - 1);
};
identifier => {
callback("on_attribute", data, encoding, ts, te);
};

View File

@ -14,7 +14,7 @@ module Oga
#
# @!attribute [rw] attributes
# The attributes of the element.
# @return [Hash]
# @return [Array<Oga::XML::Attribute>]
#
class Element < Node
attr_accessor :name, :namespace, :attributes
@ -24,24 +24,38 @@ module Oga
#
# @option options [String] :name The name of the element.
# @option options [String] :namespace The namespace of the element.
# @option options [Hash] :attributes The attributes of the element.
# @option options [Array<Oga::XML::Attribute>] :attributes The attributes
# of the element as an Array.
#
def initialize(options = {})
super
@name = options[:name]
@namespace = options[:namespace]
@attributes = options[:attributes] || {}
@attributes = options[:attributes] || []
end
##
# Returns the value of the specified attribute.
# Returns the attribute of the given name.
#
# @example
# # find an attribute that only has the name "foo"
# attribute('foo')
#
# # find an attribute with namespace "foo" and name bar"
# attribute('foo:bar')
#
# @param [String] name
# @return [String]
#
def attribute(name)
return attributes[name.to_sym]
name, ns = split_name(name)
attributes.each do |attr|
return attr if attribute_matches?(attr, ns, name)
end
return
end
alias_method :attr, :attribute
@ -117,6 +131,38 @@ module Oga
def node_type
return :element
end
private
##
# @param [String] name
# @return [Array]
#
def split_name(name)
segments = name.to_s.split(':')
return segments.pop, segments.pop
end
##
# @param [Oga::XML::Attribute] attr
# @param [String] ns
# @param [String] name
# @return [TrueClass|FalseClass]
#
def attribute_matches?(attr, ns, name)
name_matches = attr.name == name
ns_matches = false
if ns
ns_matches = attr.namespace == ns
elsif name_matches
ns_matches = true
end
return name_matches && ns_matches
end
end # Element
end # XML
end # Oga

View File

@ -354,6 +354,15 @@ module Oga
end
end
##
# Called on attribute namespaces.
#
# @param [String] value
#
def on_attribute_ns(value)
add_token(:T_ATTR_NS, value)
end
##
# Called on tag attributes.
#

View File

@ -13,7 +13,7 @@ token T_STRING T_TEXT
token T_DOCTYPE_START T_DOCTYPE_END T_DOCTYPE_TYPE T_DOCTYPE_NAME
token T_DOCTYPE_INLINE
token T_CDATA T_COMMENT
token T_ELEM_START T_ELEM_NAME T_ELEM_NS T_ELEM_END T_ATTR
token T_ELEM_START T_ELEM_NAME T_ELEM_NS T_ELEM_END T_ATTR T_ATTR_NS
token T_XML_DECL_START T_XML_DECL_END
options no_result_var
@ -122,8 +122,8 @@ rule
# Attributes
attributes
: attributes_ { on_attributes(val[0]) }
| /* none */ { {} }
: attributes_ { val[0] }
| /* none */ { [] }
;
attributes_
@ -133,10 +133,22 @@ rule
attribute
# foo
: T_ATTR { {val[0].to_sym => nil} }
: attribute_name { val[0] }
# foo="bar"
| T_ATTR T_STRING { {val[0].to_sym => val[1]} }
| attribute_name T_STRING
{
val[0].value = val[1]
val[0]
}
;
attribute_name
# foo
: T_ATTR { Attribute.new(:name => val[0]) }
# foo:bar
| T_ATTR_NS T_ATTR { Attribute.new(:namespace => val[0], :name => val[1]) }
;
# XML declarations
@ -293,11 +305,17 @@ Unexpected #{name} with value #{value.inspect} on line #{@line}:
end
##
# @param [Hash] attributes
# @param [Array] attributes
# @return [Oga::XML::XmlDeclaration]
#
def on_xml_decl(attributes = {})
return XmlDeclaration.new(attributes)
def on_xml_decl(attributes = [])
options = {}
attributes.each do |attr|
options[attr.name.to_sym] = attr.value
end
return XmlDeclaration.new(options)
end
##
@ -344,18 +362,4 @@ Unexpected #{name} with value #{value.inspect} on line #{@line}:
return element
end
##
# @param [Array] pairs
# @return [Hash]
#
def on_attributes(pairs)
attrs = {}
pairs.each do |pair|
attrs = attrs.merge(pair)
end
return attrs
end
# vim: set ft=racc:

View File

@ -14,17 +14,50 @@ describe Oga::XML::Element do
end
example 'set the default attributes' do
described_class.new.attributes.should == {}
described_class.new.attributes.should == []
end
end
context '#attribute' do
before do
@instance = described_class.new(:attributes => {:key => 'value'})
attributes = [
Oga::XML::Attribute.new(:name => 'key', :value => 'value'),
Oga::XML::Attribute.new(
:name => 'key',
:value => 'foo',
:namespace => 'x'
)
]
@instance = described_class.new(:attributes => attributes)
end
example 'return an attribute' do
@instance.attribute('key').should == 'value'
example 'return an attribute with only a name' do
@instance.attribute('key').value.should == 'value'
end
example 'return an attribute with only a name when using a Symbol' do
@instance.attribute(:key).value.should == 'value'
end
example 'return an attribute with a name and namespace' do
@instance.attribute('x:key').value.should == 'foo'
end
example 'return an attribute with a name and namespace when using a Symbol' do
@instance.attribute(:'x:key').value.should == 'foo'
end
example 'return nil when the name matches but the namespace does not' do
@instance.attribute('y:key').nil?.should == true
end
example 'return nil when the namespace matches but the name does not' do
@instance.attribute('x:foobar').nil?.should == true
end
example 'return nil for a non existing attribute' do
@instance.attribute('foobar').nil?.should == true
end
end

View File

@ -78,7 +78,8 @@ describe Oga::XML::Lexer do
lex('<p foo:bar="baz"></p>').should == [
[:T_ELEM_START, nil, 1],
[:T_ELEM_NAME, 'p', 1],
[:T_ATTR, 'foo:bar', 1],
[:T_ATTR_NS, 'foo', 1],
[:T_ATTR, 'bar', 1],
[:T_STRING, 'baz', 1],
[:T_ELEM_END, nil, 1]
]

View File

@ -271,13 +271,14 @@ describe Oga::XML::NodeSet do
context '#attribute' do
before do
@el = Oga::XML::Element.new(:name => 'a', :attributes => {:a => '1'})
@attr = Oga::XML::Attribute.new(:name => 'a', :value => '1')
@el = Oga::XML::Element.new(:name => 'a', :attributes => [@attr])
@txt = Oga::XML::Text.new(:text => 'foo')
@set = described_class.new([@el, @txt])
end
example 'return the values of an attribute' do
@set.attribute('a').should == ['1']
@set.attribute('a').should == [@attr]
end
end

View File

@ -43,7 +43,29 @@ describe Oga::XML::Parser do
end
example 'set the bar attribute' do
@element.attribute('bar').should == 'baz'
@element.attribute('bar').value.should == 'baz'
end
end
context 'elements with namespaced attributes' do
before :all do
@element = parse('<foo x:bar="baz"></foo>').children[0]
end
example 'return an Element instance' do
@element.is_a?(Oga::XML::Element).should == true
end
example 'include the namespace of the attribute' do
@element.attribute('x:bar').namespace.should == 'x'
end
example 'include the name of the attribute' do
@element.attribute('x:bar').name.should == 'bar'
end
example 'include the value of the attribute' do
@element.attribute('x:bar').value.should == 'baz'
end
end

View File

@ -39,7 +39,7 @@ describe Oga::XML::Parser do
end
example 'set the attributes' do
@node.attributes.should == {:href => 'foo'}
@node.attribute('href').value.should == 'foo'
end
end
end