# # Copyright (c) 2006 Michael Koziarski # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in the # Software without restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the # Software, and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. require 'rubygems' require 'xml/libxml' class FasterXmlSimple class << self def xml_in(string, options={}) new(string, options).out end end attr_reader :doc, :options, :current_element def initialize(string, options) @doc = parse(string) @options = default_options.merge options @current_element = nil end def out {doc.root.name => collapse(doc.root)} end private def default_options {'contentkey' => '__content__', 'forcearray' => []} end def collapse(element) result = hash_of_attributes(element) if text_node? element text = collapse_text(element) result.merge!(content_key => text) if text =~ /\S/ elsif element.children? element.inject(result) do |hash, child| unless child.text? child_result = collapse(child) (hash[child.name] ||= []) << child_result end hash end end if result.empty? return empty_element end # Compact them to ensure it complies with the user's requests inline_single_element_arrays(result) suppress_empty_content(result) unless force_content? remove_empty_elements(result) if suppress_empty? result end def empty_element if !options.has_key? 'suppressempty' {} else options['suppressempty'] end end def content_key options['contentkey'] end def force_array?(key_name) Array(options['forcearray']).include?(key_name) end def inline_single_element_arrays(result) result.each do |key, value| if value.size == 1 && value.is_a?(Array) && !force_array?(key) result[key] = value.first end end end def remove_empty_elements(result) result.each do |key, value| if value == empty_element result.delete key end end end def suppress_empty? options['suppressempty'] == true end def suppress_empty_content(result) result.delete content_key if result[content_key] !~ /\S/ end def force_content? options['forcecontent'] end # a text node is one with 1 or more child nodes which are # text nodes, and no non-text children def text_node?(element) !element.text? && element.all? {|c| c.text?} end # takes a text node, and collapses it into a string def collapse_text(element) element.map {|c| c.content } * '' end def hash_of_attributes(element) result = {} element.each_attr do |attribute| name = attribute.name name = [attribute.ns, attribute.name].join(':') if attribute.ns? result[name] = attribute.value end result end def parse(string) if string == '' string = ' ' end XML::Parser.string(string).parse end end class XmlSimple def self.xml_in(*args) FasterXmlSimple.xml_in *args end end