mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
78d9dd71a6
This is a version bump to REXML 3.1.4 for Ruby HEAD. This change log is identical to the log for the 1.8 branch. It includes numerous bug fixes and is a pretty big patch, but is nonetheless a minor revision bump, since the API hasn't changed. For more information, see: http:/www.germane-software.com/projects/rexml/milestone/3.1.4 For all tickets, see: http://www.germane-software.com/projects/rexml/ticket/# Where '#' is replaced with the ticket number. Changelog: * Fixed the documentation WRT the raw mode of text nodes (ticket #4) * Fixes roundup ticket #43: substring-after bug. * Fixed ticket #44, Element#xpath * Patch submitted by an anonymous doner to allow parsing of Tempfiles. I was hoping that, by now, that whole Source thing would have been changed to use duck typing and avoid this sort of ticket... but in the meantime, the patch has been applied. * Fixes ticket:30, XPath default namespace bug. The fix was provided by Lucas Nussbaum. * Aliases #size to #length, as per zdennis's request. * Fixes typo from previous commit * Fixes ticket #32, preceding-sibling fails attempting delete_if on nil nodeset * Merges a user-contributed patch for ticket #40 * Adds a forgotten-to-commit unit test for ticket #32 * Changes Date, Version, and Copyright to upper case, to avoid conflicts with the Date class. All of the other changes in the altered files are because Subversion doesn't allow block-level commits, like it should. English cased Version and Copyright are aliased to the upper case versions, for partial backward compatability. * Resolves ticket #34, SAX parser change makes it impossible to parse IO feeds. * Moves parser.source.position() to parser.position() * Fixes ticket:48, repeated writes munging text content * Fixes ticket:46, adding methods for accessing notation DTD information. * Encodes some characters and removes a brokes link in the documentation * Deals with carriage returns after XML declarations * Improved doctype handling * Whitespace handling changes * Applies a patch by David Tardon, which (incidentally) fixes ticket:50 * Closes #26, allowing anything that walks like an IO to be a source. * Ticket #31 - One unescape too many This wasn't really a bug, per se... "value" always returns a normalized string, and "value" is the method used to get the text() of an element. However, entities have no meaning in CDATA sections, so there's no justification for value to be normalizing the content of CData objects. This behavior has therefore been changed. * Ticket #45 -- Now parses notation declarations in DTDs properly. * Resolves ticket #49, Document.parse_stream returns ArgumentError * Adds documentation to clarify how XMLDecl works, to avoid invalid bug reports. * Addresses ticket #10, fixing the StreamParser API for DTDs. * Fixes ticket #42, XPath node-set function 'name' fails with relative node set parameter * Good patch by Aaron to fix ticket #53: REXML ignoring unbalanced tags at the end of a document. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@10092 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
1243 lines
41 KiB
Ruby
1243 lines
41 KiB
Ruby
require "rexml/parent"
|
|
require "rexml/namespace"
|
|
require "rexml/attribute"
|
|
require "rexml/cdata"
|
|
require "rexml/xpath"
|
|
require "rexml/parseexception"
|
|
|
|
module REXML
|
|
# An implementation note about namespaces:
|
|
# As we parse, when we find namespaces we put them in a hash and assign
|
|
# them a unique ID. We then convert the namespace prefix for the node
|
|
# to the unique ID. This makes namespace lookup much faster for the
|
|
# cost of extra memory use. We save the namespace prefix for the
|
|
# context node and convert it back when we write it.
|
|
@@namespaces = {}
|
|
|
|
# Represents a tagged XML element. Elements are characterized by
|
|
# having children, attributes, and names, and can themselves be
|
|
# children.
|
|
class Element < Parent
|
|
include Namespace
|
|
|
|
UNDEFINED = "UNDEFINED"; # The default name
|
|
|
|
# Mechanisms for accessing attributes and child elements of this
|
|
# element.
|
|
attr_reader :attributes, :elements
|
|
# The context holds information about the processing environment, such as
|
|
# whitespace handling.
|
|
attr_accessor :context
|
|
|
|
# Constructor
|
|
# arg::
|
|
# if not supplied, will be set to the default value.
|
|
# If a String, the name of this object will be set to the argument.
|
|
# If an Element, the object will be shallowly cloned; name,
|
|
# attributes, and namespaces will be copied. Children will +not+ be
|
|
# copied.
|
|
# parent::
|
|
# if supplied, must be a Parent, and will be used as
|
|
# the parent of this object.
|
|
# context::
|
|
# If supplied, must be a hash containing context items. Context items
|
|
# include:
|
|
# * <tt>:respect_whitespace</tt> the value of this is :+all+ or an array of
|
|
# strings being the names of the elements to respect
|
|
# whitespace for. Defaults to :+all+.
|
|
# * <tt>:compress_whitespace</tt> the value can be :+all+ or an array of
|
|
# strings being the names of the elements to ignore whitespace on.
|
|
# Overrides :+respect_whitespace+.
|
|
# * <tt>:ignore_whitespace_nodes</tt> the value can be :+all+ or an array
|
|
# of strings being the names of the elements in which to ignore
|
|
# whitespace-only nodes. If this is set, Text nodes which contain only
|
|
# whitespace will not be added to the document tree.
|
|
# * <tt>:raw</tt> can be :+all+, or an array of strings being the names of
|
|
# the elements to process in raw mode. In raw mode, special
|
|
# characters in text is not converted to or from entities.
|
|
def initialize( arg = UNDEFINED, parent=nil, context=nil )
|
|
super(parent)
|
|
|
|
@elements = Elements.new(self)
|
|
@attributes = Attributes.new(self)
|
|
@context = context
|
|
|
|
if arg.kind_of? String
|
|
self.name = arg
|
|
elsif arg.kind_of? Element
|
|
self.name = arg.expanded_name
|
|
arg.attributes.each_attribute{ |attribute|
|
|
@attributes << Attribute.new( attribute )
|
|
}
|
|
@context = arg.context
|
|
end
|
|
end
|
|
|
|
def inspect
|
|
rv = "<#@expanded_name"
|
|
|
|
@attributes.each_attribute do |attr|
|
|
rv << " "
|
|
attr.write( rv, 0 )
|
|
end
|
|
|
|
if children.size > 0
|
|
rv << "> ... </>"
|
|
else
|
|
rv << "/>"
|
|
end
|
|
end
|
|
|
|
|
|
# Creates a shallow copy of self.
|
|
# d = Document.new "<a><b/><b/><c><d/></c></a>"
|
|
# new_a = d.root.clone
|
|
# puts new_a # => "<a/>"
|
|
def clone
|
|
Element.new self
|
|
end
|
|
|
|
# Evaluates to the root node of the document that this element
|
|
# belongs to. If this element doesn't belong to a document, but does
|
|
# belong to another Element, the parent's root will be returned, until the
|
|
# earliest ancestor is found.
|
|
#
|
|
# Note that this is not the same as the document element.
|
|
# In the following example, <a> is the document element, and the root
|
|
# node is the parent node of the document element. You may ask yourself
|
|
# why the root node is useful: consider the doctype and XML declaration,
|
|
# and any processing instructions before the document element... they
|
|
# are children of the root node, or siblings of the document element.
|
|
# The only time this isn't true is when an Element is created that is
|
|
# not part of any Document. In this case, the ancestor that has no
|
|
# parent acts as the root node.
|
|
# d = Document.new '<a><b><c/></b></a>'
|
|
# a = d[1] ; c = a[1][1]
|
|
# d.root_node == d # TRUE
|
|
# a.root_node # namely, d
|
|
# c.root_node # again, d
|
|
def root_node
|
|
parent.nil? ? self : parent.root_node
|
|
end
|
|
|
|
def root
|
|
return elements[1] if self.kind_of? Document
|
|
return self if parent.kind_of? Document or parent.nil?
|
|
return parent.root
|
|
end
|
|
|
|
# Evaluates to the document to which this element belongs, or nil if this
|
|
# element doesn't belong to a document.
|
|
def document
|
|
rt = root
|
|
rt.parent if rt
|
|
end
|
|
|
|
# Evaluates to +true+ if whitespace is respected for this element. This
|
|
# is the case if:
|
|
# 1. Neither :+respect_whitespace+ nor :+compress_whitespace+ has any value
|
|
# 2. The context has :+respect_whitespace+ set to :+all+ or
|
|
# an array containing the name of this element, and
|
|
# :+compress_whitespace+ isn't set to :+all+ or an array containing the
|
|
# name of this element.
|
|
# The evaluation is tested against +expanded_name+, and so is namespace
|
|
# sensitive.
|
|
def whitespace
|
|
@whitespace = nil
|
|
if @context
|
|
if @context[:respect_whitespace]
|
|
@whitespace = (@context[:respect_whitespace] == :all or
|
|
@context[:respect_whitespace].include? expanded_name)
|
|
end
|
|
@whitespace = false if (@context[:compress_whitespace] and
|
|
(@context[:compress_whitespace] == :all or
|
|
@context[:compress_whitespace].include? expanded_name)
|
|
)
|
|
end
|
|
@whitespace = true unless @whitespace == false
|
|
@whitespace
|
|
end
|
|
|
|
def ignore_whitespace_nodes
|
|
@ignore_whitespace_nodes = false
|
|
if @context
|
|
if @context[:ignore_whitespace_nodes]
|
|
@ignore_whitespace_nodes =
|
|
(@context[:ignore_whitespace_nodes] == :all or
|
|
@context[:ignore_whitespace_nodes].include? expanded_name)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Evaluates to +true+ if raw mode is set for this element. This
|
|
# is the case if the context has :+raw+ set to :+all+ or
|
|
# an array containing the name of this element.
|
|
#
|
|
# The evaluation is tested against +expanded_name+, and so is namespace
|
|
# sensitive.
|
|
def raw
|
|
@raw = (@context and @context[:raw] and
|
|
(@context[:raw] == :all or
|
|
@context[:raw].include? expanded_name))
|
|
@raw
|
|
end
|
|
|
|
#once :whitespace, :raw, :ignore_whitespace_nodes
|
|
|
|
#################################################
|
|
# Namespaces #
|
|
#################################################
|
|
|
|
# Evaluates to an +Array+ containing the prefixes (names) of all defined
|
|
# namespaces at this context node.
|
|
# doc = Document.new("<a xmlns:x='1' xmlns:y='2'><b/><c xmlns:z='3'/></a>")
|
|
# doc.elements['//b'].prefixes # -> ['x', 'y']
|
|
def prefixes
|
|
prefixes = []
|
|
prefixes = parent.prefixes if parent
|
|
prefixes |= attributes.prefixes
|
|
return prefixes
|
|
end
|
|
|
|
def namespaces
|
|
namespaces = []
|
|
namespaces = parent.namespaces if parent
|
|
namespaces |= attributes.namespaces
|
|
return namespaces
|
|
end
|
|
|
|
# Evalutas to the URI for a prefix, or the empty string if no such
|
|
# namespace is declared for this element. Evaluates recursively for
|
|
# ancestors. Returns the default namespace, if there is one.
|
|
# prefix::
|
|
# the prefix to search for. If not supplied, returns the default
|
|
# namespace if one exists
|
|
# Returns::
|
|
# the namespace URI as a String, or nil if no such namespace
|
|
# exists. If the namespace is undefined, returns an empty string
|
|
# doc = Document.new("<a xmlns='1' xmlns:y='2'><b/><c xmlns:z='3'/></a>")
|
|
# b = doc.elements['//b']
|
|
# b.namespace # -> '1'
|
|
# b.namespace("y") # -> '2'
|
|
def namespace(prefix=nil)
|
|
if prefix.nil?
|
|
prefix = prefix()
|
|
end
|
|
if prefix == ''
|
|
prefix = "xmlns"
|
|
else
|
|
prefix = "xmlns:#{prefix}" unless prefix[0,5] == 'xmlns'
|
|
end
|
|
ns = attributes[ prefix ]
|
|
ns = parent.namespace(prefix) if ns.nil? and parent
|
|
ns = '' if ns.nil? and prefix == 'xmlns'
|
|
return ns
|
|
end
|
|
|
|
# Adds a namespace to this element.
|
|
# prefix::
|
|
# the prefix string, or the namespace URI if +uri+ is not
|
|
# supplied
|
|
# uri::
|
|
# the namespace URI. May be nil, in which +prefix+ is used as
|
|
# the URI
|
|
# Evaluates to: this Element
|
|
# a = Element.new("a")
|
|
# a.add_namespace("xmlns:foo", "bar" )
|
|
# a.add_namespace("foo", "bar") # shorthand for previous line
|
|
# a.add_namespace("twiddle")
|
|
# puts a #-> <a xmlns:foo='bar' xmlns='twiddle'/>
|
|
def add_namespace( prefix, uri=nil )
|
|
unless uri
|
|
@attributes["xmlns"] = prefix
|
|
else
|
|
prefix = "xmlns:#{prefix}" unless prefix =~ /^xmlns:/
|
|
@attributes[ prefix ] = uri
|
|
end
|
|
self
|
|
end
|
|
|
|
# Removes a namespace from this node. This only works if the namespace is
|
|
# actually declared in this node. If no argument is passed, deletes the
|
|
# default namespace.
|
|
#
|
|
# Evaluates to: this element
|
|
# doc = Document.new "<a xmlns:foo='bar' xmlns='twiddle'/>"
|
|
# doc.root.delete_namespace
|
|
# puts doc # -> <a xmlns:foo='bar'/>
|
|
# doc.root.delete_namespace 'foo'
|
|
# puts doc # -> <a/>
|
|
def delete_namespace namespace="xmlns"
|
|
namespace = "xmlns:#{namespace}" unless namespace == 'xmlns'
|
|
attribute = attributes.get_attribute(namespace)
|
|
attribute.remove unless attribute.nil?
|
|
self
|
|
end
|
|
|
|
#################################################
|
|
# Elements #
|
|
#################################################
|
|
|
|
# Adds a child to this element, optionally setting attributes in
|
|
# the element.
|
|
# element::
|
|
# optional. If Element, the element is added.
|
|
# Otherwise, a new Element is constructed with the argument (see
|
|
# Element.initialize).
|
|
# attrs::
|
|
# If supplied, must be a Hash containing String name,value
|
|
# pairs, which will be used to set the attributes of the new Element.
|
|
# Returns:: the Element that was added
|
|
# el = doc.add_element 'my-tag'
|
|
# el = doc.add_element 'my-tag', {'attr1'=>'val1', 'attr2'=>'val2'}
|
|
# el = Element.new 'my-tag'
|
|
# doc.add_element el
|
|
def add_element element, attrs=nil
|
|
raise "First argument must be either an element name, or an Element object" if element.nil?
|
|
el = @elements.add(element)
|
|
if attrs.kind_of? Hash
|
|
attrs.each do |key, value|
|
|
el.attributes[key]=value if key =~ /^xmlns:/
|
|
end
|
|
attrs.each do |key, value|
|
|
el.attributes[key]=value if key !~ /^xmlns:/
|
|
end
|
|
end
|
|
el
|
|
end
|
|
|
|
# Deletes a child element.
|
|
# element::
|
|
# Must be an +Element+, +String+, or +Integer+. If Element,
|
|
# the element is removed. If String, the element is found (via XPath)
|
|
# and removed. <em>This means that any parent can remove any
|
|
# descendant.<em> If Integer, the Element indexed by that number will be
|
|
# removed.
|
|
# Returns:: the element that was removed.
|
|
# doc.delete_element "/a/b/c[@id='4']"
|
|
# doc.delete_element doc.elements["//k"]
|
|
# doc.delete_element 1
|
|
def delete_element element
|
|
@elements.delete element
|
|
end
|
|
|
|
# Evaluates to +true+ if this element has at least one child Element
|
|
# doc = Document.new "<a><b/><c>Text</c></a>"
|
|
# doc.root.has_elements # -> true
|
|
# doc.elements["/a/b"].has_elements # -> false
|
|
# doc.elements["/a/c"].has_elements # -> false
|
|
def has_elements?
|
|
!@elements.empty?
|
|
end
|
|
|
|
# Iterates through the child elements, yielding for each Element that
|
|
# has a particular attribute set.
|
|
# key::
|
|
# the name of the attribute to search for
|
|
# value::
|
|
# the value of the attribute
|
|
# max::
|
|
# (optional) causes this method to return after yielding
|
|
# for this number of matching children
|
|
# name::
|
|
# (optional) if supplied, this is an XPath that filters
|
|
# the children to check.
|
|
#
|
|
# doc = Document.new "<a><b @id='1'/><c @id='2'/><d @id='1'/><e/></a>"
|
|
# # Yields b, c, d
|
|
# doc.root.each_element_with_attribute( 'id' ) {|e| p e}
|
|
# # Yields b, d
|
|
# doc.root.each_element_with_attribute( 'id', '1' ) {|e| p e}
|
|
# # Yields b
|
|
# doc.root.each_element_with_attribute( 'id', '1', 1 ) {|e| p e}
|
|
# # Yields d
|
|
# doc.root.each_element_with_attribute( 'id', '1', 0, 'd' ) {|e| p e}
|
|
def each_element_with_attribute( key, value=nil, max=0, name=nil, &block ) # :yields: Element
|
|
each_with_something( proc {|child|
|
|
if value.nil?
|
|
child.attributes[key] != nil
|
|
else
|
|
child.attributes[key]==value
|
|
end
|
|
}, max, name, &block )
|
|
end
|
|
|
|
# Iterates through the children, yielding for each Element that
|
|
# has a particular text set.
|
|
# text::
|
|
# the text to search for. If nil, or not supplied, will itterate
|
|
# over all +Element+ children that contain at least one +Text+ node.
|
|
# max::
|
|
# (optional) causes this method to return after yielding
|
|
# for this number of matching children
|
|
# name::
|
|
# (optional) if supplied, this is an XPath that filters
|
|
# the children to check.
|
|
#
|
|
# doc = Document.new '<a><b>b</b><c>b</c><d>d</d><e/></a>'
|
|
# # Yields b, c, d
|
|
# doc.each_element_with_text {|e|p e}
|
|
# # Yields b, c
|
|
# doc.each_element_with_text('b'){|e|p e}
|
|
# # Yields b
|
|
# doc.each_element_with_text('b', 1){|e|p e}
|
|
# # Yields d
|
|
# doc.each_element_with_text(nil, 0, 'd'){|e|p e}
|
|
def each_element_with_text( text=nil, max=0, name=nil, &block ) # :yields: Element
|
|
each_with_something( proc {|child|
|
|
if text.nil?
|
|
child.has_text?
|
|
else
|
|
child.text == text
|
|
end
|
|
}, max, name, &block )
|
|
end
|
|
|
|
# Synonym for Element.elements.each
|
|
def each_element( xpath=nil, &block ) # :yields: Element
|
|
@elements.each( xpath, &block )
|
|
end
|
|
|
|
# Synonym for Element.to_a
|
|
# This is a little slower than calling elements.each directly.
|
|
# xpath:: any XPath by which to search for elements in the tree
|
|
# Returns:: an array of Elements that match the supplied path
|
|
def get_elements( xpath )
|
|
@elements.to_a( xpath )
|
|
end
|
|
|
|
# Returns the next sibling that is an element, or nil if there is
|
|
# no Element sibling after this one
|
|
# doc = Document.new '<a><b/>text<c/></a>'
|
|
# doc.root.elements['b'].next_element #-> <c/>
|
|
# doc.root.elements['c'].next_element #-> nil
|
|
def next_element
|
|
element = next_sibling
|
|
element = element.next_sibling until element.nil? or element.kind_of? Element
|
|
return element
|
|
end
|
|
|
|
# Returns the previous sibling that is an element, or nil if there is
|
|
# no Element sibling prior to this one
|
|
# doc = Document.new '<a><b/>text<c/></a>'
|
|
# doc.root.elements['c'].previous_element #-> <b/>
|
|
# doc.root.elements['b'].previous_element #-> nil
|
|
def previous_element
|
|
element = previous_sibling
|
|
element = element.previous_sibling until element.nil? or element.kind_of? Element
|
|
return element
|
|
end
|
|
|
|
|
|
#################################################
|
|
# Text #
|
|
#################################################
|
|
|
|
# Evaluates to +true+ if this element has at least one Text child
|
|
def has_text?
|
|
not text().nil?
|
|
end
|
|
|
|
# A convenience method which returns the String value of the _first_
|
|
# child text element, if one exists, and +nil+ otherwise.
|
|
#
|
|
# <em>Note that an element may have multiple Text elements, perhaps
|
|
# separated by other children</em>. Be aware that this method only returns
|
|
# the first Text node.
|
|
#
|
|
# This method returns the +value+ of the first text child node, which
|
|
# ignores the +raw+ setting, so always returns normalized text. See
|
|
# the Text::value documentation.
|
|
#
|
|
# doc = Document.new "<p>some text <b>this is bold!</b> more text</p>"
|
|
# # The element 'p' has two text elements, "some text " and " more text".
|
|
# doc.root.text #-> "some text "
|
|
def text( path = nil )
|
|
rv = get_text(path)
|
|
return rv.value unless rv.nil?
|
|
nil
|
|
end
|
|
|
|
# Returns the first child Text node, if any, or +nil+ otherwise.
|
|
# This method returns the actual +Text+ node, rather than the String content.
|
|
# doc = Document.new "<p>some text <b>this is bold!</b> more text</p>"
|
|
# # The element 'p' has two text elements, "some text " and " more text".
|
|
# doc.root.get_text.value #-> "some text "
|
|
def get_text path = nil
|
|
rv = nil
|
|
if path
|
|
element = @elements[ path ]
|
|
rv = element.get_text unless element.nil?
|
|
else
|
|
rv = @children.find { |node| node.kind_of? Text }
|
|
end
|
|
return rv
|
|
end
|
|
|
|
# Sets the first Text child of this object. See text() for a
|
|
# discussion about Text children.
|
|
#
|
|
# If a Text child already exists, the child is replaced by this
|
|
# content. This means that Text content can be deleted by calling
|
|
# this method with a nil argument. In this case, the next Text
|
|
# child becomes the first Text child. In no case is the order of
|
|
# any siblings disturbed.
|
|
# text::
|
|
# If a String, a new Text child is created and added to
|
|
# this Element as the first Text child. If Text, the text is set
|
|
# as the first Child element. If nil, then any existing first Text
|
|
# child is removed.
|
|
# Returns:: this Element.
|
|
# doc = Document.new '<a><b/></a>'
|
|
# doc.root.text = 'Sean' #-> '<a><b/>Sean</a>'
|
|
# doc.root.text = 'Elliott' #-> '<a><b/>Elliott</a>'
|
|
# doc.root.add_element 'c' #-> '<a><b/>Elliott<c/></a>'
|
|
# doc.root.text = 'Russell' #-> '<a><b/>Russell<c/></a>'
|
|
# doc.root.text = nil #-> '<a><b/><c/></a>'
|
|
def text=( text )
|
|
if text.kind_of? String
|
|
text = Text.new( text, whitespace(), nil, raw() )
|
|
elsif text and !text.kind_of? Text
|
|
text = Text.new( text.to_s, whitespace(), nil, raw() )
|
|
end
|
|
|
|
old_text = get_text
|
|
if text.nil?
|
|
old_text.remove unless old_text.nil?
|
|
else
|
|
if old_text.nil?
|
|
self << text
|
|
else
|
|
old_text.replace_with( text )
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
# A helper method to add a Text child. Actual Text instances can
|
|
# be added with regular Parent methods, such as add() and <<()
|
|
# text::
|
|
# if a String, a new Text instance is created and added
|
|
# to the parent. If Text, the object is added directly.
|
|
# Returns:: this Element
|
|
# e = Element.new('a') #-> <e/>
|
|
# e.add_text 'foo' #-> <e>foo</e>
|
|
# e.add_text Text.new(' bar') #-> <e>foo bar</e>
|
|
# Note that at the end of this example, the branch has <b>3</b> nodes; the 'e'
|
|
# element and <b>2</b> Text node children.
|
|
def add_text( text )
|
|
if text.kind_of? String
|
|
if @children[-1].kind_of? Text
|
|
@children[-1] << text
|
|
return
|
|
end
|
|
text = Text.new( text, whitespace(), nil, raw() )
|
|
end
|
|
self << text unless text.nil?
|
|
return self
|
|
end
|
|
|
|
def node_type
|
|
:element
|
|
end
|
|
|
|
def xpath
|
|
path_elements = []
|
|
cur = self
|
|
path_elements << __to_xpath_helper( self )
|
|
while cur.parent
|
|
cur = cur.parent
|
|
path_elements << __to_xpath_helper( cur )
|
|
end
|
|
return path_elements.reverse.join( "/" )
|
|
end
|
|
|
|
#################################################
|
|
# Attributes #
|
|
#################################################
|
|
|
|
def attribute( name, namespace=nil )
|
|
prefix = ''
|
|
if namespace
|
|
prefix = attributes.prefixes.each { |prefix|
|
|
return "#{prefix}:" if namespace( prefix ) == namespace
|
|
} || ''
|
|
end
|
|
attributes.get_attribute( "#{prefix}#{name}" )
|
|
end
|
|
|
|
# Evaluates to +true+ if this element has any attributes set, false
|
|
# otherwise.
|
|
def has_attributes?
|
|
return !@attributes.empty?
|
|
end
|
|
|
|
# Adds an attribute to this element, overwriting any existing attribute
|
|
# by the same name.
|
|
# key::
|
|
# can be either an Attribute or a String. If an Attribute,
|
|
# the attribute is added to the list of Element attributes. If String,
|
|
# the argument is used as the name of the new attribute, and the value
|
|
# parameter must be supplied.
|
|
# value::
|
|
# Required if +key+ is a String, and ignored if the first argument is
|
|
# an Attribute. This is a String, and is used as the value
|
|
# of the new Attribute.
|
|
# Returns:: the Attribute added
|
|
# e = Element.new 'e'
|
|
# e.add_attribute( 'a', 'b' ) #-> <e a='b'/>
|
|
# e.add_attribute( 'x:a', 'c' ) #-> <e a='b' x:a='c'/>
|
|
# e.add_attribute Attribute.new('b', 'd') #-> <e a='b' x:a='c' b='d'/>
|
|
def add_attribute( key, value=nil )
|
|
if key.kind_of? Attribute
|
|
@attributes << key
|
|
else
|
|
@attributes[key] = value
|
|
end
|
|
end
|
|
|
|
# Add multiple attributes to this element.
|
|
# hash:: is either a hash, or array of arrays
|
|
# el.add_attributes( {"name1"=>"value1", "name2"=>"value2"} )
|
|
# el.add_attributes( [ ["name1","value1"], ["name2"=>"value2"] ] )
|
|
def add_attributes hash
|
|
if hash.kind_of? Hash
|
|
hash.each_pair {|key, value| @attributes[key] = value }
|
|
elsif hash.kind_of? Array
|
|
hash.each { |value| @attributes[ value[0] ] = value[1] }
|
|
end
|
|
end
|
|
|
|
# Removes an attribute
|
|
# key::
|
|
# either an Attribute or a String. In either case, the
|
|
# attribute is found by matching the attribute name to the argument,
|
|
# and then removed. If no attribute is found, no action is taken.
|
|
# Returns::
|
|
# the attribute removed, or nil if this Element did not contain
|
|
# a matching attribute
|
|
# e = Element.new('E')
|
|
# e.add_attribute( 'name', 'Sean' ) #-> <E name='Sean'/>
|
|
# r = e.add_attribute( 'sur:name', 'Russell' ) #-> <E name='Sean' sur:name='Russell'/>
|
|
# e.delete_attribute( 'name' ) #-> <E sur:name='Russell'/>
|
|
# e.delete_attribute( r ) #-> <E/>
|
|
def delete_attribute(key)
|
|
attr = @attributes.get_attribute(key)
|
|
attr.remove unless attr.nil?
|
|
end
|
|
|
|
#################################################
|
|
# Other Utilities #
|
|
#################################################
|
|
|
|
# Get an array of all CData children.
|
|
# IMMUTABLE
|
|
def cdatas
|
|
find_all { |child| child.kind_of? CData }.freeze
|
|
end
|
|
|
|
# Get an array of all Comment children.
|
|
# IMMUTABLE
|
|
def comments
|
|
find_all { |child| child.kind_of? Comment }.freeze
|
|
end
|
|
|
|
# Get an array of all Instruction children.
|
|
# IMMUTABLE
|
|
def instructions
|
|
find_all { |child| child.kind_of? Instruction }.freeze
|
|
end
|
|
|
|
# Get an array of all Text children.
|
|
# IMMUTABLE
|
|
def texts
|
|
find_all { |child| child.kind_of? Text }.freeze
|
|
end
|
|
|
|
# Writes out this element, and recursively, all children.
|
|
# output::
|
|
# output an object which supports '<< string'; this is where the
|
|
# document will be written.
|
|
# indent::
|
|
# An integer. If -1, no indenting will be used; otherwise, the
|
|
# indentation will be this number of spaces, and children will be
|
|
# indented an additional amount. Defaults to -1
|
|
# transitive::
|
|
# If transitive is true and indent is >= 0, then the output will be
|
|
# pretty-printed in such a way that the added whitespace does not affect
|
|
# the parse tree of the document
|
|
# ie_hack::
|
|
# Internet Explorer is the worst piece of crap to have ever been
|
|
# written, with the possible exception of Windows itself. Since IE is
|
|
# unable to parse proper XML, we have to provide a hack to generate XML
|
|
# that IE's limited abilities can handle. This hack inserts a space
|
|
# before the /> on empty tags. Defaults to false
|
|
#
|
|
# out = ''
|
|
# doc.write( out ) #-> doc is written to the string 'out'
|
|
# doc.write( $stdout ) #-> doc written to the console
|
|
def write(writer=$stdout, indent=-1, transitive=false, ie_hack=false)
|
|
#print "ID:#{indent}"
|
|
writer << "<#@expanded_name"
|
|
|
|
@attributes.each_attribute do |attr|
|
|
writer << " "
|
|
attr.write( writer, indent )
|
|
end unless @attributes.empty?
|
|
|
|
if @children.empty?
|
|
if transitive and indent>-1
|
|
writer << "\n"
|
|
indent( writer, indent )
|
|
elsif ie_hack
|
|
writer << " "
|
|
end
|
|
writer << "/"
|
|
else
|
|
if transitive and indent>-1 and !@children[0].kind_of? Text
|
|
writer << "\n"
|
|
indent writer, indent+1
|
|
end
|
|
writer << ">"
|
|
write_children( writer, indent, transitive, ie_hack )
|
|
writer << "</#{expanded_name}"
|
|
end
|
|
if transitive and indent>-1 and !@children.empty?
|
|
writer << "\n"
|
|
indent -= 1 if next_sibling.nil?
|
|
indent(writer, indent)
|
|
end
|
|
writer << ">"
|
|
end
|
|
|
|
|
|
private
|
|
def __to_xpath_helper node
|
|
rv = node.expanded_name.clone
|
|
if node.parent
|
|
results = node.parent.find_all {|n|
|
|
n.kind_of?(REXML::Element) and n.expanded_name == node.expanded_name
|
|
}
|
|
if results.length > 1
|
|
idx = results.index( node )
|
|
rv << "[#{idx+1}]"
|
|
end
|
|
end
|
|
rv
|
|
end
|
|
|
|
# A private helper method
|
|
def each_with_something( test, max=0, name=nil )
|
|
num = 0
|
|
child=nil
|
|
@elements.each( name ){ |child|
|
|
yield child if test.call(child) and num += 1
|
|
return if max>0 and num == max
|
|
}
|
|
end
|
|
|
|
# A private helper method
|
|
def write_children( writer, indent, transitive, ie_hack )
|
|
cr = (indent < 0) ? '' : "\n"
|
|
if indent == -1
|
|
each { |child| child.write( writer, indent, transitive, ie_hack ) }
|
|
else
|
|
next_indent = indent+1
|
|
last_child=nil
|
|
each { |child|
|
|
unless child.kind_of? Text or last_child.kind_of? Text or transitive
|
|
writer << cr
|
|
indent(writer, next_indent)
|
|
end
|
|
child.write( writer, next_indent, transitive, ie_hack )
|
|
last_child = child
|
|
}
|
|
unless last_child.kind_of? Text or transitive
|
|
writer << cr
|
|
indent( writer, indent )
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
########################################################################
|
|
# ELEMENTS #
|
|
########################################################################
|
|
|
|
# A class which provides filtering of children for Elements, and
|
|
# XPath search support. You are expected to only encounter this class as
|
|
# the <tt>element.elements</tt> object. Therefore, you are
|
|
# _not_ expected to instantiate this yourself.
|
|
class Elements
|
|
include Enumerable
|
|
# Constructor
|
|
# parent:: the parent Element
|
|
def initialize parent
|
|
@element = parent
|
|
end
|
|
|
|
# Fetches a child element. Filters only Element children, regardless of
|
|
# the XPath match.
|
|
# index::
|
|
# the search parameter. This is either an Integer, which
|
|
# will be used to find the index'th child Element, or an XPath,
|
|
# which will be used to search for the Element. <em>Because
|
|
# of the nature of XPath searches, any element in the connected XML
|
|
# document can be fetched through any other element.</em> <b>The
|
|
# Integer index is 1-based, not 0-based.</b> This means that the first
|
|
# child element is at index 1, not 0, and the +n+th element is at index
|
|
# +n+, not <tt>n-1</tt>. This is because XPath indexes element children
|
|
# starting from 1, not 0, and the indexes should be the same.
|
|
# name::
|
|
# optional, and only used in the first argument is an
|
|
# Integer. In that case, the index'th child Element that has the
|
|
# supplied name will be returned. Note again that the indexes start at 1.
|
|
# Returns:: the first matching Element, or nil if no child matched
|
|
# doc = Document.new '<a><b/><c id="1"/><c id="2"/><d/></a>'
|
|
# doc.root.elements[1] #-> <b/>
|
|
# doc.root.elements['c'] #-> <c id="1"/>
|
|
# doc.root.elements[2,'c'] #-> <c id="2"/>
|
|
def []( index, name=nil)
|
|
if index.kind_of? Integer
|
|
raise "index (#{index}) must be >= 1" if index < 1
|
|
name = literalize(name) if name
|
|
num = 0
|
|
child = nil
|
|
@element.find { |child|
|
|
child.kind_of? Element and
|
|
(name.nil? ? true : child.has_name?( name )) and
|
|
(num += 1) == index
|
|
}
|
|
else
|
|
return XPath::first( @element, index )
|
|
#{ |element|
|
|
# return element if element.kind_of? Element
|
|
#}
|
|
#return nil
|
|
end
|
|
end
|
|
|
|
# Sets an element, replacing any previous matching element. If no
|
|
# existing element is found ,the element is added.
|
|
# index:: Used to find a matching element to replace. See []().
|
|
# element::
|
|
# The element to replace the existing element with
|
|
# the previous element
|
|
# Returns:: nil if no previous element was found.
|
|
#
|
|
# doc = Document.new '<a/>'
|
|
# doc.root.elements[10] = Element.new('b') #-> <a><b/></a>
|
|
# doc.root.elements[1] #-> <b/>
|
|
# doc.root.elements[1] = Element.new('c') #-> <a><c/></a>
|
|
# doc.root.elements['c'] = Element.new('d') #-> <a><d/></a>
|
|
def []=( index, element )
|
|
previous = self[index]
|
|
if previous.nil?
|
|
@element.add element
|
|
else
|
|
previous.replace_with element
|
|
end
|
|
return previous
|
|
end
|
|
|
|
# Returns +true+ if there are no +Element+ children, +false+ otherwise
|
|
def empty?
|
|
@element.find{ |child| child.kind_of? Element}.nil?
|
|
end
|
|
|
|
# Returns the index of the supplied child (starting at 1), or -1 if
|
|
# the element is not a child
|
|
# element:: an +Element+ child
|
|
def index element
|
|
rv = 0
|
|
found = @element.find do |child|
|
|
child.kind_of? Element and
|
|
(rv += 1) and
|
|
child == element
|
|
end
|
|
return rv if found == element
|
|
return -1
|
|
end
|
|
|
|
# Deletes a child Element
|
|
# element::
|
|
# Either an Element, which is removed directly; an
|
|
# xpath, where the first matching child is removed; or an Integer,
|
|
# where the n'th Element is removed.
|
|
# Returns:: the removed child
|
|
# doc = Document.new '<a><b/><c/><c id="1"/></a>'
|
|
# b = doc.root.elements[1]
|
|
# doc.root.elements.delete b #-> <a><c/><c id="1"/></a>
|
|
# doc.elements.delete("a/c[@id='1']") #-> <a><c/></a>
|
|
# doc.root.elements.delete 1 #-> <a/>
|
|
def delete element
|
|
if element.kind_of? Element
|
|
@element.delete element
|
|
else
|
|
el = self[element]
|
|
el.remove if el
|
|
end
|
|
end
|
|
|
|
# Removes multiple elements. Filters for Element children, regardless of
|
|
# XPath matching.
|
|
# xpath:: all elements matching this String path are removed.
|
|
# Returns:: an Array of Elements that have been removed
|
|
# doc = Document.new '<a><c/><c/><c/><c/></a>'
|
|
# deleted = doc.elements.delete_all 'a/c' #-> [<c/>, <c/>, <c/>, <c/>]
|
|
def delete_all( xpath )
|
|
rv = []
|
|
XPath::each( @element, xpath) {|element|
|
|
rv << element if element.kind_of? Element
|
|
}
|
|
rv.each do |element|
|
|
@element.delete element
|
|
element.remove
|
|
end
|
|
return rv
|
|
end
|
|
|
|
# Adds an element
|
|
# element::
|
|
# if supplied, is either an Element, String, or
|
|
# Source (see Element.initialize). If not supplied or nil, a
|
|
# new, default Element will be constructed
|
|
# Returns:: the added Element
|
|
# a = Element.new 'a'
|
|
# a.elements.add Element.new 'b' #-> <a><b/></a>
|
|
# a.elements.add 'c' #-> <a><b/><c/></a>
|
|
def add element=nil
|
|
rv = nil
|
|
if element.nil?
|
|
Element.new "", self, @element.context
|
|
elsif not element.kind_of?(Element)
|
|
Element.new element, self, @element.context
|
|
else
|
|
@element << element
|
|
element.context = @element.context
|
|
element
|
|
end
|
|
end
|
|
|
|
alias :<< :add
|
|
|
|
# Iterates through all of the child Elements, optionally filtering
|
|
# them by a given XPath
|
|
# xpath::
|
|
# optional. If supplied, this is a String XPath, and is used to
|
|
# filter the children, so that only matching children are yielded. Note
|
|
# that XPaths are automatically filtered for Elements, so that
|
|
# non-Element children will not be yielded
|
|
# doc = Document.new '<a><b/><c/><d/>sean<b/><c/><d/></a>'
|
|
# doc.root.each {|e|p e} #-> Yields b, c, d, b, c, d elements
|
|
# doc.root.each('b') {|e|p e} #-> Yields b, b elements
|
|
# doc.root.each('child::node()') {|e|p e}
|
|
# #-> Yields <b/>, <c/>, <d/>, <b/>, <c/>, <d/>
|
|
# XPath.each(doc.root, 'child::node()', &block)
|
|
# #-> Yields <b/>, <c/>, <d/>, sean, <b/>, <c/>, <d/>
|
|
def each( xpath=nil, &block)
|
|
XPath::each( @element, xpath ) {|e| yield e if e.kind_of? Element }
|
|
end
|
|
|
|
# Returns the number of +Element+ children of the parent object.
|
|
# doc = Document.new '<a>sean<b/>elliott<b/>russell<b/></a>'
|
|
# doc.root.size #-> 6, 3 element and 3 text nodes
|
|
# doc.root.elements.size #-> 3
|
|
def size
|
|
count = 0
|
|
@element.each {|child| count+=1 if child.kind_of? Element }
|
|
count
|
|
end
|
|
|
|
# Returns an Array of Element children. An XPath may be supplied to
|
|
# filter the children. Only Element children are returned, even if the
|
|
# supplied XPath matches non-Element children.
|
|
# doc = Document.new '<a>sean<b/>elliott<c/></a>'
|
|
# doc.root.elements.to_a #-> [ <b/>, <c/> ]
|
|
# doc.root.elements.to_a("child::node()") #-> [ <b/>, <c/> ]
|
|
# XPath.match(doc.root, "child::node()") #-> [ sean, <b/>, elliott, <c/> ]
|
|
def to_a( xpath=nil )
|
|
rv = XPath.match( @element, xpath )
|
|
return rv.find_all{|e| e.kind_of? Element} if xpath
|
|
rv
|
|
end
|
|
|
|
private
|
|
# Private helper class. Removes quotes from quoted strings
|
|
def literalize name
|
|
name = name[1..-2] if name[0] == ?' or name[0] == ?" #'
|
|
name
|
|
end
|
|
end
|
|
|
|
########################################################################
|
|
# ATTRIBUTES #
|
|
########################################################################
|
|
|
|
# A class that defines the set of Attributes of an Element and provides
|
|
# operations for accessing elements in that set.
|
|
class Attributes < Hash
|
|
# Constructor
|
|
# element:: the Element of which this is an Attribute
|
|
def initialize element
|
|
@element = element
|
|
end
|
|
|
|
# Fetches an attribute value. If you want to get the Attribute itself,
|
|
# use get_attribute()
|
|
# name:: an XPath attribute name. Namespaces are relevant here.
|
|
# Returns::
|
|
# the String value of the matching attribute, or +nil+ if no
|
|
# matching attribute was found.
|
|
#
|
|
# doc = Document.new "<a foo:att='1' bar:att='2' att='3'/>"
|
|
# doc.root.attributes['att'] #-> '3'
|
|
# doc.root.attributes['bar:att'] #-> '2'
|
|
def [](name)
|
|
attr = get_attribute(name)
|
|
return attr.value unless attr.nil?
|
|
return nil
|
|
end
|
|
|
|
def to_a
|
|
values.flatten
|
|
end
|
|
|
|
# Returns the number of attributes the owning Element contains.
|
|
# doc = Document "<a x='1' y='2' foo:x='3'/>"
|
|
# doc.root.attributes.length #-> 3
|
|
def length
|
|
c = 0
|
|
each_attribute { c+=1 }
|
|
c
|
|
end
|
|
alias :size :length
|
|
|
|
# Itterates over the attributes of an Element. Yields actual Attribute
|
|
# nodes, not String values.
|
|
#
|
|
# doc = Document.new '<a x="1" y="2"/>'
|
|
# doc.root.attributes.each_attribute {|attr|
|
|
# p attr.expanded_name+" => "+attr.value
|
|
# }
|
|
def each_attribute # :yields: attribute
|
|
each_value do |val|
|
|
if val.kind_of? Attribute
|
|
yield val
|
|
else
|
|
val.each_value { |atr| yield atr }
|
|
end
|
|
end
|
|
end
|
|
|
|
# Itterates over each attribute of an Element, yielding the expanded name
|
|
# and value as a pair of Strings.
|
|
#
|
|
# doc = Document.new '<a x="1" y="2"/>'
|
|
# doc.root.attributes.each {|name, value| p name+" => "+value }
|
|
def each
|
|
each_attribute do |attr|
|
|
yield attr.expanded_name, attr.value
|
|
end
|
|
end
|
|
|
|
# Fetches an attribute
|
|
# name::
|
|
# the name by which to search for the attribute. Can be a
|
|
# <tt>prefix:name</tt> namespace name.
|
|
# Returns:: The first matching attribute, or nil if there was none. This
|
|
# value is an Attribute node, not the String value of the attribute.
|
|
# doc = Document.new '<a x:foo="1" foo="2" bar="3"/>'
|
|
# doc.root.attributes.get_attribute("foo").value #-> "2"
|
|
# doc.root.attributes.get_attribute("x:foo").value #-> "1"
|
|
def get_attribute( name )
|
|
attr = fetch( name, nil )
|
|
if attr.nil?
|
|
return nil if name.nil?
|
|
# Look for prefix
|
|
name =~ Namespace::NAMESPLIT
|
|
prefix, n = $1, $2
|
|
if prefix
|
|
attr = fetch( n, nil )
|
|
# check prefix
|
|
if attr == nil
|
|
elsif attr.kind_of? Attribute
|
|
return attr if prefix == attr.prefix
|
|
else
|
|
attr = attr[ prefix ]
|
|
return attr
|
|
end
|
|
end
|
|
element_document = @element.document
|
|
if element_document and element_document.doctype
|
|
expn = @element.expanded_name
|
|
expn = element_document.doctype.name if expn.size == 0
|
|
attr_val = element_document.doctype.attribute_of(expn, name)
|
|
return Attribute.new( name, attr_val ) if attr_val
|
|
end
|
|
return nil
|
|
end
|
|
if attr.kind_of? Hash
|
|
attr = attr[ @element.prefix ]
|
|
end
|
|
return attr
|
|
end
|
|
|
|
# Sets an attribute, overwriting any existing attribute value by the
|
|
# same name. Namespace is significant.
|
|
# name:: the name of the attribute
|
|
# value::
|
|
# (optional) If supplied, the value of the attribute. If
|
|
# nil, any existing matching attribute is deleted.
|
|
# Returns::
|
|
# Owning element
|
|
# doc = Document.new "<a x:foo='1' foo='3'/>"
|
|
# doc.root.attributes['y:foo'] = '2'
|
|
# doc.root.attributes['foo'] = '4'
|
|
# doc.root.attributes['x:foo'] = nil
|
|
def []=( name, value )
|
|
if value.nil? # Delete the named attribute
|
|
attr = get_attribute(name)
|
|
delete attr
|
|
return
|
|
end
|
|
value = Attribute.new(name, value) unless value.kind_of? Attribute
|
|
value.element = @element
|
|
old_attr = fetch(value.name, nil)
|
|
if old_attr.nil?
|
|
store(value.name, value)
|
|
elsif old_attr.kind_of? Hash
|
|
old_attr[value.prefix] = value
|
|
elsif old_attr.prefix != value.prefix
|
|
# Check for conflicting namespaces
|
|
raise ParseException.new(
|
|
"Namespace conflict in adding attribute \"#{value.name}\": "+
|
|
"Prefix \"#{old_attr.prefix}\" = "+
|
|
"\"#{@element.namespace(old_attr.prefix)}\" and prefix "+
|
|
"\"#{value.prefix}\" = \"#{@element.namespace(value.prefix)}\"") if
|
|
value.prefix != "xmlns" and old_attr.prefix != "xmlns" and
|
|
@element.namespace( old_attr.prefix ) ==
|
|
@element.namespace( value.prefix )
|
|
store value.name, { old_attr.prefix => old_attr,
|
|
value.prefix => value }
|
|
else
|
|
store value.name, value
|
|
end
|
|
return @element
|
|
end
|
|
|
|
# Returns an array of Strings containing all of the prefixes declared
|
|
# by this set of # attributes. The array does not include the default
|
|
# namespace declaration, if one exists.
|
|
# doc = Document.new("<a xmlns='foo' xmlns:x='bar' xmlns:y='twee' "+
|
|
# "z='glorp' p:k='gru'/>")
|
|
# prefixes = doc.root.attributes.prefixes #-> ['x', 'y']
|
|
def prefixes
|
|
ns = []
|
|
each_attribute do |attribute|
|
|
ns << attribute.name if attribute.prefix == 'xmlns'
|
|
end
|
|
if @element.document and @element.document.doctype
|
|
expn = @element.expanded_name
|
|
expn = @element.document.doctype.name if expn.size == 0
|
|
@element.document.doctype.attributes_of(expn).each {
|
|
|attribute|
|
|
ns << attribute.name if attribute.prefix == 'xmlns'
|
|
}
|
|
end
|
|
ns
|
|
end
|
|
|
|
def namespaces
|
|
namespaces = []
|
|
each_attribute do |attribute|
|
|
namespaces << attribute.value if attribute.prefix == 'xmlns' or attribute.name == 'xmlns'
|
|
end
|
|
if @element.document and @element.document.doctype
|
|
expn = @element.expanded_name
|
|
expn = @element.document.doctype.name if expn.size == 0
|
|
@element.document.doctype.attributes_of(expn).each {
|
|
|attribute|
|
|
namespaces << attribute.value if attribute.prefix == 'xmlns' or attribute.name == 'xmlns'
|
|
}
|
|
end
|
|
namespaces
|
|
end
|
|
|
|
# Removes an attribute
|
|
# attribute::
|
|
# either a String, which is the name of the attribute to remove --
|
|
# namespaces are significant here -- or the attribute to remove.
|
|
# Returns:: the owning element
|
|
# doc = Document.new "<a y:foo='0' x:foo='1' foo='3' z:foo='4'/>"
|
|
# doc.root.attributes.delete 'foo' #-> <a y:foo='0' x:foo='1' z:foo='4'/>"
|
|
# doc.root.attributes.delete 'x:foo' #-> <a y:foo='0' z:foo='4'/>"
|
|
# attr = doc.root.attributes.get_attribute('y:foo')
|
|
# doc.root.attributes.delete attr #-> <a z:foo='4'/>"
|
|
def delete( attribute )
|
|
name = nil
|
|
prefix = nil
|
|
if attribute.kind_of? Attribute
|
|
name = attribute.name
|
|
prefix = attribute.prefix
|
|
else
|
|
attribute =~ Namespace::NAMESPLIT
|
|
prefix, name = $1, $2
|
|
prefix = '' unless prefix
|
|
end
|
|
old = fetch(name, nil)
|
|
attr = nil
|
|
if old.kind_of? Hash # the supplied attribute is one of many
|
|
attr = old.delete(prefix)
|
|
if old.size == 1
|
|
repl = nil
|
|
old.each_value{|v| repl = v}
|
|
store name, repl
|
|
end
|
|
elsif old.nil?
|
|
return @element
|
|
else # the supplied attribute is a top-level one
|
|
attr = old
|
|
res = super(name)
|
|
end
|
|
@element
|
|
end
|
|
|
|
# Adds an attribute, overriding any existing attribute by the
|
|
# same name. Namespaces are significant.
|
|
# attribute:: An Attribute
|
|
def add( attribute )
|
|
self[attribute.name] = attribute
|
|
end
|
|
|
|
alias :<< :add
|
|
|
|
# Deletes all attributes matching a name. Namespaces are significant.
|
|
# name::
|
|
# A String; all attributes that match this path will be removed
|
|
# Returns:: an Array of the Attributes that were removed
|
|
def delete_all( name )
|
|
rv = []
|
|
each_attribute { |attribute|
|
|
rv << attribute if attribute.expanded_name == name
|
|
}
|
|
rv.each{ |attr| attr.remove }
|
|
return rv
|
|
end
|
|
|
|
# The +get_attribute_ns+ method retrieves a method by its namespace
|
|
# and name. Thus it is possible to reliably identify an attribute
|
|
# even if an XML processor has changed the prefix.
|
|
#
|
|
# Method contributed by Henrik Martensson
|
|
def get_attribute_ns(namespace, name)
|
|
each_attribute() { |attribute|
|
|
if name == attribute.name &&
|
|
namespace == attribute.namespace()
|
|
return attribute
|
|
end
|
|
}
|
|
nil
|
|
end
|
|
end
|
|
end
|