tilt 1.0.1 → 1.2.2

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/README.md CHANGED
@@ -34,6 +34,15 @@ Support for these template engines is included with the package:
34
34
  RedCloth .textile redcloth
35
35
  RDoc .rdoc rdoc
36
36
  Radius .radius radius
37
+ Markaby .mab markaby
38
+ Nokogiri .nokogiri nokogiri
39
+ CoffeeScript .coffee coffee-script (+node coffee)
40
+
41
+ These template engines ship with their own Tilt integration:
42
+
43
+ ENGINE FILE EXTENSIONS REQUIRED LIBRARIES
44
+ -------------------------- ----------------- ----------------------------
45
+ Slim .slim slim (>= 0.7)
37
46
 
38
47
  See [TEMPLATES.md][t] for detailed information on template engine
39
48
  options and supported features.
@@ -150,44 +159,13 @@ Or, use BlueCloth for markdown instead of RDiscount:
150
159
  Template Compilation
151
160
  --------------------
152
161
 
153
- Tilt can compile generated Ruby source code produced by template engines and
154
- reuse on subsequent template invocations. Benchmarks show this yields a 5x-10x
162
+ Tilt compiles generated Ruby source code produced by template engines and reuses
163
+ it on subsequent template invocations. Benchmarks show this yields a 5x-10x
155
164
  performance increase over evaluating the Ruby source on each invocation.
156
165
 
157
166
  Template compilation is currently supported for these template engines:
158
167
  StringTemplate, ERB, Erubis, Haml, and Builder.
159
168
 
160
- To enable template compilation, the `Tilt::CompileSite` module must be mixed in
161
- to the scope object passed to the template's `#render` method. This can be
162
- accomplished by including (with `Module#include`) the module in the class used
163
- for scope objects or by extending (with `Object#extend`) scope objects before
164
- passing to `Template#render`:
165
-
166
- require 'tilt'
167
-
168
- template = Tilt::ERBTemplate.new('foo.erb')
169
-
170
- # Slow. Uses Object#instance_eval to process template
171
- class Scope
172
- end
173
- scope = Scope.new
174
- template.render(scope)
175
-
176
- # Fast. Uses compiled template and Object#send to process template
177
- class Scope
178
- include Tilt::CompileSite
179
- end
180
- scope = Scope.new
181
- template.render(scope)
182
-
183
- # Also fast, though a bit a slower due to having to extend each time
184
- scope = Object.new
185
- scope.extend Tilt::CompileSite
186
- template.render(scope)
187
-
188
- When the `Tilt::CompileSite` module is not present, template execution falls
189
- back to evaluating the template from source on each invocation.
190
-
191
169
  LICENSE
192
170
  -------
193
171
 
data/lib/tilt.rb CHANGED
@@ -1,7 +1,6 @@
1
- require 'digest/md5'
2
-
3
1
  module Tilt
4
- VERSION = '1.0.1'
2
+ TOPOBJECT = defined?(BasicObject) ? BasicObject : Object
3
+ VERSION = '1.2.2'
5
4
 
6
5
  @template_mappings = {}
7
6
 
@@ -42,24 +41,12 @@ module Tilt
42
41
  @template_mappings[pattern]
43
42
  end
44
43
 
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.
44
+ # Deprecated module.
56
45
  module CompileSite
57
- def __tilt__
58
- end
59
46
  end
60
47
 
61
48
  # Base class for template implementations. Subclasses must implement
62
- # the #prepare method and one of the #evaluate or #evaluate_source
49
+ # the #prepare method and one of the #evaluate or #precompiled_template
63
50
  # methods.
64
51
  class Template
65
52
  # Template source; loaded from a file or given directly.
@@ -111,12 +98,15 @@ module Tilt
111
98
  self.class.engine_initialized = true
112
99
  end
113
100
 
114
- # used to generate unique method names for template compilation
115
- @stamp = (Time.now.to_f * 10000).to_i
116
- @compiled_method_names = {}
101
+ # used to hold compiled template methods
102
+ @compiled_method = {}
117
103
 
118
- # load template data and prepare
119
- @reader = block || lambda { |t| File.read(@file) }
104
+ # used on 1.9 to set the encoding if it is not set elsewhere (like a magic comment)
105
+ # currently only used if template compiles to ruby
106
+ @default_encoding = @options.delete :default_encoding
107
+
108
+ # load template data and prepare (uses binread to avoid encoding issues)
109
+ @reader = block || lambda { |t| File.respond_to?(:binread) ? File.binread(@file) : File.read(@file) }
120
110
  @data = @reader.call(self)
121
111
  prepare
122
112
  end
@@ -174,26 +164,26 @@ module Tilt
174
164
  raise NotImplementedError
175
165
  end
176
166
  end
177
-
178
- # Process the template and return the result. When the scope mixes in
179
- # the Tilt::CompileSite module, the template is compiled to a method and
180
- # reused given identical locals keys. When the scope object
181
- # does not mix in the CompileSite module, the template source is
182
- # evaluated with instance_eval. In any case, template executation
183
- # is guaranteed to be performed in the scope object with the locals
184
- # specified and with support for yielding to the block.
167
+
185
168
  def evaluate(scope, locals, &block)
186
- if scope.respond_to?(:__tilt__)
187
- method_name = compiled_method_name(locals.keys)
188
- if scope.respond_to?(method_name)
189
- scope.send(method_name, locals, &block)
190
- else
191
- compile_template_method(method_name, locals)
192
- scope.send(method_name, locals, &block)
193
- end
194
- else
195
- evaluate_source(scope, locals, &block)
169
+ cached_evaluate(scope, locals, &block)
170
+ end
171
+
172
+ # Process the template and return the result. The first time this
173
+ # method is called, the template source is evaluated with instance_eval.
174
+ # On the sequential method calls it will compile the template to an
175
+ # unbound method which will lead to better performance. In any case,
176
+ # template executation is guaranteed to be performed in the scope object
177
+ # with the locals specified and with support for yielding to the block.
178
+ def cached_evaluate(scope, locals, &block)
179
+ # Redefine itself to use method compilation the next time:
180
+ def self.cached_evaluate(scope, locals, &block)
181
+ method = compiled_method(locals.keys)
182
+ method.bind(scope).call(locals, &block)
196
183
  end
184
+
185
+ # Use instance_eval the first time:
186
+ evaluate_source(scope, locals, &block)
197
187
  end
198
188
 
199
189
  # Generates all template source by combining the preamble, template, and
@@ -207,9 +197,16 @@ module Tilt
207
197
  # easier and more appropriate.
208
198
  def precompiled(locals)
209
199
  preamble = precompiled_preamble(locals)
200
+ template = precompiled_template(locals)
201
+ magic_comment = extract_magic_comment(template)
202
+ if magic_comment
203
+ # Magic comment e.g. "# coding: utf-8" has to be in the first line.
204
+ # So we copy the magic comment to the first line.
205
+ preamble = magic_comment + "\n" + preamble
206
+ end
210
207
  parts = [
211
208
  preamble,
212
- precompiled_template(locals),
209
+ template,
213
210
  precompiled_postamble(locals)
214
211
  ]
215
212
  [parts.join("\n"), preamble.count("\n") + 1]
@@ -241,10 +238,10 @@ module Tilt
241
238
  ''
242
239
  end
243
240
 
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)
241
+ # The compiled method for the locals keys provided.
242
+ def compiled_method(locals_keys)
243
+ @compiled_method[locals_keys] ||=
244
+ compile_template_method(locals_keys)
248
245
  end
249
246
 
250
247
  private
@@ -274,36 +271,59 @@ module Tilt
274
271
  end
275
272
  end
276
273
 
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}"
281
- end
282
-
283
- def compile_template_method(method_name, locals)
274
+ def compile_template_method(locals)
284
275
  source, offset = precompiled(locals)
285
- offset += 1
286
- CompileSite.module_eval <<-RUBY, eval_file, line - offset
287
- def #{method_name}(locals)
288
- #{source}
276
+ offset += 5
277
+ method_name = "__tilt_#{Thread.current.object_id.abs}"
278
+ Object.class_eval <<-RUBY, eval_file, line - offset
279
+ #{extract_magic_comment source}
280
+ TOPOBJECT.class_eval do
281
+ def #{method_name}(locals)
282
+ Thread.current[:tilt_vars] = [self, locals]
283
+ class << self
284
+ this, locals = Thread.current[:tilt_vars]
285
+ this.instance_eval do
286
+ #{source}
287
+ end
288
+ end
289
+ end
289
290
  end
290
291
  RUBY
292
+ unbind_compiled_method(method_name)
293
+ end
291
294
 
292
- ObjectSpace.define_finalizer self,
293
- Template.compiled_template_method_remover(CompileSite, method_name)
295
+ def unbind_compiled_method(method_name)
296
+ method = TOPOBJECT.instance_method(method_name)
297
+ TOPOBJECT.class_eval { remove_method(method_name) }
298
+ method
294
299
  end
295
300
 
296
- def self.compiled_template_method_remover(site, method_name)
297
- proc { |oid| garbage_collect_compiled_template_method(site, method_name) }
301
+ def extract_magic_comment(script)
302
+ comment = script.slice(/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/)
303
+ return comment if comment and not %w[ascii-8bit binary].include?($1.downcase)
304
+ "# coding: #{@default_encoding}" if @default_encoding
298
305
  end
299
306
 
300
- def self.garbage_collect_compiled_template_method(site, method_name)
301
- site.module_eval do
302
- begin
303
- remove_method(method_name)
304
- rescue NameError
305
- # method was already removed (ruby >= 1.9)
306
- end
307
+ # Special case Ruby 1.9.1's broken yield.
308
+ #
309
+ # http://github.com/rtomayko/tilt/commit/20c01a5
310
+ # http://redmine.ruby-lang.org/issues/show/3601
311
+ #
312
+ # Remove when 1.9.2 dominates 1.9.1 installs in the wild.
313
+ if RUBY_VERSION =~ /^1.9.1/
314
+ undef compile_template_method
315
+ def compile_template_method(locals)
316
+ source, offset = precompiled(locals)
317
+ offset += 1
318
+ method_name = "__tilt_#{Thread.current.object_id}"
319
+ Object.class_eval <<-RUBY, eval_file, line - offset
320
+ TOPOBJECT.class_eval do
321
+ def #{method_name}(locals)
322
+ #{source}
323
+ end
324
+ end
325
+ RUBY
326
+ unbind_compiled_method(method_name)
307
327
  end
308
328
  end
309
329
  end
@@ -528,11 +548,19 @@ module Tilt
528
548
 
529
549
  private
530
550
  def sass_options
531
- options.merge(:filename => eval_file, :line => line)
551
+ options.merge(:filename => eval_file, :line => line, :syntax => :sass)
532
552
  end
533
553
  end
534
554
  register 'sass', SassTemplate
535
555
 
556
+ # Sass's new .scss type template implementation.
557
+ class ScssTemplate < SassTemplate
558
+ private
559
+ def sass_options
560
+ options.merge(:filename => eval_file, :line => line, :syntax => :scss)
561
+ end
562
+ end
563
+ register 'scss', ScssTemplate
536
564
 
537
565
  # Lessscss template implementation. See:
538
566
  # http://lesscss.org/
@@ -555,6 +583,73 @@ module Tilt
555
583
  register 'less', LessTemplate
556
584
 
557
585
 
586
+ # CoffeeScript template implementation. See:
587
+ # http://coffeescript.org/
588
+ #
589
+ # CoffeeScript templates do not support object scopes, locals, or yield.
590
+ class CoffeeScriptTemplate < Template
591
+ @@default_no_wrap = false
592
+
593
+ def self.default_no_wrap
594
+ @@default_no_wrap
595
+ end
596
+
597
+ def self.default_no_wrap=(value)
598
+ @@default_no_wrap = value
599
+ end
600
+
601
+ def initialize_engine
602
+ return if defined? ::CoffeeScript
603
+ require_template_library 'coffee_script'
604
+ end
605
+
606
+ def prepare
607
+ @no_wrap = options.key?(:no_wrap) ? options[:no_wrap] :
608
+ self.class.default_no_wrap
609
+ end
610
+
611
+ def evaluate(scope, locals, &block)
612
+ @output ||= CoffeeScript.compile(data, :no_wrap => @no_wrap)
613
+ end
614
+ end
615
+ register 'coffee', CoffeeScriptTemplate
616
+
617
+
618
+ # Nokogiri template implementation. See:
619
+ # http://nokogiri.org/
620
+ class NokogiriTemplate < Template
621
+ def initialize_engine
622
+ return if defined?(::Nokogiri)
623
+ require_template_library 'nokogiri'
624
+ end
625
+
626
+ def prepare; end
627
+
628
+ def evaluate(scope, locals, &block)
629
+ block &&= proc { yield.gsub(/^<\?xml version=\"1\.0\"\?>\n?/, "") }
630
+
631
+ if data.respond_to?(:to_str)
632
+ super(scope, locals, &block)
633
+ else
634
+ ::Nokogiri::XML::Builder.new.tap(&data).to_xml
635
+ end
636
+ end
637
+
638
+ def precompiled_preamble(locals)
639
+ return super if locals.include? :xml
640
+ "xml = ::Nokogiri::XML::Builder.new\n#{super}"
641
+ end
642
+
643
+ def precompiled_postamble(locals)
644
+ "xml.to_xml"
645
+ end
646
+
647
+ def precompiled_template(locals)
648
+ data.to_str
649
+ end
650
+ end
651
+ register 'nokogiri', NokogiriTemplate
652
+
558
653
  # Builder template implementation. See:
559
654
  # http://builder.rubyforge.org/
560
655
  class BuilderTemplate < Template
@@ -563,20 +658,24 @@ module Tilt
563
658
  require_template_library 'builder'
564
659
  end
565
660
 
566
- def prepare
567
- end
661
+ def prepare; end
568
662
 
569
663
  def evaluate(scope, locals, &block)
664
+ return super(scope, locals, &block) if data.respond_to?(:to_str)
570
665
  xml = ::Builder::XmlMarkup.new(:indent => 2)
571
- if data.respond_to?(:to_str)
572
- locals[:xml] = xml
573
- super(scope, locals, &block)
574
- elsif data.kind_of?(Proc)
575
- data.call(xml)
576
- end
666
+ data.call(xml)
577
667
  xml.target!
578
668
  end
579
669
 
670
+ def precompiled_preamble(locals)
671
+ return super if locals.include? :xml
672
+ "xml = ::Builder::XmlMarkup.new(:indent => 2)\n#{super}"
673
+ end
674
+
675
+ def precompiled_postamble(locals)
676
+ "xml.target!"
677
+ end
678
+
580
679
  def precompiled_template(locals)
581
680
  data.to_str
582
681
  end
@@ -651,6 +750,29 @@ module Tilt
651
750
  register 'md', RDiscountTemplate
652
751
 
653
752
 
753
+ # BlueCloth Markdown implementation. See:
754
+ # http://deveiate.org/projects/BlueCloth/
755
+ #
756
+ # RDiscount is a simple text filter. It does not support +scope+ or
757
+ # +locals+. The +:smartypants+ and +:escape_html+ options may be set true
758
+ # to enable those flags on the underlying BlueCloth object.
759
+ class BlueClothTemplate < Template
760
+ def initialize_engine
761
+ return if defined? ::BlueCloth
762
+ require_template_library 'bluecloth'
763
+ end
764
+
765
+ def prepare
766
+ @engine = BlueCloth.new(data, options)
767
+ @output = nil
768
+ end
769
+
770
+ def evaluate(scope, locals, &block)
771
+ @output ||= @engine.to_html
772
+ end
773
+ end
774
+
775
+
654
776
  # RedCloth implementation. See:
655
777
  # http://redcloth.org/
656
778
  class RedClothTemplate < Template
@@ -729,4 +851,51 @@ module Tilt
729
851
  end
730
852
  end
731
853
  register 'radius', RadiusTemplate
854
+
855
+
856
+ # Markaby
857
+ # http://github.com/markaby/markaby
858
+ class MarkabyTemplate < Template
859
+ def self.builder_class
860
+ @builder_class ||= Class.new(Markaby::Builder) do
861
+ def __capture_markaby_tilt__(&block)
862
+ __run_markaby_tilt__ do
863
+ text capture(&block)
864
+ end
865
+ end
866
+ end
867
+ end
868
+
869
+ def initialize_engine
870
+ return if defined? ::Markaby
871
+ require_template_library 'markaby'
872
+ end
873
+
874
+ def prepare
875
+ end
876
+
877
+ def evaluate(scope, locals, &block)
878
+ builder = self.class.builder_class.new({}, scope)
879
+ builder.locals = locals
880
+
881
+ if data.kind_of? Proc
882
+ (class << builder; self end).send(:define_method, :__run_markaby_tilt__, &data)
883
+ else
884
+ builder.instance_eval <<-CODE, __FILE__, __LINE__
885
+ def __run_markaby_tilt__
886
+ #{data}
887
+ end
888
+ CODE
889
+ end
890
+
891
+ if block
892
+ builder.__capture_markaby_tilt__(&block)
893
+ else
894
+ builder.__run_markaby_tilt__
895
+ end
896
+
897
+ builder.to_s
898
+ end
899
+ end
900
+ register 'mab', MarkabyTemplate
732
901
  end
@@ -0,0 +1 @@
1
+ li foo
@@ -0,0 +1 @@
1
+ text "hello from markaby!"
@@ -0,0 +1 @@
1
+ text "_why?"
@@ -0,0 +1 @@
1
+ text "foo"
@@ -0,0 +1 @@
1
+ li foo
@@ -0,0 +1,2 @@
1
+ text("Hey ")
2
+ yield
@@ -0,0 +1,64 @@
1
+ require 'contest'
2
+ require 'tilt'
3
+
4
+ begin
5
+ require 'bluecloth'
6
+
7
+ class BlueClothTemplateTest < Test::Unit::TestCase
8
+ setup do
9
+ Tilt.register('markdown', Tilt::BlueClothTemplate)
10
+ Tilt.register('md', Tilt::BlueClothTemplate)
11
+ Tilt.register('mkd', Tilt::BlueClothTemplate)
12
+ end
13
+
14
+ teardown do
15
+ # Need to revert to RDiscount, otherwise the RDiscount test will fail
16
+ Tilt.register('markdown', Tilt::RDiscountTemplate)
17
+ Tilt.register('md', Tilt::RDiscountTemplate)
18
+ Tilt.register('mkd', Tilt::RDiscountTemplate)
19
+ end
20
+
21
+ test "registered for '.markdown' files unless RDiscount is loaded" do
22
+ unless defined?(RDiscount)
23
+ assert_equal Tilt::BlueClothTemplate, Tilt['test.markdown']
24
+ end
25
+ end
26
+
27
+ test "registered for '.md' files unless RDiscount is loaded" do
28
+ unless defined?(RDiscount)
29
+ assert_equal Tilt::BlueClothTemplate, Tilt['test.md']
30
+ end
31
+ end
32
+
33
+ test "registered for '.mkd' files unless RDiscount is loaded" do
34
+ unless defined?(RDiscount)
35
+ assert_equal Tilt::BlueClothTemplate, Tilt['test.mkd']
36
+ end
37
+ end
38
+
39
+ test "preparing and evaluating templates on #render" do
40
+ template = Tilt::BlueClothTemplate.new { |t| "# Hello World!" }
41
+ assert_equal "<h1>Hello World!</h1>", template.render
42
+ end
43
+
44
+ test "can be rendered more than once" do
45
+ template = Tilt::BlueClothTemplate.new { |t| "# Hello World!" }
46
+ 3.times { assert_equal "<h1>Hello World!</h1>", template.render }
47
+ end
48
+
49
+ test "smartypants when :smart is set" do
50
+ template = Tilt::BlueClothTemplate.new(:smartypants => true) { |t|
51
+ "OKAY -- 'Smarty Pants'" }
52
+ assert_equal "<p>OKAY &mdash; &lsquo;Smarty Pants&rsquo;</p>",
53
+ template.render
54
+ end
55
+
56
+ test "stripping HTML when :filter_html is set" do
57
+ template = Tilt::BlueClothTemplate.new(:escape_html => true) { |t|
58
+ "HELLO <blink>WORLD</blink>" }
59
+ assert_equal "<p>HELLO &lt;blink>WORLD&lt;/blink></p>", template.render
60
+ end
61
+ end
62
+ rescue LoadError => boom
63
+ warn "Tilt::BlueClothTemplate (disabled)\n"
64
+ end
@@ -14,6 +14,11 @@ begin
14
14
  assert_equal "<em>Hello World!</em>\n", template.render
15
15
  end
16
16
 
17
+ test "can be rendered more than once" do
18
+ template = Tilt::BuilderTemplate.new { |t| "xml.em 'Hello World!'" }
19
+ 3.times { assert_equal "<em>Hello World!</em>\n", template.render }
20
+ end
21
+
17
22
  test "passing locals" do
18
23
  template = Tilt::BuilderTemplate.new { "xml.em('Hey ' + name + '!')" }
19
24
  assert_equal "<em>Hey Joe!</em>\n", template.render(Object.new, :name => 'Joe')
@@ -28,7 +33,7 @@ begin
28
33
 
29
34
  test "passing a block for yield" do
30
35
  template = Tilt::BuilderTemplate.new { "xml.em('Hey ' + yield + '!')" }
31
- assert_equal "<em>Hey Joe!</em>\n", template.render { 'Joe' }
36
+ 3.times { assert_equal "<em>Hey Joe!</em>\n", template.render { 'Joe' }}
32
37
  end
33
38
 
34
39
  test "block style templates" do
@@ -38,6 +43,16 @@ begin
38
43
  end
39
44
  assert_equal "<em>Hey Joe!</em>\n", template.render
40
45
  end
46
+
47
+ test "allows nesting raw XML" do
48
+ subtemplate = Tilt::BuilderTemplate.new { "xml.em 'Hello World!'" }
49
+ template = Tilt::BuilderTemplate.new { "xml.strong { xml << yield }" }
50
+ 3.times do
51
+ options = { :xml => Builder::XmlMarkup.new }
52
+ assert_equal "<strong>\n<em>Hello World!</em>\n</strong>\n",
53
+ template.render(options) { subtemplate.render(options) }
54
+ end
55
+ end
41
56
  end
42
57
  rescue LoadError
43
58
  warn "Tilt::BuilderTemplate (disabled)"
@@ -14,10 +14,9 @@ class TiltCacheTest < Test::Unit::TestCase
14
14
 
15
15
  test "caching with multiple complex arguments to #fetch" do
16
16
  template = nil
17
- args = ['hello', {:foo => 'bar', :baz => 'bizzle'}]
18
- result = @cache.fetch(*args) { template = Tilt::StringTemplate.new {''} }
17
+ result = @cache.fetch('hello', {:foo => 'bar', :baz => 'bizzle'}) { template = Tilt::StringTemplate.new {''} }
19
18
  assert_same template, result
20
- result = @cache.fetch(*args) { fail 'should be cached' }
19
+ result = @cache.fetch('hello', {:foo => 'bar', :baz => 'bizzle'}) { fail 'should be cached' }
21
20
  assert_same template, result
22
21
  end
23
22
 
@@ -0,0 +1,30 @@
1
+ require 'contest'
2
+ require 'tilt'
3
+
4
+ begin
5
+ require 'coffee_script'
6
+
7
+ class CoffeeScriptTemplateTest < Test::Unit::TestCase
8
+ test "is registered for '.coffee' files" do
9
+ assert_equal Tilt::CoffeeScriptTemplate, Tilt['test.coffee']
10
+ end
11
+
12
+ test "compiles and evaluates the template on #render" do
13
+ template = Tilt::CoffeeScriptTemplate.new { |t| "puts 'Hello, World!'\n" }
14
+ assert_match "puts('Hello, World!');", template.render
15
+ end
16
+
17
+ test "can be rendered more than once" do
18
+ template = Tilt::CoffeeScriptTemplate.new { |t| "puts 'Hello, World!'\n" }
19
+ 3.times { assert_match "puts('Hello, World!');", template.render }
20
+ end
21
+
22
+ test "disabling coffee-script wrapper" do
23
+ template = Tilt::CoffeeScriptTemplate.new(:no_wrap => true) { |t| "puts 'Hello, World!'\n" }
24
+ assert_equal "puts('Hello, World!');", template.render
25
+ end
26
+ end
27
+
28
+ rescue LoadError => boom
29
+ warn "Tilt::CoffeeScriptTemplate (disabled)\n"
30
+ end