## # Stolen shamelessly from RDoc, with a few custom modifications # # Contributors: # Yohanes Santoso # Multiple !INCLUDE! per template, named !INCLUDE!s ## # # Cheap-n-cheerful HTML page template system. You create a # template containing the following types of keys: # # * variable names between percent signs (%fred%) # * variable name '.' method between percent signs (%fred.size%). # Calls method on variable. # * classname '#' method between percent signs (%Array#length%). # Calls method on variable of type classname. # # Keys are resolved in that order. # # and: # * Foreach blocks # # FOREACH:key # ... stuff # ENDEACH:key # # If key is enumerable (responds to :each) the block is run once for # each item in key. Blocks can be nested arbitrarily deeply. # # * With blocks # # WITH:key # ... stuff # ENDWITH:key # # Allows a key's value to be used as the basis for content. # # * If blocks: # # IF:key # ... stuff # ENDIF:key # # _stuff_ will only be included in the output if the corresponding # key is set in the values given # # * !INCLUDE! statements (described below) # # You feed the template an object or hash. The values are resolved via # the [] operator (for hashes) or by calling the methods specified in # the template. # # Usage: # # T1 = "header\n!INCLUDE!\nfooter" # T2 = "%name%\n%state%\nletters in name: %name.size%" # # values = { "name" => "Dave", "state" => "TX" } # # t = TemplatePage.new(T1, T2) # # File.open(name, "w") {|f| t.write_html_on(f, values)} # # or # # res = '' # t.write_html_on(res, values) # # Which will result in the following output: # # header # Dave # TX # letters in name: 4 # footer # class Template ## # Regex for parsing keys KEY_RE = "([a-zA-Z](?:[\\w\\.#]|::)*)" DDLB_RE = /%ddlb:#{KEY_RE}:#{KEY_RE}(?::#{KEY_RE}(?::#{KEY_RE})?)?%/ SDDLB_RE = /%sddlb:#{KEY_RE}:#{KEY_RE}(?::#{KEY_RE}(?::#{KEY_RE}(?::#{KEY_RE})?)?)?%/ ########## # A context holds a stack of key/value pairs or objects to call # methods on (acts 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 VAR_METHOD_RE = /^([\w:]+)\.(\w+)$/ CLASS_METHOD_RE = /^([\w:]+)#(\w+)$/ INDEX = '[]'.intern ## # Create an empty stack def initialize @stack = [] end ## # Add a frame to the stack def push(*frames) @stack.push(*frames) unless frames.empty? end ## # Remove a frame from the stack 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) $stderr.puts "finding scalar for: #{key}" if $DEBUG @stack.reverse_each do |level| val = find_in(level, key) return val.to_s unless val.nil? end raise "Template error: can't find variable '#{key}'" end ## # Lookup a key in the stack def lookup(key) $stderr.puts "looking for: #{key}" if $DEBUG return @stack[-1] if key == 'self' @stack.reverse_each do |level| val = find_in(level, key) return val if val end return nil end private ## # Find +key+ in the stack frame +level+. # # Look for key via the [] accessor in the frame # then via var#method, calling method on level[var] # then via classname#method (provided the frame class == classname), # invoking method on the frame. def find_in(level, key) $stderr.puts "in #{level.inspect}" if $DEBUG val = nil if level.respond_to? INDEX $stderr.puts "responds to :[]" if $DEBUG begin val = level[key] rescue TypeError rescue NameError end end if key =~ VAR_METHOD_RE var, method = $1, $2 $stderr.puts "found call #{var}.#{method}" if $DEBUG val = level[var].send(method) elsif key =~ CLASS_METHOD_RE klass, method = $1, $2 $stderr.puts \ "found call #{klass}##{method}, in class #{level.class}" if $DEBUG val = level.send(method) if klass == level.class.to_s else $stderr.puts "everything failed, trying #{level}.#{key}" if $DEBUG begin val = level.send(key) rescue NameError end end return val ensure $stderr.puts "result: #{val.inspect}" if $DEBUG 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 == 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| unless content.is_a? Hash result.sub!(/!INCLUDE!/, content) else labels = content labels.each {|label, content| re = Regexp.new("!INCLUDE:#{label}!") result.sub!(re, content.to_str) } end end @raw = result @lines = LineReader.new(result.split($/)) end ## # Render the templates into HTML, storing the result on +op+ # using the method <<. The values contain # data used to drive the substitution (as described above). def write_html_on(op, *values) @context = Context.new substitute_into(op, @lines, *values) op.tr("\000", '\\') end def to_str @raw end private ## # Substitute data into the given template. # # Keys have values substituted directly into the page. # # The IF:_key_ directive removes chunks of the template # if the corresponding key does not appear in the hash. # # The FOREACH:_key_ directive loops its contents for # each value in an enumerable (responds to :each). # # The WITH:_key_ directive, pushes the key's value onto # stack for template evaluation. # # Uses String#[] instead of Regexp#=== for a speed increase def substitute_into(op, lines, *values) @context.push(*values) result = [] while line = lines.read if line[0..2] == 'IF:' then tag = line[3..-1] lines.read_up_to("ENDIF:#{tag}") unless @context.lookup(tag) elsif line[0..5] == 'ENDIF:' then # nop elsif line[0..7] == 'FOREACH:' then tag = line[8..-1] body = lines.read_up_to("ENDEACH:#{tag}") inner_values = @context.lookup(tag) raise "unknown tag: #{tag}" unless inner_values if inner_values.respond_to? :each inner_values.each do |vals| substitute_into(op, body.dup, vals) end end elsif line[0..4] == 'WITH:' then tag = line[5..-1] body = lines.read_up_to("ENDWITH:#{tag}") values = @context.lookup(tag) raise "unknown tag: #{tag}" unless values substitute_into(op, body.dup, values) else op << expand_line(line.dup) << "\n" end end @context.pop 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(/%#{KEY_RE}%/) { val = @context.find_scalar($1) val.tr('\\', "\000") } line = line.gsub(DDLB_RE) { ddlb($1, $2, nil, $3, $4) } line = line.gsub(SDDLB_RE) { ddlb($1, $2, $3, $4, $5) } return line end ## # Expand name and options into a drop down list box HTML select def ddlb(name, options_key, selected_key = nil, value_key = nil, text_key = nil) options = @context.lookup(options_key) selected = selected_key && @context.lookup(selected_key) selected = [selected] unless selected.kind_of? Enumerable unless options and options.respond_to? :each then raise "Options #{options_key} for ddlb #{value_key} does not respond to :each" end res = %!\n" end end