PageTemplate 1.1.2 → 1.2.0

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