Skip to content

Commit 42e3adf

Browse files
committed
Simplify element.attributes structure
Change REXML::Attributes internal structure, Refactor ATTLIST handling in Attributes. Fix inconsistent beavior of handling ATTLIST. Stop fuzzy get_attribute/get_attribute_ns search and make it Document Object Model complient.
1 parent a6aa43c commit 42e3adf

7 files changed

Lines changed: 131 additions & 140 deletions

File tree

lib/rexml/attribute.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def element=( element )
180180
#
181181
# This method is usually not called directly.
182182
def remove
183-
@element.attributes.delete self.name unless @element.nil?
183+
@element.attributes.delete self unless @element.nil?
184184
end
185185

186186
# Writes this attribute (EG, puts 'key="value"' to the output)
@@ -205,6 +205,15 @@ def xpath
205205
def document
206206
@element&.document
207207
end
208+
209+
# Returns true if this attribute is a namespace declaration, false otherwise.
210+
# "foo" => false
211+
# "xmlns" => true
212+
# "xmlns:foo" => true
213+
# "foo:xmlns" => false
214+
def namespace_declaration?
215+
prefix == 'xmlns' || (prefix.empty? && name == 'xmlns')
216+
end
208217
end
209218
end
210219
#vim:ts=2 sw=2 noexpandtab:

lib/rexml/doctype.rb

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,23 +113,27 @@ def node_type
113113
end
114114

115115
def attributes_of element
116-
rv = []
117-
each do |child|
118-
child.each do |key,val|
119-
rv << Attribute.new(key,val)
120-
end if child.kind_of? AttlistDecl and child.element_name == element
116+
attribute_declarations_of(element).map do |key, value|
117+
Attribute.new(key, value)
121118
end
122-
rv
123119
end
124120

125121
def attribute_of element, attribute
126-
att_decl = find do |child|
127-
child.kind_of? AttlistDecl and
128-
child.element_name == element and
129-
child.include? attribute
122+
attribute_declarations_of(element)[attribute]
123+
end
124+
125+
def attribute_declarations_of(name)
126+
raw_attributes = {}
127+
decls = select do |child|
128+
child.kind_of?(AttlistDecl) && child.element_name == name
129+
end
130+
decls.each do |child|
131+
child.each do |key, val|
132+
# First declaration wins
133+
raw_attributes[key] = val unless raw_attributes.key? key
134+
end
130135
end
131-
return nil unless att_decl
132-
att_decl[attribute]
136+
raw_attributes
133137
end
134138

135139
def clone

lib/rexml/element.rb

Lines changed: 76 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -2240,15 +2240,10 @@ def length
22402240
# [REXML::Attribute, bar:att='2']
22412241
# [REXML::Attribute, att='&lt;']
22422242
#
2243-
def each_attribute # :yields: attribute
2244-
return to_enum(__method__) unless block_given?
2245-
each_value do |val|
2246-
if val.kind_of? Attribute
2247-
yield val
2248-
else
2249-
val.each_value { |atr| yield atr }
2250-
end
2251-
end
2243+
# This method doesn't iterate attributes in the DTD. This may be a bug.
2244+
#
2245+
def each_attribute(&block) # :yields: attribute
2246+
each_own_attribute(&block)
22522247
end
22532248

22542249
# :call-seq:
@@ -2273,6 +2268,8 @@ def each_attribute # :yields: attribute
22732268
# ["bar:att", "2"]
22742269
# ["att", "<"]
22752270
#
2271+
# This method doesn't iterate attributes in the DTD. This may be a bug.
2272+
#
22762273
def each
22772274
return to_enum(__method__) unless block_given?
22782275
each_attribute do |attr|
@@ -2300,36 +2297,7 @@ def each
23002297
# attrs.get_attribute('nosuch') # => nil
23012298
#
23022299
def get_attribute( name )
2303-
attr = fetch( name, nil )
2304-
if attr.nil?
2305-
return nil if name.nil?
2306-
# Look for prefix
2307-
name =~ Namespace::NAMESPLIT
2308-
prefix, n = $1, $2
2309-
if prefix
2310-
attr = fetch( n, nil )
2311-
# check prefix
2312-
if attr == nil
2313-
elsif attr.kind_of? Attribute
2314-
return attr if prefix == attr.prefix
2315-
else
2316-
attr = attr[ prefix ]
2317-
return attr
2318-
end
2319-
end
2320-
doctype = @element.document&.doctype
2321-
if doctype
2322-
expn = @element.expanded_name
2323-
expn = doctype.name if expn.size == 0
2324-
attr_val = doctype.attribute_of(expn, name)
2325-
return Attribute.new( name, attr_val ) if attr_val
2326-
end
2327-
return nil
2328-
end
2329-
if attr.kind_of? Hash
2330-
attr = attr[ @element.prefix ]
2331-
end
2332-
attr
2300+
fetch(name, nil) || attlist_attributes&.[](name)
23332301
end
23342302

23352303
# :call-seq:
@@ -2357,8 +2325,7 @@ def get_attribute( name )
23572325
#
23582326
def []=( name, value )
23592327
if value.nil? # Delete the named attribute
2360-
attr = get_attribute(name)
2361-
delete attr
2328+
delete name
23622329
return
23632330
end
23642331

@@ -2372,17 +2339,7 @@ def []=( name, value )
23722339
value = Attribute.new(name, value)
23732340
end
23742341
value.element = @element
2375-
old_attr = fetch(value.name, nil)
2376-
if old_attr.nil?
2377-
store(value.name, value)
2378-
elsif old_attr.kind_of? Hash
2379-
old_attr[value.prefix] = value
2380-
elsif old_attr.prefix != value.prefix
2381-
store value.name, {old_attr.prefix => old_attr,
2382-
value.prefix => value}
2383-
else
2384-
store value.name, value
2385-
end
2342+
store name, value
23862343
@element
23872344
end
23882345

@@ -2398,20 +2355,7 @@ def []=( name, value )
23982355
# d.root.attributes.prefixes # => ["x", "y"]
23992356
#
24002357
def prefixes
2401-
ns = []
2402-
each_attribute do |attribute|
2403-
ns << attribute.name if attribute.prefix == 'xmlns'
2404-
end
2405-
doctype = @element.document&.doctype
2406-
if doctype
2407-
expn = @element.expanded_name
2408-
expn = doctype.name if expn.size == 0
2409-
doctype.attributes_of(expn).each {
2410-
|attribute|
2411-
ns << attribute.name if attribute.prefix == 'xmlns'
2412-
}
2413-
end
2414-
ns
2358+
namespaces.keys - ['xmlns']
24152359
end
24162360

24172361
# :call-seq:
@@ -2425,17 +2369,8 @@ def prefixes
24252369
#
24262370
def namespaces
24272371
namespaces = {}
2428-
each_attribute do |attribute|
2429-
namespaces[attribute.name] = attribute.value if attribute.prefix == 'xmlns' or attribute.name == 'xmlns'
2430-
end
2431-
doctype = @element.document&.doctype
2432-
if doctype
2433-
expn = @element.expanded_name
2434-
expn = doctype.name if expn.size == 0
2435-
doctype.attributes_of(expn).each {
2436-
|attribute|
2437-
namespaces[attribute.name] = attribute.value if attribute.prefix == 'xmlns' or attribute.name == 'xmlns'
2438-
}
2372+
each_effective_attribute do |attribute|
2373+
namespaces[attribute.name] = attribute.value if attribute.namespace_declaration?
24392374
end
24402375
namespaces
24412376
end
@@ -2468,28 +2403,11 @@ def namespaces
24682403
# attrs.delete(attr) # => <ele att='&lt;'/> # => <ele att='&lt;'/>
24692404
# attrs.delete(attr) # => <ele att='&lt;'/> # => <ele/>
24702405
#
2471-
def delete( attribute )
2472-
name = nil
2473-
prefix = nil
2474-
if attribute.kind_of? Attribute
2475-
name = attribute.name
2476-
prefix = attribute.prefix
2477-
else
2478-
attribute =~ Namespace::NAMESPLIT
2479-
prefix, name = $1, $2
2480-
prefix = '' unless prefix
2481-
end
2482-
old = fetch(name, nil)
2483-
if old.kind_of? Hash # the supplied attribute is one of many
2484-
old.delete(prefix)
2485-
if old.size == 1
2486-
repl = nil
2487-
old.each_value{|v| repl = v}
2488-
store name, repl
2489-
end
2490-
elsif old # the supplied attribute is a top-level one
2491-
super(name)
2492-
end
2406+
def delete(attribute_or_name)
2407+
key = attribute_or_name
2408+
key = attribute_or_name.expanded_name if attribute_or_name.kind_of? Attribute
2409+
super(key)
2410+
24932411
@element
24942412
end
24952413

@@ -2514,7 +2432,7 @@ def delete( attribute )
25142432
# attrs.include?('baz') # => true
25152433
#
25162434
def add( attribute )
2517-
self[attribute.name] = attribute
2435+
self[attribute.expanded_name] = attribute
25182436
end
25192437

25202438
alias :<< :add
@@ -2527,21 +2445,26 @@ def add( attribute )
25272445
#
25282446
# xml_string = <<-EOT
25292447
# <root xmlns:foo="http://foo" xmlns:bar="http://bar">
2530-
# <ele foo:att='1' bar:att='2' att='&lt;'/>
2448+
# <ele foo:att='1' att='&lt;' foo:other='2' other='3'/>
25312449
# </root>
25322450
# EOT
25332451
# d = REXML::Document.new(xml_string)
2534-
# ele = d.root.elements['//ele'] # => <a foo:att='1' bar:att='2' att='&lt;'/>
2452+
# ele = d.root.elements['//ele'] # => <a foo:att='1' att='&lt;' foo:other='2' other='3'/>
25352453
# attrs = ele.attributes
2536-
# attrs.delete_all('att') # => [att='&lt;']
2454+
# attrs.delete_all('att') # => [foo:att='1', att='&lt;']
2455+
# attrs.each_attribute.map(&:expanded_name) #=> ['foo:other', 'other']
25372456
#
25382457
def delete_all( name )
2539-
rv = []
2540-
each_attribute { |attribute|
2541-
rv << attribute if attribute.expanded_name == name
2542-
}
2543-
rv.each{ |attr| attr.remove }
2544-
rv
2458+
attributes = each_attribute.select do |attribute|
2459+
# For <element xmlns:foo="url" ns:foo="value"/>
2460+
# delete_all('foo') should not delete xmlns:foo="url"
2461+
# because it is a namespace declaration, not a normal attribute.
2462+
(!attribute.namespace_declaration? && attribute.name == name) || attribute.expanded_name == name
2463+
end
2464+
attributes.each do |attribute|
2465+
delete attribute.expanded_name
2466+
end
2467+
attributes
25452468
end
25462469

25472470
# :call-seq:
@@ -2562,17 +2485,51 @@ def delete_all( name )
25622485
# attrs.get_attribute_ns('http://foo', 'nosuch') # => nil
25632486
#
25642487
def get_attribute_ns(namespace, name)
2565-
result = nil
2566-
each_attribute() { |attribute|
2567-
if name == attribute.name &&
2568-
namespace == attribute.namespace() &&
2569-
( !namespace.empty? || !attribute.fully_expanded_name.index(':') )
2570-
# foo will match xmlns:foo, but only if foo isn't also an attribute
2571-
result = attribute if !result or !namespace.empty? or
2572-
!attribute.fully_expanded_name.index(':')
2488+
each_effective_attribute.find do |attribute|
2489+
if attribute.namespace_declaration?
2490+
# namespace declarations are not considered as attributes in this method.
2491+
# For example: <elem1 xmlns="" xmlns:foo="url" /><elem2 xmlns="bar" xmlns:foo="url" />
2492+
# Both elem1.get_attribute_ns('', 'foo') and elem2.get_attribute_ns('bar', 'foo')
2493+
# should not match.
2494+
false
2495+
elsif namespace.empty? && !attribute.prefix.empty?
2496+
# If prefix is present, namespace url shouldn't be empty, so it never matches.
2497+
# For example, <elem foo:att="value" />, even if attribute.namespace returns '',
2498+
# it just means that namespace lookup failed, it shouldn't match.
2499+
false
2500+
else
2501+
name == attribute.name && namespace == attribute.namespace()
25732502
end
2574-
}
2575-
result
2503+
end
2504+
end
2505+
2506+
private
2507+
2508+
# Iterates over the attributes of the element and those in the DTD.
2509+
# If the element has an attribute with the same name as one in the DTD,
2510+
# the element's attribute takes precedence.
2511+
def each_effective_attribute
2512+
return to_enum(__method__) unless block_given?
2513+
attr = attlist_attributes
2514+
attr = attr ? attr.merge(self) : self
2515+
attr.each_value do |attribute|
2516+
yield attribute
2517+
end
2518+
end
2519+
2520+
alias each_own_attribute each_value
2521+
private :each_own_attribute
2522+
2523+
def attlist_attributes
2524+
doctype = @element.document&.doctype
2525+
if doctype
2526+
expn = @element.is_a?(Document) ? doctype.name : @element.expanded_name
2527+
doctype.attribute_declarations_of(expn).map do |key, value|
2528+
attr = Attribute.new(key, value)
2529+
attr.element = @element
2530+
[key, attr]
2531+
end.to_h
2532+
end
25762533
end
25772534
end
25782535
end

test/test_attributes.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ def test_delete
7474
assert_equal '4', doc.root.attributes['z:foo']
7575
end
7676

77+
def test_delete_all
78+
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'/>")
79+
doc.root.attributes.delete_all 'x'
80+
assert_equal ['xmlns:x', 'xmlns:y', 'y', 'x:y', 'x:z', 'y:y'], doc.root.attributes.each_attribute.map(&:expanded_name)
81+
doc.root.attributes.delete_all 'x:y'
82+
assert_equal ['xmlns:x', 'xmlns:y', 'y', 'x:z', 'y:y'], doc.root.attributes.each_attribute.map(&:expanded_name)
83+
doc.root.attributes.delete_all 'xmlns:y'
84+
assert_equal ['xmlns:x', 'y', 'x:z', 'y:y'], doc.root.attributes.each_attribute.map(&:expanded_name)
85+
end
86+
7787
def test_prefixes
7888
doc = Document.new("<a xmlns='foo' xmlns:x='bar' xmlns:y='twee' z='glorp' x:k='gru'/>")
7989
prefixes = doc.root.attributes.prefixes

test/test_attributes_mixin.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ class TestAttributes < Test::Unit::TestCase
55
def setup
66
@ns_a = "urn:x-test:a"
77
@ns_b = "urn:x-test:b"
8+
@ns_default = "urn:x-test:default"
89
element_string = <<-"XMLEND"
9-
<test xmlns:a="#{@ns_a}"
10+
<test xmlns="#{@ns_default}"
11+
xmlns:a="#{@ns_a}"
1012
xmlns:b="#{@ns_b}"
1113
a = "1"
1214
b = '2'
@@ -25,6 +27,8 @@ def test_get_attribute_ns
2527
assert_equal("4", @attributes.get_attribute_ns(@ns_a, "d").value)
2628
assert_equal("5", @attributes.get_attribute_ns(@ns_a, "e").value)
2729
assert_equal("6", @attributes.get_attribute_ns(@ns_b, "f").value)
30+
assert_nil(@attributes.get_attribute_ns(@ns_default, "a"))
31+
assert_nil(@attributes.get_attribute_ns("", "xmlns"))
2832
end
2933
end
3034
end

test/test_contrib.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,10 @@ def test_element_cloning_namespace_Chris
286286
aDoc = REXML::Document.new '<h1 tpl:content="title" xmlns:tpl="1">Dummy title</h1>'
287287

288288
anElement = anElement = aDoc.elements[1]
289-
elementAttrPrefix = anElement.attributes.get_attribute('content').prefix
289+
elementAttrPrefix = anElement.attributes.get_attribute('tpl:content').prefix
290290

291291
aClone = anElement.clone
292-
cloneAttrPrefix = aClone.attributes.get_attribute('content').prefix
292+
cloneAttrPrefix = aClone.attributes.get_attribute('tpl:content').prefix
293293

294294
assert_equal( elementAttrPrefix , cloneAttrPrefix )
295295
end

0 commit comments

Comments
 (0)