зеркало из https://github.com/github/ruby.git
235 строки
5.8 KiB
Ruby
235 строки
5.8 KiB
Ruby
# 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
|
|
|