PageTemplate 1.1.1 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/MANIFEST +4 -0
- data/Rakefile +54 -0
- data/config.save +12 -0
- data/doc/classes/BlockCommand.html +229 -0
- data/doc/classes/BlockCommand.src/M000004.html +18 -0
- data/doc/classes/BlockCommand.src/M000005.html +25 -0
- data/doc/classes/BlockCommand.src/M000006.html +18 -0
- data/doc/classes/BlockCommand.src/M000007.html +23 -0
- data/doc/classes/BlockCommand.src/M000008.html +24 -0
- data/doc/classes/Command.html +167 -0
- data/doc/classes/Command.src/M000029.html +19 -0
- data/doc/classes/Command.src/M000030.html +18 -0
- data/doc/classes/IfCommand.html +193 -0
- data/doc/classes/IfCommand.src/M000039.html +19 -0
- data/doc/classes/IfCommand.src/M000040.html +20 -0
- data/doc/classes/IfCommand.src/M000041.html +20 -0
- data/doc/classes/IfElseCommand.html +189 -0
- data/doc/classes/IfElseCommand.src/M000023.html +19 -0
- data/doc/classes/IfElseCommand.src/M000024.html +23 -0
- data/doc/classes/IfElseCommand.src/M000025.html +20 -0
- data/doc/classes/IncludeCommand.html +178 -0
- data/doc/classes/IncludeCommand.src/M000017.html +21 -0
- data/doc/classes/IncludeCommand.src/M000018.html +23 -0
- data/doc/classes/IncludeCommand.src/M000019.html +18 -0
- data/doc/classes/LoopCommand.html +197 -0
- data/doc/classes/LoopCommand.src/M000001.html +19 -0
- data/doc/classes/LoopCommand.src/M000002.html +33 -0
- data/doc/classes/LoopCommand.src/M000003.html +20 -0
- data/doc/classes/LoopElseCommand.html +190 -0
- data/doc/classes/LoopElseCommand.src/M000020.html +19 -0
- data/doc/classes/LoopElseCommand.src/M000021.html +23 -0
- data/doc/classes/LoopElseCommand.src/M000022.html +20 -0
- data/doc/classes/Namespace.html +297 -0
- data/doc/classes/Namespace.src/M000031.html +19 -0
- data/doc/classes/Namespace.src/M000032.html +18 -0
- data/doc/classes/Namespace.src/M000033.html +18 -0
- data/doc/classes/Namespace.src/M000034.html +19 -0
- data/doc/classes/Namespace.src/M000035.html +36 -0
- data/doc/classes/Namespace.src/M000036.html +21 -0
- data/doc/classes/Namespace.src/M000037.html +18 -0
- data/doc/classes/Namespace.src/M000038.html +18 -0
- data/doc/classes/PageTemplate.html +352 -0
- data/doc/classes/PageTemplate.src/M000009.html +46 -0
- data/doc/classes/PageTemplate.src/M000010.html +29 -0
- data/doc/classes/PageTemplate.src/M000011.html +18 -0
- data/doc/classes/PageTemplate.src/M000012.html +18 -0
- data/doc/classes/PageTemplate.src/M000013.html +18 -0
- data/doc/classes/PageTemplate.src/M000014.html +18 -0
- data/doc/classes/PageTemplate.src/M000015.html +18 -0
- data/doc/classes/PageTemplate.src/M000016.html +19 -0
- data/doc/classes/Syntax.html +139 -0
- data/doc/classes/Syntax/CachedParser.html +163 -0
- data/doc/classes/Syntax/CachedParser.src/M000045.html +19 -0
- data/doc/classes/Syntax/CachedParser.src/M000046.html +52 -0
- data/doc/classes/Syntax/Glossary.html +245 -0
- data/doc/classes/Syntax/Glossary.src/M000047.html +20 -0
- data/doc/classes/Syntax/Glossary.src/M000048.html +29 -0
- data/doc/classes/Syntax/Parser.html +261 -0
- data/doc/classes/Syntax/Parser.src/M000049.html +22 -0
- data/doc/classes/Syntax/Parser.src/M000050.html +35 -0
- data/doc/classes/Syntax/Parser.src/M000051.html +18 -0
- data/doc/classes/Syntax/Parser.src/M000052.html +21 -0
- data/doc/classes/Syntax/Parser.src/M000053.html +53 -0
- data/doc/classes/Syntax/Parser.src/M000054.html +107 -0
- data/doc/classes/TextCommand.html +185 -0
- data/doc/classes/TextCommand.src/M000042.html +18 -0
- data/doc/classes/TextCommand.src/M000043.html +18 -0
- data/doc/classes/TextCommand.src/M000044.html +18 -0
- data/doc/classes/ValueCommand.html +186 -0
- data/doc/classes/ValueCommand.src/M000026.html +18 -0
- data/doc/classes/ValueCommand.src/M000027.html +18 -0
- data/doc/classes/ValueCommand.src/M000028.html +20 -0
- data/doc/created.rid +1 -0
- data/doc/files/README_txt.html +224 -0
- data/doc/files/lib/PageTemplate_rb.html +119 -0
- data/doc/fr_class_index.html +41 -0
- data/doc/fr_file_index.html +28 -0
- data/doc/fr_method_index.html +80 -0
- data/doc/index.html +24 -0
- data/doc/rdoc-style.css +208 -0
- data/install.rb +1015 -0
- data/lib/MANIFEST +2 -0
- data/lib/PageTemplate.rb +936 -0
- data/pkg/PageTemplate-1.1.2.gem +0 -0
- data/tdata/complex.txt +116 -0
- data/tdata/i1.txt +1 -0
- data/tdata/i2.txt +7 -0
- data/tdata/ib1.txt +7 -0
- data/tdata/ib2.txt +13 -0
- data/tdata/ie1.txt +4 -0
- data/tdata/ie2.txt +15 -0
- data/tdata/include.1.txt +1 -0
- data/tdata/include.2.txt +1 -0
- data/tdata/include.3.txt +1 -0
- data/tdata/include.4.out.txt +3 -0
- data/tdata/include.4.txt +2 -0
- data/tdata/include.4a.txt +1 -0
- data/tdata/include.4b.txt +2 -0
- data/tdata/include.5.txt +2 -0
- data/tdata/l1.txt +5 -0
- data/tdata/l2.txt +7 -0
- data/tdata/nm.txt +4 -0
- data/tdata/p/_b1.txt +0 -0
- data/tdata/p/b1.txt +4 -0
- data/tdata/p/cb1.txt +4 -0
- data/tdata/t.txt +2 -0
- data/tdata/v1.txt +11 -0
- data/tdata/v2.txt +11 -0
- data/test-install.rb +2 -0
- metadata +137 -4
data/lib/MANIFEST
ADDED
data/lib/PageTemplate.rb
ADDED
@@ -0,0 +1,936 @@
|
|
1
|
+
#!/usr/local/bin/ruby -w
|
2
|
+
|
3
|
+
# Use PageTemplate.rb to create output based on template pages and
|
4
|
+
# the code of your program. This package is inspired by, but not
|
5
|
+
# quite like, Perl's HTML::Template package. Its main intent is to
|
6
|
+
# separate design and code for CGI programs, but it could be useful
|
7
|
+
# in other contexts as well (Ex: site generation packages).
|
8
|
+
#
|
9
|
+
# See the README.txt file for the real documentation, such as it is.
|
10
|
+
#
|
11
|
+
# As a side note: if you are using PageTemplate in your projects, or
|
12
|
+
# add features to your copy, I'd love to hear about it.
|
13
|
+
#
|
14
|
+
|
15
|
+
############################################################
|
16
|
+
|
17
|
+
# Allows nested storage of variable names and values.
|
18
|
+
class Namespace
|
19
|
+
|
20
|
+
def initialize()
|
21
|
+
@global = {}
|
22
|
+
@nSpaces = []
|
23
|
+
end
|
24
|
+
|
25
|
+
# An alias for Namespace#set(key, value)
|
26
|
+
def []=(key, value)
|
27
|
+
self.set(key, value)
|
28
|
+
end
|
29
|
+
|
30
|
+
# An alias for Namespace#get(key)
|
31
|
+
def [](key)
|
32
|
+
return self.get(key)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Saves a variable +key+ as the string +value+ in the global
|
36
|
+
# namespace.
|
37
|
+
def set(key, value)
|
38
|
+
puts "Namespace setting global #{key} to #{value}" if $DEBUG
|
39
|
+
@global[key] = value
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns the first value found for +key+ in the nested namespaces.
|
43
|
+
# Returns nil if no value is found.
|
44
|
+
#
|
45
|
+
# Values are checked for in this order through each level of namespace:
|
46
|
+
#
|
47
|
+
# * level.key
|
48
|
+
# * level.key()
|
49
|
+
# * level[key]
|
50
|
+
#
|
51
|
+
# If a value is not found in any of the nested namespaces, get()
|
52
|
+
# searches for the key in the global namespace.
|
53
|
+
def get(key)
|
54
|
+
value = nil
|
55
|
+
|
56
|
+
@nSpaces.reverse.each do |ns|
|
57
|
+
|
58
|
+
if ns.class.method_defined?(":#{key}")
|
59
|
+
value = ns.send(":#{key}")
|
60
|
+
elsif ns.class.method_defined?("#{key}")
|
61
|
+
value = ns.send(key)
|
62
|
+
elsif ns.class.method_defined?("[]")
|
63
|
+
value = ns[key]
|
64
|
+
end
|
65
|
+
|
66
|
+
break if value
|
67
|
+
end
|
68
|
+
|
69
|
+
value = @global[key] unless value
|
70
|
+
puts "Namespace #{key}: #{value}" if $DEBUG
|
71
|
+
|
72
|
+
return value
|
73
|
+
end
|
74
|
+
|
75
|
+
# A convenience method to test whether a variable has a true
|
76
|
+
# value. Returns nil if +flag+ is not found in the namespace,
|
77
|
+
# or +flag+ has a nil value attached to it.
|
78
|
+
def true?(flag)
|
79
|
+
value = get(flag)
|
80
|
+
return nil unless value
|
81
|
+
|
82
|
+
return value
|
83
|
+
end
|
84
|
+
|
85
|
+
# Nest namespaces by adding the object +namespace+ to the current set
|
86
|
+
# of names.
|
87
|
+
def push(namespace)
|
88
|
+
@nSpaces.push(namespace)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Un-nest namespaces by removing the most recently pushed object.
|
92
|
+
#
|
93
|
+
# Returns nil if there are no more namespaces to pop
|
94
|
+
def pop
|
95
|
+
@nSpaces.pop
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
############################################################
|
101
|
+
|
102
|
+
# Command classes generate text output based on conditions which vary
|
103
|
+
# between each class. Command provides an abstract base class to show
|
104
|
+
# interface.
|
105
|
+
class Command
|
106
|
+
|
107
|
+
# Subclasses of Command use the output method to generate their text
|
108
|
+
# output. +namespace+ is a Namespace object, which may be
|
109
|
+
# required by a particular subclass.
|
110
|
+
def output(namespace = nil)
|
111
|
+
raise NotImplementedError,
|
112
|
+
"output() must be implemented by subclasses"
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_s
|
116
|
+
return "#{self.class}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# BlockCommand provides a single interface to multiple Command
|
121
|
+
# objects.
|
122
|
+
class BlockCommand < Command
|
123
|
+
|
124
|
+
def initialize()
|
125
|
+
@commandBlock = []
|
126
|
+
end
|
127
|
+
|
128
|
+
# Return Commands held, as a string
|
129
|
+
def to_s
|
130
|
+
str = "BlockCommand: "
|
131
|
+
if @commandBlock
|
132
|
+
@commandBlock.each { |c| str << "[" + c.to_s + "]"}
|
133
|
+
else
|
134
|
+
str << "Empty"
|
135
|
+
end
|
136
|
+
|
137
|
+
return str
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns the number of Commands held in this BlockCommand
|
141
|
+
def length
|
142
|
+
return @commandBlock.length
|
143
|
+
end
|
144
|
+
|
145
|
+
# Adds +command+ to the end of the BlockCommand's
|
146
|
+
# chain of Commands.
|
147
|
+
#
|
148
|
+
# A TypeError is raised if the object being added is not a ((<Command>)).
|
149
|
+
def add(command)
|
150
|
+
if command.is_a?(Command)
|
151
|
+
@commandBlock << command
|
152
|
+
else
|
153
|
+
raise TypeError,
|
154
|
+
"BlockCommand.add: Attempt to add non-Command object"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Applies Command#output(namespace) to each Command contained in this
|
159
|
+
# object. The output is returned as a single string. If no output
|
160
|
+
# is generated, returns nil.
|
161
|
+
def output(namespace = nil)
|
162
|
+
text = ""
|
163
|
+
@commandBlock.each do |x|
|
164
|
+
cOutput = x.output(namespace)
|
165
|
+
text += cOutput if cOutput
|
166
|
+
end
|
167
|
+
|
168
|
+
return text unless text == ""
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# A very simple Command which outputs a static string of text
|
173
|
+
class TextCommand < Command
|
174
|
+
|
175
|
+
# Creates a TextCommand object, saving +text+ for future output.
|
176
|
+
def initialize(text)
|
177
|
+
@text = text
|
178
|
+
end
|
179
|
+
|
180
|
+
def to_s
|
181
|
+
return "#{self.type}: '#{@text}'"
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns the string provided during this object's creation.
|
185
|
+
def output(namespace = nil)
|
186
|
+
return @text
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# A Command that is tied to a variable name.
|
191
|
+
class ValueCommand < Command
|
192
|
+
|
193
|
+
# Creates the ValueCommand, with +value+ as the name of the variable
|
194
|
+
# to be inserted during output.
|
195
|
+
def initialize(value)
|
196
|
+
@value = value
|
197
|
+
end
|
198
|
+
|
199
|
+
# Requests the value of this object's saved value name from
|
200
|
+
# +namespace+, and returns the string representation of that
|
201
|
+
# value to the caller.
|
202
|
+
def output(namespace)
|
203
|
+
return namespace.get(@value).to_s
|
204
|
+
end
|
205
|
+
|
206
|
+
def to_s
|
207
|
+
str = super.to_s
|
208
|
+
str << "(#{@value})"
|
209
|
+
return str
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# The IfCommand generates output from an associated BlockCommand
|
214
|
+
# if a flag variable is true.
|
215
|
+
class IfCommand < ValueCommand
|
216
|
+
# Creates an IfCommand object, with +flag+ used as the value
|
217
|
+
# to check for truth, and +commandBlock+ as the BlockCommand to
|
218
|
+
# execute when the flag is true.
|
219
|
+
def initialize(flag, commandBlock)
|
220
|
+
super(flag)
|
221
|
+
@commands = commandBlock
|
222
|
+
end
|
223
|
+
|
224
|
+
# If +namespace+ has a true value for this Command's flag, returns
|
225
|
+
# the output generated by the CommandBlock. Otherwise, returns nil.
|
226
|
+
def output(namespace)
|
227
|
+
if namespace.true?(@value)
|
228
|
+
return @commands.output(namespace)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Return a text representation of this command
|
233
|
+
def to_s
|
234
|
+
str = super.to_s
|
235
|
+
str << @commands.to_s
|
236
|
+
return str
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
# An IfCommand with an alternate BlockCommand to use if the
|
242
|
+
# flag variable if false.
|
243
|
+
class IfElseCommand < IfCommand
|
244
|
+
|
245
|
+
# Generates an IfElseCommand with +elseBlock+ as the
|
246
|
+
# BlockCommand to execute when +flag+ is false.
|
247
|
+
def initialize(flag, ifBlock, elseBlock)
|
248
|
+
super(flag, ifBlock)
|
249
|
+
@elseCommands = elseBlock
|
250
|
+
end
|
251
|
+
|
252
|
+
# Returns the output of the if BlockCommand if +flag+ is true in
|
253
|
+
# +namespace+, or the output of the else BlockCommand otherwise.
|
254
|
+
def output(namespace)
|
255
|
+
result = super
|
256
|
+
unless result
|
257
|
+
result = @elseCommands.output(namespace)
|
258
|
+
end
|
259
|
+
|
260
|
+
return result
|
261
|
+
end
|
262
|
+
|
263
|
+
def to_s
|
264
|
+
str = super.to_s
|
265
|
+
str << " - ELSE " + @elseCommands.to_s
|
266
|
+
return str
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# LoopCommand repeatedly steps through its CommandBlock for each item
|
271
|
+
# in a array of hashes associated with the LoopCommand's variable.
|
272
|
+
class LoopCommand < ValueCommand
|
273
|
+
|
274
|
+
# Creates a new LoopCommand asociated with +listName+ and using
|
275
|
+
# +commandBlock+ as the command to be executed when output is
|
276
|
+
# requested.
|
277
|
+
#
|
278
|
+
# *NOTE:* The value associated with +listName+ in the caller's
|
279
|
+
# Namespace must be an array of hashes. Each hash should have
|
280
|
+
# the same keys, but that is not required. (I can think of situations
|
281
|
+
# where it wouldn't be an issue, so just think of 'identical keys' as a
|
282
|
+
# guideline)
|
283
|
+
def initialize(listName, commandBlock)
|
284
|
+
super(listName)
|
285
|
+
@commands = commandBlock
|
286
|
+
end
|
287
|
+
|
288
|
+
# Executes the object's CommandBlock for each item in the associated
|
289
|
+
# array of hashes. This method nests namespaces, allowing local variables
|
290
|
+
# within the loop. Returns the combined output of each repetition, or
|
291
|
+
# nil if the loop value is false in +namespace+.
|
292
|
+
def output(namespace)
|
293
|
+
|
294
|
+
return nil unless namespace.true?(@value)
|
295
|
+
|
296
|
+
items = namespace.get(@value)
|
297
|
+
|
298
|
+
return nil if items.empty?
|
299
|
+
|
300
|
+
result = ""
|
301
|
+
|
302
|
+
items.each do |item|
|
303
|
+
namespace.push(item)
|
304
|
+
result += @commands.output(namespace)
|
305
|
+
namespace.pop()
|
306
|
+
end
|
307
|
+
|
308
|
+
return result
|
309
|
+
end
|
310
|
+
|
311
|
+
def to_s
|
312
|
+
str = super.to_s
|
313
|
+
str << @commands.to_s
|
314
|
+
return str
|
315
|
+
end
|
316
|
+
|
317
|
+
end
|
318
|
+
|
319
|
+
# A LoopCommand with an alternate CommandBlock to execute
|
320
|
+
# if there is no loop associated with the Command variable does not exist.
|
321
|
+
class LoopElseCommand < LoopCommand
|
322
|
+
|
323
|
+
# Generates a LoopElseCommand with a loop variable, a BlockCommand to
|
324
|
+
# execute if the loop exists, and a BlockCommand to execute if the
|
325
|
+
# loop does not exist.
|
326
|
+
def initialize(loopName, trueBlock, elseBlock)
|
327
|
+
super(loopName, trueBlock)
|
328
|
+
@elseCommands = elseBlock
|
329
|
+
end
|
330
|
+
|
331
|
+
# Returns the output of the loop BlockCommand if the loop exists, or the
|
332
|
+
# output of the else BlockCommand if not.
|
333
|
+
def output(namespace)
|
334
|
+
result = super
|
335
|
+
unless result
|
336
|
+
result = @elseCommands.output(namespace)
|
337
|
+
end
|
338
|
+
|
339
|
+
return result
|
340
|
+
end
|
341
|
+
|
342
|
+
def to_s
|
343
|
+
str = super.to_s
|
344
|
+
str << " - NO " + @elseCommands.to_s
|
345
|
+
return str
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
# This module provides guidelines for the template parser.
|
350
|
+
module Syntax
|
351
|
+
|
352
|
+
# Glossary holds regular expression patterns associated with markup in a
|
353
|
+
# PageTemplate document. It enables the PageTemplate to tell the
|
354
|
+
# difference between regular document content and template markup. It
|
355
|
+
# also defines the syntax for the available markup directives:
|
356
|
+
#
|
357
|
+
# === Syntax::Glossary read accessors
|
358
|
+
#
|
359
|
+
# [:directive]
|
360
|
+
# The regular expression object which describes a chunk of
|
361
|
+
# markup text.
|
362
|
+
# [:glossary]
|
363
|
+
# A hash of regular expression objects, describing parser keys.
|
364
|
+
class Glossary
|
365
|
+
|
366
|
+
attr_reader :directive, :glossary
|
367
|
+
|
368
|
+
# Creates a new Syntax::Glossary with regular expression +directive+,
|
369
|
+
# and hash of regular expressions glossary, as defined above.
|
370
|
+
#
|
371
|
+
# +directive+ must have a single grouping, which holds the actual
|
372
|
+
# directive that is supposed to be processed (as opposed to the
|
373
|
+
# markers that "wrap" the directive)
|
374
|
+
#
|
375
|
+
# The following keys are required in glossary by a Syntax::Glossary
|
376
|
+
# object:
|
377
|
+
#
|
378
|
+
# * value
|
379
|
+
# * ifopen
|
380
|
+
# * ifclose
|
381
|
+
# * ifbranch
|
382
|
+
# * loopopen
|
383
|
+
# * loopclose
|
384
|
+
# * loopbranch
|
385
|
+
#
|
386
|
+
# An ArgumentError is raised if glossary is missing any of these keys.
|
387
|
+
#
|
388
|
+
# You may add keys, but it is up to you to make the PageTemplate parse
|
389
|
+
# method understand their meaning.
|
390
|
+
def initialize(directive, glossary)
|
391
|
+
@directive = directive
|
392
|
+
checkGlossary(glossary)
|
393
|
+
@glossary = glossary
|
394
|
+
end
|
395
|
+
|
396
|
+
# Find out which parser directive corresponds to +text+.
|
397
|
+
# +text+ is the grouping found by a directive match.
|
398
|
+
#
|
399
|
+
# If no match is found, returns nil.
|
400
|
+
# If a match is found, returns an array of the parse command and
|
401
|
+
# variable associated with it (or the parse command and nil for the
|
402
|
+
# variable if there is no variable match).
|
403
|
+
def lookup(text)
|
404
|
+
command = nil
|
405
|
+
variable = nil
|
406
|
+
@glossary.each_key do |key|
|
407
|
+
if match = @glossary[key].match(text)
|
408
|
+
command = key
|
409
|
+
variable = match[1] if match[1]
|
410
|
+
break
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
return nil if command == nil
|
415
|
+
return [command, variable]
|
416
|
+
end
|
417
|
+
|
418
|
+
# Ensures that glossary contains all of the required keys.
|
419
|
+
private
|
420
|
+
def checkGlossary(glossary)
|
421
|
+
%w[ value
|
422
|
+
ifopen ifclose ifbranch
|
423
|
+
loopopen loopclose loopbranch
|
424
|
+
].each do |key|
|
425
|
+
unless glossary.has_key?(key)
|
426
|
+
raise ArgumentError,
|
427
|
+
"Syntax::Glossary:missing '#{key}' key"
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
return true
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
# This Syntax::Glossary is provided for those of us who
|
436
|
+
# don't feel like defining our own syntax.
|
437
|
+
|
438
|
+
# Here are the keys and patterns associated with Syntax::DEFAULT
|
439
|
+
|
440
|
+
# * directive: /\[%(.+)%\]/
|
441
|
+
# * value: /var (\w+)/
|
442
|
+
# * ifopen: /if (\w+)/
|
443
|
+
# * ifclose: /endif/
|
444
|
+
# * ifbranch: /else/
|
445
|
+
# * loopopen: /in (\w+)/
|
446
|
+
# * loopclose: /endin/
|
447
|
+
# * loopbranch: /no/
|
448
|
+
# * include: /include (\S+)/
|
449
|
+
|
450
|
+
# For example +[%var title %]+ is a template directive to
|
451
|
+
# insert the value of the variable 'title'.
|
452
|
+
|
453
|
+
# Notice that I used a space before the closing braces. This
|
454
|
+
# Glossary doesn't require it, but I think it makes for a more readable
|
455
|
+
# style.
|
456
|
+
|
457
|
+
DEFAULT = Syntax::Glossary.new( /\[%(.+?)%\]/,
|
458
|
+
'value' => /^\s*var (\w+)\s*$/,
|
459
|
+
'ifopen' => /^\s*if (\w+)\s*$/,
|
460
|
+
'ifclose' => /^\s*endif\s*$/,
|
461
|
+
'ifbranch' => /^\s*else\s*$/,
|
462
|
+
'loopopen' => /^\s*in (\w+)\s*$/,
|
463
|
+
'loopclose' => /^\s*endin\s*$/,
|
464
|
+
'loopbranch' => /^\s*no\s*$/,
|
465
|
+
'include' => /^\s*include (\S+)\s*$/
|
466
|
+
)
|
467
|
+
|
468
|
+
########################################################################
|
469
|
+
|
470
|
+
# Uses a Syntax::Glossary to translate a string of text into a
|
471
|
+
# series of Commands.
|
472
|
+
class Parser
|
473
|
+
|
474
|
+
attr_reader :commands
|
475
|
+
|
476
|
+
def initialize(glossary, path=Dir.getwd)
|
477
|
+
@path = path
|
478
|
+
@syntax = glossary
|
479
|
+
@commands = nil
|
480
|
+
@lastFile = nil
|
481
|
+
@mTime = Time.at(0)
|
482
|
+
end
|
483
|
+
|
484
|
+
# Parse and compile a file containing PageTemplate directives.
|
485
|
+
def build(filename)
|
486
|
+
if File.exists?(filename)
|
487
|
+
file = File.new(filename)
|
488
|
+
else
|
489
|
+
file = File.new(File.join(@path, filename))
|
490
|
+
end
|
491
|
+
|
492
|
+
# If this is a new file, or the file has been changed,
|
493
|
+
# parse and compile.
|
494
|
+
if filename != @lastFile or file.mtime < @mTime
|
495
|
+
reset
|
496
|
+
source = file.readlines
|
497
|
+
parsed = parse(source, filename)
|
498
|
+
@commands = compile(parsed, filename)
|
499
|
+
@lastFile = filename
|
500
|
+
@mTime = file.mtime
|
501
|
+
end
|
502
|
+
|
503
|
+
return 1 if @commands
|
504
|
+
end
|
505
|
+
|
506
|
+
# Returns the result of executing the Parser's internal compiled
|
507
|
+
# CommandBlock, using ((|namespace||)).
|
508
|
+
def output(namespace = nil)
|
509
|
+
return @commands.output(namespace)
|
510
|
+
end
|
511
|
+
|
512
|
+
# Clear the Parser's CommandBlock.
|
513
|
+
def reset
|
514
|
+
@lastFile = nil
|
515
|
+
@mTime = Time.at(0)
|
516
|
+
@commands = nil
|
517
|
+
return 1
|
518
|
+
end
|
519
|
+
|
520
|
+
# Turn an array of strings +source+ into a series of directives that
|
521
|
+
# can be handled by Parser#compile.
|
522
|
+
#
|
523
|
+
# If provided, +filename+ is assumed to be the name of the
|
524
|
+
# file which contains the original +source+.
|
525
|
+
def parse(source, filename = "?")
|
526
|
+
puts "#{filename}: Parsing ..." if $DEBUG
|
527
|
+
|
528
|
+
# Translate source into listing of text and directives
|
529
|
+
lineNo = 1
|
530
|
+
parsed = []
|
531
|
+
source.each do |line|
|
532
|
+
# split into plain text and directives
|
533
|
+
while line
|
534
|
+
dirMatch = @syntax.directive.match(line)
|
535
|
+
if dirMatch
|
536
|
+
matchText = dirMatch[1]
|
537
|
+
pre = dirMatch.pre_match
|
538
|
+
directive = @syntax.lookup(matchText)
|
539
|
+
parsed.push [lineNo, pre]
|
540
|
+
|
541
|
+
if directive
|
542
|
+
parsed.push [lineNo, directive]
|
543
|
+
else
|
544
|
+
# Unrecognized directives are treated as
|
545
|
+
# plain text.
|
546
|
+
parsed.push [lineNo, dirMatch[0]]
|
547
|
+
end
|
548
|
+
|
549
|
+
fragment = dirMatch.post_match
|
550
|
+
line = fragment
|
551
|
+
else
|
552
|
+
parsed.push [lineNo, line]
|
553
|
+
break
|
554
|
+
end
|
555
|
+
end
|
556
|
+
lineNo += 1
|
557
|
+
end
|
558
|
+
|
559
|
+
puts "#{filename}: #{@parsed.length} directives" if $DEBUG
|
560
|
+
|
561
|
+
return parsed
|
562
|
+
end
|
563
|
+
|
564
|
+
# Turn an array of parsed directives into a CommandBlock.
|
565
|
+
#
|
566
|
+
# If provided, +filename+ is assumed to be the name of the
|
567
|
+
# file which contains the original +source+.
|
568
|
+
def compile(lines, filename = "?")
|
569
|
+
|
570
|
+
puts "#{filename}: Compiling ..." if $DEBUG
|
571
|
+
# Translate listing of text and directives into a BlockCommand
|
572
|
+
commands = BlockCommand.new()
|
573
|
+
max = lines.length
|
574
|
+
i = 0
|
575
|
+
while i < max
|
576
|
+
line = lines[i]
|
577
|
+
type = line[1].class
|
578
|
+
command = nil
|
579
|
+
#puts "#{filename}/#{line[0]}: #{type} -- #{line}"
|
580
|
+
|
581
|
+
if type == String
|
582
|
+
command = TextCommand.new(line[1])
|
583
|
+
elsif type == Array
|
584
|
+
directive = line[1][0]
|
585
|
+
value = line[1][1]
|
586
|
+
print "(#{directive} #{value}) " if $DEBUG
|
587
|
+
|
588
|
+
if directive == "value"
|
589
|
+
command = ValueCommand.new(value)
|
590
|
+
elsif directive == "include"
|
591
|
+
command = IncludeCommand.new(value, @path)
|
592
|
+
elsif directive =~ /^(\w+?)open$/
|
593
|
+
mainBlock = nil
|
594
|
+
branchBlock = nil
|
595
|
+
close = nil
|
596
|
+
branch = nil
|
597
|
+
|
598
|
+
block = $1
|
599
|
+
startIndex = i + 1
|
600
|
+
closers = findClose(lines[startIndex..-1],
|
601
|
+
startIndex, block)
|
602
|
+
|
603
|
+
if closers['close']
|
604
|
+
close = closers['close']
|
605
|
+
if closers['branch']
|
606
|
+
branch = closers['branch']
|
607
|
+
mainLines = lines[startIndex...branch]
|
608
|
+
mainBlock = compile(mainLines)
|
609
|
+
branchLines = lines[branch+1...close]
|
610
|
+
branchBlock = compile(branchLines)
|
611
|
+
else
|
612
|
+
mainLines = lines[startIndex...close]
|
613
|
+
mainBlock = compile(mainLines)
|
614
|
+
end
|
615
|
+
else
|
616
|
+
raise "#{@file}:#{line[0]} - " +
|
617
|
+
"#{block} #{value} doesn't close"
|
618
|
+
end
|
619
|
+
|
620
|
+
if block == "if"
|
621
|
+
if mainBlock and branchBlock
|
622
|
+
command = IfElseCommand.new(value,
|
623
|
+
mainBlock, branchBlock)
|
624
|
+
elsif mainBlock
|
625
|
+
command = IfCommand.new(value, mainBlock)
|
626
|
+
end
|
627
|
+
elsif block == "loop"
|
628
|
+
if mainBlock and branchBlock
|
629
|
+
command = LoopElseCommand.new(value,
|
630
|
+
mainBlock, branchBlock)
|
631
|
+
elsif mainBlock
|
632
|
+
command = LoopCommand.new(value, mainBlock)
|
633
|
+
end
|
634
|
+
else
|
635
|
+
raise "#{filename}: #{line[0]} - " +
|
636
|
+
"Unrecognized block type '#{block}'"
|
637
|
+
end
|
638
|
+
|
639
|
+
# Skip to the line after the block closer.
|
640
|
+
i = close
|
641
|
+
elsif %Q[
|
642
|
+
ifbranch ifclose loopbranch loopclose
|
643
|
+
].include?(directive)
|
644
|
+
raise "#{@file}:#{line[0]} - #{directive}" +
|
645
|
+
" without corresponding opening directive"
|
646
|
+
else
|
647
|
+
raise "#{@file}:#{line[0]} - Unknown command #{directive}"
|
648
|
+
end
|
649
|
+
else
|
650
|
+
raise "#{@file}: Unknown instruction in line #{line}"
|
651
|
+
end
|
652
|
+
|
653
|
+
commands.add(command)
|
654
|
+
i += 1
|
655
|
+
end
|
656
|
+
|
657
|
+
# Return compiled BlockCommand
|
658
|
+
return commands
|
659
|
+
end
|
660
|
+
|
661
|
+
private
|
662
|
+
def findClose(lines, mIndex, dType)
|
663
|
+
branchIndex = nil
|
664
|
+
closeIndex = nil
|
665
|
+
|
666
|
+
openTerm = dType + "open"
|
667
|
+
closeTerm = dType + "close"
|
668
|
+
branchTerm = dType + "branch"
|
669
|
+
|
670
|
+
j = 0
|
671
|
+
nesting = 0
|
672
|
+
max = lines.length
|
673
|
+
while j < max
|
674
|
+
jLine = lines[j]
|
675
|
+
jDirective = jLine[1][0]
|
676
|
+
line = jLine[0]
|
677
|
+
|
678
|
+
if jDirective == openTerm
|
679
|
+
nesting += 1
|
680
|
+
elsif jDirective == branchTerm
|
681
|
+
if nesting == 0
|
682
|
+
if branchIndex
|
683
|
+
raise RuntimeError,
|
684
|
+
"#{@file}:#{line} - Multiple branching statements"
|
685
|
+
else
|
686
|
+
branchIndex = j
|
687
|
+
end
|
688
|
+
end
|
689
|
+
elsif jDirective == closeTerm
|
690
|
+
if nesting > 0
|
691
|
+
nesting -= 1
|
692
|
+
else
|
693
|
+
closeIndex = j
|
694
|
+
end
|
695
|
+
|
696
|
+
end
|
697
|
+
|
698
|
+
j += 1
|
699
|
+
end
|
700
|
+
|
701
|
+
branchIndex += mIndex if branchIndex
|
702
|
+
closeIndex += mIndex if closeIndex
|
703
|
+
|
704
|
+
return { 'branch'=> branchIndex, 'close'=> closeIndex }
|
705
|
+
end
|
706
|
+
|
707
|
+
end
|
708
|
+
|
709
|
+
|
710
|
+
# A Syntax::Parser that can save its compiled CommandBlock for reuse later.
|
711
|
+
class CachedParser < Parser
|
712
|
+
def initialize(glossary, path=Dir.getwd)
|
713
|
+
super(glossary, path)
|
714
|
+
# Create the cache directory if it does not exist.
|
715
|
+
end
|
716
|
+
|
717
|
+
def build(filename)
|
718
|
+
# Determine the name of the cache file.
|
719
|
+
cacheFile = File.dirname(filename) + "/_" + File.basename(filename)
|
720
|
+
saveCache = false
|
721
|
+
|
722
|
+
# See if cache exists and is newer than source.
|
723
|
+
if File.exists?(cacheFile)
|
724
|
+
cacheStat = File.stat(cacheFile)
|
725
|
+
if cacheStat.mtime > File.stat(filename).mtime
|
726
|
+
# See if cache is newer than internal CommandBlock.
|
727
|
+
if cacheStat.mtime > @mTime
|
728
|
+
# Load the cache into memory
|
729
|
+
@commands = Marshal.load(IO.readlines(cacheFile).join)
|
730
|
+
@mTime = cacheStat.mtime
|
731
|
+
elsif cacheStat.mtime < @mTime
|
732
|
+
saveCache = true
|
733
|
+
end
|
734
|
+
else
|
735
|
+
# Build the CommandBlock
|
736
|
+
super(filename)
|
737
|
+
saveCache = true
|
738
|
+
end
|
739
|
+
else
|
740
|
+
super(filename)
|
741
|
+
saveCache = true
|
742
|
+
end
|
743
|
+
|
744
|
+
if saveCache
|
745
|
+
# Write the cache to disk.
|
746
|
+
data = Marshal.dump(@commands)
|
747
|
+
f = File.new(cacheFile, "w")
|
748
|
+
f.write(data)
|
749
|
+
f.close
|
750
|
+
end
|
751
|
+
|
752
|
+
return 1 if @commands
|
753
|
+
end
|
754
|
+
end
|
755
|
+
end
|
756
|
+
|
757
|
+
############################################################
|
758
|
+
|
759
|
+
# An object which parses text files littered with template directives
|
760
|
+
# and can produce text based on set parameters and the instructions
|
761
|
+
# contained within the text file. Each PageTemplate has its own
|
762
|
+
# Namespace, so there shouldn't be any problems using it in a threaded
|
763
|
+
# environment.
|
764
|
+
#
|
765
|
+
# PageTemplate is the primary user Class for this package.
|
766
|
+
class PageTemplate
|
767
|
+
attr_reader :file, :syntax, :parser
|
768
|
+
|
769
|
+
# Creates a new PageTemplate object, using +args+ to provide
|
770
|
+
# optional initialization.
|
771
|
+
#
|
772
|
+
# ==== Possible Keys For Args
|
773
|
+
#
|
774
|
+
# [:include_path]
|
775
|
+
# The directory that PageTemplate will look for template files in.
|
776
|
+
#
|
777
|
+
# [:filename]
|
778
|
+
# The name of a text file to use for a template.
|
779
|
+
#
|
780
|
+
# [:syntax]
|
781
|
+
# A Syntax::Glossary object, provided if the user wants
|
782
|
+
# an alternate syntax for template parse directives. If not
|
783
|
+
# provided, then Syntax::DEFAULT is used.
|
784
|
+
#
|
785
|
+
# [:use_cache]
|
786
|
+
# If +true+, the PageTemplate object will use a Syntax::CachedParser
|
787
|
+
# which stores compiled template data to disk in the same directory as the
|
788
|
+
# template file itself. This cached template still needs you to provide
|
789
|
+
# a Namespace during output, though.
|
790
|
+
def initialize(args = {})
|
791
|
+
|
792
|
+
@source = []
|
793
|
+
@params = Namespace.new()
|
794
|
+
|
795
|
+
if args['syntax']
|
796
|
+
@syntax = args['syntax']
|
797
|
+
else
|
798
|
+
@syntax = Syntax::DEFAULT
|
799
|
+
end
|
800
|
+
|
801
|
+
if args['include_path']
|
802
|
+
@path = args['include_path']
|
803
|
+
else
|
804
|
+
@path = Dir.getwd()
|
805
|
+
end
|
806
|
+
|
807
|
+
if args['use_cache']
|
808
|
+
@parser = Syntax::CachedParser.new(@syntax, @path)
|
809
|
+
else
|
810
|
+
@parser = Syntax::Parser.new(@syntax, @path)
|
811
|
+
end
|
812
|
+
|
813
|
+
if args['filename']
|
814
|
+
@file = args['filename']
|
815
|
+
load(@file)
|
816
|
+
else
|
817
|
+
@file = nil
|
818
|
+
end
|
819
|
+
|
820
|
+
end
|
821
|
+
|
822
|
+
# Open +file+ and parse its contents so they will be ready
|
823
|
+
# for template generation.
|
824
|
+
def load(file)
|
825
|
+
clearFile()
|
826
|
+
if File.exists?(file) then
|
827
|
+
@file = file
|
828
|
+
else
|
829
|
+
filepath = File.join(@path, file)
|
830
|
+
if File.exists?(filepath) then
|
831
|
+
@file = filepath
|
832
|
+
else
|
833
|
+
raise ArgumentError, "Cannot find #{file} in include path"
|
834
|
+
end
|
835
|
+
end
|
836
|
+
@parser.build(@file)
|
837
|
+
end
|
838
|
+
|
839
|
+
# Set the namespace's +name+ to +value+.
|
840
|
+
#
|
841
|
+
# If you need to reset a flag or loop variable to a false value, the
|
842
|
+
# easiest way is to do just that: <tt>template.setParameter("myflag", false)</tt>
|
843
|
+
def setParameter(name, value)
|
844
|
+
@params.set(name, value)
|
845
|
+
end
|
846
|
+
|
847
|
+
# Get the value associated with +name+ from the namespace. Returns
|
848
|
+
# nil if the value is not set.
|
849
|
+
def getParameter(name)
|
850
|
+
return @params.get(name)
|
851
|
+
end
|
852
|
+
|
853
|
+
# An alias for PageTemplate.setParameter(key, value)
|
854
|
+
def []=(key, value)
|
855
|
+
setParameter(key, value)
|
856
|
+
end
|
857
|
+
|
858
|
+
# An alias for PageTemplate.getParameter(key)
|
859
|
+
def [](key)
|
860
|
+
return getParameter(key)
|
861
|
+
end
|
862
|
+
|
863
|
+
# Returns the text result of stepping through the compiled template.
|
864
|
+
# Returns nil if there is no compiled content (usually indicates
|
865
|
+
# that a file has not been loaded yet).
|
866
|
+
#
|
867
|
+
# *NOTE:* This checks the PageTemplate's namespace each time it
|
868
|
+
# is called, so you can generate different custom pages without
|
869
|
+
# reloading the template page.
|
870
|
+
def output
|
871
|
+
return @parser.commands.output(@params)
|
872
|
+
end
|
873
|
+
|
874
|
+
# Remove all template data from the PageTemplate, but leave the
|
875
|
+
# Namespace intact.
|
876
|
+
def clearFile
|
877
|
+
@file = nil
|
878
|
+
@parser.reset()
|
879
|
+
end
|
880
|
+
end
|
881
|
+
|
882
|
+
# IncludeCommand allows including text from external files.
|
883
|
+
class IncludeCommand < Command
|
884
|
+
|
885
|
+
def initialize(filename, path = Dir.getwd)
|
886
|
+
super()
|
887
|
+
@filename = filename
|
888
|
+
@path = path
|
889
|
+
@parser = Syntax::Parser.new(Syntax::DEFAULT, @path)
|
890
|
+
end
|
891
|
+
|
892
|
+
def output(ns=nil)
|
893
|
+
# First check to see if the file exists in the current working directory
|
894
|
+
# or as an absolute filename.
|
895
|
+
file = determine_file(ns)
|
896
|
+
text = File.open(file).read()
|
897
|
+
@parser.build(file)
|
898
|
+
@parser.output(ns)
|
899
|
+
end
|
900
|
+
|
901
|
+
def to_s
|
902
|
+
return "IncludeCommand {path: '#{@path}' filename: '#{@filename}'}"
|
903
|
+
end
|
904
|
+
|
905
|
+
private
|
906
|
+
def determine_file(ns)
|
907
|
+
path = @path
|
908
|
+
if ns
|
909
|
+
base = ns.get(@filename) || @filename
|
910
|
+
else
|
911
|
+
base = @filename
|
912
|
+
end
|
913
|
+
f = File.expand_path(File.join(path, base))
|
914
|
+
if File.exists?(f)
|
915
|
+
file = f
|
916
|
+
else
|
917
|
+
raise RuntimeError, "Cannot find #{base} in include path (#{path})"
|
918
|
+
end
|
919
|
+
|
920
|
+
return file
|
921
|
+
end
|
922
|
+
|
923
|
+
end
|
924
|
+
# = License
|
925
|
+
#
|
926
|
+
# ==The MIT License
|
927
|
+
#
|
928
|
+
# Copyright (c) 2002 Brian Wisti
|
929
|
+
#
|
930
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
931
|
+
#
|
932
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
933
|
+
#
|
934
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
935
|
+
#
|
936
|
+
# Brian Wisti (brian@coolnamehere.com)
|