PageTemplate 1.1.1 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. data/MANIFEST +4 -0
  2. data/Rakefile +54 -0
  3. data/config.save +12 -0
  4. data/doc/classes/BlockCommand.html +229 -0
  5. data/doc/classes/BlockCommand.src/M000004.html +18 -0
  6. data/doc/classes/BlockCommand.src/M000005.html +25 -0
  7. data/doc/classes/BlockCommand.src/M000006.html +18 -0
  8. data/doc/classes/BlockCommand.src/M000007.html +23 -0
  9. data/doc/classes/BlockCommand.src/M000008.html +24 -0
  10. data/doc/classes/Command.html +167 -0
  11. data/doc/classes/Command.src/M000029.html +19 -0
  12. data/doc/classes/Command.src/M000030.html +18 -0
  13. data/doc/classes/IfCommand.html +193 -0
  14. data/doc/classes/IfCommand.src/M000039.html +19 -0
  15. data/doc/classes/IfCommand.src/M000040.html +20 -0
  16. data/doc/classes/IfCommand.src/M000041.html +20 -0
  17. data/doc/classes/IfElseCommand.html +189 -0
  18. data/doc/classes/IfElseCommand.src/M000023.html +19 -0
  19. data/doc/classes/IfElseCommand.src/M000024.html +23 -0
  20. data/doc/classes/IfElseCommand.src/M000025.html +20 -0
  21. data/doc/classes/IncludeCommand.html +178 -0
  22. data/doc/classes/IncludeCommand.src/M000017.html +21 -0
  23. data/doc/classes/IncludeCommand.src/M000018.html +23 -0
  24. data/doc/classes/IncludeCommand.src/M000019.html +18 -0
  25. data/doc/classes/LoopCommand.html +197 -0
  26. data/doc/classes/LoopCommand.src/M000001.html +19 -0
  27. data/doc/classes/LoopCommand.src/M000002.html +33 -0
  28. data/doc/classes/LoopCommand.src/M000003.html +20 -0
  29. data/doc/classes/LoopElseCommand.html +190 -0
  30. data/doc/classes/LoopElseCommand.src/M000020.html +19 -0
  31. data/doc/classes/LoopElseCommand.src/M000021.html +23 -0
  32. data/doc/classes/LoopElseCommand.src/M000022.html +20 -0
  33. data/doc/classes/Namespace.html +297 -0
  34. data/doc/classes/Namespace.src/M000031.html +19 -0
  35. data/doc/classes/Namespace.src/M000032.html +18 -0
  36. data/doc/classes/Namespace.src/M000033.html +18 -0
  37. data/doc/classes/Namespace.src/M000034.html +19 -0
  38. data/doc/classes/Namespace.src/M000035.html +36 -0
  39. data/doc/classes/Namespace.src/M000036.html +21 -0
  40. data/doc/classes/Namespace.src/M000037.html +18 -0
  41. data/doc/classes/Namespace.src/M000038.html +18 -0
  42. data/doc/classes/PageTemplate.html +352 -0
  43. data/doc/classes/PageTemplate.src/M000009.html +46 -0
  44. data/doc/classes/PageTemplate.src/M000010.html +29 -0
  45. data/doc/classes/PageTemplate.src/M000011.html +18 -0
  46. data/doc/classes/PageTemplate.src/M000012.html +18 -0
  47. data/doc/classes/PageTemplate.src/M000013.html +18 -0
  48. data/doc/classes/PageTemplate.src/M000014.html +18 -0
  49. data/doc/classes/PageTemplate.src/M000015.html +18 -0
  50. data/doc/classes/PageTemplate.src/M000016.html +19 -0
  51. data/doc/classes/Syntax.html +139 -0
  52. data/doc/classes/Syntax/CachedParser.html +163 -0
  53. data/doc/classes/Syntax/CachedParser.src/M000045.html +19 -0
  54. data/doc/classes/Syntax/CachedParser.src/M000046.html +52 -0
  55. data/doc/classes/Syntax/Glossary.html +245 -0
  56. data/doc/classes/Syntax/Glossary.src/M000047.html +20 -0
  57. data/doc/classes/Syntax/Glossary.src/M000048.html +29 -0
  58. data/doc/classes/Syntax/Parser.html +261 -0
  59. data/doc/classes/Syntax/Parser.src/M000049.html +22 -0
  60. data/doc/classes/Syntax/Parser.src/M000050.html +35 -0
  61. data/doc/classes/Syntax/Parser.src/M000051.html +18 -0
  62. data/doc/classes/Syntax/Parser.src/M000052.html +21 -0
  63. data/doc/classes/Syntax/Parser.src/M000053.html +53 -0
  64. data/doc/classes/Syntax/Parser.src/M000054.html +107 -0
  65. data/doc/classes/TextCommand.html +185 -0
  66. data/doc/classes/TextCommand.src/M000042.html +18 -0
  67. data/doc/classes/TextCommand.src/M000043.html +18 -0
  68. data/doc/classes/TextCommand.src/M000044.html +18 -0
  69. data/doc/classes/ValueCommand.html +186 -0
  70. data/doc/classes/ValueCommand.src/M000026.html +18 -0
  71. data/doc/classes/ValueCommand.src/M000027.html +18 -0
  72. data/doc/classes/ValueCommand.src/M000028.html +20 -0
  73. data/doc/created.rid +1 -0
  74. data/doc/files/README_txt.html +224 -0
  75. data/doc/files/lib/PageTemplate_rb.html +119 -0
  76. data/doc/fr_class_index.html +41 -0
  77. data/doc/fr_file_index.html +28 -0
  78. data/doc/fr_method_index.html +80 -0
  79. data/doc/index.html +24 -0
  80. data/doc/rdoc-style.css +208 -0
  81. data/install.rb +1015 -0
  82. data/lib/MANIFEST +2 -0
  83. data/lib/PageTemplate.rb +936 -0
  84. data/pkg/PageTemplate-1.1.2.gem +0 -0
  85. data/tdata/complex.txt +116 -0
  86. data/tdata/i1.txt +1 -0
  87. data/tdata/i2.txt +7 -0
  88. data/tdata/ib1.txt +7 -0
  89. data/tdata/ib2.txt +13 -0
  90. data/tdata/ie1.txt +4 -0
  91. data/tdata/ie2.txt +15 -0
  92. data/tdata/include.1.txt +1 -0
  93. data/tdata/include.2.txt +1 -0
  94. data/tdata/include.3.txt +1 -0
  95. data/tdata/include.4.out.txt +3 -0
  96. data/tdata/include.4.txt +2 -0
  97. data/tdata/include.4a.txt +1 -0
  98. data/tdata/include.4b.txt +2 -0
  99. data/tdata/include.5.txt +2 -0
  100. data/tdata/l1.txt +5 -0
  101. data/tdata/l2.txt +7 -0
  102. data/tdata/nm.txt +4 -0
  103. data/tdata/p/_b1.txt +0 -0
  104. data/tdata/p/b1.txt +4 -0
  105. data/tdata/p/cb1.txt +4 -0
  106. data/tdata/t.txt +2 -0
  107. data/tdata/v1.txt +11 -0
  108. data/tdata/v2.txt +11 -0
  109. data/test-install.rb +2 -0
  110. metadata +137 -4
data/lib/MANIFEST ADDED
@@ -0,0 +1,2 @@
1
+ MANIFEST
2
+ PageTemplate.rb
@@ -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)