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/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&lt;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
+ ?& => '&amp;',
987
+ ?< => '&lt;',
988
+ ?> => '&gt;',
989
+ ?" => '&quot;',
990
+ ?' => '&#039;'
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