Mini Shell
# Cheap-n-cheerful HTML page template system. You create a
# template containing:
#
# * variable names between percent signs (<tt>%fred%</tt>)
# * blocks of repeating stuff:
#
# START:key
# ... stuff
# END:key
#
# You feed the code a hash. For simple variables, the values
# are resolved directly from the hash. For blocks, the hash entry
# corresponding to +key+ will be an array of hashes. The block will
# be generated once for each entry. Blocks can be nested arbitrarily
# deeply.
#
# The template may also contain
#
# IF:key
# ... stuff
# ENDIF:key
#
# _stuff_ will only be included in the output if the corresponding
# key is set in the value hash.
#
# Usage: Given a set of templates <tt>T1, T2,</tt> etc
#
# values = { "name" => "Dave", state => "TX" }
#
# t = TemplatePage.new(T1, T2, T3)
# File.open(name, "w") {|f| t.write_html_on(f, values)}
# or
# res = ''
# t.write_html_on(res, values)
#
#
class TemplatePage
##########
# A context holds a stack of key/value pairs (like a symbol
# table). When asked to resolve a key, it first searches the top of
# the stack, then the next level, and so on until it finds a match
# (or runs out of entries)
class Context
def initialize
@stack = []
end
def push(hash)
@stack.push(hash)
end
def pop
@stack.pop
end
# Find a scalar value, throwing an exception if not found. This
# method is used when substituting the %xxx% constructs
def find_scalar(key)
@stack.reverse_each do |level|
if val = level[key]
return val unless val.kind_of? Array
end
end
raise "Template error: can't find variable '#{key}'"
end
# Lookup any key in the stack of hashes
def lookup(key)
@stack.reverse_each do |level|
val = level[key]
return val if val
end
nil
end
end
#########
# Simple class to read lines out of a string
class LineReader
# we're initialized with an array of lines
def initialize(lines)
@lines = lines
end
# read the next line
def read
@lines.shift
end
# Return a list of lines up to the line that matches
# a pattern. That last line is discarded.
def read_up_to(pattern)
res = []
while line = read
if pattern.match(line)
return LineReader.new(res)
else
res << line
end
end
raise "Missing end tag in template: #{pattern.source}"
end
# Return a copy of ourselves that can be modified without
# affecting us
def dup
LineReader.new(@lines.dup)
end
end
# +templates+ is an array of strings containing the templates.
# We start at the first, and substitute in subsequent ones
# where the string <tt>!INCLUDE!</tt> occurs. For example,
# we could have the overall page template containing
#
# <html><body>
# <h1>Master</h1>
# !INCLUDE!
# </bost></html>
#
# and substitute subpages in to it by passing [master, sub_page].
# This gives us a cheap way of framing pages
def initialize(*templates)
result = "!INCLUDE!"
templates.each do |content|
result.sub!(/!INCLUDE!/, content)
end
@lines = LineReader.new(result.split($/))
end
# Render the templates into HTML, storing the result on +op+
# using the method <tt><<</tt>. The <tt>value_hash</tt> contains
# key/value pairs used to drive the substitution (as described above)
def write_html_on(op, value_hash)
@context = Context.new
op << substitute_into(@lines, value_hash).tr("\000", '\\')
end
# Substitute a set of key/value pairs into the given template.
# Keys with scalar values have them substituted directly into
# the page. Those with array values invoke <tt>substitute_array</tt>
# (below), which examples a block of the template once for each
# row in the array.
#
# This routine also copes with the <tt>IF:</tt>_key_ directive,
# removing chunks of the template if the corresponding key
# does not appear in the hash, and the START: directive, which
# loops its contents for each value in an array
def substitute_into(lines, values)
@context.push(values)
skip_to = nil
result = []
while line = lines.read
case line
when /^IF:(\w+)/
lines.read_up_to(/^ENDIF:#$1/) unless @context.lookup($1)
when /^IFNOT:(\w+)/
lines.read_up_to(/^ENDIF:#$1/) if @context.lookup($1)
when /^ENDIF:/
;
when /^START:(\w+)/
tag = $1
body = lines.read_up_to(/^END:#{tag}/)
inner_values = @context.lookup(tag)
raise "unknown tag: #{tag}" unless inner_values
raise "not array: #{tag}" unless inner_values.kind_of?(Array)
inner_values.each do |vals|
result << substitute_into(body.dup, vals)
end
else
result << expand_line(line.dup)
end
end
@context.pop
result.join("\n")
end
# Given an individual line, we look for %xxx% constructs and
# HREF:ref:name: constructs, substituting for each.
def expand_line(line)
# Generate a cross reference if a reference is given,
# otherwise just fill in the name part
line.gsub!(/HREF:(\w+?):(\w+?):/) {
ref = @context.lookup($1)
name = @context.find_scalar($2)
if ref and !ref.kind_of?(Array)
"<a href=\"#{ref}\">#{name}</a>"
else
name
end
}
# Substitute in values for %xxx% constructs. This is made complex
# because the replacement string may contain characters that are
# meaningful to the regexp (like \1)
line = line.gsub(/%([a-zA-Z]\w*)%/) {
val = @context.find_scalar($1)
val.tr('\\', "\000")
}
line
rescue Exception => e
$stderr.puts "Error in template: #{e}"
$stderr.puts "Original line: #{line}"
exit
end
end
Zerion Mini Shell 1.0