# Cheap-n-cheerful HTML page template system. You create a # template containing: # # * variable names between percent signs (%fred%) # * 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 T1, T2, 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 !INCLUDE! occurs. For example, # we could have the overall page template containing # # #

Master

# !INCLUDE! # # # 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 <<. The value_hash 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 substitute_array # (below), which examples a block of the template once for each # row in the array. # # This routine also copes with the IF:_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) "#{name}" 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