tilt 2.0.11 → 2.6.1

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/COPYING +1 -0
  3. data/bin/tilt +2 -120
  4. data/lib/tilt/_emacs_org.rb +2 -0
  5. data/lib/tilt/_handlebars.rb +2 -0
  6. data/lib/tilt/_jbuilder.rb +2 -0
  7. data/lib/tilt/_org.rb +2 -0
  8. data/lib/tilt/asciidoc.rb +11 -23
  9. data/lib/tilt/babel.rb +5 -13
  10. data/lib/tilt/builder.rb +18 -13
  11. data/lib/tilt/cli.rb +134 -0
  12. data/lib/tilt/coffee.rb +12 -31
  13. data/lib/tilt/commonmarker.rb +82 -75
  14. data/lib/tilt/creole.rb +10 -19
  15. data/lib/tilt/csv.rb +6 -18
  16. data/lib/tilt/erb.rb +23 -21
  17. data/lib/tilt/erubi.rb +29 -6
  18. data/lib/tilt/etanni.rb +5 -4
  19. data/lib/tilt/haml.rb +73 -65
  20. data/lib/tilt/kramdown.rb +8 -20
  21. data/lib/tilt/liquid.rb +10 -17
  22. data/lib/tilt/livescript.rb +8 -20
  23. data/lib/tilt/mapping.rb +228 -109
  24. data/lib/tilt/markaby.rb +5 -7
  25. data/lib/tilt/nokogiri.rb +11 -10
  26. data/lib/tilt/pandoc.rb +33 -51
  27. data/lib/tilt/pipeline.rb +19 -0
  28. data/lib/tilt/plain.rb +4 -15
  29. data/lib/tilt/prawn.rb +10 -25
  30. data/lib/tilt/radius.rb +15 -22
  31. data/lib/tilt/rdiscount.rb +17 -33
  32. data/lib/tilt/rdoc.rb +6 -35
  33. data/lib/tilt/redcarpet.rb +20 -75
  34. data/lib/tilt/redcloth.rb +9 -19
  35. data/lib/tilt/rst-pandoc.rb +7 -20
  36. data/lib/tilt/sass.rb +43 -43
  37. data/lib/tilt/slim.rb +5 -0
  38. data/lib/tilt/string.rb +9 -3
  39. data/lib/tilt/template.rb +392 -89
  40. data/lib/tilt/typescript.rb +11 -18
  41. data/lib/tilt/yajl.rb +5 -11
  42. data/lib/tilt.rb +68 -43
  43. metadata +21 -18
  44. data/lib/tilt/bluecloth.rb +0 -24
  45. data/lib/tilt/dummy.rb +0 -3
  46. data/lib/tilt/erubis.rb +0 -43
  47. data/lib/tilt/less.rb +0 -30
  48. data/lib/tilt/maruku.rb +0 -22
  49. data/lib/tilt/sigil.rb +0 -34
  50. data/lib/tilt/wikicloth.rb +0 -22
data/lib/tilt/template.rb CHANGED
@@ -1,17 +1,12 @@
1
- require 'thread'
2
-
1
+ # frozen_string_literal: true
3
2
  module Tilt
4
3
  # @private
5
- TOPOBJECT = if RUBY_VERSION >= '2.0'
6
- # @private
7
- module CompiledTemplates
8
- self
9
- end
10
- elsif RUBY_VERSION >= '1.9'
11
- BasicObject
12
- else
13
- Object
4
+ module CompiledTemplates
14
5
  end
6
+
7
+ # @private
8
+ TOPOBJECT = CompiledTemplates
9
+
15
10
  # @private
16
11
  LOCK = Mutex.new
17
12
 
@@ -33,6 +28,12 @@ module Tilt
33
28
  # interface.
34
29
  attr_reader :options
35
30
 
31
+ # A path ending in .rb that the template code will be written to, then
32
+ # required, instead of being evaled. This is useful for determining
33
+ # coverage of compiled template code, or to use static analysis tools
34
+ # on the compiled template code.
35
+ attr_reader :compiled_path
36
+
36
37
  class << self
37
38
  # An empty Hash that the template engine can populate with various
38
39
  # metadata.
@@ -40,12 +41,12 @@ module Tilt
40
41
  @metadata ||= {}
41
42
  end
42
43
 
43
- # @deprecated Use `.metadata[:mime_type]` instead.
44
+ # Use `.metadata[:mime_type]` instead.
44
45
  def default_mime_type
45
46
  metadata[:mime_type]
46
47
  end
47
48
 
48
- # @deprecated Use `.metadata[:mime_type] = val` instead.
49
+ # Use `.metadata[:mime_type] = val` instead.
49
50
  def default_mime_type=(value)
50
51
  metadata[:mime_type] = value
51
52
  end
@@ -56,38 +57,63 @@ module Tilt
56
57
  # it should read template data and return as a String. When file is nil,
57
58
  # a block is required.
58
59
  #
59
- # All arguments are optional.
60
- def initialize(file=nil, line=1, options={}, &block)
61
- @file, @line, @options = nil, 1, {}
62
-
63
- [options, line, file].compact.each do |arg|
64
- case
65
- when arg.respond_to?(:to_str) ; @file = arg.to_str
66
- when arg.respond_to?(:to_int) ; @line = arg.to_int
67
- when arg.respond_to?(:to_hash) ; @options = arg.to_hash.dup
68
- when arg.respond_to?(:path) ; @file = arg.path
69
- when arg.respond_to?(:to_path) ; @file = arg.to_path
70
- else raise TypeError, "Can't load the template file. Pass a string with a path " +
71
- "or an object that responds to 'to_str', 'path' or 'to_path'"
72
- end
73
- end
74
-
75
- raise ArgumentError, "file or block required" if (@file || block).nil?
60
+ # All arguments are optional. The following options are respected and
61
+ # are used by Tilt::Template itself and not the underlying template
62
+ # libraries:
63
+ #
64
+ # :default_encoding :: Force the encoding of the template to the given
65
+ # encoding.
66
+ # :skip_compiled_encoding_detection :: Do not scan template code for
67
+ # an encoding magic comment.
68
+ # :fixed_locals :: Force a specific method parameter signature, and call
69
+ # the method with a splat of locals, instead of passing
70
+ # the locals hash as a positional argument, and
71
+ # extracting locals from that. Should be a string
72
+ # containing the parameters for the compiled method,
73
+ # surrounded by parentheses. Can be set to false to
74
+ # disable the scan for embedded fixed locals.
75
+ # :extract_fixed_locals :: Whether embedded fixed locals should be scanned for
76
+ # and extracted from the template code.
77
+ # :default_fixed_locals :: Similar to fixed_locals, but lowest priority,
78
+ # only used if :fixed_locals is not provided
79
+ # and no embedded locals are found (or scanned for).
80
+ # :scope_class :: Force the scope class used for the method. By default,
81
+ # uses the class of the scope provided to render.
82
+ def initialize(file=nil, line=nil, options=nil)
83
+ @file, @line, @options = nil, 1, nil
84
+
85
+ process_arg(options)
86
+ process_arg(line)
87
+ process_arg(file)
88
+
89
+ raise ArgumentError, "file or block required" unless @file || block_given?
90
+
91
+ @options ||= {}
92
+
93
+ # Force a specific scope class, instead of using the class of the provided
94
+ # scope as the scope class.
95
+ @scope_class = @options.delete :scope_class
96
+
97
+ # Force the encoding of the input data
98
+ @default_encoding = @options.delete :default_encoding
76
99
 
77
- # used to hold compiled template methods
78
- @compiled_method = {}
100
+ # Skip encoding detection from magic comments and forcing that encoding
101
+ # for compiled templates
102
+ @skip_compiled_encoding_detection = @options.delete :skip_compiled_encoding_detection
79
103
 
80
- # used on 1.9 to set the encoding if it is not set elsewhere (like a magic comment)
81
- # currently only used if template compiles to ruby
82
- @default_encoding = @options.delete :default_encoding
104
+ # Compiled path to use. This must be specified as an option if
105
+ # providing the :scope_class option and using fixed locals,
106
+ # since template compilation occurs during initialization in that case.
107
+ if compiled_path = @options.delete(:compiled_path)
108
+ self.compiled_path = compiled_path
109
+ end
83
110
 
84
111
  # load template data and prepare (uses binread to avoid encoding issues)
85
- @reader = block || lambda { |t| read_template_file }
86
- @data = @reader.call(self)
112
+ @data = block_given? ? yield(self) : read_template_file
87
113
 
88
114
  if @data.respond_to?(:force_encoding)
89
115
  if default_encoding
90
- @data = @data.dup if @data.frozen?
116
+ @data = _dup_string_if_frozen(@data)
91
117
  @data.force_encoding(default_encoding)
92
118
  end
93
119
 
@@ -96,34 +122,38 @@ module Tilt
96
122
  end
97
123
  end
98
124
 
125
+ set_fixed_locals
99
126
  prepare
127
+ set_compiled_method_cache
100
128
  end
101
129
 
102
130
  # Render the template in the given scope with the locals specified. If a
103
131
  # block is given, it is typically available within the template via
104
132
  # +yield+.
105
- def render(scope=nil, locals={}, &block)
106
- scope ||= Object.new
107
- current_template = Thread.current[:tilt_current_template]
108
- Thread.current[:tilt_current_template] = self
109
- evaluate(scope, locals || {}, &block)
110
- ensure
111
- Thread.current[:tilt_current_template] = current_template
133
+ def render(scope=nil, locals=nil, &block)
134
+ evaluate(scope || Object.new, locals || EMPTY_HASH, &block)
112
135
  end
113
136
 
114
137
  # The basename of the template file.
115
138
  def basename(suffix='')
116
- File.basename(file, suffix) if file
139
+ File.basename(@file, suffix) if @file
117
140
  end
118
141
 
119
142
  # The template file's basename with all extensions chomped off.
120
143
  def name
121
- basename.split('.', 2).first if basename
144
+ if bname = basename
145
+ bname.split('.', 2).first
146
+ end
122
147
  end
123
148
 
124
149
  # The filename used in backtraces to describe the template.
125
150
  def eval_file
126
- file || '(__TEMPLATE__)'
151
+ @file || '(__TEMPLATE__)'
152
+ end
153
+
154
+ # Whether the template uses fixed locals.
155
+ def fixed_locals?
156
+ @fixed_locals ? true : false
127
157
  end
128
158
 
129
159
  # An empty Hash that the template engine can populate with various
@@ -136,6 +166,53 @@ module Tilt
136
166
  end
137
167
  end
138
168
 
169
+ # Set the prefix to use for compiled paths, similar to using the
170
+ # :compiled_path template option. Note that this only
171
+ # has affect for future template compilations. When using the
172
+ # :scope_class template option, and using fixed_locals, calling
173
+ # this after the template is created has no effect, since the
174
+ # template is compiled during initialization in that case. It
175
+ # is recommended to use the :compiled_path template option
176
+ # instead of this method in new code.
177
+ def compiled_path=(path)
178
+ if path
179
+ # Use expanded paths when loading, since that is helpful
180
+ # for coverage. Remove any .rb suffix, since that will
181
+ # be added back later.
182
+ path = File.expand_path(path.sub(/\.rb\z/i, ''))
183
+ end
184
+ @compiled_path = path
185
+ end
186
+
187
+ # The compiled method for the locals keys and scope_class provided.
188
+ # Returns an UnboundMethod, which can be used to define methods
189
+ # directly on the scope class, which are much faster to call than
190
+ # Tilt's normal rendering.
191
+ def compiled_method(locals_keys, scope_class=nil)
192
+ if @fixed_locals
193
+ if @scope_class
194
+ return @compiled_method
195
+ else
196
+ key = scope_class
197
+ end
198
+ elsif @scope_class
199
+ key = locals_keys.dup.freeze
200
+ else
201
+ key = [scope_class, locals_keys].freeze
202
+ end
203
+
204
+ LOCK.synchronize do
205
+ if meth = @compiled_method[key]
206
+ return meth
207
+ end
208
+ end
209
+ meth = compile_template_method(locals_keys, scope_class)
210
+ LOCK.synchronize do
211
+ @compiled_method[key] = meth
212
+ end
213
+ meth
214
+ end
215
+
139
216
  protected
140
217
 
141
218
  # @!group For template implementations
@@ -144,20 +221,22 @@ module Tilt
144
221
  # default_encoding-option if present. You may override this method
145
222
  # in your template class if you have a better hint of the data's
146
223
  # encoding.
147
- def default_encoding
148
- @default_encoding
224
+ attr_reader :default_encoding
225
+
226
+ def skip_compiled_encoding_detection?
227
+ @skip_compiled_encoding_detection
149
228
  end
150
229
 
151
230
  # Do whatever preparation is necessary to setup the underlying template
152
231
  # engine. Called immediately after template data is loaded. Instance
153
232
  # variables set in this method are available when #evaluate is called.
154
233
  #
155
- # Subclasses must provide an implementation of this method.
234
+ # Empty by default as some subclasses do not need separate preparation.
156
235
  def prepare
157
- raise NotImplementedError
158
236
  end
159
237
 
160
238
  CLASS_METHOD = Kernel.instance_method(:class)
239
+ USE_BIND_CALL = RUBY_VERSION >= '3'
161
240
 
162
241
  # Execute the compiled template and return the result string. Template
163
242
  # evaluation is guaranteed to be performed in the scope object with the
@@ -166,19 +245,25 @@ module Tilt
166
245
  # This method is only used by source generating templates. Subclasses that
167
246
  # override render() may not support all features.
168
247
  def evaluate(scope, locals, &block)
169
- locals_keys = locals.keys
170
- locals_keys.sort!{|x, y| x.to_s <=> y.to_s}
171
- case scope
172
- when Object
173
- method = compiled_method(locals_keys, Module === scope ? scope : scope.class)
248
+ if @fixed_locals
249
+ locals_keys = EMPTY_ARRAY
174
250
  else
175
- if RUBY_VERSION >= '2'
176
- method = compiled_method(locals_keys, CLASS_METHOD.bind(scope).call)
251
+ locals_keys = locals.keys
252
+ locals_keys.sort!{|x, y| x.to_s <=> y.to_s}
253
+ end
254
+
255
+ unless scope_class = @scope_class
256
+ scope_class = case scope
257
+ when Object
258
+ Module === scope ? scope : scope.class
177
259
  else
178
- method = compiled_method(locals_keys, Object)
260
+ # :nocov:
261
+ USE_BIND_CALL ? CLASS_METHOD.bind_call(scope) : CLASS_METHOD.bind(scope).call
262
+ # :nocov:
179
263
  end
180
264
  end
181
- method.bind(scope).call(locals, &block)
265
+
266
+ evaluate_method(compiled_method(locals_keys, scope_class), scope, locals, &block)
182
267
  end
183
268
 
184
269
  # Generates all template source by combining the preamble, template, and
@@ -196,15 +281,19 @@ module Tilt
196
281
  postamble = precompiled_postamble(local_keys)
197
282
  source = String.new
198
283
 
199
- # Ensure that our generated source code has the same encoding as the
200
- # the source code generated by the template engine.
201
- if source.respond_to?(:force_encoding)
202
- template_encoding = extract_encoding(template)
284
+ unless skip_compiled_encoding_detection?
285
+ # Ensure that our generated source code has the same encoding as the
286
+ # the source code generated by the template engine.
287
+ template_encoding = extract_encoding(template){|t| template = t}
203
288
 
204
- source.force_encoding(template_encoding)
205
- template.force_encoding(template_encoding)
289
+ if template.encoding != template_encoding
290
+ # template should never be frozen here. If it was frozen originally,
291
+ # then extract_encoding should yield a dup.
292
+ template.force_encoding(template_encoding)
293
+ end
206
294
  end
207
295
 
296
+ source.force_encoding(template.encoding)
208
297
  source << preamble << "\n" << template << "\n" << postamble
209
298
 
210
299
  [source, preamble.count("\n")+1]
@@ -232,71 +321,231 @@ module Tilt
232
321
 
233
322
  private
234
323
 
235
- def read_template_file
236
- data = File.open(file, 'rb') { |io| io.read }
237
- if data.respond_to?(:force_encoding)
238
- # Set it to the default external (without verifying)
239
- data.force_encoding(Encoding.default_external) if Encoding.default_external
324
+ if RUBY_VERSION >= '2.3'
325
+ def _dup_string_if_frozen(string)
326
+ +string
240
327
  end
328
+ # :nocov:
329
+ else
330
+ def _dup_string_if_frozen(string)
331
+ string.frozen? ? string.dup : string
332
+ end
333
+ end
334
+ # :nocov:
335
+
336
+ def process_arg(arg)
337
+ if arg
338
+ case
339
+ when arg.respond_to?(:to_str) ; @file = arg.to_str
340
+ when arg.respond_to?(:to_int) ; @line = arg.to_int
341
+ when arg.respond_to?(:to_hash) ; @options = arg.to_hash.dup
342
+ when arg.respond_to?(:path) ; @file = arg.path
343
+ when arg.respond_to?(:to_path) ; @file = arg.to_path
344
+ else raise TypeError, "Can't load the template file. Pass a string with a path " +
345
+ "or an object that responds to 'to_str', 'path' or 'to_path'"
346
+ end
347
+ end
348
+ end
349
+
350
+ def read_template_file
351
+ data = File.binread(file)
352
+ # Set it to the default external (without verifying)
353
+ # :nocov:
354
+ data.force_encoding(Encoding.default_external) if Encoding.default_external
355
+ # :nocov:
241
356
  data
242
357
  end
243
358
 
244
- # The compiled method for the locals keys provided.
245
- def compiled_method(locals_keys, scope_class=nil)
246
- LOCK.synchronize do
247
- @compiled_method[[scope_class, locals_keys]] ||= compile_template_method(locals_keys, scope_class)
359
+ def set_compiled_method_cache
360
+ @compiled_method = if @fixed_locals && @scope_class
361
+ # No hash needed, only a single compiled method per template.
362
+ compile_template_method(EMPTY_ARRAY, @scope_class)
363
+ else
364
+ {}
248
365
  end
249
366
  end
250
367
 
251
368
  def local_extraction(local_keys)
252
- local_keys.map do |k|
369
+ assignments = local_keys.map do |k|
253
370
  if k.to_s =~ /\A[a-z_][a-zA-Z_0-9]*\z/
254
371
  "#{k} = locals[#{k.inspect}]"
255
372
  else
256
373
  raise "invalid locals key: #{k.inspect} (keys must be variable names)"
257
374
  end
258
- end.join("\n")
375
+ end
376
+
377
+ s = "locals = locals[:locals]"
378
+ if assignments.delete(s)
379
+ # If there is a locals key itself named `locals`, delete it from the ordered keys so we can
380
+ # assign it last. This is important because the assignment of all other locals depends on the
381
+ # `locals` local variable still matching the `locals` method argument given to the method
382
+ # created in `#compile_template_method`.
383
+ assignments << s
384
+ end
385
+
386
+ assignments.join("\n")
387
+ end
388
+
389
+ if USE_BIND_CALL
390
+ def evaluate_method(method, scope, locals, &block)
391
+ if @fixed_locals
392
+ method.bind_call(scope, **locals, &block)
393
+ else
394
+ method.bind_call(scope, locals, &block)
395
+ end
396
+ end
397
+ # :nocov:
398
+ else
399
+ def evaluate_method(method, scope, locals, &block)
400
+ if @fixed_locals
401
+ if locals.empty?
402
+ # Empty keyword splat on Ruby 2.0-2.6 passes empty hash
403
+ method.bind(scope).call(&block)
404
+ else
405
+ method.bind(scope).call(**locals, &block)
406
+ end
407
+ else
408
+ method.bind(scope).call(locals, &block)
409
+ end
410
+ end
259
411
  end
412
+ # :nocov:
260
413
 
261
414
  def compile_template_method(local_keys, scope_class=nil)
262
415
  source, offset = precompiled(local_keys)
263
- local_code = local_extraction(local_keys)
416
+ if @fixed_locals
417
+ method_args = @fixed_locals
418
+ else
419
+ method_args = "(locals)"
420
+ local_code = local_extraction(local_keys)
421
+ end
264
422
 
265
423
  method_name = "__tilt_#{Thread.current.object_id.abs}"
266
424
  method_source = String.new
425
+ method_source.force_encoding(source.encoding)
267
426
 
268
- if method_source.respond_to?(:force_encoding)
269
- method_source.force_encoding(source.encoding)
427
+ if freeze_string_literals?
428
+ method_source << "# frozen-string-literal: true\n"
270
429
  end
271
430
 
272
- method_source << <<-RUBY
273
- TOPOBJECT.class_eval do
274
- def #{method_name}(locals)
275
- #{local_code}
276
- RUBY
431
+ # Don't indent method source, to avoid indentation warnings when using compiled paths
432
+ method_source << "::Tilt::TOPOBJECT.class_eval do\ndef #{method_name}#{method_args}\n#{local_code}\n"
433
+
277
434
  offset += method_source.count("\n")
278
435
  method_source << source
279
436
  method_source << "\nend;end;"
280
- (scope_class || Object).class_eval(method_source, eval_file, line - offset)
437
+
438
+ bind_compiled_method(method_source, offset, scope_class)
281
439
  unbind_compiled_method(method_name)
282
440
  end
283
441
 
442
+ def bind_compiled_method(method_source, offset, scope_class)
443
+ path = compiled_path
444
+ if path && scope_class.name
445
+ path = path.dup
446
+
447
+ if defined?(@compiled_path_counter)
448
+ path << '-' << @compiled_path_counter.succ!
449
+ else
450
+ @compiled_path_counter = "0".dup
451
+ end
452
+ path << ".rb"
453
+
454
+ # Wrap method source in a class block for the scope, so constant lookup works
455
+ if freeze_string_literals?
456
+ method_source_prefix = "# frozen-string-literal: true\n"
457
+ method_source = method_source.sub(/\A# frozen-string-literal: true\n/, '')
458
+ end
459
+ method_source = "#{method_source_prefix}class #{scope_class.name}\n#{method_source}\nend"
460
+
461
+ load_compiled_method(path, method_source)
462
+ else
463
+ if path
464
+ warn "compiled_path (#{compiled_path.inspect}) ignored on template with anonymous scope_class (#{scope_class.inspect})"
465
+ end
466
+
467
+ eval_compiled_method(method_source, offset, scope_class)
468
+ end
469
+ end
470
+
471
+ def eval_compiled_method(method_source, offset, scope_class)
472
+ (scope_class || Object).class_eval(method_source, eval_file, line - offset)
473
+ end
474
+
475
+ def load_compiled_method(path, method_source)
476
+ # Write to a temporary path specific to the current process, and
477
+ # rename after writing. This prevents issues during parallel
478
+ # coverage testing.
479
+ tmp_path = "#{path}-#{$$}"
480
+ File.binwrite(tmp_path, method_source)
481
+ File.rename(tmp_path, path)
482
+
483
+ # Use load and not require, so unbind_compiled_method does not
484
+ # break if the same path is used more than once.
485
+ load path
486
+ end
487
+
284
488
  def unbind_compiled_method(method_name)
285
489
  method = TOPOBJECT.instance_method(method_name)
286
490
  TOPOBJECT.class_eval { remove_method(method_name) }
287
491
  method
288
492
  end
289
493
 
290
- def extract_encoding(script)
291
- extract_magic_comment(script) || script.encoding
494
+ # Set the fixed locals for the template, which may be nil if no fixed locals can
495
+ # be determined.
496
+ def set_fixed_locals
497
+ fixed_locals = @options.delete(:fixed_locals)
498
+ extract_fixed_locals = @options.delete(:extract_fixed_locals)
499
+ default_fixed_locals = @options.delete(:default_fixed_locals)
500
+
501
+ if fixed_locals.nil?
502
+ if extract_fixed_locals.nil?
503
+ extract_fixed_locals = Tilt.extract_fixed_locals
504
+ end
505
+
506
+ if extract_fixed_locals
507
+ fixed_locals = extract_fixed_locals()
508
+ end
509
+
510
+ if fixed_locals.nil?
511
+ fixed_locals = default_fixed_locals
512
+ end
513
+ end
514
+
515
+ @fixed_locals = fixed_locals
516
+ end
517
+
518
+ # Extract fixed locals from the template code string. Should return nil
519
+ # if there are no fixed locals specified, or a method argument string
520
+ # surrounded by parentheses if there are fixed locals. The method
521
+ # argument string will be used when defining the template method if given.
522
+ def extract_fixed_locals
523
+ if @data.is_a?(String) && (match = /\#\s*locals:\s*(\(.*\))/.match(@data))
524
+ match[1]
525
+ end
526
+ end
527
+
528
+ def extract_encoding(script, &block)
529
+ extract_magic_comment(script, &block) || script.encoding
292
530
  end
293
531
 
294
532
  def extract_magic_comment(script)
533
+ was_frozen = script.frozen?
534
+ script = _dup_string_if_frozen(script)
535
+
536
+ if was_frozen
537
+ yield script
538
+ end
539
+
295
540
  binary(script) do
296
541
  script[/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/n, 1]
297
542
  end
298
543
  end
299
544
 
545
+ def freeze_string_literals?
546
+ false
547
+ end
548
+
300
549
  def binary(string)
301
550
  original_encoding = string.encoding
302
551
  string.force_encoding(Encoding::BINARY)
@@ -305,4 +554,58 @@ module Tilt
305
554
  string.force_encoding(original_encoding)
306
555
  end
307
556
  end
557
+
558
+ # Static templates are templates that return the same output for every render
559
+ #
560
+ # Instead of inheriting from the StaticTemplate class, you will use the .subclass
561
+ # method with a block which processes @data and returns the transformed value.
562
+ #
563
+ # Basic example which transforms the template to uppercase:
564
+ #
565
+ # UppercaseTemplate = Tilt::StaticTemplate.subclass do
566
+ # @data.upcase
567
+ # end
568
+ class StaticTemplate < Template
569
+ def self.subclass(mime_type: 'text/html', &block)
570
+ Class.new(self) do
571
+ self.default_mime_type = mime_type
572
+
573
+ private
574
+
575
+ define_method(:_prepare_output, &block)
576
+ end
577
+ end
578
+
579
+ # Static templates always return the prepared output.
580
+ def render(scope=nil, locals=nil)
581
+ @output
582
+ end
583
+
584
+ # Raise NotImplementedError, since static templates
585
+ # do not support compiled methods.
586
+ def compiled_method(locals_keys, scope_class=nil)
587
+ raise NotImplementedError
588
+ end
589
+
590
+ # Static templates never allow script.
591
+ def allows_script?
592
+ false
593
+ end
594
+
595
+ protected
596
+
597
+ def prepare
598
+ @output = _prepare_output
599
+ end
600
+
601
+ private
602
+
603
+ # Do nothing, since compiled method cache is not used.
604
+ def set_compiled_method_cache
605
+ end
606
+
607
+ # Do nothing, since fixed locals are not used.
608
+ def set_fixed_locals
609
+ end
610
+ end
308
611
  end
@@ -1,26 +1,19 @@
1
- require 'tilt/template'
1
+ # frozen_string_literal: true
2
+ require_relative 'template'
2
3
  require 'typescript-node'
3
4
 
4
- module Tilt
5
- class TypeScriptTemplate < Template
6
- self.default_mime_type = 'application/javascript'
5
+ Tilt::TypeScriptTemplate = Tilt::StaticTemplate.subclass(mime_type: 'application/javascript') do
6
+ option_args = []
7
7
 
8
- def prepare
9
- @option_args = []
8
+ @options.each do |key, value|
9
+ next unless value
10
10
 
11
- options.each do |key, value|
12
- next unless value
11
+ option_args << "--#{key}"
13
12
 
14
- @option_args << "--#{key}"
15
-
16
- if value != true
17
- @option_args << value.to_s
18
- end
19
- end
20
- end
21
-
22
- def evaluate(scope, locals, &block)
23
- @output ||= TypeScript::Node.compile(data, *@option_args)
13
+ if value != true
14
+ option_args << value.to_s
24
15
  end
25
16
  end
17
+
18
+ TypeScript::Node.compile(@data, *option_args)
26
19
  end