From b42f9aaf322c6bb67a3ddfd2b350d72a45c1fd8f Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Sun, 12 Apr 2015 20:22:33 +0200 Subject: [PATCH] Cache output of Element#available_namespaces This cache is flushed whenever Element#register_namespace is called. When this cache is flushed it's also recursively flushed for all child elements. This makes calls to Element#register_namespace a bit more expensive but in turn calls to Element#available_namespaces will be a lot faster. --- lib/oga/xml/element.rb | 49 ++++++++++++++++++++++++++++-------- spec/oga/xml/element_spec.rb | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 10 deletions(-) diff --git a/lib/oga/xml/element.rb b/lib/oga/xml/element.rb index 32cab3a..f07a551 100644 --- a/lib/oga/xml/element.rb +++ b/lib/oga/xml/element.rb @@ -299,14 +299,17 @@ module Oga # # @param [String] name # @param [String] uri + # @param [TrueClass|FalseClass] flush # @see [Oga::XML::Namespace#initialize] # - def register_namespace(name, uri) + def register_namespace(name, uri, flush = true) if namespaces[name] raise ArgumentError, "The namespace #{name.inspect} already exists" end namespaces[name] = Namespace.new(:name => name, :uri => uri) + + flush_namespaces_cache if flush end ## @@ -316,20 +319,25 @@ module Oga # @return [Hash] # def available_namespaces - return {} if html? # HTML(5) completely ignores namespaces + # HTML(5) completely ignores namespaces + if html? + return @available_namespaces ||= {} + elsif !@available_namespaces + merged = namespaces.dup + node = parent - merged = namespaces.dup - node = parent + while node && node.respond_to?(:namespaces) + node.namespaces.each do |prefix, ns| + merged[prefix] = ns unless merged[prefix] + end - while node && node.respond_to?(:namespaces) - node.namespaces.each do |prefix, ns| - merged[prefix] = ns unless merged[prefix] + node = node.parent end - node = node.parent + @available_namespaces = merged end - return merged + return @available_namespaces end ## @@ -349,19 +357,40 @@ module Oga return self_closing end + ## + # Flushes the namespaces cache of the current element and all its child + # elements. + # + def flush_namespaces_cache + @available_namespaces = nil + @namespace = nil + + children.each do |child| + child.flush_namespaces_cache if child.is_a?(Element) + end + end + private ## # Registers namespaces based on any "xmlns" attributes. # def register_namespaces_from_attributes + flush = false + attributes.each do |attr| # We're using `namespace_name` opposed to `namespace.name` as "xmlns" # is not a registered namespace. if attr.name == XMLNS_PREFIX or attr.namespace_name == XMLNS_PREFIX - register_namespace(attr.name, attr.value) + flush = true + + # Ensures we only flush the cache once instead of flushing it on + # every register_namespace call. + register_namespace(attr.name, attr.value, false) end end + + flush_namespaces_cache if flush end ## diff --git a/spec/oga/xml/element_spec.rb b/spec/oga/xml/element_spec.rb index fb04e80..db92080 100644 --- a/spec/oga/xml/element_spec.rb +++ b/spec/oga/xml/element_spec.rb @@ -488,6 +488,25 @@ describe Oga::XML::Element do block.should raise_error(ArgumentError) end + + it 'flushes the cache when registering a namespace' do + @element.available_namespaces.should == { + 'foo' => @element.namespaces['foo'] + } + + @element.register_namespace('bar', 'http://exmaple.com') + + @element.available_namespaces.should == { + 'foo' => @element.namespaces['foo'], + 'bar' => @element.namespaces['bar'] + } + end + + it 'does not flush the cache when "flush" is set to false' do + @element.should_not receive(:flush_namespaces_cache) + + @element.register_namespace('bar', 'http://example.com', false) + end end describe '#available_namespaces' do @@ -564,4 +583,29 @@ describe Oga::XML::Element do element.should_not be_self_closing end end + + describe '#flush_namespaces_cache' do + it 'flushes the namespaces cache of the current element' do + element = described_class.new(:name => 'a') + + element.available_namespaces.should == {} + + element.register_namespace('foo', 'bar', false) + + element.flush_namespaces_cache + + element.available_namespaces.should == { + 'foo' => element.namespaces['foo'] + } + end + + it 'flushes the namespace cache of all child elements' do + child = described_class.new(:name => 'b') + parent = described_class.new(:name => 'a', :children => [child]) + + child.should_receive(:flush_namespaces_cache) + + parent.flush_namespaces_cache + end + end end