diff --git a/lib/rexml/document.rb b/lib/rexml/document.rb index d5db713d..5b22969e 100644 --- a/lib/rexml/document.rb +++ b/lib/rexml/document.rb @@ -449,17 +449,24 @@ def document private - attr_accessor :namespaces_cache - - # New document level cache is created and available in this block. - # This API is thread unsafe. Users can't change this document in this block. - def enable_cache - @namespaces_cache = {} - begin - yield - ensure - @namespaces_cache = nil + # Returns namespaces defined in attribute list declarations for each element name. + # { element_name => { prefix => uri, ... }, ... } + def attlist_per_element_namespaces # :nodoc: + per_element_namespaces = {} + if doctype + doctype.each do |child| + next unless child.kind_of? AttlistDecl + element_name = child.element_name + child.each do |name, value| + attr = Attribute.new(name, value) + if attr.prefix == 'xmlns' || attr.name == 'xmlns' + namespaces = per_element_namespaces[element_name] ||= {} + namespaces[attr.name] = attr.value + end + end + end end + per_element_namespaces end def build( source ) diff --git a/lib/rexml/element.rb b/lib/rexml/element.rb index abdb28a7..91d5f5a5 100644 --- a/lib/rexml/element.rb +++ b/lib/rexml/element.rb @@ -588,12 +588,7 @@ def prefixes # d.elements['//c'].namespaces # => {"x"=>"1", "y"=>"2", "z"=>"3"} # def namespaces - namespaces_cache = document&.__send__(:namespaces_cache) - if namespaces_cache - namespaces_cache[self] ||= calculate_namespaces - else - calculate_namespaces - end + calculate_namespaces end # :call-seq: @@ -618,13 +613,10 @@ def namespaces # def namespace(prefix=nil) if prefix.nil? - prefix = prefix() + namespace_internal + else + namespace_lookup_internal(prefix) end - prefix = (prefix == '') ? 'xmlns' : prefix.delete_prefix("xmlns:") - ns = namespaces[prefix] - - ns = '' if ns.nil? and prefix == 'xmlns' - ns end # :call-seq: @@ -1508,12 +1500,34 @@ def write(output=$stdout, indent=-1, transitive=false, ie_hack=false) end private - def calculate_namespaces - if parent - parent.namespaces.merge(attributes.namespaces) - else - attributes.namespaces - end + + # Returns namespace of the element + def namespace_internal(namespaces = calculate_namespaces) + namespace_lookup_internal(prefix, namespaces) + end + + # Lookup namespace for the given prefix in the context of the element + def namespace_lookup_internal(prefix, namespaces = calculate_namespaces) + prefix = (prefix == '') ? 'xmlns' : prefix.delete_prefix("xmlns:") + ns = namespaces[prefix] + ns = '' if ns.nil? and prefix == 'xmlns' + ns + end + + def calculate_namespaces(cache_hash = nil, attlist_element_namespaces = nil) + return cache_hash[self] if cache_hash && cache_hash.key?(self) + + inherited_namespaces = parent ? parent.send(:calculate_namespaces, cache_hash, attlist_element_namespaces) : {} + attlist_element_namespaces ||= document&.send(:attlist_per_element_namespaces) + attlist_namespaces = attlist_element_namespaces&.[](is_a?(Document) ? doctype&.name : expanded_name) + own_namespaces = attributes.send(:own_namespaces) + + # Inherited namespaces can be overridden by attribute list declaration, and both can be overridden by its own attributes + namespaces = inherited_namespaces + namespaces = namespaces.merge(attlist_namespaces) if attlist_namespaces + namespaces = namespaces.merge(own_namespaces) if own_namespaces.any? + cache_hash[self] = namespaces if cache_hash + namespaces end def __to_xpath_helper node @@ -2386,6 +2400,15 @@ def []=( name, value ) @element end + # Returns namespaces directly declared in this attribute set + private def own_namespaces # :nodoc: + namespaces = {} + each_attribute do |attribute| + namespaces[attribute.name] = attribute.value if attribute.prefix == 'xmlns' or attribute.name == 'xmlns' + end + namespaces + end + # :call-seq: # prefixes -> array_of_prefix_strings # @@ -2424,19 +2447,12 @@ def prefixes # d.root.attributes.namespaces # => {"xmlns"=>"foo", "x"=>"bar", "y"=>"twee"} # 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' - } - end + doc = @element.document + doctype = doc&.doctype + attlist_element_namespaces = doc&.send(:attlist_per_element_namespaces) + attlist_namespaces = attlist_element_namespaces&.[](@element.is_a?(Document) ? doctype&.name : @element.expanded_name) + namespaces = own_namespaces + namespaces = attlist_namespaces.merge(namespaces) if attlist_namespaces namespaces end diff --git a/lib/rexml/xpath_parser.rb b/lib/rexml/xpath_parser.rb index 821cca95..c73db927 100644 --- a/lib/rexml/xpath_parser.rb +++ b/lib/rexml/xpath_parser.rb @@ -63,6 +63,9 @@ def initialize(strict: false) @namespaces = nil @variables = {} @functions = FunctionsClass.new + @attlist_per_element_namespaces = nil + @document = nil + @element_namespaces_cache = {} @nest = 0 @strict = strict end @@ -85,14 +88,8 @@ def parse path, node node = node.first end - document = node.document - if document - document.__send__(:enable_cache) do - match( path_stack, node ) - end - else - match( path_stack, node ) - end + @document = node.document + match( path_stack, node ) end def get_first path, node @@ -150,7 +147,6 @@ def first( path_stack, node ) end end - def match(path_stack, node) nodeset = [node] result = expr(path_stack, nodeset) @@ -167,20 +163,45 @@ def strict? @strict end - # Returns a String namespace for a node, given a prefix + # Returns a String namespace for a prefix used in xpath. # The rules are: # # 1. Use the supplied namespace mapping first. - # 2. If no mapping was supplied, use the context node to look up the namespace - def get_namespace( node, prefix ) + # 2. If no mapping was supplied, use the context node to look up the namespace as a fallback. + def get_xpath_namespace( node, prefix ) if @namespaces @namespaces[prefix] || '' + elsif node.node_type == :element + element_namespace_lookup(node, prefix) else - return node.namespace( prefix ) if node.node_type == :element '' end end + # Returns attribute's namespace URI while caching the + # intermediate result to speed up retrieval of namespaces + def attribute_namespace(attribute) + attribute.prefix == '' ? '' : element_namespace_lookup(attribute.element, attribute.prefix) + end + + # Return element's namespace URI while caching the + # intermediate result to speed up retrieval of namespaces + def element_namespace(element) + element.send(:namespace_internal, element_namespaces(element)) + end + + # Returns a hash of namespaces for the given element while caching the + # intermediate result to speed up retrieval of namespaces + def element_namespaces(element) + @attlist_per_element_namespaces ||= @document&.send(:attlist_per_element_namespaces) || {} + element.send(:calculate_namespaces, @element_namespaces_cache, @attlist_per_element_namespaces) + end + + # Returns namespace of the prefix in the context of the element, + # while caching the intermediate result to speed up retrieval of namespaces + def element_namespace_lookup(element, prefix) + element.send(:namespace_lookup_internal, prefix, element_namespaces(element)) + end # Expr takes a stack of path elements and a set of nodes (either a Parent # or an Array and returns an Array of matching nodes @@ -641,20 +662,20 @@ def node_test(path_stack, any_type: :element) node.name == name elsif prefix.empty? if strict? - node.name == name and node.namespace == "" + node.name == name and element_namespace(node) == "" else - node.name == name and node.namespace == get_namespace(node, prefix) + node.name == name and element_namespace(node) == get_xpath_namespace(node, prefix) end else - node.name == name and node.namespace == get_namespace(node, prefix) + node.name == name and element_namespace(node) == get_xpath_namespace(node, prefix) end when :attribute if prefix.nil? node.name == name elsif prefix.empty? - node.name == name and node.namespace == "" + node.name == name and attribute_namespace(node) == "" else - node.name == name and node.namespace == get_namespace(node.element, prefix) + node.name == name and attribute_namespace(node) == get_xpath_namespace(node.element, prefix) end else false @@ -665,11 +686,11 @@ def node_test(path_stack, any_type: :element) ->(node) do case node.node_type when :element - namespaces = @namespaces || node.namespaces - node.namespace == namespaces[prefix] + namespaces = @namespaces || element_namespaces(node) + element_namespace(node) == namespaces[prefix] when :attribute - namespaces = @namespaces || node.element.namespaces - node.namespace == namespaces[prefix] + namespaces = @namespaces || element_namespaces(node.element) + attribute_namespace(node) == namespaces[prefix] else false end diff --git a/test/test_core.rb b/test/test_core.rb index beaaeed7..d66beb45 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -916,6 +916,33 @@ def test_attlist_decl assert_equal correct, prefixes end + def test_attlist_namespace_priority + doc = Document.new <<~XML + + ]> + + + + + + XML + a1, a2, d = doc.root.children.grep(REXML::Element) + b = a1.first + c = a2.first + assert_equal('foo', doc.root.namespace) + assert_equal('bar', a1.namespace) + assert_equal('bar', b.namespace) + assert_equal('baz', a2.namespace) + assert_equal('baz', c.namespace) + assert_equal('foo', d.namespace) + assert_equal({}, doc.attributes.namespaces) + assert_equal({ 'xmlns' => 'foo' }, doc.root.attributes.namespaces) + assert_equal({ 'xmlns' => 'bar' }, a1.attributes.namespaces) + assert_equal({ 'xmlns' => 'baz' }, a2.attributes.namespaces) + end + def test_attlist_write doc = File.open(fixture_path("foo.xml")) {|file| Document.new file } out = '' diff --git a/test/xpath/test_base.rb b/test/xpath/test_base.rb index 43fe3e99..0e7635a5 100644 --- a/test/xpath/test_base.rb +++ b/test/xpath/test_base.rb @@ -1312,16 +1312,6 @@ def test_namespaces_0 assert_equal( 1, XPath.match( d, "//x:*" ).size ) end - def test_namespaces_cache - doc = Document.new("") - assert_equal("", XPath.first(doc, "//b[namespace-uri()='1']").to_s) - assert_nil(XPath.first(doc, "//b[namespace-uri()='']")) - - doc.root.delete_namespace - assert_nil(XPath.first(doc, "//b[namespace-uri()='1']")) - assert_equal("", XPath.first(doc, "//b[namespace-uri()='']").to_s) - end - def test_ticket_71 doc = Document.new(%Q{}) el = doc.root.elements[1]