frank 0.2.6 → 0.3.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/Featurelist +6 -0
  2. data/README.md +44 -32
  3. data/Rakefile +3 -3
  4. data/frank.gemspec +51 -45
  5. data/lib/frank.rb +1 -0
  6. data/lib/frank/base.rb +144 -94
  7. data/lib/frank/lorem.rb +77 -36
  8. data/lib/frank/middleware/imager.rb +43 -0
  9. data/lib/frank/middleware/refresh.rb +42 -0
  10. data/lib/frank/middleware/statik.rb +41 -0
  11. data/lib/frank/output.rb +25 -37
  12. data/lib/frank/rescue.rb +13 -6
  13. data/lib/frank/template_helpers.rb +34 -3
  14. data/lib/frank/templates/404.haml +1 -0
  15. data/lib/frank/templates/500.haml +2 -0
  16. data/lib/frank/tilt.rb +389 -141
  17. data/lib/template/{dynamic/layout.haml → layouts/default.haml} +0 -0
  18. data/lib/template/settings.yml +15 -28
  19. data/spec/base_spec.rb +88 -0
  20. data/{test → spec}/helper.rb +0 -7
  21. data/spec/output_spec.rb +220 -0
  22. data/spec/render_spec.rb +106 -0
  23. data/spec/template/dynamic/500.haml +1 -0
  24. data/{test → spec}/template/dynamic/_partial.haml +0 -0
  25. data/{test → spec}/template/dynamic/builder.builder +0 -0
  26. data/{test → spec}/template/dynamic/coffee.coffee +0 -0
  27. data/{test → spec}/template/dynamic/erb.erb +0 -0
  28. data/{test → spec}/template/dynamic/helper_test.haml +0 -0
  29. data/spec/template/dynamic/index.haml +5 -0
  30. data/spec/template/dynamic/layout2_test.haml +4 -0
  31. data/{test → spec}/template/dynamic/liquid.liquid +0 -0
  32. data/spec/template/dynamic/lorem_test.haml +7 -0
  33. data/{test → spec}/template/dynamic/markdown.md +0 -0
  34. data/spec/template/dynamic/markdown_in_haml.md +4 -0
  35. data/{test → spec}/template/dynamic/mustache.mustache +0 -0
  36. data/spec/template/dynamic/nested/child.haml +1 -0
  37. data/spec/template/dynamic/nested/deeper/deep.haml +1 -0
  38. data/spec/template/dynamic/no_layout.haml +4 -0
  39. data/spec/template/dynamic/partial_test.haml +2 -0
  40. data/{test → spec}/template/dynamic/redcloth.textile +0 -0
  41. data/spec/template/dynamic/refresh.haml +1 -0
  42. data/{test → spec}/template/dynamic/sass.sass +0 -0
  43. data/{test → spec}/template/helpers.rb +0 -0
  44. data/spec/template/layouts/default.haml +3 -0
  45. data/{test/template/dynamic → spec/template/layouts/explicit}/layout2.haml +0 -0
  46. data/spec/template/layouts/nested/default.haml +2 -0
  47. data/spec/template/settings.yml +45 -0
  48. data/{test/template/static → spec/template/static/files}/static.html +0 -0
  49. data/spec/template_helpers_spec.rb +78 -0
  50. metadata +57 -49
  51. data/lib/frank/imager.rb +0 -39
  52. data/lib/frank/statik.rb +0 -39
  53. data/test/suite.rb +0 -4
  54. data/test/template/dynamic/index.haml +0 -2
  55. data/test/template/dynamic/layout.haml +0 -2
  56. data/test/template/dynamic/layout2_test.haml +0 -1
  57. data/test/template/dynamic/layout_test.haml +0 -1
  58. data/test/template/dynamic/lorem_test.haml +0 -7
  59. data/test/template/dynamic/partial_test.haml +0 -2
  60. data/test/template/settings.yml +0 -62
  61. data/test/test_base.rb +0 -81
  62. data/test/test_helpers.rb +0 -71
  63. data/test/test_output.rb +0 -160
  64. data/test/test_render.rb +0 -89
@@ -9,6 +9,7 @@
9
9
  h1 { margin: 24px -30px 10px -3px; font: bold 48px Georgia; line-height:48px;}
10
10
  img { margin: 60px 0px 0px -22px; }
11
11
  tt { color: #4D9EEF; color:#999; font-family: Inconsolata, Monaco, monospace }
12
+ =refresh
12
13
  %body
13
14
  #wrapper
14
15
  %img{:src=>'/__frank__/frank-404.png'}
@@ -11,6 +11,8 @@
11
11
  tt, pre { color:#666; font-family: Inconsolata, Monaco, monospace; }
12
12
  pre { width:600px; padding-bottom:40px; }
13
13
  p.summary { font-size:18px; border-bottom:1px #ccc solid; margin-bottom:60px; padding-bottom:20px; }
14
+ =refresh
15
+
14
16
  %script{:type=>'text/javascript'}
15
17
  %body
16
18
  #wrapper
@@ -1,10 +1,11 @@
1
+ require 'digest/md5'
2
+
1
3
  module Tilt
2
- VERSION = '0.5'
4
+ VERSION = '0.9'
3
5
 
4
6
  @template_mappings = {}
5
7
 
6
- # Hash of template path pattern => template implementation
7
- # class mappings.
8
+ # Hash of template path pattern => template implementation class mappings.
8
9
  def self.mappings
9
10
  @template_mappings
10
11
  end
@@ -15,6 +16,11 @@ module Tilt
15
16
  mappings[ext.downcase] = template_class
16
17
  end
17
18
 
19
+ # Returns true when a template exists on an exact match of the provided file extension
20
+ def self.registered?(ext)
21
+ mappings.key?(ext.downcase)
22
+ end
23
+
18
24
  # Create a new template for the given file using the file's extension
19
25
  # to determine the the template mapping.
20
26
  def self.new(file, line=nil, options={}, &block)
@@ -25,28 +31,35 @@ module Tilt
25
31
  end
26
32
  end
27
33
 
28
- # Lookup a template class given for the given filename or file
34
+ # Lookup a template class for the given filename or file
29
35
  # extension. Return nil when no implementation is found.
30
36
  def self.[](file)
31
- if @template_mappings.key?(pattern = file.to_s.downcase)
32
- @template_mappings[pattern]
33
- elsif @template_mappings.key?(pattern = File.basename(pattern))
34
- @template_mappings[pattern]
35
- else
36
- while !pattern.empty?
37
- if @template_mappings.key?(pattern)
38
- return @template_mappings[pattern]
39
- else
40
- pattern = pattern.sub(/^[^.]*\.?/, '')
41
- end
42
- end
43
- nil
37
+ pattern = file.to_s.downcase
38
+ unless registered?(pattern)
39
+ pattern = File.basename(pattern)
40
+ pattern.sub!(/^[^.]*\.?/, '') until (pattern.empty? || registered?(pattern))
44
41
  end
42
+ @template_mappings[pattern]
45
43
  end
46
44
 
45
+ # Mixin allowing template compilation on scope objects.
46
+ #
47
+ # Including this module in scope objects passed to Template#render
48
+ # causes template source to be compiled to methods the first time they're
49
+ # used. This can yield significant (5x-10x) performance increases for
50
+ # templates that support it (ERB, Erubis, Builder).
51
+ #
52
+ # It's also possible (though not recommended) to include this module in
53
+ # Object to enable template compilation globally. The downside is that
54
+ # the template methods will polute the global namespace and could lead to
55
+ # unexpected behavior.
56
+ module CompileSite
57
+ def __tilt__
58
+ end
59
+ end
47
60
 
48
61
  # Base class for template implementations. Subclasses must implement
49
- # the #compile! method and one of the #evaluate or #template_source
62
+ # the #prepare method and one of the #evaluate or #template_source
50
63
  # methods.
51
64
  class Template
52
65
  # Template source; loaded from a file or given directly.
@@ -63,51 +76,60 @@ module Tilt
63
76
  # interface.
64
77
  attr_reader :options
65
78
 
79
+ # Used to determine if this class's initialize_engine method has
80
+ # been called yet.
81
+ @engine_initialized = false
82
+ class << self
83
+ attr_accessor :engine_initialized
84
+ alias engine_initialized? engine_initialized
85
+ end
86
+
66
87
  # Create a new template with the file, line, and options specified. By
67
- # default, template data is read from the file specified. When a block
68
- # is given, it should read template data and return as a String. When
69
- # file is nil, a block is required.
88
+ # default, template data is read from the file. When a block is given,
89
+ # it should read template data and return as a String. When file is nil,
90
+ # a block is required.
70
91
  #
71
- # The #initialize_engine method is called if this is the very first
72
- # time this template subclass has been initialized.
92
+ # All arguments are optional.
73
93
  def initialize(file=nil, line=1, options={}, &block)
74
- raise ArgumentError, "file or block required" if file.nil? && block.nil?
75
- options, line = line, 1 if line.is_a?(Hash)
76
- @file = file
77
- @line = line || 1
78
- @options = options || {}
79
- @reader = block || lambda { |t| File.read(file) }
80
-
81
- if !self.class.engine_initialized
94
+ @file, @line, @options = nil, 1, {}
95
+
96
+ [options, line, file].compact.each do |arg|
97
+ case
98
+ when arg.respond_to?(:to_str) ; @file = arg.to_str
99
+ when arg.respond_to?(:to_int) ; @line = arg.to_int
100
+ when arg.respond_to?(:to_hash) ; @options = arg.to_hash.dup
101
+ else raise TypeError
102
+ end
103
+ end
104
+
105
+ raise ArgumentError, "file or block required" if (@file || block).nil?
106
+
107
+ # call the initialize_engine method if this is the very first time
108
+ # an instance of this class has been created.
109
+ if !self.class.engine_initialized?
82
110
  initialize_engine
83
111
  self.class.engine_initialized = true
84
112
  end
85
- end
86
113
 
87
- # Called once and only once for each template subclass the first time
88
- # the template class is initialized. This should be used to require the
89
- # underlying template library and perform any initial setup.
90
- def initialize_engine
91
- end
92
- @engine_initialized = false
93
- class << self ; attr_accessor :engine_initialized ; end
114
+ # used to generate unique method names for template compilation
115
+ @stamp = (Time.now.to_f * 10000).to_i
116
+ @compiled_method_names = {}
94
117
 
95
-
96
- # Load template source and compile the template. The template is
97
- # loaded and compiled the first time this method is called; subsequent
98
- # calls are no-ops.
99
- def compile
100
- if @data.nil?
101
- @data = @reader.call(self)
102
- compile!
118
+ # load template data and prepare
119
+ if @file.match(/^[^\n]+$/) && File.exist?(@file)
120
+ @reader = block || lambda { |t| File.read(@file) }
121
+ else
122
+ @reader = block || lambda { |t| @file }
103
123
  end
124
+
125
+ @data = @reader.call(self)
126
+ prepare
104
127
  end
105
128
 
106
129
  # Render the template in the given scope with the locals specified. If a
107
130
  # block is given, it is typically available within the template via
108
131
  # +yield+.
109
132
  def render(scope=Object.new, locals={}, &block)
110
- compile
111
133
  evaluate scope, locals || {}, &block
112
134
  end
113
135
 
@@ -127,43 +149,167 @@ module Tilt
127
149
  end
128
150
 
129
151
  protected
130
- # Do whatever preparation is necessary to "compile" the template.
131
- # Called immediately after template #data is loaded. Instance variables
132
- # set in this method are available when #evaluate is called.
152
+ # Called once and only once for each template subclass the first time
153
+ # the template class is initialized. This should be used to require the
154
+ # underlying template library and perform any initial setup.
155
+ def initialize_engine
156
+ end
157
+
158
+ # Like Kernel::require but issues a warning urging a manual require when
159
+ # running under a threaded environment.
160
+ def require_template_library(name)
161
+ # if Thread.list.size > 1
162
+ # warn "WARN: tilt autoloading '#{name}' in a non thread-safe way; " +
163
+ # "explicit require '#{name}' suggested."
164
+ # end
165
+ require name
166
+ end
167
+
168
+ # Do whatever preparation is necessary to setup the underlying template
169
+ # engine. Called immediately after template data is loaded. Instance
170
+ # variables set in this method are available when #evaluate is called.
133
171
  #
134
172
  # Subclasses must provide an implementation of this method.
135
- def compile!
136
- raise NotImplementedError
173
+ def prepare
174
+ if respond_to?(:compile!)
175
+ # backward compat with tilt < 0.6; just in case
176
+ warn 'Tilt::Template#compile! is deprecated; implement #prepare instead.'
177
+ compile!
178
+ else
179
+ raise NotImplementedError
180
+ end
137
181
  end
138
182
 
139
- # Process the template and return the result. Subclasses should override
140
- # this method unless they implement the #template_source.
183
+ # Process the template and return the result. When the scope mixes in
184
+ # the Tilt::CompileSite module, the template is compiled to a method and
185
+ # reused given identical locals keys. When the scope object
186
+ # does not mix in the CompileSite module, the template source is
187
+ # evaluated with instance_eval. In any case, template executation
188
+ # is guaranteed to be performed in the scope object with the locals
189
+ # specified and with support for yielding to the block.
141
190
  def evaluate(scope, locals, &block)
142
- source, offset = local_assignment_code(locals)
143
- source = [source, template_source].join("\n")
144
- scope.instance_eval source, eval_file, line - offset
191
+ if scope.respond_to?(:__tilt__)
192
+ method_name = compiled_method_name(locals.keys)
193
+ if scope.respond_to?(method_name)
194
+ scope.send(method_name, locals, &block)
195
+ else
196
+ compile_template_method(method_name, locals)
197
+ scope.send(method_name, locals, &block)
198
+ end
199
+ else
200
+ evaluate_source(scope, locals, &block)
201
+ end
145
202
  end
146
203
 
147
- # Return a string containing the (Ruby) source code for the template. The
148
- # default Template#evaluate implementation requires this method be
149
- # defined.
150
- def template_source
204
+ # Generates all template source by combining the preamble, template, and
205
+ # postamble and returns a two-tuple of the form: [source, offset], where
206
+ # source is the string containing (Ruby) source code for the template and
207
+ # offset is the integer line offset where line reporting should begin.
208
+ #
209
+ # Template subclasses may override this method when they need complete
210
+ # control over source generation or want to adjust the default line
211
+ # offset. In most cases, overriding the #precompiled_template method is
212
+ # easier and more appropriate.
213
+ def precompiled(locals)
214
+ preamble = precompiled_preamble(locals)
215
+ parts = [
216
+ preamble,
217
+ precompiled_template(locals),
218
+ precompiled_postamble(locals)
219
+ ]
220
+ [parts.join("\n"), preamble.count("\n") + 1]
221
+ end
222
+
223
+ # A string containing the (Ruby) source code for the template. The
224
+ # default Template#evaluate implementation requires either this method
225
+ # or the #precompiled method be overridden. When defined, the base
226
+ # Template guarantees correct file/line handling, locals support, custom
227
+ # scopes, and support for template compilation when the scope object
228
+ # allows it.
229
+ def precompiled_template(locals)
151
230
  raise NotImplementedError
152
231
  end
153
232
 
233
+ # Generates preamble code for initializing template state, and performing
234
+ # locals assignment. The default implementation performs locals
235
+ # assignment only. Lines included in the preamble are subtracted from the
236
+ # source line offset, so adding code to the preamble does not effect line
237
+ # reporting in Kernel::caller and backtraces.
238
+ def precompiled_preamble(locals)
239
+ locals.map { |k,v| "#{k} = locals[:#{k}]" }.join("\n")
240
+ end
241
+
242
+ # Generates postamble code for the precompiled template source. The
243
+ # string returned from this method is appended to the precompiled
244
+ # template source.
245
+ def precompiled_postamble(locals)
246
+ ''
247
+ end
248
+
249
+ # The unique compiled method name for the locals keys provided.
250
+ def compiled_method_name(locals_keys)
251
+ @compiled_method_names[locals_keys] ||=
252
+ generate_compiled_method_name(locals_keys)
253
+ end
254
+
154
255
  private
155
- def local_assignment_code(locals)
156
- return ['', 1] if locals.empty?
157
- source = locals.collect { |k,v| "#{k} = locals[:#{k}]" }
158
- [source.join("\n"), source.length]
256
+ # Evaluate the template source in the context of the scope object.
257
+ def evaluate_source(scope, locals, &block)
258
+ source, offset = precompiled(locals)
259
+ scope.instance_eval(source, eval_file, line - offset)
159
260
  end
160
261
 
161
- def require_template_library(name)
162
- if Thread.list.size > 1
163
- warn "WARN: tilt autoloading '#{name}' in a non thread-safe way; " +
164
- "explicit require '#{name}' suggested."
262
+ # JRuby doesn't allow Object#instance_eval to yield to the block it's
263
+ # closed over. This is by design and (ostensibly) something that will
264
+ # change in MRI, though no current MRI version tested (1.8.6 - 1.9.2)
265
+ # exhibits the behavior. More info here:
266
+ #
267
+ # http://jira.codehaus.org/browse/JRUBY-2599
268
+ #
269
+ # Additionally, JRuby's eval line reporting is off by one compared to
270
+ # all MRI versions tested.
271
+ #
272
+ # We redefine evaluate_source to work around both issues.
273
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
274
+ undef evaluate_source
275
+ def evaluate_source(scope, locals, &block)
276
+ source, offset = precompiled(locals)
277
+ file, lineno = eval_file, (line - offset) - 1
278
+ scope.instance_eval { Kernel::eval(source, binding, file, lineno) }
279
+ end
280
+ end
281
+
282
+ def generate_compiled_method_name(locals_keys)
283
+ parts = [object_id, @stamp] + locals_keys.map { |k| k.to_s }.sort
284
+ digest = Digest::MD5.hexdigest(parts.join(':'))
285
+ "__tilt_#{digest}"
286
+ end
287
+
288
+ def compile_template_method(method_name, locals)
289
+ source, offset = precompiled(locals)
290
+ offset += 1
291
+ CompileSite.module_eval <<-RUBY, eval_file, line - offset
292
+ def #{method_name}(locals)
293
+ #{source}
294
+ end
295
+ RUBY
296
+
297
+ ObjectSpace.define_finalizer self,
298
+ Template.compiled_template_method_remover(CompileSite, method_name)
299
+ end
300
+
301
+ def self.compiled_template_method_remover(site, method_name)
302
+ proc { |oid| garbage_collect_compiled_template_method(site, method_name) }
303
+ end
304
+
305
+ def self.garbage_collect_compiled_template_method(site, method_name)
306
+ site.module_eval do
307
+ begin
308
+ remove_method(method_name)
309
+ rescue NameError
310
+ # method was already removed (ruby >= 1.9)
311
+ end
165
312
  end
166
- require name
167
313
  end
168
314
  end
169
315
 
@@ -173,7 +319,7 @@ module Tilt
173
319
  # cache = Tilt::Cache.new
174
320
  # cache.fetch(path, line, options) { Tilt.new(path, line, options) }
175
321
  #
176
- # Subsequent invocations return the already compiled template object.
322
+ # Subsequent invocations return the already loaded template object.
177
323
  class Cache
178
324
  def initialize
179
325
  @cache = {}
@@ -195,11 +341,11 @@ module Tilt
195
341
  # The template source is evaluated as a Ruby string. The #{} interpolation
196
342
  # syntax can be used to generated dynamic output.
197
343
  class StringTemplate < Template
198
- def compile!
344
+ def prepare
199
345
  @code = "%Q{#{data}}"
200
346
  end
201
347
 
202
- def template_source
348
+ def precompiled_template(locals)
203
349
  @code
204
350
  end
205
351
  end
@@ -210,65 +356,87 @@ module Tilt
210
356
  # http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/classes/ERB.html
211
357
  class ERBTemplate < Template
212
358
  def initialize_engine
213
- require_template_library 'erb' unless defined? ::ERB
359
+ return if defined? ::ERB
360
+ require_template_library 'erb'
214
361
  end
215
362
 
216
- def compile!
217
- @engine = ::ERB.new(data, options[:safe], options[:trim], '@_out_buf')
363
+ def prepare
364
+ @outvar = (options[:outvar] || '_erbout').to_s
365
+ @engine = ::ERB.new(data, options[:safe], options[:trim], @outvar)
218
366
  end
219
367
 
220
- def template_source
368
+ def precompiled_template(locals)
221
369
  @engine.src
222
370
  end
223
371
 
224
- def evaluate(scope, locals, &block)
225
- source, offset = local_assignment_code(locals)
226
- source = [source, template_source].join("\n")
227
-
228
- original_out_buf =
229
- scope.instance_variables.any? { |var| var.to_sym == :@_out_buf } &&
230
- scope.instance_variable_get(:@_out_buf)
231
-
232
- scope.instance_eval source, eval_file, line - offset
233
-
234
- output = scope.instance_variable_get(:@_out_buf)
235
- scope.instance_variable_set(:@_out_buf, original_out_buf)
236
-
237
- output
372
+ def precompiled_preamble(locals)
373
+ <<-RUBY
374
+ begin
375
+ __original_outvar = #{@outvar} if defined?(#{@outvar})
376
+ #{super}
377
+ RUBY
238
378
  end
239
379
 
240
- private
380
+ def precompiled_postamble(locals)
381
+ <<-RUBY
382
+ #{super}
383
+ ensure
384
+ #{@outvar} = __original_outvar
385
+ end
386
+ RUBY
387
+ end
241
388
 
242
389
  # ERB generates a line to specify the character coding of the generated
243
390
  # source in 1.9. Account for this in the line offset.
244
391
  if RUBY_VERSION >= '1.9.0'
245
- def local_assignment_code(locals)
392
+ def precompiled(locals)
246
393
  source, offset = super
247
394
  [source, offset + 1]
248
395
  end
249
396
  end
250
397
  end
398
+
251
399
  %w[erb rhtml].each { |ext| register ext, ERBTemplate }
252
400
 
253
401
 
254
402
  # Erubis template implementation. See:
255
403
  # http://www.kuwata-lab.com/erubis/
404
+ #
405
+ # ErubisTemplate supports the following additional options, which are not
406
+ # passed down to the Erubis engine:
407
+ #
408
+ # :engine_class allows you to specify a custom engine class to use
409
+ # instead of the default (which is ::Erubis::Eruby).
410
+ #
411
+ # :escape_html when true, ::Erubis::EscapedEruby will be used as
412
+ # the engine class instead of the default. All content
413
+ # within <%= %> blocks will be automatically html escaped.
256
414
  class ErubisTemplate < ERBTemplate
257
415
  def initialize_engine
258
- require_template_library 'erubis' unless defined? ::Erubis
416
+ return if defined? ::Erubis
417
+ require_template_library 'erubis'
259
418
  end
260
419
 
261
- def compile!
262
- Erubis::Eruby.class_eval(%Q{def add_preamble(src) src << "@_out_buf = _buf = '';" end})
263
- @engine = ::Erubis::Eruby.new(data, options)
420
+ def prepare
421
+ @options.merge!(:preamble => false, :postamble => false)
422
+ @outvar = (options.delete(:outvar) || '_erbout').to_s
423
+ engine_class = options.delete(:engine_class)
424
+ engine_class = ::Erubis::EscapedEruby if options.delete(:escape_html)
425
+ @engine = (engine_class || ::Erubis::Eruby).new(data, options)
264
426
  end
265
427
 
266
- private
428
+ def precompiled_preamble(locals)
429
+ [super, "#{@outvar} = _buf = ''"].join("\n")
430
+ end
267
431
 
268
- # Erubis doesn't have ERB's line-off-by-one under 1.9 problem. Override
269
- # and adjust back.
432
+ def precompiled_postamble(locals)
433
+ ["_buf", super].join("\n")
434
+ end
435
+
436
+ # Erubis doesn't have ERB's line-off-by-one under 1.9 problem.
437
+ # Override and adjust back.
270
438
  if RUBY_VERSION >= '1.9.0'
271
- def local_assignment_code(locals)
439
+ def precompiled(locals)
272
440
  source, offset = super
273
441
  [source, offset - 1]
274
442
  end
@@ -281,20 +449,54 @@ module Tilt
281
449
  # http://haml.hamptoncatlin.com/
282
450
  class HamlTemplate < Template
283
451
  def initialize_engine
284
- require_template_library 'haml' unless defined? ::Haml::Engine
452
+ return if defined? ::Haml::Engine
453
+ require_template_library 'haml'
285
454
  end
286
455
 
287
- def compile!
288
- @engine = ::Haml::Engine.new(data, haml_options)
456
+ def prepare
457
+ options = @options.merge(:filename => eval_file, :line => line)
458
+ @engine = ::Haml::Engine.new(data, options)
289
459
  end
290
460
 
291
461
  def evaluate(scope, locals, &block)
292
- @engine.render(scope, locals, &block)
462
+ if @engine.respond_to?(:precompiled_method_return_value, true)
463
+ super
464
+ else
465
+ @engine.render(scope, locals, &block)
466
+ end
293
467
  end
294
468
 
295
- private
296
- def haml_options
297
- options.merge(:filename => eval_file, :line => line)
469
+ # Precompiled Haml source. Taken from the precompiled_with_ambles
470
+ # method in Haml::Precompiler:
471
+ # http://github.com/nex3/haml/blob/master/lib/haml/precompiler.rb#L111-126
472
+ def precompiled_template(locals)
473
+ @engine.precompiled
474
+ end
475
+
476
+ def precompiled_preamble(locals)
477
+ local_assigns = super
478
+ @engine.instance_eval do
479
+ <<-RUBY
480
+ begin
481
+ extend Haml::Helpers
482
+ _hamlout = @haml_buffer = Haml::Buffer.new(@haml_buffer, #{options_for_buffer.inspect})
483
+ _erbout = _hamlout.buffer
484
+ __in_erb_template = true
485
+ _haml_locals = locals
486
+ #{local_assigns}
487
+ RUBY
488
+ end
489
+ end
490
+
491
+ def precompiled_postamble(locals)
492
+ @engine.instance_eval do
493
+ <<-RUBY
494
+ #{precompiled_method_return_value}
495
+ ensure
496
+ @haml_buffer = @haml_buffer.upper
497
+ end
498
+ RUBY
499
+ end
298
500
  end
299
501
  end
300
502
  register 'haml', HamlTemplate
@@ -306,15 +508,16 @@ module Tilt
306
508
  # Sass templates do not support object scopes, locals, or yield.
307
509
  class SassTemplate < Template
308
510
  def initialize_engine
309
- require_template_library 'sass' unless defined? ::Sass::Engine
511
+ return if defined? ::Sass::Engine
512
+ require_template_library 'sass'
310
513
  end
311
514
 
312
- def compile!
515
+ def prepare
313
516
  @engine = ::Sass::Engine.new(data, sass_options)
314
517
  end
315
518
 
316
519
  def evaluate(scope, locals, &block)
317
- @engine.render
520
+ @output ||= @engine.render
318
521
  end
319
522
 
320
523
  private
@@ -331,10 +534,11 @@ module Tilt
331
534
  # Less templates do not support object scopes, locals, or yield.
332
535
  class LessTemplate < Template
333
536
  def initialize_engine
334
- require_template_library 'less' unless defined? ::Less::Engine
537
+ return if defined? ::Less::Engine
538
+ require_template_library 'less'
335
539
  end
336
540
 
337
- def compile!
541
+ def prepare
338
542
  @engine = ::Less::Engine.new(data)
339
543
  end
340
544
 
@@ -344,14 +548,16 @@ module Tilt
344
548
  end
345
549
  register 'less', LessTemplate
346
550
 
551
+
347
552
  # Builder template implementation. See:
348
553
  # http://builder.rubyforge.org/
349
554
  class BuilderTemplate < Template
350
555
  def initialize_engine
351
- require_template_library 'builder' unless defined?(::Builder)
556
+ return if defined?(::Builder)
557
+ require_template_library 'builder'
352
558
  end
353
559
 
354
- def compile!
560
+ def prepare
355
561
  end
356
562
 
357
563
  def evaluate(scope, locals, &block)
@@ -365,7 +571,7 @@ module Tilt
365
571
  xml.target!
366
572
  end
367
573
 
368
- def template_source
574
+ def precompiled_template(locals)
369
575
  data.to_str
370
576
  end
371
577
  end
@@ -387,10 +593,11 @@ module Tilt
387
593
  # time when using this template engine.
388
594
  class LiquidTemplate < Template
389
595
  def initialize_engine
390
- require_template_library 'liquid' unless defined? ::Liquid::Template
596
+ return if defined? ::Liquid::Template
597
+ require_template_library 'liquid'
391
598
  end
392
599
 
393
- def compile!
600
+ def prepare
394
601
  @engine = ::Liquid::Template.parse(data)
395
602
  end
396
603
 
@@ -400,9 +607,8 @@ module Tilt
400
607
  scope = scope.to_h.inject({}){ |h,(k,v)| h[k.to_s] = v ; h }
401
608
  locals = scope.merge(locals)
402
609
  end
403
- # TODO: Is it possible to lazy yield ?
404
610
  locals['yield'] = block.nil? ? '' : yield
405
- locals['content'] = block.nil? ? '' : yield
611
+ locals['content'] = locals['yield']
406
612
  @engine.render(locals)
407
613
  end
408
614
  end
@@ -421,15 +627,17 @@ module Tilt
421
627
  end
422
628
 
423
629
  def initialize_engine
424
- require_template_library 'rdiscount' unless defined? ::RDiscount
630
+ return if defined? ::RDiscount
631
+ require_template_library 'rdiscount'
425
632
  end
426
633
 
427
- def compile!
634
+ def prepare
428
635
  @engine = RDiscount.new(data, *flags)
636
+ @output = nil
429
637
  end
430
638
 
431
639
  def evaluate(scope, locals, &block)
432
- @engine.to_html
640
+ @output ||= @engine.to_html
433
641
  end
434
642
  end
435
643
  register 'markdown', RDiscountTemplate
@@ -441,15 +649,17 @@ module Tilt
441
649
  # http://redcloth.org/
442
650
  class RedClothTemplate < Template
443
651
  def initialize_engine
444
- require_template_library 'redcloth' unless defined? ::RedCloth
652
+ return if defined? ::RedCloth
653
+ require_template_library 'redcloth'
445
654
  end
446
655
 
447
- def compile!
656
+ def prepare
448
657
  @engine = RedCloth.new(data)
658
+ @output = nil
449
659
  end
450
660
 
451
661
  def evaluate(scope, locals, &block)
452
- @engine.to_html
662
+ @output ||= @engine.to_html
453
663
  end
454
664
  end
455
665
  register 'textile', RedClothTemplate
@@ -465,14 +675,16 @@ module Tilt
465
675
  attr_reader :engine
466
676
 
467
677
  def initialize_engine
468
- require_template_library 'mustache' unless defined? ::Mustache
678
+ return if defined? ::Mustache
679
+ require_template_library 'mustache'
469
680
  end
470
681
 
471
- def compile!
682
+ def prepare
472
683
  Mustache.view_namespace = options[:namespace]
684
+ Mustache.view_path = options[:view_path] || options[:mustaches]
473
685
  @engine = options[:view] || Mustache.view_class(name)
474
686
  options.each do |key, value|
475
- next if %w[view namespace mustaches].include?(key.to_s)
687
+ next if %w[view view_path namespace mustaches].include?(key.to_s)
476
688
  @engine.send("#{key}=", value) if @engine.respond_to? "#{key}="
477
689
  end
478
690
  end
@@ -501,6 +713,7 @@ module Tilt
501
713
  end
502
714
  register 'mustache', MustacheTemplate
503
715
 
716
+
504
717
  # RDoc template. See:
505
718
  # http://rdoc.rubyforge.org/
506
719
  #
@@ -509,37 +722,72 @@ module Tilt
509
722
  # engine.
510
723
  class RDocTemplate < Template
511
724
  def initialize_engine
512
- unless defined?(::RDoc::Markup)
513
- require_template_library 'rdoc/markup'
514
- require_template_library 'rdoc/markup/to_html'
515
- end
725
+ return if defined?(::RDoc::Markup)
726
+ require_template_library 'rdoc/markup'
727
+ require_template_library 'rdoc/markup/to_html'
516
728
  end
517
729
 
518
- def compile!
730
+ def prepare
519
731
  markup = RDoc::Markup::ToHtml.new
520
732
  @engine = markup.convert(data)
733
+ @output = nil
521
734
  end
522
735
 
523
736
  def evaluate(scope, locals, &block)
524
- @engine.to_s
737
+ @output ||= @engine.to_s
525
738
  end
526
739
  end
527
740
  register 'rdoc', RDocTemplate
528
-
741
+
742
+
529
743
  # CoffeeScript info:
530
744
  # http://jashkenas.github.com/coffee-script/
531
745
  class CoffeeTemplate < Template
532
746
  def initialize_engine
533
- require_template_library 'coffee-script' unless defined? ::CoffeeScript
747
+ return if defined? ::CoffeeScript
748
+ require_template_library 'coffee-script'
534
749
  end
535
750
 
536
- def compile!
537
- @engine = ::CoffeeScript::compile(data, options)
751
+ def prepare
752
+ @output = nil
538
753
  end
539
754
 
540
755
  def evaluate(scope, locals, &block)
541
- @engine
756
+ @output ||= ::CoffeeScript::compile(data, options)
542
757
  end
543
758
  end
544
759
  register 'coffee', CoffeeTemplate
760
+
761
+ # Radius Template
762
+ # http://github.com/jlong/radius/
763
+ class RadiusTemplate < Template
764
+ def initialize_engine
765
+ return if defined? ::Radius
766
+ require_template_library 'radius'
767
+ end
768
+
769
+ def prepare
770
+ @context = Class.new(Radius::Context).new
771
+ end
772
+
773
+ def evaluate(scope, locals, &block)
774
+ @context.define_tag("yield") do
775
+ block.call
776
+ end
777
+ (class << @context; self; end).class_eval do
778
+ define_method :tag_missing do |tag, attr, &block|
779
+ if locals.key?(tag.to_sym)
780
+ locals[tag.to_sym]
781
+ else
782
+ scope.__send__(tag) # any way to support attr as args?
783
+ end
784
+ end
785
+ end
786
+ # TODO: how to config tag prefix?
787
+ parser = Radius::Parser.new(@context, :tag_prefix => 'r')
788
+ parser.parse(data)
789
+ end
790
+ end
791
+ register 'radius', RadiusTemplate
792
+
545
793
  end