PageTemplate 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ # htmltemplate.rb
2
+ #
3
+ # An emulation layer on top of PT to mimic HTML::Template
4
+ #
5
+
6
+ ############################################################
7
+
8
+ class PageTemplate
9
+ class HTGlossary < SyntaxGlossary
10
+ @directive = /<!--(.+?)-->/m
11
+
12
+ default { |command,parser|
13
+ TextCommand.new("<!--#{command}-->",parser)
14
+ }
15
+
16
+ define(/^tmpl_var (?:name=)?"?((?:\w+)(?:\.\w+\??)*)"?(?:\s+processor="?(\w+)"?)?$/i) {
17
+ |match,parser|
18
+ # Value, Preprocessor, Parser
19
+ ValueCommand.new(match[1],match[2],parser)
20
+ }
21
+
22
+ define(/^(tmpl_if|tmpl_unless) (?:name=)?"?((\w+)(\.\w+\??)*)"?$/i) {
23
+ |match,parser|
24
+ # Called_As, Value, parser
25
+ IfCommand.new(match[1],match[2],parser)
26
+ }
27
+
28
+ define(/^(tmpl_loop) (?:name=)"?(\w+(?:\.\w+\??)*)"?(?:\s+iterators?="?((?:\s*\w+)+)"?)?$/i) {
29
+ |match,parser|
30
+ # Called_As, Value, Iterators
31
+ LoopCommand.new(match[1],match[2],match[3])
32
+ }
33
+
34
+ define(/^tmpl_include (?:name=|file=)?"?((\w+)(?:\.\w+\??)*)"?$/i) {
35
+ |match,parser|
36
+ # Value, parser
37
+ IncludeCommand.new(match[1],parser)
38
+ }
39
+
40
+ # Command#else's expect only to be called
41
+ modifier(:else) { |cmd,command|
42
+ if command =~ /^tmpl_else$/i
43
+ cmd.else
44
+ true
45
+ else
46
+ false
47
+ end
48
+ }
49
+ modifier(:end) { |cmd,command|
50
+ if command =~ /^\/\s*#{cmd.called_as}$/i
51
+ cmd.end
52
+ true
53
+ else
54
+ false
55
+ end
56
+ }
57
+ modifier(:when) { |cmd,command|
58
+ case command
59
+ when /^tmpl_when\s+(?:name=)"?(\w(?:.\w+\??)*)"?$/i
60
+ cmd.when($1)
61
+ true
62
+ when /^tmpl_else$/i
63
+ cmd.else($1)
64
+ true
65
+ else
66
+ false
67
+ end
68
+ }
69
+ end
70
+ end
@@ -19,22 +19,19 @@ class PageTemplate
19
19
  #
20
20
  # Cache: A cache ensures that a method on an object will only be
21
21
  # called once.
22
- class Namespace
22
+ module NamespaceItem
23
23
  attr_accessor :parent, :object
24
- def initialize(parent=nil,object=nil)
25
- @values = Hash.new
26
- @parent = parent
27
- @object = object
28
- end
29
24
 
30
25
  # Clears the cache
31
- def clear_cache
26
+ def clear
32
27
  @values = Hash.new
33
28
  end
29
+ alias_method :clear_cache, :clear
34
30
 
35
31
  # Saves a variable +key+ as the string +value+ in the global
36
32
  # namespace.
37
33
  def set(key, value)
34
+ @values ||= {}
38
35
  @values[key] = value
39
36
  end
40
37
  alias_method :[]=, :set
@@ -54,7 +51,11 @@ class PageTemplate
54
51
  # If +val+ is a dot-separated list of words, then 'key' is the
55
52
  # first part of it. The remainder of the words are sent to key
56
53
  # (and further results) to premit accessing attributes of objects.
57
- def get(val)
54
+ def get(val,clean_rescue=true)
55
+ args = parser.args
56
+ clean_rescue = !args['raise_on_error'] if clean_rescue
57
+ @values ||= {}
58
+ @object ||= nil
58
59
  # Is it ours, or is it cached?
59
60
  key, rest = val.split(/\./,2)
60
61
  value = case
@@ -62,13 +63,14 @@ class PageTemplate
62
63
  @values[key]
63
64
  when @object.respond_to?(:has_key?) && @object.has_key?(key)
64
65
  @values[key] = @object[key]
65
- when @object && @object.respond_to?(key.to_sym)
66
+ when @object.respond_to?(key.to_sym)
66
67
  # This sets the cache and returns the value, too!
67
68
  @values[key] = @object.send(key)
68
69
  when @object && key == '__ITEM__'
69
70
  @object
70
71
  when @parent
71
- @parent.get(key)
72
+ # Return what the parent gets
73
+ return @parent.get(val)
72
74
  else
73
75
  # We're the global namespace
74
76
  nil
@@ -77,21 +79,68 @@ class PageTemplate
77
79
  if rest
78
80
  rest.split(/\./).each do |i|
79
81
  begin
80
- value = value.send(i)
82
+ value = case
83
+ when value.respond_to?(:has_key?) && value.has_key?(i)
84
+ value[i]
85
+ when value.respond_to?(:[]) && i =~ /^\d+$/
86
+ value[i.to_i]
87
+ when value.respond_to?(i)
88
+ value.send(i)
89
+ else
90
+ nil
91
+ end
81
92
  rescue NoMethodError => er
82
93
  return nil
83
94
  end
84
95
  end
85
96
  end
86
97
  value
98
+ rescue Exception => e
99
+ if clean_rescue
100
+ "[ Error: #{e.message} ]"
101
+ else
102
+ raise e
103
+ end
87
104
  end
88
105
  alias_method :[], :get
89
106
 
107
+ # parser: most namespace objects won't be a parser, but pass this
108
+ # query up to the parser object.
109
+ def parser
110
+ if @parent
111
+ @parent.parser
112
+ else
113
+ Parser.recent_parser
114
+ end
115
+ end
116
+
90
117
  # A convenience method to test whether a variable has a true
91
118
  # value. Returns nil if +flag+ is not found in the namespace,
92
119
  # or +flag+ has a nil value attached to it.
93
120
  def true?(flag)
94
- get(flag) ? true : false
121
+ args = parser.args
122
+ val = get(flag,false)
123
+ case
124
+ when ! val
125
+ false
126
+ when args['empty_is_true']
127
+ true
128
+ when val.respond_to?(:empty?)
129
+ ! val.empty?
130
+ else
131
+ true
132
+ end
133
+ rescue Exception => er
134
+ false
135
+ end
136
+ end
137
+
138
+ class Namespace
139
+ include NamespaceItem
140
+ def initialize(parent=nil,object=nil)
141
+ @values = Hash.new
142
+ @parent = parent
143
+ @object = object
95
144
  end
96
145
  end
97
146
 
@@ -120,7 +169,7 @@ class PageTemplate
120
169
  @args = args
121
170
  @paths = @args['include_paths']
122
171
  @paths ||= [@args['include_path']].compact
123
- @paths = [Dir.getwd] if @paths.empty?
172
+ @paths = [Dir.getwd,'/'] if @paths.empty?
124
173
  end
125
174
 
126
175
  # Return the contents of the file +name+, which must be within the
@@ -158,54 +207,147 @@ class PageTemplate
158
207
  # regexps should not contain PageTemplate command text. i.e:
159
208
  # /var \w+/i should be used instead of /[% var %]/
160
209
  class SyntaxGlossary
161
- attr_accessor :directive
162
-
163
- def initialize(directive, glossary)
164
- @directive = directive
165
- @glossary = glossary
166
- end
210
+ class << self
211
+ attr_accessor :directive
212
+ attr_writer :default
213
+ def default(&block)
214
+ if block_given?
215
+ @default = block
216
+ else
217
+ @default
218
+ end
219
+ end
167
220
 
168
- # Look up +text+ to see if it matches a command within the lookup
169
- # table, returning the command for it, or UnknownCommand if none
170
- # match.
171
- def lookup(text)
172
- @glossary.each do |key,val|
173
- if m = key.match(text)
174
- return val
221
+ # Look up +command+ to see if it matches a command within the lookup
222
+ # table, returning the instance of the command for it, or
223
+ # an UnknownCommand if none match.
224
+ def lookup(command)
225
+ @glossary.each do |key,val|
226
+ if m = key.match(command)
227
+ return val.call(m)
228
+ end
175
229
  end
230
+
231
+ return @default.call(command)
232
+ end
233
+ def modifies?(modifier,cmd,command)
234
+ @modifiers[modifier].call(cmd,command)
235
+ end
236
+
237
+ # Define a regexp -> Command mapping.
238
+ # +rx+ is inserted in the lookup table as a key for +command+
239
+ def define(rx,&block)
240
+ raise ArgumentError, 'First argument to define must be a Regexp' unless rx.is_a?(Regexp)
241
+ raise ArgumentError, 'Block expected' unless block
242
+ @glossary ||= {}
243
+ @glossary[rx] = block
176
244
  end
177
245
 
178
- return UnknownCommand
179
- end
180
-
181
- # Define a regexp -> Command mapping.
182
- # +rx+ is inserted in the lookup table as a key for +command+
183
- def define(rx,command)
184
- raise ArgumentError, 'First argument to define must be a Regexp' unless rx.is_a?(Regexp)
185
- @glossary[rx] = command
186
- end
187
- # This is shorthand for define(+key+,Valuecommand), and also
188
- # allows +key+ to be a string, converting it to a regexp before
189
- # adding it to the dictionary.
190
- def define_global_var(key)
191
- rx = key
192
- rx = /^#{key.to_s}(?:\.\w+\??)*(?:\s:(\w+))?$/ unless key.is_a?(Regexp)
193
- @glossary[rx] = ValueCommand
246
+ def modifier(sym,&block)
247
+ raise ArgumentError, 'First argument to define must be a Symbol' unless sym.is_a?(Symbol)
248
+ raise ArgumentError, 'Block expected' unless block
249
+ @modifiers ||= Hash.new(lambda { false })
250
+ @modifiers[sym] = block
251
+ end
252
+
253
+ # This is shorthand for define(+key+,Valuecommand), and also
254
+ # allows +key+ to be a string, converting it to a regexp before
255
+ # adding it to the dictionary.
256
+ def define_global_var(rx)
257
+ @glossary ||= {}
258
+ rx = /^(#{key.to_s}(?:\.\w+\??)*)(?:\s:(\w+))?$/ unless rx.is_a?(Regexp)
259
+ @glossary[rx] = lambda { |match|
260
+ ValueCommand.new(match[1],match[2])
261
+ }
262
+ end
194
263
  end
195
264
  end
196
265
 
197
266
  # This is the regexp we tried using for grabbing an entire line
198
267
  # Good for everything but ValueCommand :-/.
199
268
  # /(?:^\s*\[%([^\]]+?)%\]\s*$\r?\n?|\[%(.+?)%\])/m,
200
- DEFAULTGLOSSARY = SyntaxGlossary.new(
201
- /\[%(.+?)%\]/m,
269
+ class DefaultGlossary < SyntaxGlossary
270
+ @directive = /\[%(.+?)%\]/m
271
+
272
+ default { |command|
273
+ UnknownCommand.new(command)
274
+ }
275
+
276
+ define(/^var ((?:\w+)(?:\.\w+\??)*)(?:\s+:(\w+))?$/i) { |match|
277
+ # Value, Preprocessor
278
+ ValueCommand.new(match[1],match[2])
279
+ }
280
+
281
+ define(/^--(.+)$/i) { |match|
282
+ # The Comment
283
+ CommentCommand.new(match[1])
284
+ }
285
+
286
+ define(/^(if|unless) ((\w+)(\.\w+\??)*)$/i) { |match|
287
+ # Called_As, Value
288
+ IfCommand.new(match[1],match[2])
289
+ }
290
+
291
+ define(/^(in|loop) (\w+(?:\.\w+\??)*)(?:\:((?:\s+\w+)+))?$/i) { |match|
292
+ # Called_As, Value, Iterators
293
+ LoopCommand.new(match[1],match[2],match[3])
294
+ }
295
+
296
+ define(/^include ((\w+)(?:\.\w+\??)*)$/i) { |match|
297
+ # Value
298
+ IncludeCommand.new(match[1])
299
+ }
300
+
301
+ # Command#else's expect only to be called
302
+ modifier(:else) { |cmd,command|
303
+ case command
304
+ when /^else|no|empty$/i
305
+ cmd.else
306
+ true
307
+ else
308
+ false
309
+ end
310
+ }
311
+
312
+ # elsif: accepts else and elsif
313
+ modifier(:elsif) { |cmd,command|
314
+ case command
315
+ when /^(?:elsif|elseif|else if) (.+)$/i
316
+ cmd.elsif($1)
317
+ true
318
+ when /^else|no|empty$/i
319
+ cmd.else
320
+ true
321
+ else
322
+ false
323
+ end
324
+ }
202
325
 
203
- /^var ((\w+)(\.\w+\??)*)(\s+:\w+)?$/i => ValueCommand,
204
- /^--/i => CommentCommand,
205
- /^(if|unless) ((\w+)(\.\w+\??)*)$/i => IfCommand,
206
- /^(in|loop) ((\w+)(\.\w+\??)*)(:\s+\w+)?$/i => LoopCommand,
207
- /^(include) ((\w+)(\.\w+\??)*)$/i => IncludeCommand
208
- )
326
+ # Command#end
327
+ # accepts 'end', 'end(name)' and 'end (name)'
328
+ modifier(:end) { |cmd,command|
329
+ case command
330
+ when /^end\s*(#{cmd.called_as})?$/i
331
+ cmd.end
332
+ true
333
+ else
334
+ false
335
+ end
336
+ }
337
+ # For case statements.
338
+ modifier(:when) { |cmd,command|
339
+ case command
340
+ when /^when\s+(\w(?:.\w+\??)*)$/i
341
+ cmd.when($1)
342
+ true
343
+ when /^else$/i
344
+ cmd.else($1)
345
+ true
346
+ else
347
+ false
348
+ end
349
+ }
350
+ end
209
351
 
210
352
  # Preprocessor is a parent class for preprocessors.
211
353
  # A preprocessor is used to process or otherwise prepare output for
@@ -249,7 +391,7 @@ class PageTemplate
249
391
  # keys: (with defaults)
250
392
  #
251
393
  # 'namespace' => A namespace object. (A new namespace)
252
- # 'glossary' => A SyntaxGlossary object. (a dup of DEFAULTGLOSSARY)
394
+ # 'glossary' => A SyntaxGlossary class singleton. (DefaultGlossary)
253
395
  # 'preprocessor' => The preprocessor. (DefaultPreprocessor)
254
396
  # 'default_processor' => The processor. (:process)
255
397
  # 'source' => The Source for templates. (FileSource)
@@ -268,6 +410,12 @@ class PageTemplate
268
410
  attr_reader :preprocessor, :default_processor
269
411
  attr_reader :glossary, :namespace, :source
270
412
  attr_reader :args, :commands
413
+
414
+ # This is corny, but recent_parser returns the most recently created
415
+ # Parser.
416
+ def Parser.recent_parser
417
+ @@recent_parser
418
+ end
271
419
  # Parser.new() accepts a hash as an argument, and looks for these
272
420
  # keys: (with defaults)
273
421
  #
@@ -277,9 +425,11 @@ class PageTemplate
277
425
  # 'default_processor' => The processor. (:process)
278
426
  # 'source' => The Source for templates. (FileSource)
279
427
  def initialize(args = {})
428
+ @namespace = self
429
+ @@recent_parser = self
280
430
  @args = args # For sub-commands
281
- @namespace = args['namespace'] || Namespace.new
282
- @glossary = args['glossary'] || DEFAULTGLOSSARY.dup
431
+ @parent = args['namespace'] || nil
432
+ @glossary = args['glossary'] || DefaultGlossary
283
433
  @preprocessor = args['preprocessor'] || DefaultPreprocessor
284
434
  @default_processor = args['default_processor'] || :process
285
435
  @source = (args['source'] || FileSource).new(@args)
@@ -304,6 +454,10 @@ class PageTemplate
304
454
  def parse(body)
305
455
  rx = @glossary.directive
306
456
  stack = [Template.new(self)]
457
+ stack[0].parent = self
458
+ last = stack.last
459
+ modifier = nil
460
+ closer = nil
307
461
  while (m = rx.match(body))
308
462
  pre = m.pre_match
309
463
  command = m[1].strip.gsub(/\s+/,' ')
@@ -316,31 +470,28 @@ class PageTemplate
316
470
 
317
471
  # If the command at the top of the stack is a 'Stacker',
318
472
  # Does this command modify it? If so, just skip back.
319
- next if stack.last.modifies?(command)
473
+ next if modifier && @glossary.modifies?(modifier,last,command)
320
474
 
321
475
  # If it closes, we're done changing this. Pop it off the
322
476
  # Stack and add it to the one above it.
323
- if stack.last.closes?(command)
477
+ if closer and @glossary.modifies?(closer,last,command)
324
478
  cmd = stack.pop
325
- stack.last.add(cmd)
479
+ last = stack.last
480
+ last.add(cmd)
326
481
  next
327
482
  end
328
483
 
329
- # Find what command it is.
484
+ # Create the command
330
485
  cmd = @glossary.lookup(command)
331
486
 
332
- # If the command takes it, pass the parser.
333
- block = if (cmd.instance_method(:initialize).arity == 2)
334
- cmd.new(command,self)
335
- else
336
- cmd.new(command)
337
- end
338
-
339
487
  # If it's a stacking command, push it on the stack
340
- if block.is_a?(StackableCommand)
341
- stack.push block
488
+ if cmd.is_a?(StackableCommand)
489
+ modifier = cmd.class.modifier
490
+ closer = cmd.class.closer
491
+ stack.push cmd
492
+ last = cmd
342
493
  else
343
- stack.last.add block
494
+ last.add cmd
344
495
  end
345
496
  end
346
497
  stack.last.add TextCommand.new(body) if body && body.length > 0
@@ -349,25 +500,22 @@ class PageTemplate
349
500
  end
350
501
  stack[0]
351
502
  end
503
+
504
+ # Since a Parser is also a namespace object, include NamespaceItem
505
+ include NamespaceItem
506
+ # But redefine parser
507
+ def parser
508
+ self
509
+ end
510
+
352
511
  # Not really of any point, but clears the saved commands.
353
512
  def clearCommands
354
513
  @commands = nil
355
514
  end
356
- # Sets this parser's namespace values.
357
- def []=(key,val)
358
- @namespace[key] = val
359
- end
360
- # Gets this parser's namespace values.
361
- def [](key)
362
- @namespace[key]
363
- end
364
515
  # If any commands are loaded and saved, return a string of it.
365
- def output(namespace=nil)
516
+ def output(*args)
366
517
  return '' unless @commands
367
- @namespace.parent = namespace if namespace
368
- str = @commands.output(@namespace)
369
- @namespace.parent = nil
370
- str
518
+ @commands.output(*args)
371
519
  end
372
520
  end
373
521
  end