1 | # Cheap-n-cheerful HTML page template system. You create a
|
---|
2 | # template containing:
|
---|
3 | #
|
---|
4 | # * variable names between percent signs (<tt>%fred%</tt>)
|
---|
5 | # * blocks of repeating stuff:
|
---|
6 | #
|
---|
7 | # START:key
|
---|
8 | # ... stuff
|
---|
9 | # END:key
|
---|
10 | #
|
---|
11 | # You feed the code a hash. For simple variables, the values
|
---|
12 | # are resolved directly from the hash. For blocks, the hash entry
|
---|
13 | # corresponding to +key+ will be an array of hashes. The block will
|
---|
14 | # be generated once for each entry. Blocks can be nested arbitrarily
|
---|
15 | # deeply.
|
---|
16 | #
|
---|
17 | # The template may also contain
|
---|
18 | #
|
---|
19 | # IF:key
|
---|
20 | # ... stuff
|
---|
21 | # ENDIF:key
|
---|
22 | #
|
---|
23 | # _stuff_ will only be included in the output if the corresponding
|
---|
24 | # key is set in the value hash.
|
---|
25 | #
|
---|
26 | # Usage: Given a set of templates <tt>T1, T2,</tt> etc
|
---|
27 | #
|
---|
28 | # values = { "name" => "Dave", state => "TX" }
|
---|
29 | #
|
---|
30 | # t = TemplatePage.new(T1, T2, T3)
|
---|
31 | # File.open(name, "w") {|f| t.write_html_on(f, values)}
|
---|
32 | # or
|
---|
33 | # res = ''
|
---|
34 | # t.write_html_on(res, values)
|
---|
35 | #
|
---|
36 | #
|
---|
37 |
|
---|
38 | class TemplatePage
|
---|
39 |
|
---|
40 | ##########
|
---|
41 | # A context holds a stack of key/value pairs (like a symbol
|
---|
42 | # table). When asked to resolve a key, it first searches the top of
|
---|
43 | # the stack, then the next level, and so on until it finds a match
|
---|
44 | # (or runs out of entries)
|
---|
45 |
|
---|
46 | class Context
|
---|
47 | def initialize
|
---|
48 | @stack = []
|
---|
49 | end
|
---|
50 |
|
---|
51 | def push(hash)
|
---|
52 | @stack.push(hash)
|
---|
53 | end
|
---|
54 |
|
---|
55 | def pop
|
---|
56 | @stack.pop
|
---|
57 | end
|
---|
58 |
|
---|
59 | # Find a scalar value, throwing an exception if not found. This
|
---|
60 | # method is used when substituting the %xxx% constructs
|
---|
61 |
|
---|
62 | def find_scalar(key)
|
---|
63 | @stack.reverse_each do |level|
|
---|
64 | if val = level[key]
|
---|
65 | return val unless val.kind_of? Array
|
---|
66 | end
|
---|
67 | end
|
---|
68 | raise "Template error: can't find variable '#{key}'"
|
---|
69 | end
|
---|
70 |
|
---|
71 | # Lookup any key in the stack of hashes
|
---|
72 |
|
---|
73 | def lookup(key)
|
---|
74 | @stack.reverse_each do |level|
|
---|
75 | val = level[key]
|
---|
76 | return val if val
|
---|
77 | end
|
---|
78 | nil
|
---|
79 | end
|
---|
80 | end
|
---|
81 |
|
---|
82 | #########
|
---|
83 | # Simple class to read lines out of a string
|
---|
84 |
|
---|
85 | class LineReader
|
---|
86 | # we're initialized with an array of lines
|
---|
87 | def initialize(lines)
|
---|
88 | @lines = lines
|
---|
89 | end
|
---|
90 |
|
---|
91 | # read the next line
|
---|
92 | def read
|
---|
93 | @lines.shift
|
---|
94 | end
|
---|
95 |
|
---|
96 | # Return a list of lines up to the line that matches
|
---|
97 | # a pattern. That last line is discarded.
|
---|
98 | def read_up_to(pattern)
|
---|
99 | res = []
|
---|
100 | while line = read
|
---|
101 | if pattern.match(line)
|
---|
102 | return LineReader.new(res)
|
---|
103 | else
|
---|
104 | res << line
|
---|
105 | end
|
---|
106 | end
|
---|
107 | raise "Missing end tag in template: #{pattern.source}"
|
---|
108 | end
|
---|
109 |
|
---|
110 | # Return a copy of ourselves that can be modified without
|
---|
111 | # affecting us
|
---|
112 | def dup
|
---|
113 | LineReader.new(@lines.dup)
|
---|
114 | end
|
---|
115 | end
|
---|
116 |
|
---|
117 |
|
---|
118 |
|
---|
119 | # +templates+ is an array of strings containing the templates.
|
---|
120 | # We start at the first, and substitute in subsequent ones
|
---|
121 | # where the string <tt>!INCLUDE!</tt> occurs. For example,
|
---|
122 | # we could have the overall page template containing
|
---|
123 | #
|
---|
124 | # <html><body>
|
---|
125 | # <h1>Master</h1>
|
---|
126 | # !INCLUDE!
|
---|
127 | # </bost></html>
|
---|
128 | #
|
---|
129 | # and substitute subpages in to it by passing [master, sub_page].
|
---|
130 | # This gives us a cheap way of framing pages
|
---|
131 |
|
---|
132 | def initialize(*templates)
|
---|
133 | result = "!INCLUDE!"
|
---|
134 | templates.each do |content|
|
---|
135 | result.sub!(/!INCLUDE!/, content)
|
---|
136 | end
|
---|
137 | @lines = LineReader.new(result.split($/))
|
---|
138 | end
|
---|
139 |
|
---|
140 | # Render the templates into HTML, storing the result on +op+
|
---|
141 | # using the method <tt><<</tt>. The <tt>value_hash</tt> contains
|
---|
142 | # key/value pairs used to drive the substitution (as described above)
|
---|
143 |
|
---|
144 | def write_html_on(op, value_hash)
|
---|
145 | @context = Context.new
|
---|
146 | op << substitute_into(@lines, value_hash).tr("\000", '\\')
|
---|
147 | end
|
---|
148 |
|
---|
149 |
|
---|
150 | # Substitute a set of key/value pairs into the given template.
|
---|
151 | # Keys with scalar values have them substituted directly into
|
---|
152 | # the page. Those with array values invoke <tt>substitute_array</tt>
|
---|
153 | # (below), which examples a block of the template once for each
|
---|
154 | # row in the array.
|
---|
155 | #
|
---|
156 | # This routine also copes with the <tt>IF:</tt>_key_ directive,
|
---|
157 | # removing chunks of the template if the corresponding key
|
---|
158 | # does not appear in the hash, and the START: directive, which
|
---|
159 | # loops its contents for each value in an array
|
---|
160 |
|
---|
161 | def substitute_into(lines, values)
|
---|
162 | @context.push(values)
|
---|
163 | skip_to = nil
|
---|
164 | result = []
|
---|
165 |
|
---|
166 | while line = lines.read
|
---|
167 |
|
---|
168 | case line
|
---|
169 |
|
---|
170 | when /^IF:(\w+)/
|
---|
171 | lines.read_up_to(/^ENDIF:#$1/) unless @context.lookup($1)
|
---|
172 |
|
---|
173 | when /^IFNOT:(\w+)/
|
---|
174 | lines.read_up_to(/^ENDIF:#$1/) if @context.lookup($1)
|
---|
175 |
|
---|
176 | when /^ENDIF:/
|
---|
177 | ;
|
---|
178 |
|
---|
179 | when /^START:(\w+)/
|
---|
180 | tag = $1
|
---|
181 | body = lines.read_up_to(/^END:#{tag}/)
|
---|
182 | inner_values = @context.lookup(tag)
|
---|
183 | raise "unknown tag: #{tag}" unless inner_values
|
---|
184 | raise "not array: #{tag}" unless inner_values.kind_of?(Array)
|
---|
185 | inner_values.each do |vals|
|
---|
186 | result << substitute_into(body.dup, vals)
|
---|
187 | end
|
---|
188 | else
|
---|
189 | result << expand_line(line.dup)
|
---|
190 | end
|
---|
191 | end
|
---|
192 |
|
---|
193 | @context.pop
|
---|
194 |
|
---|
195 | result.join("\n")
|
---|
196 | end
|
---|
197 |
|
---|
198 | # Given an individual line, we look for %xxx% constructs and
|
---|
199 | # HREF:ref:name: constructs, substituting for each.
|
---|
200 |
|
---|
201 | def expand_line(line)
|
---|
202 | # Generate a cross reference if a reference is given,
|
---|
203 | # otherwise just fill in the name part
|
---|
204 |
|
---|
205 | line.gsub!(/HREF:(\w+?):(\w+?):/) {
|
---|
206 | ref = @context.lookup($1)
|
---|
207 | name = @context.find_scalar($2)
|
---|
208 |
|
---|
209 | if ref and !ref.kind_of?(Array)
|
---|
210 | "<a href=\"#{ref}\">#{name}</a>"
|
---|
211 | else
|
---|
212 | name
|
---|
213 | end
|
---|
214 | }
|
---|
215 |
|
---|
216 | # Substitute in values for %xxx% constructs. This is made complex
|
---|
217 | # because the replacement string may contain characters that are
|
---|
218 | # meaningful to the regexp (like \1)
|
---|
219 |
|
---|
220 | line = line.gsub(/%([a-zA-Z]\w*)%/) {
|
---|
221 | val = @context.find_scalar($1)
|
---|
222 | val.tr('\\', "\000")
|
---|
223 | }
|
---|
224 |
|
---|
225 |
|
---|
226 | line
|
---|
227 | rescue Exception => e
|
---|
228 | $stderr.puts "Error in template: #{e}"
|
---|
229 | $stderr.puts "Original line: #{line}"
|
---|
230 | exit
|
---|
231 | end
|
---|
232 |
|
---|
233 | end
|
---|
234 |
|
---|