PageTemplate 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|