flott 1.0.0

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