flott 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|