class XmlSimple

Easy API to maintain XML (especially configuration files).

Constants

DEF_ANONYMOUS_TAG
DEF_ATTR_TO_SYMBOL
DEF_CONTENT_KEY
DEF_FORCE_ARRAY
DEF_INDENTATION
DEF_KEBAB_TO_SNAKE
DEF_KEY_ATTRIBUTES

Define some reasonable defaults.

DEF_KEY_TO_SYMBOL
DEF_ROOT_NAME
DEF_XML_DECLARATION
KNOWN_OPTIONS

Declare options that are valid for xml_in and xml_out.

Public Class Methods

new(defaults = nil) click to toggle source

Creates and initializes a new XmlSimple object.

defaults

Default values for options.

# File lib/xmlsimple.rb, line 128
def initialize(defaults = nil)
  unless defaults.nil? || defaults.is_a?(Hash)
    raise ArgumentError, "Options have to be a Hash."
  end
  @default_options = normalize_option_names(defaults, (KNOWN_OPTIONS['in'] + KNOWN_OPTIONS['out']).uniq)
  @options = Hash.new
  @_var_values = nil
end
xml_in(string = nil, options = nil) click to toggle source

This is the functional version of the instance method xml_in.

# File lib/xmlsimple.rb, line 201
def XmlSimple.xml_in(string = nil, options = nil)
  xml_simple = XmlSimple.new
  xml_simple.xml_in(string, options)
end
xml_out(hash, options = nil) click to toggle source

This is the functional version of the instance method xml_out.

# File lib/xmlsimple.rb, line 257
def XmlSimple.xml_out(hash, options = nil)
  xml_simple = XmlSimple.new
  xml_simple.xml_out(hash, options)
end

Public Instance Methods

xml_in(string = nil, options = nil) click to toggle source

Converts an XML document in the same way as the Perl module XML::Simple.

string

XML source. Could be one of the following:

  • nil: Tries to load and parse ‘<scriptname>.xml’.

  • filename: Tries to load and parse filename.

  • IO object: Reads from object until EOF is detected and parses result.

  • XML string: Parses string.

options

Options to be used.

# File lib/xmlsimple.rb, line 149
def xml_in(string = nil, options = nil)
  handle_options('in', options)

  # If no XML string or filename was supplied look for scriptname.xml.
  if string.nil?
    string = File::basename($0).dup
    string.sub!(/\.[^.]+$/, '')
    string += '.xml'

    directory = File::dirname($0)
    @options['searchpath'].unshift(directory) unless directory.nil?
  end

  if string.is_a?(String)
    if string =~ /<.*?>/m
      @doc = parse(string)
    elsif string == '-'
      @doc = parse($stdin.read)
    else
      filename = find_xml_file(string, @options['searchpath'])

      if @options.has_key?('cache')
        @options['cache'].each { |scheme|
          case(scheme)
          when 'storable'
            content = @@cache.restore_storable(filename)
          when 'mem_share'
            content = @@cache.restore_mem_share(filename)
          when 'mem_copy'
            content = @@cache.restore_mem_copy(filename)
          else
            raise ArgumentError, "Unsupported caching scheme: <#{scheme}>."
          end
          return content if content
        }
      end

      @doc = load_xml_file(filename)
    end
  elsif string.respond_to?(:read)
    @doc = parse(string.read)
  else
    raise ArgumentError, "Could not parse object of type: <#{string.class}>."
  end

  result = collapse(@doc.root)
  result = @options['keeproot'] ? merge({}, @doc.root.name, result) : result
  put_into_cache(result, filename)
  result
end
xml_out(ref, options = nil) click to toggle source

Converts a data structure into an XML document.

ref

Reference to data structure to be converted into XML.

options

Options to be used.

# File lib/xmlsimple.rb, line 212
def xml_out(ref, options = nil)
  handle_options('out', options)
  if ref.is_a?(Array)
    ref = { @options['anonymoustag'] => ref }
  end

  if @options['keeproot']
    keys = ref.keys
    if keys.size == 1
      ref = ref[keys[0]]
      @options['rootname'] = keys[0]
    end
  elsif @options['rootname'] == ''
    if ref.is_a?(Hash)
      refsave = ref
      ref = {}
      refsave.each { |key, value|
        if !scalar(value)
          ref[key] = value
        else
          ref[key] = [ value.to_s ]
        end
      }
    end
  end

  @ancestors = []
  xml = value_to_xml(ref, @options['rootname'], '')
  @ancestors = nil

  if @options['xmldeclaration']
    xml = @options['xmldeclaration'] + "\n" + xml
  end

  if @options.has_key?('outputfile')
    if @options['outputfile'].kind_of?(IO)
      return @options['outputfile'].write(xml)
    else
      File.open(@options['outputfile'], "w") { |file| file.write(xml) }
    end
  end
  xml
end

Private Instance Methods

collapse(element) click to toggle source

Actually converts an XML document element into a data structure.

element

The document element to be collapsed.

# File lib/xmlsimple.rb, line 466
def collapse(element)
  result = @options['noattr'] ? {} : get_attributes(element)

  if @options['normalisespace'] == 2
    result.each { |k, v| result[k] = normalise_space(v) }
  end

  if element.has_elements?
    element.each_element { |child|
      value = collapse(child)
      if empty(value) && (element.attributes.empty? || @options['noattr'])
        next if @options.has_key?('suppressempty') && @options['suppressempty'] == true
      end
      result = merge(result, child.name, value)
    }
    if has_mixed_content?(element)
      # normalisespace?
      content = element.texts.map { |x| x.to_s }
      content = content[0] if content.size == 1
      result[@options['contentkey']] = content
    end
  elsif element.has_text? # i.e. it has only text.
    return collapse_text_node(result, element) # calls merge, which converts
  end

  # Turn Arrays into Hashes if key fields present.
  count = fold_arrays(result)

  # Disintermediate grouped tags.
  if @options.has_key?('grouptags')
    result.each { |key, value|
      # In results, key should already be converted
      raise("Unconverted key '#{key}' found. Should be '#{kebab_to_snake_case key}'.") if (key != kebab_to_snake_case(key))
      next unless (value.is_a?(Hash) && (value.size == 1))
      child_key, child_value = value.to_a[0]
      child_key = kebab_to_snake_case child_key # todo test whether necessary
      if @options['grouptags'][key] == child_key
        result[key] = child_value
      end
    }
  end

  # Fold Hashes containing a single anonymous Array up into just the Array.
  if count == 1
    anonymoustag = @options['anonymoustag']
    if result.has_key?(anonymoustag) && result[anonymoustag].is_a?(Array)
      return result[anonymoustag]
    end
  end

  if result.empty? && @options.has_key?('suppressempty')
    return @options['suppressempty'] == '' ? '' : nil
  end

  result
end
collapse_content(hash) click to toggle source

Tries to collapse a Hash even more ;-)

hash

Hash to be collapsed again.

# File lib/xmlsimple.rb, line 633
def collapse_content(hash)
  content_key = @options['contentkey']
  hash.each_value { |value|
    return hash unless value.is_a?(Hash) && value.size == 1 && value.has_key?(content_key)
    hash.each_key { |key| hash[key] = hash[key][content_key] }
  }
  hash
end
collapse_text_node(hash, element) click to toggle source

Collapses a text node and merges it with an existing Hash, if possible. Thanks to Curtis Schofield for reporting a subtle bug.

hash

Hash to merge text node value with, if possible.

element

Text node to be collapsed.

# File lib/xmlsimple.rb, line 531
def collapse_text_node(hash, element)
  value = node_to_text(element)
  if empty(value) && !element.has_attributes?
    return {}
  end

  if element.has_attributes? && !@options['noattr']
    return merge(hash, @options['contentkey'], value)
  else
    if @options['forcecontent']
      return merge(hash, @options['contentkey'], value)
    else
      return value
    end
  end
end
empty(value) click to toggle source

Checks, if an object is nil, an empty String or an empty Hash. Thanks to Norbert Gawor for a bugfix.

value

Value to be checked for emptyness.

# File lib/xmlsimple.rb, line 938
def empty(value)
  case value
    when Hash
      return value.empty?
    when String
      return value !~ /\S/m
    else
      return value.nil?
  end
end
escape_value(data) click to toggle source

Replaces XML markup characters by their external entities.

data

The string to be escaped.

# File lib/xmlsimple.rb, line 920
def escape_value(data)
  Text::normalize(data)
end
find_xml_file(file, searchpath) click to toggle source

Searches in a list of paths for a certain file. Returns the full path to the file, if it could be found. Otherwise, an exception will be raised.

filename

Name of the file to search for.

searchpath

List of paths to search in.

# File lib/xmlsimple.rb, line 990
def find_xml_file(file, searchpath)
  filename = File::basename(file)

  if filename != file
    return file if File::file?(file)
  else
    searchpath.each { |path|
      full_path = File::join(path, filename)
      return full_path if File::file?(full_path)
    }
  end

  if searchpath.empty?
    return file if File::file?(file)
    raise ArgumentError, "File does not exist: #{file}."
  end
  raise ArgumentError, "Could not find <#{filename}> in <#{searchpath.join(':')}>"
end
fold_array(array) click to toggle source

Folds an Array to a Hash, if possible. Folding happens according to the content of keyattr, which has to be an array.

array

Array to be folded.

# File lib/xmlsimple.rb, line 577
def fold_array(array)
  hash = Hash.new
  array.each { |x|
    return array unless x.is_a?(Hash)
    key_matched = false
    @options['keyattr'].each { |key|
      if x.has_key?(key)
        key_matched = true
        value = x[key]
        return array if value.is_a?(Hash) || value.is_a?(Array)
        value = normalise_space(value) if @options['normalisespace'] == 1
        x.delete(key)
        hash[value] = x
        break
      end
    }
    return array unless key_matched
  }
  hash = collapse_content(hash) if @options['collapseagain']
  hash
end
fold_array_by_name(name, array) click to toggle source

Folds an Array to a Hash, if possible. Folding happens according to the content of keyattr, which has to be a Hash.

name

Name of the attribute to be folded upon.

array

Array to be folded.

# File lib/xmlsimple.rb, line 607
def fold_array_by_name(name, array)
  return array unless @options['keyattr'].has_key?(name)
  key, flag = @options['keyattr'][name]

  hash = Hash.new
  array.each { |x|
    if x.is_a?(Hash) && x.has_key?(key)
      value = x[key]
      return array if value.is_a?(Hash) || value.is_a?(Array)
      value = normalise_space(value) if @options['normalisespace'] == 1
      hash[value] = x
      hash[value]["-#{key}"] = hash[value][key] if flag == '-'
      hash[value].delete(key) unless flag == '+'
    else
      $stderr.puts("Warning: <#{name}> element has no '#{key}' attribute.")
      return array
    end
  }
  hash = collapse_content(hash) if @options['collapseagain']
  hash
end
fold_arrays(hash) click to toggle source

Folds all arrays in a Hash.

hash

Hash to be folded.

# File lib/xmlsimple.rb, line 552
def fold_arrays(hash)
  fold_amount = 0
  keyattr = @options['keyattr']
  if (keyattr.is_a?(Array) || keyattr.is_a?(Hash))
    hash.each { |key, value|
      key = kebab_to_snake_case key
      if value.is_a?(Array)
        if keyattr.is_a?(Array)
          hash[key] = fold_array(value)
        else
          hash[key] = fold_array_by_name(key, value)
        end
        fold_amount += 1
      end
    }
  end
  fold_amount
end
force_array?(key) click to toggle source

Checks, if the ‘forcearray’ option has to be used for a certain key.

# File lib/xmlsimple.rb, line 703
def force_array?(key)
  return false if key == @options['contentkey']
  return true if @options['forcearray'] == true
  forcearray = @options['forcearray']
  if forcearray.is_a?(Hash)
    return true if forcearray.has_key?(key)
    return false unless forcearray.has_key?('_regex')
    forcearray['_regex'].each { |x| return true if key =~ x }
  end
  return false
end
get_attributes(node) click to toggle source

Converts the attributes array of a document node into a Hash. Returns an empty Hash, if node has no attributes.

node

Document node to extract attributes from.

# File lib/xmlsimple.rb, line 720
def get_attributes(node)
  attributes = {}
  if @options['attrprefix']
    node.attributes.each { |n,v| attributes["@" + kebab_to_snake_case(n)] = v }
  elsif @options.has_key?('attrtosymbol') and @options['attrtosymbol'] == true
    #patch for converting attribute names to symbols
    node.attributes.each { |n,v| attributes[kebab_to_snake_case(n).to_sym] = v }
  else
    node.attributes.each { |n,v| attributes[kebab_to_snake_case(n)] = v }
  end

  attributes
end
get_var(name) click to toggle source

Called during variable substitution to get the value for the named variable.

# File lib/xmlsimple.rb, line 754
def get_var(name)
  if @_var_values.has_key?(name)
    return @_var_values[name]
  else
    return "${#{name}}"
  end
end
handle_options(direction, options) click to toggle source

Merges a set of options with the default options.

direction

‘in’: If options should be handled for xml_in. ‘out’: If options should be handled for xml_out.

options

Options to be merged with the default options.

# File lib/xmlsimple.rb, line 320
def handle_options(direction, options)
  @options = options || Hash.new

  raise ArgumentError, "Options must be a Hash!" unless @options.is_a?(Hash)

  unless KNOWN_OPTIONS.has_key?(direction)
    raise ArgumentError, "Unknown direction: <#{direction}>."
  end

  known_options = KNOWN_OPTIONS[direction]
  @options = normalize_option_names(@options, known_options)

  unless @default_options.nil?
    known_options.each { |option|
      unless @options.has_key?(option)
        if @default_options.has_key?(option)
          @options[option] = @default_options[option]
        end
      end
    }
  end

  unless @options.has_key?('noattr')
      @options['noattr'] = false
  end

  if @options.has_key?('rootname')
    @options['rootname'] = '' if @options['rootname'].nil?
  else
    @options['rootname'] = DEF_ROOT_NAME
  end

  if @options.has_key?('xmldeclaration') && @options['xmldeclaration'] == true
    @options['xmldeclaration'] = DEF_XML_DECLARATION
  end

  @options['keytosymbol'] = DEF_KEY_TO_SYMBOL unless @options.has_key?('keytosymbol')

  @options['attrtosymbol'] = DEF_ATTR_TO_SYMBOL unless @options.has_key?('attrtosymbol')

  if @options.has_key?('contentkey')
    if @options['contentkey'] =~ /^-(.*)$/
      @options['contentkey']    = $1
      @options['collapseagain'] = true
    end
  else
    @options['contentkey'] = DEF_CONTENT_KEY
  end

  unless @options.has_key?('normalisespace')
    @options['normalisespace'] = @options['normalizespace']
  end
  @options['normalisespace'] = 0 if @options['normalisespace'].nil?

  if @options.has_key?('searchpath')
    unless @options['searchpath'].is_a?(Array)
      @options['searchpath'] = [ @options['searchpath'] ]
    end
  else
    @options['searchpath'] = []
  end

  if @options.has_key?('cache') && scalar(@options['cache'])
    @options['cache'] = [ @options['cache'] ]
  end

  @options['anonymoustag'] = DEF_ANONYMOUS_TAG unless @options.has_key?('anonymoustag')

  if !@options.has_key?('indent') || @options['indent'].nil?
    @options['indent'] = DEF_INDENTATION
  end

  @options['indent'] = '' if @options.has_key?('noindent')

  # Special cleanup for 'keyattr' which could be an array or
  # a hash or left to default to array.
  if @options.has_key?('keyattr')
    if !scalar(@options['keyattr'])
      # Convert keyattr => { elem => '+attr' }
      #      to keyattr => { elem => ['attr', '+'] }
      if @options['keyattr'].is_a?(Hash)
        @options['keyattr'].each { |key, value|
          if value =~ /^([-+])?(.*)$/
            @options['keyattr'][key] = [$2, $1 ? $1 : '']
          end
        }
      elsif !@options['keyattr'].is_a?(Array)
        raise ArgumentError, "'keyattr' must be String, Hash, or Array!"
      end
    else
      @options['keyattr'] = [ @options['keyattr'] ]
    end
  else
    @options['keyattr'] = DEF_KEY_ATTRIBUTES
  end

  if @options.has_key?('forcearray')
    if @options['forcearray'].is_a?(Regexp)
      @options['forcearray'] = [ @options['forcearray'] ]
    end

    if @options['forcearray'].is_a?(Array)
      force_list = @options['forcearray']
      unless force_list.empty?
        @options['forcearray'] = {}
        force_list.each { |tag|
          if tag.is_a?(Regexp)
            unless @options['forcearray']['_regex'].is_a?(Array)
              @options['forcearray']['_regex'] = []
            end
            @options['forcearray']['_regex'] << tag
          else
            @options['forcearray'][tag] = true
          end
        }
      else
        @options['forcearray'] = false
      end
    else
      @options['forcearray'] = @options['forcearray'] ? true : false
    end
  else
    @options['forcearray'] = DEF_FORCE_ARRAY
  end

  if @options.has_key?('grouptags') && !@options['grouptags'].is_a?(Hash)
    raise ArgumentError, "Illegal value for 'GroupTags' option - expected a Hash."
  end

  if @options.has_key?('variables') && !@options['variables'].is_a?(Hash)
    raise ArgumentError, "Illegal value for 'Variables' option - expected a Hash."
  end

  if @options.has_key?('variables')
    @_var_values = @options['variables']
  elsif @options.has_key?('varattr')
    @_var_values = {}
  end

  @options['kebabtosnakecase'] = DEF_KEBAB_TO_SNAKE unless @options.has_key?('kebabtosnakecase')
end
has_mixed_content?(element) click to toggle source

Determines, if a document element has mixed content.

element

Document element to be checked.

# File lib/xmlsimple.rb, line 738
def has_mixed_content?(element)
  element.has_text? &&
    element.has_elements? &&
    !element.texts.join('').strip.empty?
end
hash_to_array(parent, hashref) click to toggle source

Attempts to unfold a hash of hashes into an array of hashes. Returns a reference to th array on success or the original hash, if unfolding is not possible.

parent
hashref

Reference to the hash to be unfolded.

# File lib/xmlsimple.rb, line 901
def hash_to_array(parent, hashref)
  arrayref = []
  hashref.each { |key, value|
    return hashref unless value.is_a?(Hash)

    if @options['keyattr'].is_a?(Hash)
      return hashref unless @options['keyattr'].has_key?(parent)
      arrayref << { @options['keyattr'][parent][0] => key }.update(value)
    else
      arrayref << { @options['keyattr'][0] => key }.update(value)
    end
  }
  arrayref
end
kebab_to_snake_case(key) click to toggle source

Substitutes underscores for hyphens if the KebabToSnakeCase option is selected. For when you don’t want to refer to keys by hash but instead as hash

key

Key to be converted.

# File lib/xmlsimple.rb, line 1052
def kebab_to_snake_case(key)
  return key unless (@options['kebabtosnakecase'])

  is_symbol = key.is_a? Symbol
  key = key.to_s.gsub(/-/, '_')
  key = key.to_sym if is_symbol
  key
end
load_xml_file(filename) click to toggle source

Loads and parses an XML configuration file.

filename

Name of the configuration file to be loaded.

The following exceptions may be raised:

Errno::ENOENT

If the specified file does not exist.

REXML::ParseException

If the specified file is not wellformed.

# File lib/xmlsimple.rb, line 1020
def load_xml_file(filename)
  parse(IO::read(filename))
end
merge(hash, key, value) click to toggle source

Adds a new key/value pair to an existing Hash. If the key to be added does already exist and the existing value associated with key is not an Array, it will be converted into an Array. Then the new value is appended to that Array.

hash

Hash to add key/value pair to.

key

Key to be added.

value

Value to be associated with key.

# File lib/xmlsimple.rb, line 653
def merge(hash, key, value)
  key = kebab_to_snake_case key
  if value.is_a?(String)
    value = normalise_space(value) if @options['normalisespace'] == 2

    if conv = @options['conversions'] and conv = conv.find {|c,_| c.match(key)} and conv = conv.at(1)
      value = conv.call(value)
    end

    # do variable substitutions
    unless @_var_values.nil? || @_var_values.empty?
      value.gsub!(/\$\{(\w+)\}/) { |x| get_var($1) }
    end

    # look for variable definitions
    if @options.has_key?('varattr')
      varattr = kebab_to_snake_case @options['varattr']
      if hash.has_key?(varattr)
        set_var(hash[varattr], value)
      end
    end
  end

  #patch for converting keys to symbols
  if @options.has_key?('keytosymbol')
    if @options['keytosymbol'] == true
      key = key.to_s.downcase.to_sym
    end
  end

  if hash.has_key?(key)
    if hash[key].is_a?(Array)
      hash[key] << value
    else
      hash[key] = [ hash[key], value ]
    end
  elsif value.is_a?(Array) # Handle anonymous arrays.
    hash[key] = [ value ]
  else
    if force_array?(key)
      hash[key] = [ value ]
    else
      hash[key] = value
    end
  end
  hash
end
node_to_text(node, default = nil) click to toggle source

Converts a document node into a String. If the node could not be converted into a String for any reason, default will be returned.

node

Document node to be converted.

default

Value to be returned, if node could not be converted.

# File lib/xmlsimple.rb, line 957
def node_to_text(node, default = nil)
  if node.is_a?(REXML::Element)
    node.texts.map { |t| t.value }.join('')
  elsif node.is_a?(REXML::Attribute)
    node.value.nil? ? default : node.value.strip
  elsif node.is_a?(REXML::Text)
    node.value.strip
  else
    default
  end
end
normalise_space(text) click to toggle source

Removes leading and trailing whitespace and sequences of whitespaces from a string.

text

String to be normalised.

# File lib/xmlsimple.rb, line 929
def normalise_space(text)
  text.strip.gsub(/\s\s+/, ' ')
end
normalize_option_names(options, known_options) click to toggle source

Normalizes option names in a hash, i.e., turns all characters to lower case and removes all underscores. Additionally, this method checks if an unknown option was used, and raises an according exception.

options

Hash to be normalized.

known_options

List of known options.

# File lib/xmlsimple.rb, line 300
def normalize_option_names(options, known_options)
  return nil if options.nil?
  result = Hash.new
  options.each { |key, value|
    lkey = key.to_s.downcase.gsub(/_/, '')
    if !known_options.member?(lkey)
      raise ArgumentError, "Unrecognized option: #{lkey}."
    end
    result[lkey] = value
  }
  result
end
parse(xml_string) click to toggle source

Parses an XML string and returns the according document.

xml_string

XML string to be parsed.

The following exception may be raised:

REXML::ParseException

If the specified file is not wellformed.

# File lib/xmlsimple.rb, line 978
def parse(xml_string)
  Document.new(xml_string)
end
put_into_cache(data, filename) click to toggle source

Caches the data belonging to a certain file.

data

Data to be cached.

filename

Name of file the data was read from.

# File lib/xmlsimple.rb, line 1030
def put_into_cache(data, filename)
  if @options.has_key?('cache')
    @options['cache'].each { |scheme|
      case(scheme)
      when 'storable'
        @@cache.save_storable(data, filename)
      when 'mem_share'
        @@cache.save_mem_share(data, filename)
      when 'mem_copy'
        @@cache.save_mem_copy(data, filename)
      else
        raise ArgumentError, "Unsupported caching scheme: <#{scheme}>."
      end
    }
  end
end
scalar(value) click to toggle source

Checks, if a certain value is a “scalar” value. Whatever that will be in Ruby … ;-)

value

Value to be checked.

# File lib/xmlsimple.rb, line 888
def scalar(value)
  return false if value.is_a?(Hash) || value.is_a?(Array)
  return true
end
set_var(name, value) click to toggle source

Called when a variable definition is encountered in the XML. A variable definition looks like

<element attrname="name">value</element>

where attrname matches the varattr setting.

# File lib/xmlsimple.rb, line 748
def set_var(name, value)
  @_var_values[name] = value
end
value_to_xml(ref, name, indent) click to toggle source

Recurses through a data structure building up and returning an XML representation of that structure as a string.

ref

Reference to the data structure to be encoded.

name

The XML tag name to be used for this item.

indent

A string of spaces for use as the current indent level.

# File lib/xmlsimple.rb, line 771
def value_to_xml(ref, name, indent)
  named = !name.nil? && name != ''
  nl    = @options.has_key?('noindent') ? '' : "\n"

  if !scalar(ref)
    if @ancestors.member?(ref)
      raise ArgumentError, "Circular data structures not supported!"
    end
    @ancestors << ref
  else
    if named
      return [indent, '<', name, '>', @options['noescape'] ? ref.to_s : escape_value(ref.to_s), '</', name, '>', nl].join('')
    else
      return ref.to_s + nl
    end
  end

  # Unfold hash to array if possible.
  if ref.is_a?(Hash) && !ref.empty? && !@options['keyattr'].empty? && indent != ''
    ref = hash_to_array(name, ref)
  end

  result = []
  if ref.is_a?(Hash)
    # Reintermediate grouped values if applicable.
    if @options.has_key?('grouptags')
      ref.each { |key, value|
        if @options['grouptags'].has_key?(key)
          ref[key] = { @options['grouptags'][key] => value }
        end
      }
    end

    nested = []
    text_content = nil
    if named
      result << indent << '<' << name
    end

    if !ref.empty?
      ref.each { |key, value|
        next if !key.nil? && key.to_s[0, 1] == '-'
        if value.nil?
          unless @options.has_key?('suppressempty') && @options['suppressempty'].nil?
            raise ArgumentError, "Use of uninitialized value!"
          end
          value = {}
        end

        # Check for the '@' attribute prefix to allow separation of attributes and elements

        if (@options['noattr'] ||
            (@options['attrprefix'] && !(key =~ /^@(.*)/)) ||
            !scalar(value)
           ) &&
           key != @options['contentkey']
          nested << value_to_xml(value, key, indent + @options['indent'])
        else
          value = value.to_s
          value = escape_value(value) unless @options['noescape']
          if key == @options['contentkey']
            text_content = value
          else
            result << ' ' << ($1||key) << '="' << value << '"'
          end
        end
      }
    elsif !@options['selfclose']
      text_content = ''
    end

    if !nested.empty? || !text_content.nil?
      if named
        result << '>'
        if !text_content.nil?
          result << text_content
          nested[0].sub!(/^\s+/, '') if !nested.empty?
        else
          result << nl
        end
        if !nested.empty?
          result << nested << indent
        end
        result << '</' << name << '>' << nl
      else
        result << nested
      end
    else
      result << ' />' << nl
    end
  elsif ref.is_a?(Array)
    ref.each { |value|
      if scalar(value)
        result << indent << '<' << name << '>'
        result << (@options['noescape'] ? value.to_s : escape_value(value.to_s))
        result << '</' << name << '>' << nl
      elsif value.is_a?(Hash)
        result << value_to_xml(value, name, indent)
      else
        result << indent << '<' << name << '>' << nl
        result << value_to_xml(value, @options['anonymoustag'], indent + @options['indent'])
        result << indent << '</' << name << '>' << nl
      end
    }
  else
    # Probably, this is obsolete.
    raise ArgumentError, "Can't encode a value of type: #{ref.type}."
  end
  @ancestors.pop if !scalar(ref)
  result.join('')
end