I historically have used ERB and REXML for things like this, since they both ship with Ruby (removing gem dependencies). You can combine one XML file (content) with one .erb file (for layout) and get simple merging. Here's a script I wrote for this (most of which is argument handling and extending REXML with some convenience methods):
USAGE = <<ENDUSAGE
Usage:
rubygen source_xml [-t template_file] [-o output_file]
-t,--template The ERB template file to merge (default: xml_name.erb)
-o,--output The output file name to write (default: template.txt)
If the template_file is named "somefile_XXX.yyy",
the output_file will default instead to "somefile.XXX"
ENDUSAGE
ARGS = {}
UNFLAGGED_ARGS = [ :source_xml ]
next_arg = UNFLAGGED_ARGS.first
ARGV.each{ |arg|
case arg
when '-t','--template'
next_arg = :template_file
when '-o','--output'
next_arg = :output_file
else
if next_arg
ARGS[next_arg] = arg
UNFLAGGED_ARGS.delete( next_arg )
end
next_arg = UNFLAGGED_ARGS.first
end
}
if !ARGS[:source_xml]
puts USAGE
exit
end
extension_match = /\.[^.]+$/
template_match = /_([^._]+)\.[^.]+$/
xml_file = ARGS[ :source_xml ]
template_file = ARGS[ :template_file] || xml_file.sub( extension_match, '.erb' )
output_file = ARGS[ :output_file ] || ( ( template_file =~ template_match ) ? template_file.sub( template_match, '.\\1' ) : template_file.sub( extension_match, '.txt' ) )
require 'rexml/document'
include REXML
class REXML::Element
# Find all descendant nodes with a specified tag name and/or attributes
def find_all( tag_name='*', attributes_to_match={} )
self.each_element( ".//#{REXML::Element.xpathfor(tag_name,attributes_to_match)}" ){}
end
# Find all child nodes with a specified tag name and/or attributes
def kids( tag_name='*', attributes_to_match={} )
self.each_element( "./#{REXML::Element.xpathfor(tag_name,attributes_to_match)}" ){}
end
def self.xpathfor( tag_name='*', attributes_to_match={} )
out = "#{tag_name}"
unless attributes_to_match.empty?
out << "["
out << attributes_to_match.map{ |key,val|
if val == :not_empty
"@#{key}"
else
"@#{key}='#{val}'"
end
}.join( ' and ' )
out << "]"
end
out
end
# A hash to tag extra data onto a node during processing
def _mydata
@_mydata ||= {}
end
end
start_time = Time.new
@xmldoc = Document.new( IO.read( xml_file ), :ignore_whitespace_nodes => :all )
@root = @xmldoc.root
@root = @root.first if @root.is_a?( Array )
end_time = Time.new
puts "%.2fs to parse XML file (#{xml_file})" % ( end_time - start_time )
require 'erb'
File.open( output_file, 'w' ){ |o|
start_time = Time.new
output_code = ERB.new( IO.read( template_file ), nil, '>', 'output' ).result( binding )
end_time = Time.new
puts "%.2fs to run template (#{template_file})" % ( end_time - start_time )
start_time = Time.new
o << output_code
}
end_time = Time.new
puts "%.2fs to write output (#{output_file})" % ( end_time - start_time )
puts " "
This can be used for HTML or automated source code generation alike.
However, these days I would advocate using Haml and Nokogiri (if you want structured XML markup) or YAML (if you want simple-to-edit content), as these will make your markup cleaner and your template logic simpler.
Edit: Here's a simpler file that merges YAML with Haml. The last four lines do all the work:
#!/usr/bin/env ruby
require 'yaml'; require 'haml'; require 'trollop'
EXTENSION = /\.[^.]+$/
opts = Trollop.options do
banner "Usage:\nyamlhaml [opts] <sourcefile.yaml>"
opt :haml, "The Haml file to use (default: sourcefile.haml)", type:String
opt :output, "The file to create (default: sourcefile.html)", type:String
end
opts[:source] = ARGV.shift
Trollop.die "Please specify an input Yaml file" unless opts[:source]
Trollop.die "Could not find #{opts[:source]}" unless File.exist?(opts[:source])
opts[:haml] ||= opts[:source].sub( EXTENSION, '.haml' )
opts[:output] ||= opts[:source].sub( EXTENSION, '.html' )
Trollop.die "Could not find #{opts[:haml]}" unless File.exist?(opts[:haml])
@data = YAML.load(IO.read(opts[:source]))
File.open( opts[:output], 'w' ) do |output|
output << Haml::Engine.new(IO.read(opts[:haml])).render(self)
end
Here's a sample YAML file:
title: Hello World
main: "<h1>you have 30 files in games folder</h1>"
side: "I dunno, something goes here."
...and a sample Haml file:
!!! 5
%html
%head
%title= @data['title']
%body
#main= @data['main']
#side= @data['side']
...and finally the HTML they produce:
<!DOCTYPE html>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<div id='main'><h1>you have 30 files in games folder</h1></div>
<div id='side'>I dunno, something goes here.</div>
</body>
</html>