mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
8ef5da3be1
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@4790 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
1147 lines
38 KiB
Ruby
1147 lines
38 KiB
Ruby
require "rexml/parent"
|
|
require "rexml/namespace"
|
|
require "rexml/attribute"
|
|
require "rexml/cdata"
|
|
require "rexml/xpath"
|
|
require "rexml/parseexception"
|
|
|
|
module REXML
|
|
# 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.
|
|
# If a Source, the source will be scanned and parsed for an Element,
|
|
# and all child elements will be recursively parsed as well.
|
|
# 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
|
|
|
|
# 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 element 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.
|
|
# d = Document.new '<a><b><c/></b></a>'
|
|
# a = d[1] ; c = a[1][1]
|
|
# d.root # These all evaluate to the same Element,
|
|
# a.root # namely, <a>
|
|
# c.root #
|
|
def root
|
|
parent.nil? ? self : 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
|
|
root.parent if root
|
|
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=nil, attrs=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 = 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 )
|
|
text = Text.new( text, whitespace(), nil, raw() ) if text.kind_of? String
|
|
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
|
|
|
|
#################################################
|
|
# 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::
|
|
# What the heck does this do? Defaults to false
|
|
# 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?
|
|
writer << " " if ie_hack
|
|
writer << "/"
|
|
else
|
|
if transitive and indent>-1 and !@children[0].kind_of? Text
|
|
writer << "\n"
|
|
indent writer, indent+2
|
|
end
|
|
writer << ">"
|
|
write_children( writer, indent, transitive, ie_hack )
|
|
writer << "</#{expanded_name}"
|
|
end
|
|
if transitive and indent>-1
|
|
writer << "\n"
|
|
indent -= 2 if next_sibling.nil?
|
|
indent(writer, indent)
|
|
end
|
|
writer << ">"
|
|
end
|
|
|
|
|
|
private
|
|
# 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 size == 1 and @children[0].kind_of?(Text)
|
|
# self[0].write( writer, -1 )
|
|
if indent == -1
|
|
each { |child| child.write( writer, indent, transitive, ie_hack ) }
|
|
else
|
|
next_indent = indent+2
|
|
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
|
|
|
|
# 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
|
|
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
|
|
end
|
|
end
|