frank 0.2.6 → 0.3.0.beta

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.
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