tilt 0.7 → 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/tilt.rb CHANGED
@@ -1,5 +1,7 @@
1
+ require 'digest/md5'
2
+
1
3
  module Tilt
2
- VERSION = '0.7'
4
+ VERSION = '1.0'
3
5
 
4
6
  @template_mappings = {}
5
7
 
@@ -14,6 +16,11 @@ module Tilt
14
16
  mappings[ext.downcase] = template_class
15
17
  end
16
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
+
17
24
  # Create a new template for the given file using the file's extension
18
25
  # to determine the the template mapping.
19
26
  def self.new(file, line=nil, options={}, &block)
@@ -27,20 +34,12 @@ module Tilt
27
34
  # Lookup a template class for the given filename or file
28
35
  # extension. Return nil when no implementation is found.
29
36
  def self.[](file)
30
- if @template_mappings.key?(pattern = file.to_s.downcase)
31
- @template_mappings[pattern]
32
- elsif @template_mappings.key?(pattern = File.basename(pattern))
33
- @template_mappings[pattern]
34
- else
35
- while !pattern.empty?
36
- if @template_mappings.key?(pattern)
37
- return @template_mappings[pattern]
38
- else
39
- pattern = pattern.sub(/^[^.]*\.?/, '')
40
- end
41
- end
42
- nil
37
+ pattern = file.to_s.downcase
38
+ unless registered?(pattern)
39
+ pattern = File.basename(pattern)
40
+ pattern.sub!(/^[^.]*\.?/, '') until (pattern.empty? || registered?(pattern))
43
41
  end
42
+ @template_mappings[pattern]
44
43
  end
45
44
 
46
45
  # Mixin allowing template compilation on scope objects.
@@ -98,7 +97,7 @@ module Tilt
98
97
  case
99
98
  when arg.respond_to?(:to_str) ; @file = arg.to_str
100
99
  when arg.respond_to?(:to_int) ; @line = arg.to_int
101
- when arg.respond_to?(:to_hash) ; @options = arg.to_hash
100
+ when arg.respond_to?(:to_hash) ; @options = arg.to_hash.dup
102
101
  else raise TypeError
103
102
  end
104
103
  end
@@ -113,8 +112,8 @@ module Tilt
113
112
  end
114
113
 
115
114
  # used to generate unique method names for template compilation
116
- stamp = (Time.now.to_f * 10000).to_i
117
- @_prefix = "__tilt_O#{object_id.to_s(16)}T#{stamp.to_s(16)}"
115
+ @stamp = (Time.now.to_f * 10000).to_i
116
+ @compiled_method_names = {}
118
117
 
119
118
  # load template data and prepare
120
119
  @reader = block || lambda { |t| File.read(@file) }
@@ -151,6 +150,16 @@ module Tilt
151
150
  def initialize_engine
152
151
  end
153
152
 
153
+ # Like Kernel::require but issues a warning urging a manual require when
154
+ # running under a threaded environment.
155
+ def require_template_library(name)
156
+ if Thread.list.size > 1
157
+ warn "WARN: tilt autoloading '#{name}' in a non thread-safe way; " +
158
+ "explicit require '#{name}' suggested."
159
+ end
160
+ require name
161
+ end
162
+
154
163
  # Do whatever preparation is necessary to setup the underlying template
155
164
  # engine. Called immediately after template data is loaded. Instance
156
165
  # variables set in this method are available when #evaluate is called.
@@ -175,54 +184,111 @@ module Tilt
175
184
  # specified and with support for yielding to the block.
176
185
  def evaluate(scope, locals, &block)
177
186
  if scope.respond_to?(:__tilt__)
178
- method_name = compiled_method_name(locals.keys.hash)
187
+ method_name = compiled_method_name(locals.keys)
179
188
  if scope.respond_to?(method_name)
180
- # fast path
181
- scope.send method_name, locals, &block
189
+ scope.send(method_name, locals, &block)
182
190
  else
183
- # compile first and then run
184
191
  compile_template_method(method_name, locals)
185
- scope.send method_name, locals, &block
192
+ scope.send(method_name, locals, &block)
186
193
  end
187
194
  else
188
- source, offset = local_assignment_code(locals)
189
- source = [source, template_source].join("\n")
190
- scope.instance_eval source, eval_file, line - offset
195
+ evaluate_source(scope, locals, &block)
191
196
  end
192
197
  end
193
198
 
194
- # Return a string containing the (Ruby) source code for the template. The
195
- # default Template#evaluate implementation requires this method be
196
- # defined and guarantees correct file/line handling, custom scopes, and
197
- # support for template compilation when the scope object allows it.
198
- def template_source
199
+ # Generates all template source by combining the preamble, template, and
200
+ # postamble and returns a two-tuple of the form: [source, offset], where
201
+ # source is the string containing (Ruby) source code for the template and
202
+ # offset is the integer line offset where line reporting should begin.
203
+ #
204
+ # Template subclasses may override this method when they need complete
205
+ # control over source generation or want to adjust the default line
206
+ # offset. In most cases, overriding the #precompiled_template method is
207
+ # easier and more appropriate.
208
+ def precompiled(locals)
209
+ preamble = precompiled_preamble(locals)
210
+ parts = [
211
+ preamble,
212
+ precompiled_template(locals),
213
+ precompiled_postamble(locals)
214
+ ]
215
+ [parts.join("\n"), preamble.count("\n") + 1]
216
+ end
217
+
218
+ # A string containing the (Ruby) source code for the template. The
219
+ # default Template#evaluate implementation requires either this method
220
+ # or the #precompiled method be overridden. When defined, the base
221
+ # Template guarantees correct file/line handling, locals support, custom
222
+ # scopes, and support for template compilation when the scope object
223
+ # allows it.
224
+ def precompiled_template(locals)
199
225
  raise NotImplementedError
200
226
  end
201
227
 
228
+ # Generates preamble code for initializing template state, and performing
229
+ # locals assignment. The default implementation performs locals
230
+ # assignment only. Lines included in the preamble are subtracted from the
231
+ # source line offset, so adding code to the preamble does not effect line
232
+ # reporting in Kernel::caller and backtraces.
233
+ def precompiled_preamble(locals)
234
+ locals.map { |k,v| "#{k} = locals[:#{k}]" }.join("\n")
235
+ end
236
+
237
+ # Generates postamble code for the precompiled template source. The
238
+ # string returned from this method is appended to the precompiled
239
+ # template source.
240
+ def precompiled_postamble(locals)
241
+ ''
242
+ end
243
+
244
+ # The unique compiled method name for the locals keys provided.
245
+ def compiled_method_name(locals_keys)
246
+ @compiled_method_names[locals_keys] ||=
247
+ generate_compiled_method_name(locals_keys)
248
+ end
249
+
202
250
  private
203
- def local_assignment_code(locals)
204
- return ['', 1] if locals.empty?
205
- source = locals.collect { |k,v| "#{k} = locals[:#{k}]" }
206
- [source.join("\n"), source.length]
251
+ # Evaluate the template source in the context of the scope object.
252
+ def evaluate_source(scope, locals, &block)
253
+ source, offset = precompiled(locals)
254
+ scope.instance_eval(source, eval_file, line - offset)
207
255
  end
208
256
 
209
- def compiled_method_name(locals_hash)
210
- "#{@_prefix}L#{locals_hash.to_s(16).sub('-', 'n')}"
257
+ # JRuby doesn't allow Object#instance_eval to yield to the block it's
258
+ # closed over. This is by design and (ostensibly) something that will
259
+ # change in MRI, though no current MRI version tested (1.8.6 - 1.9.2)
260
+ # exhibits the behavior. More info here:
261
+ #
262
+ # http://jira.codehaus.org/browse/JRUBY-2599
263
+ #
264
+ # Additionally, JRuby's eval line reporting is off by one compared to
265
+ # all MRI versions tested.
266
+ #
267
+ # We redefine evaluate_source to work around both issues.
268
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
269
+ undef evaluate_source
270
+ def evaluate_source(scope, locals, &block)
271
+ source, offset = precompiled(locals)
272
+ file, lineno = eval_file, (line - offset) - 1
273
+ scope.instance_eval { Kernel::eval(source, binding, file, lineno) }
274
+ end
275
+ end
276
+
277
+ def generate_compiled_method_name(locals_keys)
278
+ parts = [object_id, @stamp] + locals_keys.map { |k| k.to_s }.sort
279
+ digest = Digest::MD5.hexdigest(parts.join(':'))
280
+ "__tilt_#{digest}"
211
281
  end
212
282
 
213
283
  def compile_template_method(method_name, locals)
214
- source, offset = local_assignment_code(locals)
215
- source = [source, template_source].join("\n")
284
+ source, offset = precompiled(locals)
216
285
  offset += 1
217
-
218
- # add the new method
219
286
  CompileSite.module_eval <<-RUBY, eval_file, line - offset
220
287
  def #{method_name}(locals)
221
288
  #{source}
222
289
  end
223
290
  RUBY
224
291
 
225
- # setup a finalizer to remove the newly added method
226
292
  ObjectSpace.define_finalizer self,
227
293
  Template.compiled_template_method_remover(CompileSite, method_name)
228
294
  end
@@ -240,14 +306,6 @@ module Tilt
240
306
  end
241
307
  end
242
308
  end
243
-
244
- def require_template_library(name)
245
- if Thread.list.size > 1
246
- warn "WARN: tilt autoloading '#{name}' in a non thread-safe way; " +
247
- "explicit require '#{name}' suggested."
248
- end
249
- require name
250
- end
251
309
  end
252
310
 
253
311
  # Extremely simple template cache implementation. Calling applications
@@ -282,7 +340,7 @@ module Tilt
282
340
  @code = "%Q{#{data}}"
283
341
  end
284
342
 
285
- def template_source
343
+ def precompiled_template(locals)
286
344
  @code
287
345
  end
288
346
  end
@@ -292,72 +350,99 @@ module Tilt
292
350
  # ERB template implementation. See:
293
351
  # http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/classes/ERB.html
294
352
  class ERBTemplate < Template
353
+ @@default_output_variable = '_erbout'
354
+
355
+ def self.default_output_variable
356
+ @@default_output_variable
357
+ end
358
+
359
+ def self.default_output_variable=(name)
360
+ @@default_output_variable = name
361
+ end
362
+
295
363
  def initialize_engine
296
- require_template_library 'erb' unless defined? ::ERB
364
+ return if defined? ::ERB
365
+ require_template_library 'erb'
297
366
  end
298
367
 
299
368
  def prepare
300
- @outvar = (options[:outvar] || '_erbout').to_s
369
+ @outvar = options[:outvar] || self.class.default_output_variable
301
370
  @engine = ::ERB.new(data, options[:safe], options[:trim], @outvar)
302
371
  end
303
372
 
304
- def template_source
305
- @engine.src
373
+ def precompiled_template(locals)
374
+ source = @engine.src
375
+ source
306
376
  end
307
377
 
308
- def evaluate(scope, locals, &block)
309
- preserve_outvar_value(scope) { super }
378
+ def precompiled_preamble(locals)
379
+ <<-RUBY
380
+ begin
381
+ __original_outvar = #{@outvar} if defined?(#{@outvar})
382
+ #{super}
383
+ RUBY
310
384
  end
311
385
 
312
- private
313
- # Retains the previous value of outvar when configured to use
314
- # an instance variable. This allows multiple templates to be rendered
315
- # within the context of an object without overwriting the outvar.
316
- def preserve_outvar_value(scope)
317
- if @outvar[0] == ?@
318
- previous = scope.instance_variable_get(@outvar)
319
- output = yield
320
- scope.instance_variable_set(@outvar, previous)
321
- output
322
- else
323
- yield
324
- end
386
+ def precompiled_postamble(locals)
387
+ <<-RUBY
388
+ #{super}
389
+ ensure
390
+ #{@outvar} = __original_outvar
391
+ end
392
+ RUBY
325
393
  end
326
394
 
327
395
  # ERB generates a line to specify the character coding of the generated
328
396
  # source in 1.9. Account for this in the line offset.
329
397
  if RUBY_VERSION >= '1.9.0'
330
- def local_assignment_code(locals)
398
+ def precompiled(locals)
331
399
  source, offset = super
332
400
  [source, offset + 1]
333
401
  end
334
402
  end
335
403
  end
404
+
336
405
  %w[erb rhtml].each { |ext| register ext, ERBTemplate }
337
406
 
338
407
 
339
408
  # Erubis template implementation. See:
340
409
  # http://www.kuwata-lab.com/erubis/
410
+ #
411
+ # ErubisTemplate supports the following additional options, which are not
412
+ # passed down to the Erubis engine:
413
+ #
414
+ # :engine_class allows you to specify a custom engine class to use
415
+ # instead of the default (which is ::Erubis::Eruby).
416
+ #
417
+ # :escape_html when true, ::Erubis::EscapedEruby will be used as
418
+ # the engine class instead of the default. All content
419
+ # within <%= %> blocks will be automatically html escaped.
341
420
  class ErubisTemplate < ERBTemplate
342
421
  def initialize_engine
343
- require_template_library 'erubis' unless defined? ::Erubis
422
+ return if defined? ::Erubis
423
+ require_template_library 'erubis'
344
424
  end
345
425
 
346
426
  def prepare
347
427
  @options.merge!(:preamble => false, :postamble => false)
348
- @outvar = (options.delete(:outvar) || '_erbout').to_s
349
- @engine = ::Erubis::Eruby.new(data, options)
428
+ @outvar = options.delete(:outvar) || self.class.default_output_variable
429
+ engine_class = options.delete(:engine_class)
430
+ engine_class = ::Erubis::EscapedEruby if options.delete(:escape_html)
431
+ @engine = (engine_class || ::Erubis::Eruby).new(data, options)
350
432
  end
351
433
 
352
- def template_source
353
- ["#{@outvar} = _buf = ''", @engine.src, "_buf.to_s"].join(";")
434
+ def precompiled_preamble(locals)
435
+ [super, "#{@outvar} = _buf = ''"].join("\n")
354
436
  end
355
437
 
356
- private
357
- # Erubis doesn't have ERB's line-off-by-one under 1.9 problem. Override
358
- # and adjust back.
438
+ def precompiled_postamble(locals)
439
+ ["_buf", super].join("\n")
440
+ end
441
+
442
+ # Erubis doesn't have ERB's line-off-by-one under 1.9 problem.
443
+ # Override and adjust back.
359
444
  if RUBY_VERSION >= '1.9.0'
360
- def local_assignment_code(locals)
445
+ def precompiled(locals)
361
446
  source, offset = super
362
447
  [source, offset - 1]
363
448
  end
@@ -370,26 +455,48 @@ module Tilt
370
455
  # http://haml.hamptoncatlin.com/
371
456
  class HamlTemplate < Template
372
457
  def initialize_engine
373
- require_template_library 'haml' unless defined? ::Haml::Engine
458
+ return if defined? ::Haml::Engine
459
+ require_template_library 'haml'
374
460
  end
375
461
 
376
462
  def prepare
377
- @engine = ::Haml::Engine.new(data, haml_options)
463
+ options = @options.merge(:filename => eval_file, :line => line)
464
+ @engine = ::Haml::Engine.new(data, options)
465
+ end
466
+
467
+ def evaluate(scope, locals, &block)
468
+ if @engine.respond_to?(:precompiled_method_return_value, true)
469
+ super
470
+ else
471
+ @engine.render(scope, locals, &block)
472
+ end
378
473
  end
379
474
 
380
475
  # Precompiled Haml source. Taken from the precompiled_with_ambles
381
476
  # method in Haml::Precompiler:
382
477
  # http://github.com/nex3/haml/blob/master/lib/haml/precompiler.rb#L111-126
383
- def template_source
478
+ def precompiled_template(locals)
479
+ @engine.precompiled
480
+ end
481
+
482
+ def precompiled_preamble(locals)
483
+ local_assigns = super
384
484
  @engine.instance_eval do
385
485
  <<-RUBY
386
- _haml_locals = locals
387
486
  begin
388
487
  extend Haml::Helpers
389
488
  _hamlout = @haml_buffer = Haml::Buffer.new(@haml_buffer, #{options_for_buffer.inspect})
390
489
  _erbout = _hamlout.buffer
391
490
  __in_erb_template = true
392
- #{precompiled}
491
+ _haml_locals = locals
492
+ #{local_assigns}
493
+ RUBY
494
+ end
495
+ end
496
+
497
+ def precompiled_postamble(locals)
498
+ @engine.instance_eval do
499
+ <<-RUBY
393
500
  #{precompiled_method_return_value}
394
501
  ensure
395
502
  @haml_buffer = @haml_buffer.upper
@@ -397,16 +504,6 @@ module Tilt
397
504
  RUBY
398
505
  end
399
506
  end
400
-
401
- private
402
- def local_assignment_code(locals)
403
- source, offset = super
404
- [source, offset + 6]
405
- end
406
-
407
- def haml_options
408
- options.merge(:filename => eval_file, :line => line)
409
- end
410
507
  end
411
508
  register 'haml', HamlTemplate
412
509
 
@@ -417,7 +514,8 @@ module Tilt
417
514
  # Sass templates do not support object scopes, locals, or yield.
418
515
  class SassTemplate < Template
419
516
  def initialize_engine
420
- require_template_library 'sass' unless defined? ::Sass::Engine
517
+ return if defined? ::Sass::Engine
518
+ require_template_library 'sass'
421
519
  end
422
520
 
423
521
  def prepare
@@ -442,7 +540,8 @@ module Tilt
442
540
  # Less templates do not support object scopes, locals, or yield.
443
541
  class LessTemplate < Template
444
542
  def initialize_engine
445
- require_template_library 'less' unless defined? ::Less::Engine
543
+ return if defined? ::Less::Engine
544
+ require_template_library 'less'
446
545
  end
447
546
 
448
547
  def prepare
@@ -460,7 +559,8 @@ module Tilt
460
559
  # http://builder.rubyforge.org/
461
560
  class BuilderTemplate < Template
462
561
  def initialize_engine
463
- require_template_library 'builder' unless defined?(::Builder)
562
+ return if defined?(::Builder)
563
+ require_template_library 'builder'
464
564
  end
465
565
 
466
566
  def prepare
@@ -477,7 +577,7 @@ module Tilt
477
577
  xml.target!
478
578
  end
479
579
 
480
- def template_source
580
+ def precompiled_template(locals)
481
581
  data.to_str
482
582
  end
483
583
  end
@@ -499,7 +599,8 @@ module Tilt
499
599
  # time when using this template engine.
500
600
  class LiquidTemplate < Template
501
601
  def initialize_engine
502
- require_template_library 'liquid' unless defined? ::Liquid::Template
602
+ return if defined? ::Liquid::Template
603
+ require_template_library 'liquid'
503
604
  end
504
605
 
505
606
  def prepare
@@ -532,15 +633,17 @@ module Tilt
532
633
  end
533
634
 
534
635
  def initialize_engine
535
- require_template_library 'rdiscount' unless defined? ::RDiscount
636
+ return if defined? ::RDiscount
637
+ require_template_library 'rdiscount'
536
638
  end
537
639
 
538
640
  def prepare
539
641
  @engine = RDiscount.new(data, *flags)
642
+ @output = nil
540
643
  end
541
644
 
542
645
  def evaluate(scope, locals, &block)
543
- @engine.to_html
646
+ @output ||= @engine.to_html
544
647
  end
545
648
  end
546
649
  register 'markdown', RDiscountTemplate
@@ -552,68 +655,22 @@ module Tilt
552
655
  # http://redcloth.org/
553
656
  class RedClothTemplate < Template
554
657
  def initialize_engine
555
- require_template_library 'redcloth' unless defined? ::RedCloth
658
+ return if defined? ::RedCloth
659
+ require_template_library 'redcloth'
556
660
  end
557
661
 
558
662
  def prepare
559
663
  @engine = RedCloth.new(data)
664
+ @output = nil
560
665
  end
561
666
 
562
667
  def evaluate(scope, locals, &block)
563
- @engine.to_html
668
+ @output ||= @engine.to_html
564
669
  end
565
670
  end
566
671
  register 'textile', RedClothTemplate
567
672
 
568
673
 
569
- # Mustache is written and maintained by Chris Wanstrath. See:
570
- # http://github.com/defunkt/mustache
571
- #
572
- # When a scope argument is provided to MustacheTemplate#render, the
573
- # instance variables are copied from the scope object to the Mustache
574
- # view.
575
- class MustacheTemplate < Template
576
- attr_reader :engine
577
-
578
- def initialize_engine
579
- require_template_library 'mustache' unless defined? ::Mustache
580
- end
581
-
582
- def prepare
583
- Mustache.view_namespace = options[:namespace]
584
- Mustache.view_path = options[:view_path] || options[:mustaches]
585
- @engine = options[:view] || Mustache.view_class(name)
586
- options.each do |key, value|
587
- next if %w[view view_path namespace mustaches].include?(key.to_s)
588
- @engine.send("#{key}=", value) if @engine.respond_to? "#{key}="
589
- end
590
- end
591
-
592
- def evaluate(scope=nil, locals={}, &block)
593
- instance = @engine.new
594
-
595
- # copy instance variables from scope to the view
596
- scope.instance_variables.each do |name|
597
- instance.instance_variable_set(name, scope.instance_variable_get(name))
598
- end
599
-
600
- # locals get added to the view's context
601
- locals.each do |local, value|
602
- instance[local] = value
603
- end
604
-
605
- # if we're passed a block it's a subview. Sticking it in yield
606
- # lets us use {{yield}} in layout.html to render the actual page.
607
- instance[:yield] = block.call if block
608
-
609
- instance.template = data unless instance.compiled?
610
-
611
- instance.to_html
612
- end
613
- end
614
- register 'mustache', MustacheTemplate
615
-
616
-
617
674
  # RDoc template. See:
618
675
  # http://rdoc.rubyforge.org/
619
676
  #
@@ -622,38 +679,54 @@ module Tilt
622
679
  # engine.
623
680
  class RDocTemplate < Template
624
681
  def initialize_engine
625
- unless defined?(::RDoc::Markup)
626
- require_template_library 'rdoc/markup'
627
- require_template_library 'rdoc/markup/to_html'
628
- end
682
+ return if defined?(::RDoc::Markup)
683
+ require_template_library 'rdoc/markup'
684
+ require_template_library 'rdoc/markup/to_html'
629
685
  end
630
686
 
631
687
  def prepare
632
688
  markup = RDoc::Markup::ToHtml.new
633
689
  @engine = markup.convert(data)
690
+ @output = nil
634
691
  end
635
692
 
636
693
  def evaluate(scope, locals, &block)
637
- @engine.to_s
694
+ @output ||= @engine.to_s
638
695
  end
639
696
  end
640
697
  register 'rdoc', RDocTemplate
641
698
 
642
699
 
643
- # CoffeeScript info:
644
- # http://jashkenas.github.com/coffee-script/
645
- class CoffeeTemplate < Template
700
+ # Radius Template
701
+ # http://github.com/jlong/radius/
702
+ class RadiusTemplate < Template
646
703
  def initialize_engine
647
- require_template_library 'coffee-script' unless defined? ::CoffeeScript
704
+ return if defined? ::Radius
705
+ require_template_library 'radius'
648
706
  end
649
707
 
650
708
  def prepare
651
- @engine = ::CoffeeScript::compile(data, options)
652
709
  end
653
710
 
654
711
  def evaluate(scope, locals, &block)
655
- @engine
712
+ context = Class.new(Radius::Context).new
713
+ context.define_tag("yield") do
714
+ block.call
715
+ end
716
+ locals.each do |tag, value|
717
+ context.define_tag(tag) do
718
+ value
719
+ end
720
+ end
721
+ (class << context; self; end).class_eval do
722
+ define_method :tag_missing do |tag, attr, &block|
723
+ scope.__send__(tag) # any way to support attr as args?
724
+ end
725
+ end
726
+ options = {:tag_prefix => 'r'}.merge(@options)
727
+ parser = Radius::Parser.new(context, options)
728
+ parser.parse(data)
656
729
  end
657
730
  end
658
- register 'coffee', CoffeeTemplate
731
+ register 'radius', RadiusTemplate
659
732
  end