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.
- data/Changes +20 -0
- data/Rakefile +6 -1
- data/lib/PageTemplate.rb +1 -1
- data/lib/PageTemplate/case.rb +21 -18
- data/lib/PageTemplate/commands.rb +131 -131
- data/lib/PageTemplate/htglossary.rb +70 -0
- data/lib/PageTemplate/parser.rb +228 -80
- data/test.rb +175 -113
- metadata +3 -2
@@ -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
|
data/lib/PageTemplate/parser.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
201
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
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
|
-
@
|
282
|
-
@glossary = args['glossary'] ||
|
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
|
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
|
477
|
+
if closer and @glossary.modifies?(closer,last,command)
|
324
478
|
cmd = stack.pop
|
325
|
-
stack.last
|
479
|
+
last = stack.last
|
480
|
+
last.add(cmd)
|
326
481
|
next
|
327
482
|
end
|
328
483
|
|
329
|
-
#
|
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
|
341
|
-
|
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
|
-
|
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(
|
516
|
+
def output(*args)
|
366
517
|
return '' unless @commands
|
367
|
-
@
|
368
|
-
str = @commands.output(@namespace)
|
369
|
-
@namespace.parent = nil
|
370
|
-
str
|
518
|
+
@commands.output(*args)
|
371
519
|
end
|
372
520
|
end
|
373
521
|
end
|