Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions lib/rexml/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,11 @@ def element=( element )
self
end

# Removes this Attribute from the tree, and returns true if successful
# Removes this Attribute from the tree
#
# This method is usually not called directly.
def remove
@element.attributes.delete self.name unless @element.nil?
@element.attributes.delete self unless @element.nil?
end

# Writes this attribute (EG, puts 'key="value"' to the output)
Expand All @@ -205,6 +205,15 @@ def xpath
def document
@element&.document
end

# Returns true if this attribute is a namespace declaration, false otherwise.
# "foo" => false
# "xmlns" => true
# "xmlns:foo" => true
# "foo:xmlns" => false
def namespace_declaration?
prefix == 'xmlns' || (prefix.empty? && name == 'xmlns')
end
end
end
#vim:ts=2 sw=2 noexpandtab:
28 changes: 16 additions & 12 deletions lib/rexml/doctype.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,23 +113,27 @@ def node_type
end

def attributes_of element
rv = []
each do |child|
child.each do |key,val|
rv << Attribute.new(key,val)
end if child.kind_of? AttlistDecl and child.element_name == element
attribute_declarations_of(element).map do |key, value|
Attribute.new(key, value)
end
rv
end

def attribute_of element, attribute
att_decl = find do |child|
child.kind_of? AttlistDecl and
child.element_name == element and
child.include? attribute
attribute_declarations_of(element)[attribute]
end

def attribute_declarations_of(name)
raw_attributes = {}
decls = select do |child|
child.kind_of?(AttlistDecl) && child.element_name == name
end
decls.each do |child|
child.each do |key, val|
# First declaration wins
raw_attributes[key] = val unless raw_attributes.key? key
end
end
return nil unless att_decl
att_decl[attribute]
raw_attributes
end
Comment thread
naitoh marked this conversation as resolved.

def clone
Expand Down
203 changes: 82 additions & 121 deletions lib/rexml/element.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2240,15 +2240,10 @@ def length
# [REXML::Attribute, bar:att='2']
# [REXML::Attribute, att='&lt;']
#
def each_attribute # :yields: attribute
return to_enum(__method__) unless block_given?
each_value do |val|
if val.kind_of? Attribute
yield val
else
val.each_value { |atr| yield atr }
end
end
# This method doesn't iterate attributes in the DTD. This may be a bug.
#
def each_attribute(&block) # :yields: attribute
each_own_attribute(&block)
end
Comment thread
naitoh marked this conversation as resolved.

# :call-seq:
Expand All @@ -2273,6 +2268,8 @@ def each_attribute # :yields: attribute
# ["bar:att", "2"]
# ["att", "<"]
#
# This method doesn't iterate attributes in the DTD. This may be a bug.
#
def each
return to_enum(__method__) unless block_given?
each_attribute do |attr|
Expand Down Expand Up @@ -2300,36 +2297,7 @@ def each
# attrs.get_attribute('nosuch') # => nil
#
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
doctype = @element.document&.doctype
if doctype
expn = @element.expanded_name
expn = doctype.name if expn.size == 0
attr_val = 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
attr
fetch(name, nil) || attlist_attributes&.[](name)
end

# :call-seq:
Expand Down Expand Up @@ -2357,12 +2325,15 @@ def get_attribute( name )
#
def []=( name, value )
if value.nil? # Delete the named attribute
attr = get_attribute(name)
delete attr
delete name
return
end

unless value.kind_of? Attribute
if value.kind_of? Attribute
# Use attribute's expanded name to avoid inconsistency.
# TODO: Explicitly document that inconsistent names are allowed, or raise/warn about inconsistency.
name = value.expanded_name
else
doctype = @element.document&.doctype
if doctype
value = Text::normalize( value, doctype )
Expand All @@ -2372,17 +2343,7 @@ def []=( name, value )
value = Attribute.new(name, value)
end
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
store value.name, {old_attr.prefix => old_attr,
value.prefix => value}
else
store value.name, value
end
store name, value
@element
end

Expand All @@ -2398,20 +2359,7 @@ def []=( name, value )
# d.root.attributes.prefixes # => ["x", "y"]
#
def prefixes
ns = []
each_attribute do |attribute|
ns << attribute.name if attribute.prefix == 'xmlns'
end
doctype = @element.document&.doctype
if doctype
expn = @element.expanded_name
expn = doctype.name if expn.size == 0
doctype.attributes_of(expn).each {
|attribute|
ns << attribute.name if attribute.prefix == 'xmlns'
}
end
ns
namespaces.keys - ['xmlns']
end

# :call-seq:
Expand All @@ -2425,17 +2373,8 @@ def prefixes
#
def namespaces
namespaces = {}
each_attribute do |attribute|
namespaces[attribute.name] = attribute.value if attribute.prefix == 'xmlns' or attribute.name == 'xmlns'
end
doctype = @element.document&.doctype
if doctype
expn = @element.expanded_name
expn = doctype.name if expn.size == 0
doctype.attributes_of(expn).each {
|attribute|
namespaces[attribute.name] = attribute.value if attribute.prefix == 'xmlns' or attribute.name == 'xmlns'
}
each_effective_attribute do |attribute|
namespaces[attribute.name] = attribute.value if attribute.namespace_declaration?
end
namespaces
end
Expand Down Expand Up @@ -2468,28 +2407,11 @@ def namespaces
# attrs.delete(attr) # => <ele att='&lt;'/> # => <ele att='&lt;'/>
# attrs.delete(attr) # => <ele att='&lt;'/> # => <ele/>
#
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)
if old.kind_of? Hash # the supplied attribute is one of many
old.delete(prefix)
if old.size == 1
repl = nil
old.each_value{|v| repl = v}
store name, repl
end
elsif old # the supplied attribute is a top-level one
super(name)
end
def delete(attribute_or_name)
key = attribute_or_name
key = attribute_or_name.expanded_name if attribute_or_name.kind_of? Attribute
super(key)

@element
end

Expand All @@ -2508,13 +2430,13 @@ def delete( attribute )
# d = REXML::Document.new(xml_string)
# ele = d.root.elements['//ele'] # => <a foo:att='1' bar:att='2' att='&lt;'/>
# attrs = ele.attributes
# attrs # => {"att"=>{"foo"=>foo:att='1', "bar"=>bar:att='2', ""=>att='&lt;'}}
# attrs # => {"foo:att" => foo:att='1', "bar:att" => bar:att='2', "att" => att='&lt;'}
# attrs.add(REXML::Attribute.new('foo:att', '2')) # => foo:att='2'
# attrs.add(REXML::Attribute.new('baz', '3')) # => baz='3'
# attrs.include?('baz') # => true
Comment thread
naitoh marked this conversation as resolved.
#
def add( attribute )
self[attribute.name] = attribute
self[attribute.expanded_name] = attribute
end
Comment thread
naitoh marked this conversation as resolved.

alias :<< :add
Expand All @@ -2527,21 +2449,26 @@ def add( attribute )
#
# xml_string = <<-EOT
# <root xmlns:foo="http://foo" xmlns:bar="http://bar">
# <ele foo:att='1' bar:att='2' att='&lt;'/>
# <ele foo:att='1' att='&lt;' foo:other='2' other='3'/>
# </root>
# EOT
# d = REXML::Document.new(xml_string)
# ele = d.root.elements['//ele'] # => <a foo:att='1' bar:att='2' att='&lt;'/>
# ele = d.root.elements['//ele'] # => <a foo:att='1' att='&lt;' foo:other='2' other='3'/>
Comment thread
naitoh marked this conversation as resolved.
# attrs = ele.attributes
# attrs.delete_all('att') # => [att='&lt;']
# attrs.delete_all('att') # => [foo:att='1', att='&lt;']
# attrs.each_attribute.map(&:expanded_name) #=> ['foo:other', 'other']
#
def delete_all( name )
Comment thread
naitoh marked this conversation as resolved.
rv = []
each_attribute { |attribute|
rv << attribute if attribute.expanded_name == name
}
rv.each{ |attr| attr.remove }
rv
attributes = each_attribute.select do |attribute|
# For <element xmlns:foo="url" ns:foo="value"/>
# delete_all('foo') should not delete xmlns:foo="url"
# because it is a namespace declaration, not a normal attribute.
(!attribute.namespace_declaration? && attribute.name == name) || attribute.expanded_name == name
end
attributes.each do |attribute|
delete attribute.expanded_name
end
attributes
Comment thread
naitoh marked this conversation as resolved.
end

# :call-seq:
Expand All @@ -2562,17 +2489,51 @@ def delete_all( name )
# attrs.get_attribute_ns('http://foo', 'nosuch') # => nil
#
def get_attribute_ns(namespace, name)
result = nil
each_attribute() { |attribute|
if name == attribute.name &&
namespace == attribute.namespace() &&
( !namespace.empty? || !attribute.fully_expanded_name.index(':') )
# foo will match xmlns:foo, but only if foo isn't also an attribute
result = attribute if !result or !namespace.empty? or
!attribute.fully_expanded_name.index(':')
each_effective_attribute.find do |attribute|
if attribute.namespace_declaration?
# namespace declarations are not considered as attributes in this method.
# For example: <elem1 xmlns="" xmlns:foo="url" /><elem2 xmlns="bar" xmlns:foo="url" />
# Both elem1.get_attribute_ns('', 'foo') and elem2.get_attribute_ns('bar', 'foo')
# should not match.
false
elsif namespace.empty? && !attribute.prefix.empty?
# If prefix is present, namespace url shouldn't be empty, so it never matches.
# For example, <elem foo:att="value" />, even if attribute.namespace returns '',
# it just means that namespace lookup failed, it shouldn't match.
false
else
name == attribute.name && namespace == attribute.namespace()
end
}
result
end
end
Comment thread
naitoh marked this conversation as resolved.

private

# Iterates over the attributes of the element and those in the DTD.
# If the element has an attribute with the same name as one in the DTD,
# the element's attribute takes precedence.
def each_effective_attribute
return to_enum(__method__) unless block_given?
attr = attlist_attributes
attr = attr ? attr.merge(self) : self
attr.each_value do |attribute|
yield attribute
end
end
Comment thread
naitoh marked this conversation as resolved.

alias each_own_attribute each_value
private :each_own_attribute

def attlist_attributes
doctype = @element.document&.doctype
if doctype
expn = @element.is_a?(Document) ? doctype.name : @element.expanded_name
doctype.attribute_declarations_of(expn).map do |key, value|
Comment thread
naitoh marked this conversation as resolved.
attr = Attribute.new(key, value)
attr.element = @element
[key, attr]
end.to_h
end
end
Comment thread
naitoh marked this conversation as resolved.
end
end
9 changes: 9 additions & 0 deletions test/test_attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,14 @@ def test_empty_prefix
"\#{PREFIX}:\#{LOCAL_NAME} or \#{LOCAL_NAME}: <\":x\">",
error.message)
end

def test_namespace_declaration
assert_equal(false, REXML::Attribute.new("name").namespace_declaration?)
assert_equal(false, REXML::Attribute.new("prefix:name").namespace_declaration?)
assert_equal(false, REXML::Attribute.new("prefix:xmlns").namespace_declaration?)
assert_equal(true, REXML::Attribute.new("xmlns").namespace_declaration?)
assert_equal(true, REXML::Attribute.new("xmlns:name").namespace_declaration?)
# REXML::Attribute.new("xmlns:xmlns") is not tested because it's invalid
end
end
end
10 changes: 10 additions & 0 deletions test/test_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ def test_delete
assert_equal '4', doc.root.attributes['z:foo']
end

def test_delete_all
doc = Document.new("<a xmlns:x='x' xmlns:y='y' x:x='0' y:x='1' x='2' y='3' x:y='4' x:z='5' y:y='6'/>")
doc.root.attributes.delete_all 'x'
assert_equal ['xmlns:x', 'xmlns:y', 'y', 'x:y', 'x:z', 'y:y'], doc.root.attributes.each_attribute.map(&:expanded_name)
doc.root.attributes.delete_all 'x:y'
assert_equal ['xmlns:x', 'xmlns:y', 'y', 'x:z', 'y:y'], doc.root.attributes.each_attribute.map(&:expanded_name)
doc.root.attributes.delete_all 'xmlns:y'
assert_equal ['xmlns:x', 'y', 'x:z', 'y:y'], doc.root.attributes.each_attribute.map(&:expanded_name)
end

def test_prefixes
doc = Document.new("<a xmlns='foo' xmlns:x='bar' xmlns:y='twee' z='glorp' x:k='gru'/>")
prefixes = doc.root.attributes.prefixes
Expand Down
Loading
Loading