flott 1.0.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/.gitignore +5 -0
- data/.travis.yml +16 -0
- data/Gemfile +9 -0
- data/README.rdoc +76 -0
- data/Rakefile +25 -0
- data/VERSION +1 -0
- data/benchmarks/data/.keep +0 -0
- data/benchmarks/flott_benchmark.rb +165 -0
- data/benchmarks/runner.rb +6 -0
- data/doc-main.txt +76 -0
- data/flott.gemspec +41 -0
- data/install.rb +16 -0
- data/lib/flott.rb +1016 -0
- data/lib/flott/cache.rb +99 -0
- data/lib/flott/version.rb +8 -0
- data/make_doc.rb +5 -0
- data/tests/templates/header +8 -0
- data/tests/templates/subdir/deeptemplate +6 -0
- data/tests/templates/subdir/included +9 -0
- data/tests/templates/subdir/subdir2/deepincluded2 +2 -0
- data/tests/templates/subdir/subdir2/included2 +1 -0
- data/tests/templates/subdir/subdir3/included3 +1 -0
- data/tests/templates/template +12 -0
- data/tests/templates/template2 +6 -0
- data/tests/templates/toplevel +2 -0
- data/tests/templates/toplevel2 +1 -0
- data/tests/test_cache.rb +70 -0
- data/tests/test_flott.rb +200 -0
- data/tests/test_flott_file.rb +155 -0
- data/tests/test_helper.rb +3 -0
- metadata +153 -0
data/lib/flott.rb
ADDED
@@ -0,0 +1,1016 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
# This module includes the Flott::Parser class, that can be used to compile
|
4
|
+
# Flott template files to Flott::Template objects, which can then be evaluted
|
5
|
+
# in a Flott::Environment.
|
6
|
+
module Flott
|
7
|
+
require 'flott/version'
|
8
|
+
require 'flott/cache'
|
9
|
+
|
10
|
+
module ::Kernel
|
11
|
+
private
|
12
|
+
|
13
|
+
# Evaluate +object+ with Flott and return the output. +object+ can either be
|
14
|
+
# like a string (responding to :to_str), an IO instance (responding to
|
15
|
+
# :to_io), or respond to :evaluate like Flott::Template. The environment
|
16
|
+
# and (root) template used is attached to the output ring as the mehthods
|
17
|
+
# environment and template respectively.
|
18
|
+
def Flott(object, env = Environment.new, &block)
|
19
|
+
if object.respond_to?(:evaluate)
|
20
|
+
Flott.evaluate(object, env, &block)
|
21
|
+
env.output
|
22
|
+
elsif object.respond_to?(:to_str)
|
23
|
+
Flott.string_from_source(object.to_str, env, &block)
|
24
|
+
elsif object.respond_to?(:to_io)
|
25
|
+
Flott.string_from_source(object.to_io.read, env, &block)
|
26
|
+
else
|
27
|
+
raise TypeError,
|
28
|
+
"require an evaluable object, a String, or an IO object"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class << self
|
34
|
+
# True switches debugging mode on, false off. Defaults to false.
|
35
|
+
attr_accessor :debug
|
36
|
+
|
37
|
+
# Return the compiled template of +source+ while passing the remaining
|
38
|
+
# arguments through to Flott::Parser.new.
|
39
|
+
def compile(source, workdir = nil, rootdir = nil, filename = nil)
|
40
|
+
parser = Flott::Parser.new(source, workdir, rootdir, filename)
|
41
|
+
parser.compile
|
42
|
+
end
|
43
|
+
|
44
|
+
# The already compiled ruby code is evaluated in the environment env. If no
|
45
|
+
# environment is given, a newly created environment is used. This method
|
46
|
+
# doesn't return the result directly, only via the effects on the environment.
|
47
|
+
def evaluate(compiled, env = Environment.new, &block)
|
48
|
+
if !(EnvironmentMixin === env) and env.respond_to? :to_hash
|
49
|
+
env = Environment.new.update(env.to_hash)
|
50
|
+
end
|
51
|
+
env.instance_eval(&block) if block
|
52
|
+
compiled.evaluate(env)
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# Evaluate the template source _source_ in environment _env_ and with the
|
57
|
+
# block _block_. If no environment is given, a newly created environment is
|
58
|
+
# used. This method doesn't return the result directly, only via the
|
59
|
+
# effects on the environment.
|
60
|
+
def evaluate_source(source, env = Environment.new, &block)
|
61
|
+
if !(EnvironmentMixin === env) and env.respond_to? :to_hash
|
62
|
+
env = Environment.new.update(env.to_hash)
|
63
|
+
end
|
64
|
+
env.instance_eval(&block) if block
|
65
|
+
parser = Parser.new(source)
|
66
|
+
parser.evaluate(env)
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
# Evaluate the template file _filename_ in environment _env_ and with the
|
71
|
+
# block _block_. If no environment is given, a newly created environment is
|
72
|
+
# used. This method doesn't return the result directly, only via the
|
73
|
+
# effects on the environment.
|
74
|
+
def evaluate_file(filename, env = Environment.new, &block)
|
75
|
+
if !(EnvironmentMixin === env) and env.respond_to? :to_hash
|
76
|
+
env = Environment.new.update(env.to_hash)
|
77
|
+
end
|
78
|
+
env.instance_eval(&block) if block
|
79
|
+
parser = Parser.from_filename(filename)
|
80
|
+
parser.evaluate(env)
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
# Create an output string from template source _source_, evaluated in the
|
85
|
+
# Environment _env_. If _block_ is given it is evaluated in the _env_
|
86
|
+
# context as well.
|
87
|
+
def string_from_source(source, env = Environment.new, &block)
|
88
|
+
if !(EnvironmentMixin === env) and env.respond_to? :to_hash
|
89
|
+
env = Environment.new.update(env.to_hash)
|
90
|
+
end
|
91
|
+
output = ''
|
92
|
+
env.output = output
|
93
|
+
env.instance_eval(&block) if block
|
94
|
+
parser = Parser.new(source)
|
95
|
+
parser.evaluate(env)
|
96
|
+
env.output
|
97
|
+
end
|
98
|
+
|
99
|
+
# Create an output string from the template file _filename_, evaluated in the
|
100
|
+
# Environment _env_. If _block_ is given it is evaluated in the _env_ context
|
101
|
+
# as well. This will set the rootdir and workdir attributes, in order to
|
102
|
+
# dynamic include other templates into this one.
|
103
|
+
def string_from_file(filename, env = Environment.new, &block)
|
104
|
+
if !(EnvironmentMixin === env) and env.respond_to? :to_hash
|
105
|
+
env = Environment.new.update(env.to_hash)
|
106
|
+
end
|
107
|
+
output = ''
|
108
|
+
env.output = output
|
109
|
+
env.instance_eval(&block) if block
|
110
|
+
parser = Parser.from_filename(filename)
|
111
|
+
parser.evaluate(env)
|
112
|
+
env.output
|
113
|
+
end
|
114
|
+
end
|
115
|
+
Flott.debug = false
|
116
|
+
|
117
|
+
# The base exception of all Flott Exceptions, Errors.
|
118
|
+
class FlottException < StandardError
|
119
|
+
# Wrap _exception_ into a FlottException, including the given backtrace.
|
120
|
+
def self.wrap(exception)
|
121
|
+
wrapper = new(exception.message)
|
122
|
+
wrapper.set_backtrace exception.backtrace
|
123
|
+
wrapper
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# The base Exception for Parser errors.
|
128
|
+
class ParserError < FlottException; end
|
129
|
+
|
130
|
+
# This exception is raised if errors happen in the compilation phase of a
|
131
|
+
# template.
|
132
|
+
class CompileError < ParserError; end
|
133
|
+
|
134
|
+
# This exception is raised if a syntax error occurs during the
|
135
|
+
# evaluation of the compiled Ruby code.
|
136
|
+
class EvalError < ParserError; end
|
137
|
+
|
138
|
+
# This exception is raised if a syntax error occurs while
|
139
|
+
# calling the evaluated Proc object.
|
140
|
+
class CallError < ParserError; end
|
141
|
+
|
142
|
+
# This exception is raised if an attempt is made to parse or include a path
|
143
|
+
# that is considered insecure.
|
144
|
+
class SecurityViolation < ParserError; end
|
145
|
+
|
146
|
+
# This module contains methods to interpret filenames of the templates.
|
147
|
+
module FilenameMixin
|
148
|
+
# Interpret filename for included templates. Beginning with '/' is the root
|
149
|
+
# directory, that is the workdir of the first parser in the tree. All other
|
150
|
+
# pathes are treated relative to this parsers workdir.
|
151
|
+
def interpret_filename(filename)
|
152
|
+
filename.untaint
|
153
|
+
if filename[0] == ?/
|
154
|
+
filename = File.join(rootdir, filename[1..-1])
|
155
|
+
elsif workdir
|
156
|
+
filename = File.join(workdir, filename)
|
157
|
+
end
|
158
|
+
File.expand_path(filename)
|
159
|
+
end
|
160
|
+
private :interpret_filename
|
161
|
+
|
162
|
+
def interpret_filename_as_page(filename)
|
163
|
+
filename.untaint
|
164
|
+
if filename[0] == ?/
|
165
|
+
filename = filename[1..-1]
|
166
|
+
elsif workdir
|
167
|
+
filename = File.expand_path(File.join(workdir, filename))
|
168
|
+
filename[rootdir] = ''
|
169
|
+
end
|
170
|
+
filename
|
171
|
+
end
|
172
|
+
private :interpret_filename
|
173
|
+
|
174
|
+
def check_secure_path(path)
|
175
|
+
if File::ALT_SEPARATOR
|
176
|
+
if path.split(File::ALT_SEPARATOR).any? { |p| p == '..' }
|
177
|
+
raise SecurityViolation, "insecure path '#{path}' because of '..'"
|
178
|
+
end
|
179
|
+
else
|
180
|
+
if path[0] == ?~
|
181
|
+
raise SecurityViolation,
|
182
|
+
"insecure path '#{path}' because of starting '~'"
|
183
|
+
end
|
184
|
+
if path.split(File::SEPARATOR).any? { |p| p == '..' }
|
185
|
+
raise SecurityViolation, "insecure path '#{path}' because of '..'"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
private :check_secure_path
|
190
|
+
|
191
|
+
|
192
|
+
def sub_path?(sp, path)
|
193
|
+
sp[/\A#{path}/] == path
|
194
|
+
end
|
195
|
+
private :sub_path?
|
196
|
+
end
|
197
|
+
|
198
|
+
# This module can be included into classes that should act as an environment
|
199
|
+
# for Flott templates. An instance variable @__output__
|
200
|
+
# (EnvironmentMixin#output) should hold an output object, that responds
|
201
|
+
# to the #<< method, usually an IO object. If no initialize method is defined
|
202
|
+
# in the including class, EnvironmentMixin#initialize uses STDOUT as this
|
203
|
+
# _output_ object.
|
204
|
+
#
|
205
|
+
# If the class has its own initialize method, the environment can be
|
206
|
+
# initialized with super(output, escape)
|
207
|
+
# class Environment
|
208
|
+
# include EnvironmentMixin
|
209
|
+
# def initialize(output, escape)
|
210
|
+
# super(output, escape)
|
211
|
+
# end
|
212
|
+
# end
|
213
|
+
module EnvironmentMixin
|
214
|
+
# Creates an Environment object, that outputs to _output_. The default
|
215
|
+
# ouput object is STDOUT, but any object that responds to #<< will do.
|
216
|
+
# _escape_ is a object that responds to #call (usually a Proc instance),
|
217
|
+
# and given a string, returns an escaped version of the string as an
|
218
|
+
# result. _escape_ defaults to Flott::Parser::HTML_ESCAPE.
|
219
|
+
def initialize(output = STDOUT, escape = Flott::Parser::HTML_ESCAPE)
|
220
|
+
@__output__ = output
|
221
|
+
@__escape__ = escape
|
222
|
+
end
|
223
|
+
|
224
|
+
# The output object for this Environment object. It should respond to the
|
225
|
+
# #<< method of appending strings.
|
226
|
+
def output
|
227
|
+
@__output__
|
228
|
+
end
|
229
|
+
|
230
|
+
# Sets the output object for this Environment object, to _output_. It
|
231
|
+
# should respond to the #<< method of appending strings.
|
232
|
+
def output=(output)
|
233
|
+
@__output__ = output
|
234
|
+
end
|
235
|
+
|
236
|
+
# The escape object for this Environment object.
|
237
|
+
attr_accessor :escape
|
238
|
+
|
239
|
+
# If the currently evaluated Template originated from a Flott::Cache this
|
240
|
+
# method returns it, otherwise nil is returned.
|
241
|
+
attr_accessor :page_cache
|
242
|
+
|
243
|
+
# The template that was evaluated in this environment last.
|
244
|
+
attr_accessor :template
|
245
|
+
|
246
|
+
# Returns the root directory of this environment, it should be
|
247
|
+
# constant during the whole evaluation.
|
248
|
+
def rootdir
|
249
|
+
@__rootdir__
|
250
|
+
end
|
251
|
+
|
252
|
+
# Returns the current work directory of this environment. This
|
253
|
+
# value changes during evaluation of a template.
|
254
|
+
def workdir
|
255
|
+
@__workdir__ or raise EvalError, "workdir was undefined"
|
256
|
+
end
|
257
|
+
|
258
|
+
# Updates the instance variables of this environment with values from
|
259
|
+
# _hash_.
|
260
|
+
def update(hash)
|
261
|
+
hash.each { |name, value| self[name] = value }
|
262
|
+
self
|
263
|
+
end
|
264
|
+
|
265
|
+
# Returns the instance variable _name_. The leading '@' can be omitted in
|
266
|
+
# _name_.
|
267
|
+
def [](name)
|
268
|
+
name = name.to_s
|
269
|
+
name = "@#{name}" unless name[0] == ?@
|
270
|
+
instance_variable_get name
|
271
|
+
end
|
272
|
+
|
273
|
+
# Sets the instance variable _name_ to _value_. The leading '@' can be
|
274
|
+
# omitted in _name_.
|
275
|
+
def []=(name, value)
|
276
|
+
name = name.to_s
|
277
|
+
name = "@#{name}" unless name[0] == ?@
|
278
|
+
instance_variable_set name, value
|
279
|
+
end
|
280
|
+
|
281
|
+
# Creates a function (actually, a singleton method) _id_ from the block
|
282
|
+
# _block_ on this object, that can be called later in the template:
|
283
|
+
# [function :fac do |n|
|
284
|
+
# if n < 2
|
285
|
+
# 1
|
286
|
+
# else
|
287
|
+
# n * fac(n - 1)
|
288
|
+
# end
|
289
|
+
# end]
|
290
|
+
# fac(10) = [=fac(10)]
|
291
|
+
def function(id, opts = {}, &block)
|
292
|
+
sc = class << self; self; end
|
293
|
+
if opts[:memoize]
|
294
|
+
cache = {}
|
295
|
+
sc.instance_eval do
|
296
|
+
define_method(id) do |*args|
|
297
|
+
if cache.key?(args)
|
298
|
+
cache[args]
|
299
|
+
else
|
300
|
+
cache[args] = block[args]
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
else
|
305
|
+
sc.instance_eval { define_method(id, &block) }
|
306
|
+
end
|
307
|
+
nil
|
308
|
+
end
|
309
|
+
|
310
|
+
alias fun function
|
311
|
+
|
312
|
+
# Memoize method with id _id_, if called.
|
313
|
+
def memoize(id)
|
314
|
+
cache = {}
|
315
|
+
old_method = method(id)
|
316
|
+
sc = class << self; self; end
|
317
|
+
sc.send(:define_method, id) do |*args|
|
318
|
+
if cache.key?(args)
|
319
|
+
cache[args]
|
320
|
+
else
|
321
|
+
cache[args] = old_method.call(*args)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
private
|
327
|
+
|
328
|
+
include Flott::FilenameMixin
|
329
|
+
|
330
|
+
# Dynamically Include the template _filename_ into the current template,
|
331
|
+
# that is, at run-time.
|
332
|
+
def include(filename)
|
333
|
+
check_secure_path(filename)
|
334
|
+
if page_cache
|
335
|
+
page_cache.get(interpret_filename_as_page(filename)).evaluate(self.dup)
|
336
|
+
else
|
337
|
+
filename = interpret_filename(filename)
|
338
|
+
source = File.read(filename)
|
339
|
+
Flott::Parser.new(source, workdir).evaluate(self.dup)
|
340
|
+
end
|
341
|
+
rescue # TODO logging??
|
342
|
+
print "[dynamic include of '#{filename}' failed]"
|
343
|
+
end
|
344
|
+
|
345
|
+
# Like Kernel#p but with escaping.
|
346
|
+
def p(*objects)
|
347
|
+
for o in objects
|
348
|
+
string = @__escape__.call(o.inspect)
|
349
|
+
@__output__ << string
|
350
|
+
@__output__ << "\n" unless string[-1] == ?\n
|
351
|
+
end
|
352
|
+
nil
|
353
|
+
end
|
354
|
+
|
355
|
+
# Like Kernel#p without any escaping.
|
356
|
+
def p!(*objects)
|
357
|
+
for o in objects
|
358
|
+
string = o.inspect
|
359
|
+
@__output__ << string
|
360
|
+
@__output__ << "\n" unless string[-1] == ?\n
|
361
|
+
end
|
362
|
+
nil
|
363
|
+
end
|
364
|
+
|
365
|
+
# Like Kernel#pp but with escaping.
|
366
|
+
def pp(*objects)
|
367
|
+
require 'pp'
|
368
|
+
for o in objects
|
369
|
+
string = ''
|
370
|
+
PP.pp(o, string)
|
371
|
+
@__output__ << @__escape__.call(string)
|
372
|
+
@__output__ << $/ unless string =~ /\r?#$/\Z/
|
373
|
+
end
|
374
|
+
nil
|
375
|
+
end
|
376
|
+
|
377
|
+
# Like Kernel#pp without any escaping.
|
378
|
+
def pp!(*objects)
|
379
|
+
require 'pp'
|
380
|
+
for o in objects
|
381
|
+
string = ''
|
382
|
+
PP.pp(o, string)
|
383
|
+
@__output__ << string
|
384
|
+
@__output__ << $/ unless string =~ /\r?#$/\Z/
|
385
|
+
end
|
386
|
+
nil
|
387
|
+
end
|
388
|
+
|
389
|
+
# The usual IO#puts call without any escaping.
|
390
|
+
def puts!(*objects)
|
391
|
+
for o in objects.flatten
|
392
|
+
string = o.to_s
|
393
|
+
@__output__ << string
|
394
|
+
@__output__ << $/ unless string =~ /\r?#$/\Z/
|
395
|
+
end
|
396
|
+
nil
|
397
|
+
end
|
398
|
+
|
399
|
+
# Like a call to IO#puts to print _objects_ after escaping all their #to_s
|
400
|
+
# call results.
|
401
|
+
def puts(*objects)
|
402
|
+
for o in objects.flatten
|
403
|
+
string = @__escape__.call(o)
|
404
|
+
@__output__ << string
|
405
|
+
@__output__ << $/ unless string =~ /\r?#$/\Z/
|
406
|
+
end
|
407
|
+
nil
|
408
|
+
end
|
409
|
+
|
410
|
+
def putc!(object)
|
411
|
+
if object.is_a? Numeric
|
412
|
+
@__output__ << object.chr
|
413
|
+
else
|
414
|
+
@__output__ << object.to_s[0, 1]
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
def putc(object)
|
419
|
+
if object.is_a? Numeric
|
420
|
+
@__output__ << @__escape__.call(object.chr)
|
421
|
+
else
|
422
|
+
@__output__ << @__escape__.call(object[0, 1])
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
# Like the usual IO#printf call without any escaping.
|
427
|
+
def printf!(format, *args)
|
428
|
+
@__output__ << sprintf(format, *args)
|
429
|
+
nil
|
430
|
+
end
|
431
|
+
|
432
|
+
# Like a call to IO#printf, but with escaping the string before printing.
|
433
|
+
def printf(format, *args)
|
434
|
+
@__output__ << @__escape__.call(sprintf(format, *args))
|
435
|
+
nil
|
436
|
+
end
|
437
|
+
|
438
|
+
# Like the usual IO#print call without any escaping.
|
439
|
+
def print!(*objects)
|
440
|
+
for o in objects
|
441
|
+
@__output__ << o.to_s
|
442
|
+
end
|
443
|
+
nil
|
444
|
+
end
|
445
|
+
|
446
|
+
# Call to IO#print to print _objects_ after escaping all their #to_s
|
447
|
+
# call results.
|
448
|
+
def print(*objects)
|
449
|
+
for o in objects
|
450
|
+
@__output__ << @__escape__.call(o)
|
451
|
+
end
|
452
|
+
nil
|
453
|
+
end
|
454
|
+
|
455
|
+
# Like the usual IO#write call without any escaping.
|
456
|
+
def write!(object)
|
457
|
+
string = object.to_s
|
458
|
+
@__output__ << string
|
459
|
+
string.size
|
460
|
+
end
|
461
|
+
|
462
|
+
# Like a call to IO#write after escaping the argument _object_'s #to_s call
|
463
|
+
# result.
|
464
|
+
def write(object)
|
465
|
+
string = @__escape__.call(object)
|
466
|
+
@__output__ << string
|
467
|
+
string.size
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
# This class can instantiate environment objects to evaluate Flott Templates
|
472
|
+
# in.
|
473
|
+
class Environment
|
474
|
+
include EnvironmentMixin
|
475
|
+
end
|
476
|
+
|
477
|
+
# Class for compiled Template objects, that can later be evaluated in a
|
478
|
+
# Flott::Environment.
|
479
|
+
class Template < Proc
|
480
|
+
# Sets up a Template instance.
|
481
|
+
def initialize
|
482
|
+
super
|
483
|
+
@pathes = []
|
484
|
+
end
|
485
|
+
|
486
|
+
# The pathes of the template and all included sub-templates.
|
487
|
+
attr_accessor :pathes
|
488
|
+
|
489
|
+
# The template's source code after compilation.
|
490
|
+
attr_accessor :source
|
491
|
+
|
492
|
+
# Returns the Flott::Cache this Template originated from or nil, if no
|
493
|
+
# cache was used.
|
494
|
+
attr_accessor :page_cache
|
495
|
+
|
496
|
+
# The environment this template was evaluated in during the last evaluate
|
497
|
+
# call. Returns nil if it wasn't evaluated yet.
|
498
|
+
attr_accessor :environment
|
499
|
+
|
500
|
+
# Returns the newest _mtime_ of all the involved #pathes.
|
501
|
+
def mtime
|
502
|
+
@pathes.map { |path| File.stat(path).mtime }.max
|
503
|
+
end
|
504
|
+
|
505
|
+
# Evaluates this Template Object in the Environment _environment_ (first
|
506
|
+
# argument).
|
507
|
+
def call(environment = Flott::Environment.new, *)
|
508
|
+
@environment = environment
|
509
|
+
@environment.template = self
|
510
|
+
@environment.page_cache = page_cache
|
511
|
+
result = super
|
512
|
+
attach_environment_to_output
|
513
|
+
result
|
514
|
+
rescue SyntaxError => e
|
515
|
+
raise CallError.wrap(e)
|
516
|
+
end
|
517
|
+
|
518
|
+
alias evaluate call
|
519
|
+
|
520
|
+
private
|
521
|
+
|
522
|
+
def attach_environment_to_output
|
523
|
+
o = @environment.output
|
524
|
+
unless o.respond_to?(:environment=)
|
525
|
+
class << o; self ; end.class_eval do
|
526
|
+
attr_accessor :environment
|
527
|
+
|
528
|
+
def template
|
529
|
+
environment.template
|
530
|
+
end
|
531
|
+
end
|
532
|
+
end
|
533
|
+
o.environment = @environment
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
# The Flott::Parser class creates parser objects, that can be used to compile
|
538
|
+
# Flott template documents or files to Flott::Template instances.
|
539
|
+
class Parser
|
540
|
+
# This class encapsulates the state, that is shared by all parsers that
|
541
|
+
# were activated during the parse phase.
|
542
|
+
class State
|
543
|
+
# Creates a new Flott::Parser::State instance to hold the current parser
|
544
|
+
# state.
|
545
|
+
def initialize
|
546
|
+
@opened = 0
|
547
|
+
@last_open = nil
|
548
|
+
@text = []
|
549
|
+
@compiled = []
|
550
|
+
@pathes = []
|
551
|
+
@directories = []
|
552
|
+
@skip_cr = false
|
553
|
+
end
|
554
|
+
|
555
|
+
# The number of current open (unescaped) brackets.
|
556
|
+
attr_accessor :opened
|
557
|
+
|
558
|
+
# The type of the last opened bracket.
|
559
|
+
attr_accessor :last_open
|
560
|
+
|
561
|
+
# An array of all scanned text fragments.
|
562
|
+
attr_reader :text
|
563
|
+
|
564
|
+
# An array of the already compiled Ruby code fragments.
|
565
|
+
attr_reader :compiled
|
566
|
+
|
567
|
+
# An array of involved template file pathes, that is, also the statically
|
568
|
+
# included template file pathes.
|
569
|
+
attr_reader :pathes
|
570
|
+
|
571
|
+
# A stack array, that contains the work directories of all active
|
572
|
+
# templates (during parsing).
|
573
|
+
attr_reader :directories
|
574
|
+
|
575
|
+
attr_accessor :skip_cr
|
576
|
+
|
577
|
+
# Transform text mode parts to compiled code parts.
|
578
|
+
def text2compiled(dont_sub = true)
|
579
|
+
return if text.empty?
|
580
|
+
text.last.sub!(/[\t ]+$/, '') unless dont_sub
|
581
|
+
compiled << '@__output__<<%q['
|
582
|
+
compiled.concat(text)
|
583
|
+
compiled << "]\n"
|
584
|
+
text.clear
|
585
|
+
end
|
586
|
+
|
587
|
+
# Return the whole compiled code as a string.
|
588
|
+
def compiled_string
|
589
|
+
compiled.join.untaint
|
590
|
+
end
|
591
|
+
|
592
|
+
# Pushs the workdir of _parser_ onto the _directories_ stack.
|
593
|
+
def push_workdir(parser)
|
594
|
+
workdir = parser.workdir
|
595
|
+
compiled << "@__workdir__ = '#{workdir}'\n"
|
596
|
+
directories << workdir
|
597
|
+
self
|
598
|
+
end
|
599
|
+
|
600
|
+
# Returns the top directory from the _directories_ stack.
|
601
|
+
def top_workdir
|
602
|
+
directories.last
|
603
|
+
end
|
604
|
+
|
605
|
+
# Pops the top directory from the _directories_ stack.
|
606
|
+
def pop_workdir
|
607
|
+
directories.empty? and raise CompileError, "state directories were empty"
|
608
|
+
directories.pop
|
609
|
+
compiled << "@__workdir__ = '#{top_workdir}'\n"
|
610
|
+
self
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
include Flott::FilenameMixin
|
615
|
+
|
616
|
+
# Regexp matching an escaped open square bracket like '\['.
|
617
|
+
ESCOPEN = /\\\[/
|
618
|
+
|
619
|
+
# [^filename]
|
620
|
+
INCOPEN = /\[\^\s*([^\]]+?)\s*(-)?\]/
|
621
|
+
# TODO allow ] in filenames?
|
622
|
+
|
623
|
+
# [="foo<bar"] "foo<bar"
|
624
|
+
PRIOPEN = /\[=\s*/
|
625
|
+
|
626
|
+
# [!"foo<bar"] "foo<bar"
|
627
|
+
RAWOPEN = /\[!\s*/
|
628
|
+
|
629
|
+
# [#comment]
|
630
|
+
COMOPEN = /\[#\s*/
|
631
|
+
|
632
|
+
# Open succeded by minus deletes previous whitespaces until start of line.
|
633
|
+
MINPRIOPEN = /\[-=/
|
634
|
+
|
635
|
+
# Open succeded by minus deletes previous whitespaces until start of line.
|
636
|
+
MINRAWOPEN = /\[-!/
|
637
|
+
|
638
|
+
# Open succeded by minus deletes previous whitespaces until start of line.
|
639
|
+
MINCOMOPEN = /\[-#/
|
640
|
+
|
641
|
+
# Open succeded by minus deletes previous whitespaces until start of line.
|
642
|
+
MINOPEN = /\[-/
|
643
|
+
|
644
|
+
# Regexp matching an open square bracket like '['.
|
645
|
+
OPEN = /\[/
|
646
|
+
|
647
|
+
# Close preceded by minus deletes next CRLF.
|
648
|
+
MINCLOSE = /-\]/
|
649
|
+
|
650
|
+
# Regexp matching an open square bracket like ']'.
|
651
|
+
CLOSE = /\]/
|
652
|
+
|
653
|
+
# Regexp matching an escaped closed square bracket like '\]'.
|
654
|
+
ESCCLOSE = /\\\]/
|
655
|
+
|
656
|
+
# Regexp matching general text, that doesn't need special handling.
|
657
|
+
TEXT = /([^-\\\]\[\{\}]+|-(?!\]))/
|
658
|
+
|
659
|
+
# Regexp matching the escape character '\'.
|
660
|
+
ESC = /\\/
|
661
|
+
|
662
|
+
# Regexp matching the escape character at least once.
|
663
|
+
ESC_CLOSURE = /\\(\\*)/
|
664
|
+
|
665
|
+
# Regexp matching curly brackets '{' or '}'.
|
666
|
+
CURLY = /[{}]/
|
667
|
+
|
668
|
+
# Regexp matching open curly bracket like '{'.
|
669
|
+
CURLYOPEN = /\{/
|
670
|
+
|
671
|
+
# Regexp matching open curly bracket like '}'.
|
672
|
+
CURLYCLOSE= /\}/
|
673
|
+
|
674
|
+
# Creates a Parser object. _workdir_ is the directory, on which relative
|
675
|
+
# template inclusions are based. _rootdir_ is the directory. On which
|
676
|
+
# absolute template inclusions (starting with '/') are based. _filename_ is
|
677
|
+
# the filename of this template (if any), which is important to track
|
678
|
+
# changes in the template file to trigger a reloading.
|
679
|
+
def initialize(source, workdir = nil, rootdir = nil, filename = nil)
|
680
|
+
if workdir
|
681
|
+
check_secure_path(workdir)
|
682
|
+
@workdir = File.expand_path(workdir)
|
683
|
+
else
|
684
|
+
@workdir = Dir.pwd
|
685
|
+
end
|
686
|
+
if rootdir
|
687
|
+
check_secure_path(rootdir)
|
688
|
+
@rootdir = File.expand_path(rootdir)
|
689
|
+
else
|
690
|
+
@rootdir = @workdir
|
691
|
+
end
|
692
|
+
sub_path?(@workdir, @rootdir) or
|
693
|
+
raise SecurityViolation, "#{@workdir} isn't a sub path of '#{@rootdir}'"
|
694
|
+
if filename
|
695
|
+
check_secure_path(filename)
|
696
|
+
@filename = File.expand_path(filename)
|
697
|
+
sub_path?(@filename, @workdir) or
|
698
|
+
raise SecurityViolation, "#{@filename} isn't a sub path of '#{@workdir}"
|
699
|
+
end
|
700
|
+
@ruby = RubyMode.new(self)
|
701
|
+
@text = TextMode.new(self)
|
702
|
+
@current_mode = @text
|
703
|
+
@scanner = StringScanner.new(source)
|
704
|
+
end
|
705
|
+
|
706
|
+
# Creates a Parser object from _filename_, the _workdir_ and _rootdir
|
707
|
+
# attributes are set to the directory the file is located in.
|
708
|
+
def self.from_filename(filename)
|
709
|
+
filename = File.expand_path(filename)
|
710
|
+
workdir = File.dirname(filename)
|
711
|
+
source = File.read(filename)
|
712
|
+
new(source, workdir, workdir, filename)
|
713
|
+
end
|
714
|
+
|
715
|
+
# The StringScanner instance of this Parser object.
|
716
|
+
attr_reader :scanner
|
717
|
+
|
718
|
+
# Returns the shared state between all parsers that are parsing the current
|
719
|
+
# template and the included templates.
|
720
|
+
def state
|
721
|
+
@state ||= parent.state
|
722
|
+
end
|
723
|
+
|
724
|
+
# Returns nil if this is the root parser, or a reference to the parent
|
725
|
+
# parser of this parser.
|
726
|
+
attr_accessor :parent
|
727
|
+
|
728
|
+
# Compute the rootdir of this parser (these parsers). Cache the result and
|
729
|
+
# return it.
|
730
|
+
attr_reader :rootdir
|
731
|
+
|
732
|
+
# Returns the current work directory of this parser.
|
733
|
+
attr_accessor :workdir
|
734
|
+
|
735
|
+
# Change parsing mode to TextMode.
|
736
|
+
def goto_text_mode
|
737
|
+
@current_mode = @text
|
738
|
+
end
|
739
|
+
|
740
|
+
# Change parsing mode to RubyMode.
|
741
|
+
def goto_ruby_mode
|
742
|
+
@current_mode = @ruby
|
743
|
+
end
|
744
|
+
|
745
|
+
# Compiles the template source and returns a Proc object to be executed
|
746
|
+
# later. This method raises a ParserError exception if source is not
|
747
|
+
# _Parser#wellformed?_.
|
748
|
+
def compile
|
749
|
+
@state = State.new
|
750
|
+
state.compiled << [
|
751
|
+
"::Flott::Template.new \{ |__env__| __env__.instance_eval %q{\n",
|
752
|
+
"@__rootdir__ = '#{rootdir}'\n",
|
753
|
+
]
|
754
|
+
state.pathes << @filename if defined?(@filename)
|
755
|
+
compile_inner
|
756
|
+
state.compiled << "\n}\n}"
|
757
|
+
source = state.compiled_string
|
758
|
+
template = eval(source, nil, '(flott)')
|
759
|
+
template.pathes = state.pathes
|
760
|
+
template.source = source
|
761
|
+
template
|
762
|
+
rescue SyntaxError => e
|
763
|
+
raise EvalError.wrap(e)
|
764
|
+
end
|
765
|
+
|
766
|
+
# Include the template file with _filename_ at the current place.
|
767
|
+
def include_template(filename)
|
768
|
+
filename = interpret_filename(filename)
|
769
|
+
if File.readable?(filename)
|
770
|
+
state.text2compiled
|
771
|
+
state.pathes << filename
|
772
|
+
source = File.read(filename)
|
773
|
+
workdir = File.dirname(filename)
|
774
|
+
fork(source, workdir, rootdir, filename)
|
775
|
+
else
|
776
|
+
raise CompileError, "Cannot open #{filename} for inclusion!"
|
777
|
+
end
|
778
|
+
end
|
779
|
+
|
780
|
+
# Fork another Parser to handle an included template.
|
781
|
+
def fork(source, workdir, rootdir, filename)
|
782
|
+
parser = self.class.new(source, workdir, rootdir, filename)
|
783
|
+
parser.parent = self
|
784
|
+
parser.compile_inner(@workdir != workdir)
|
785
|
+
end
|
786
|
+
|
787
|
+
# The base parsing mode.
|
788
|
+
class Mode
|
789
|
+
# Creates a parsing mode for _parser_.
|
790
|
+
def initialize(parser)
|
791
|
+
@parser = parser
|
792
|
+
end
|
793
|
+
|
794
|
+
# The parser this mode belongs to.
|
795
|
+
attr_reader :parser
|
796
|
+
|
797
|
+
# The parsing mode uses this StringScanner instance for it's job, its the
|
798
|
+
# StringScanner of the current _parser_.
|
799
|
+
def scanner
|
800
|
+
@parser.scanner
|
801
|
+
end
|
802
|
+
|
803
|
+
# A shortcut to reach the shared state of all the parsers involved in
|
804
|
+
# parsing the current template.
|
805
|
+
def state
|
806
|
+
@parser.state
|
807
|
+
end
|
808
|
+
end
|
809
|
+
|
810
|
+
# This Mode class handles the Parser's TextMode state.
|
811
|
+
class TextMode < Mode
|
812
|
+
# Scan the template in TextMode.
|
813
|
+
def scan
|
814
|
+
case
|
815
|
+
when state.skip_cr && scanner.skip(/[\t ]*\r?\n/)
|
816
|
+
state.skip_cr = false
|
817
|
+
when scanner.scan(ESCOPEN)
|
818
|
+
state.text << '\\['
|
819
|
+
when scanner.scan(CURLYOPEN)
|
820
|
+
state.text << '\\{'
|
821
|
+
when scanner.scan(INCOPEN)
|
822
|
+
state.last_open = :INCOPEN
|
823
|
+
parser.include_template(scanner[1])
|
824
|
+
state.skip_cr = scanner[2]
|
825
|
+
when scanner.scan(PRIOPEN)
|
826
|
+
state.last_open = :PRIOPEN
|
827
|
+
parser.goto_ruby_mode
|
828
|
+
state.text2compiled
|
829
|
+
state.compiled << "@__output__<<@__escape__.call((\n"
|
830
|
+
when scanner.scan(RAWOPEN)
|
831
|
+
state.last_open = :RAWOPEN
|
832
|
+
parser.goto_ruby_mode
|
833
|
+
state.text2compiled
|
834
|
+
state.compiled << "@__output__<<((\n"
|
835
|
+
when scanner.scan(COMOPEN)
|
836
|
+
state.last_open = :COMOPEN
|
837
|
+
parser.goto_ruby_mode
|
838
|
+
state.text2compiled
|
839
|
+
state.compiled << "\n=begin\n"
|
840
|
+
when scanner.scan(MINPRIOPEN)
|
841
|
+
state.last_open = :PRIOPEN
|
842
|
+
if t = state.text.last
|
843
|
+
t.sub!(/[\t ]*\Z/, '')
|
844
|
+
end
|
845
|
+
parser.goto_ruby_mode
|
846
|
+
state.text2compiled
|
847
|
+
state.compiled << "@__output__<<@__escape__.call((\n"
|
848
|
+
when scanner.scan(MINRAWOPEN)
|
849
|
+
state.last_open = :RAWOPEN
|
850
|
+
if t = state.text.last
|
851
|
+
t.sub!(/[\t ]*\Z/, '')
|
852
|
+
end
|
853
|
+
parser.goto_ruby_mode
|
854
|
+
state.text2compiled
|
855
|
+
state.compiled << "@__output__<<((\n"
|
856
|
+
when scanner.scan(MINCOMOPEN)
|
857
|
+
state.last_open = :COMOPEN
|
858
|
+
if t = state.text.last
|
859
|
+
t.sub!(/[\t ]*\Z/, '')
|
860
|
+
end
|
861
|
+
parser.goto_ruby_mode
|
862
|
+
state.text2compiled
|
863
|
+
state.compiled << "\n=begin\n"
|
864
|
+
when scanner.scan(MINOPEN)
|
865
|
+
state.last_open = :OPEN
|
866
|
+
if t = state.text.last
|
867
|
+
t.sub!(/[\t ]*\Z/, '')
|
868
|
+
end
|
869
|
+
parser.goto_ruby_mode
|
870
|
+
state.text2compiled(false)
|
871
|
+
when scanner.scan(OPEN)
|
872
|
+
state.last_open = :OPEN
|
873
|
+
parser.goto_ruby_mode
|
874
|
+
state.text2compiled
|
875
|
+
when scanner.scan(CLOSE)
|
876
|
+
state.text << '\\' << scanner[0]
|
877
|
+
when scanner.scan(TEXT)
|
878
|
+
state.text << scanner[0]
|
879
|
+
when scanner.scan(CURLYCLOSE)
|
880
|
+
state.text << '\\' << scanner[0]
|
881
|
+
when scanner.scan(ESC)
|
882
|
+
state.text << '\\\\' << scanner[0]
|
883
|
+
else
|
884
|
+
raise CompileError, "unknown tokens '#{scanner.peek(40)}'"
|
885
|
+
end
|
886
|
+
end
|
887
|
+
end
|
888
|
+
|
889
|
+
# This Mode class handles the Parser's RubyMode state.
|
890
|
+
class RubyMode < Mode
|
891
|
+
# Scan the template in RubyMode.
|
892
|
+
def scan
|
893
|
+
case
|
894
|
+
when state.opened == 0 && scanner.scan(MINCLOSE)
|
895
|
+
state.skip_cr = true
|
896
|
+
case state.last_open
|
897
|
+
when :PRIOPEN
|
898
|
+
state.compiled << "\n))\n"
|
899
|
+
when :RAWOPEN
|
900
|
+
state.compiled << "\n).to_s)\n"
|
901
|
+
when :COMOPEN
|
902
|
+
state.compiled << "\n=end\n"
|
903
|
+
else
|
904
|
+
state.compiled << "\n"
|
905
|
+
end
|
906
|
+
parser.goto_text_mode
|
907
|
+
state.last_open = nil
|
908
|
+
when state.opened == 0 && scanner.scan(CLOSE)
|
909
|
+
parser.goto_text_mode
|
910
|
+
case state.last_open
|
911
|
+
when :PRIOPEN
|
912
|
+
state.compiled << "\n))\n"
|
913
|
+
when :RAWOPEN
|
914
|
+
state.compiled << "\n).to_s)\n"
|
915
|
+
when :COMOPEN
|
916
|
+
state.compiled << "\n=end\n"
|
917
|
+
else
|
918
|
+
state.compiled << "\n"
|
919
|
+
end
|
920
|
+
state.last_open = nil
|
921
|
+
when scanner.scan(ESCCLOSE)
|
922
|
+
state.compiled << scanner[0]
|
923
|
+
when scanner.scan(CLOSE) && state.opened != 0,
|
924
|
+
scanner.scan(MINCLOSE) && state.opened != 0
|
925
|
+
state.opened -= 1
|
926
|
+
state.compiled << scanner[0]
|
927
|
+
when scanner.scan(ESCOPEN)
|
928
|
+
state.compiled << scanner[0]
|
929
|
+
when scanner.scan(OPEN)
|
930
|
+
state.opened += 1
|
931
|
+
state.compiled << scanner[0]
|
932
|
+
when scanner.scan(TEXT)
|
933
|
+
state.compiled << scanner[0]
|
934
|
+
when scanner.scan(CURLY)
|
935
|
+
state.compiled << '\\' << scanner[0]
|
936
|
+
when scanner.scan(ESC_CLOSURE)
|
937
|
+
s = scanner[0]
|
938
|
+
ssize = s.size
|
939
|
+
if ssize % 2 == 0
|
940
|
+
state.compiled << s * 2
|
941
|
+
else
|
942
|
+
state.compiled << s[0, ssize - 1] * 2
|
943
|
+
state.compiled << eval(%'"\\#{scanner.scan(/./)}"')
|
944
|
+
end
|
945
|
+
else
|
946
|
+
raise CompileError, "unknown tokens '#{scanner.peek(40)}'"
|
947
|
+
end
|
948
|
+
end
|
949
|
+
end
|
950
|
+
|
951
|
+
def debug_output
|
952
|
+
warn "%-20s:%s\n" % [ :mode, @current_mode.class ]
|
953
|
+
warn "%-20s:%s\n" % [ :last_open, state.last_open ]
|
954
|
+
warn "%-20s:%s\n" % [ :opened, state.opened ]
|
955
|
+
warn "%-20s:%s\n" % [ :directories, state.directories * ',' ]
|
956
|
+
warn "%-20s:%s\n" % [ :peek, scanner.peek(60) ]
|
957
|
+
warn "%-20s:%s\n" % [ :compiled, state.compiled_string ]
|
958
|
+
end
|
959
|
+
private :debug_output
|
960
|
+
|
961
|
+
def compile_inner(workdir_changed = true) # :nodoc:
|
962
|
+
scanner.reset
|
963
|
+
workdir_changed and state.push_workdir(self)
|
964
|
+
until scanner.eos?
|
965
|
+
Flott.debug && debug_output
|
966
|
+
@current_mode.scan
|
967
|
+
end
|
968
|
+
Flott.debug && debug_output
|
969
|
+
state.text2compiled
|
970
|
+
workdir_changed and state.pop_workdir
|
971
|
+
Flott.debug && debug_output
|
972
|
+
end
|
973
|
+
protected :compile_inner
|
974
|
+
|
975
|
+
# First compiles the source template and evaluates it in the environment
|
976
|
+
# env. If no environment is given, a newly created environment is used.
|
977
|
+
def evaluate(env = Environment.new, &block)
|
978
|
+
env.instance_eval(&block) if block
|
979
|
+
compile.evaluate(env)
|
980
|
+
self
|
981
|
+
end
|
982
|
+
|
983
|
+
# :stopdoc:
|
984
|
+
ESCAPE_MAP = Hash.new { |h, c| raise "unknown character '#{c}'" }
|
985
|
+
ESCAPE_MAP.update({
|
986
|
+
?& => '&',
|
987
|
+
?< => '<',
|
988
|
+
?> => '>',
|
989
|
+
?" => '"',
|
990
|
+
?' => '''
|
991
|
+
})
|
992
|
+
|
993
|
+
# This Proc object escapes _string_, by substituting &<>"' with their
|
994
|
+
# respective html entities, and returns the result.
|
995
|
+
HTML_ESCAPE = lambda do |string|
|
996
|
+
if string.respond_to?(:to_str)
|
997
|
+
string.to_str.gsub(/[&<>"']/) { |c| ESCAPE_MAP[c[0]] }
|
998
|
+
else
|
999
|
+
string = string.to_s
|
1000
|
+
string.gsub!(/[&<>"']/) { |c| ESCAPE_MAP[c[0]] }
|
1001
|
+
string
|
1002
|
+
end
|
1003
|
+
end
|
1004
|
+
# :startdoc:
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
if $0 == __FILE__
|
1009
|
+
Flott.debug = $DEBUG
|
1010
|
+
parser = if filename = ARGV.shift
|
1011
|
+
Flott::Parser.from_filename(filename)
|
1012
|
+
else
|
1013
|
+
Flott::Parser.new(STDIN.read)
|
1014
|
+
end
|
1015
|
+
parser.evaluate
|
1016
|
+
end
|