tilt 2.0.11 → 2.2.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.
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 +18 -25
  13. data/lib/tilt/commonmarker.rb +47 -81
  14. data/lib/tilt/creole.rb +9 -20
  15. data/lib/tilt/csv.rb +6 -18
  16. data/lib/tilt/erb.rb +32 -16
  17. data/lib/tilt/erubi.rb +29 -6
  18. data/lib/tilt/erubis.rb +20 -11
  19. data/lib/tilt/etanni.rb +3 -2
  20. data/lib/tilt/haml.rb +73 -65
  21. data/lib/tilt/kramdown.rb +8 -20
  22. data/lib/tilt/liquid.rb +10 -14
  23. data/lib/tilt/livescript.rb +8 -20
  24. data/lib/tilt/mapping.rb +228 -109
  25. data/lib/tilt/markaby.rb +5 -7
  26. data/lib/tilt/maruku.rb +5 -19
  27. data/lib/tilt/nokogiri.rb +11 -10
  28. data/lib/tilt/pandoc.rb +33 -51
  29. data/lib/tilt/pipeline.rb +19 -0
  30. data/lib/tilt/plain.rb +4 -15
  31. data/lib/tilt/prawn.rb +15 -23
  32. data/lib/tilt/radius.rb +15 -22
  33. data/lib/tilt/rdiscount.rb +17 -33
  34. data/lib/tilt/rdoc.rb +14 -35
  35. data/lib/tilt/redcarpet.rb +25 -67
  36. data/lib/tilt/redcloth.rb +9 -19
  37. data/lib/tilt/rst-pandoc.rb +7 -20
  38. data/lib/tilt/sass.rb +34 -43
  39. data/lib/tilt/slim.rb +5 -0
  40. data/lib/tilt/string.rb +4 -3
  41. data/lib/tilt/template.rb +225 -78
  42. data/lib/tilt/typescript.rb +11 -18
  43. data/lib/tilt/wikicloth.rb +7 -19
  44. data/lib/tilt/yajl.rb +5 -11
  45. data/lib/tilt.rb +61 -30
  46. metadata +21 -12
  47. data/lib/tilt/bluecloth.rb +0 -24
  48. data/lib/tilt/dummy.rb +0 -3
  49. data/lib/tilt/less.rb +0 -30
  50. data/lib/tilt/sigil.rb +0 -34
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
@@ -57,33 +58,28 @@ module Tilt
57
58
  # a block is required.
58
59
  #
59
60
  # All arguments are optional.
60
- def initialize(file=nil, line=1, options={}, &block)
61
- @file, @line, @options = nil, 1, {}
61
+ def initialize(file=nil, line=nil, options=nil)
62
+ @file, @line, @options = nil, 1, nil
62
63
 
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
64
+ process_arg(options)
65
+ process_arg(line)
66
+ process_arg(file)
74
67
 
75
- raise ArgumentError, "file or block required" if (@file || block).nil?
68
+ raise ArgumentError, "file or block required" unless @file || block_given?
76
69
 
77
- # used to hold compiled template methods
78
- @compiled_method = {}
70
+ @options ||= {}
79
71
 
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
72
+ set_compiled_method_cache
73
+
74
+ # Force the encoding of the input data
82
75
  @default_encoding = @options.delete :default_encoding
83
76
 
77
+ # Skip encoding detection from magic comments and forcing that encoding
78
+ # for compiled templates
79
+ @skip_compiled_encoding_detection = @options.delete :skip_compiled_encoding_detection
80
+
84
81
  # load template data and prepare (uses binread to avoid encoding issues)
85
- @reader = block || lambda { |t| read_template_file }
86
- @data = @reader.call(self)
82
+ @data = block_given? ? yield(self) : read_template_file
87
83
 
88
84
  if @data.respond_to?(:force_encoding)
89
85
  if default_encoding
@@ -102,28 +98,29 @@ module Tilt
102
98
  # Render the template in the given scope with the locals specified. If a
103
99
  # block is given, it is typically available within the template via
104
100
  # +yield+.
105
- def render(scope=nil, locals={}, &block)
106
- scope ||= Object.new
101
+ def render(scope=nil, locals=nil, &block)
107
102
  current_template = Thread.current[:tilt_current_template]
108
103
  Thread.current[:tilt_current_template] = self
109
- evaluate(scope, locals || {}, &block)
104
+ evaluate(scope || Object.new, locals || EMPTY_HASH, &block)
110
105
  ensure
111
106
  Thread.current[:tilt_current_template] = current_template
112
107
  end
113
108
 
114
109
  # The basename of the template file.
115
110
  def basename(suffix='')
116
- File.basename(file, suffix) if file
111
+ File.basename(@file, suffix) if @file
117
112
  end
118
113
 
119
114
  # The template file's basename with all extensions chomped off.
120
115
  def name
121
- basename.split('.', 2).first if basename
116
+ if bname = basename
117
+ bname.split('.', 2).first
118
+ end
122
119
  end
123
120
 
124
121
  # The filename used in backtraces to describe the template.
125
122
  def eval_file
126
- file || '(__TEMPLATE__)'
123
+ @file || '(__TEMPLATE__)'
127
124
  end
128
125
 
129
126
  # An empty Hash that the template engine can populate with various
@@ -136,6 +133,35 @@ module Tilt
136
133
  end
137
134
  end
138
135
 
136
+ # Set the prefix to use for compiled paths.
137
+ def compiled_path=(path)
138
+ if path
139
+ # Use expanded paths when loading, since that is helpful
140
+ # for coverage. Remove any .rb suffix, since that will
141
+ # be added back later.
142
+ path = File.expand_path(path.sub(/\.rb\z/i, ''))
143
+ end
144
+ @compiled_path = path
145
+ end
146
+
147
+ # The compiled method for the locals keys and scope_class provided.
148
+ # Returns an UnboundMethod, which can be used to define methods
149
+ # directly on the scope class, which are much faster to call than
150
+ # Tilt's normal rendering.
151
+ def compiled_method(locals_keys, scope_class=nil)
152
+ key = [scope_class, locals_keys].freeze
153
+ LOCK.synchronize do
154
+ if meth = @compiled_method[key]
155
+ return meth
156
+ end
157
+ end
158
+ meth = compile_template_method(locals_keys, scope_class)
159
+ LOCK.synchronize do
160
+ @compiled_method[key] = meth
161
+ end
162
+ meth
163
+ end
164
+
139
165
  protected
140
166
 
141
167
  # @!group For template implementations
@@ -144,20 +170,22 @@ module Tilt
144
170
  # default_encoding-option if present. You may override this method
145
171
  # in your template class if you have a better hint of the data's
146
172
  # encoding.
147
- def default_encoding
148
- @default_encoding
173
+ attr_reader :default_encoding
174
+
175
+ def skip_compiled_encoding_detection?
176
+ @skip_compiled_encoding_detection
149
177
  end
150
178
 
151
179
  # Do whatever preparation is necessary to setup the underlying template
152
180
  # engine. Called immediately after template data is loaded. Instance
153
181
  # variables set in this method are available when #evaluate is called.
154
182
  #
155
- # Subclasses must provide an implementation of this method.
183
+ # Empty by default as some subclasses do not need separate preparation.
156
184
  def prepare
157
- raise NotImplementedError
158
185
  end
159
186
 
160
187
  CLASS_METHOD = Kernel.instance_method(:class)
188
+ USE_BIND_CALL = RUBY_VERSION >= '2.7'
161
189
 
162
190
  # Execute the compiled template and return the result string. Template
163
191
  # evaluation is guaranteed to be performed in the scope object with the
@@ -168,17 +196,24 @@ module Tilt
168
196
  def evaluate(scope, locals, &block)
169
197
  locals_keys = locals.keys
170
198
  locals_keys.sort!{|x, y| x.to_s <=> y.to_s}
199
+
171
200
  case scope
172
201
  when Object
173
- method = compiled_method(locals_keys, Module === scope ? scope : scope.class)
202
+ scope_class = Module === scope ? scope : scope.class
174
203
  else
175
- if RUBY_VERSION >= '2'
176
- method = compiled_method(locals_keys, CLASS_METHOD.bind(scope).call)
177
- else
178
- method = compiled_method(locals_keys, Object)
179
- end
204
+ # :nocov:
205
+ scope_class = USE_BIND_CALL ? CLASS_METHOD.bind_call(scope) : CLASS_METHOD.bind(scope).call
206
+ # :nocov:
207
+ end
208
+ method = compiled_method(locals_keys, scope_class)
209
+
210
+ if USE_BIND_CALL
211
+ method.bind_call(scope, locals, &block)
212
+ # :nocov:
213
+ else
214
+ method.bind(scope).call(locals, &block)
215
+ # :nocov:
180
216
  end
181
- method.bind(scope).call(locals, &block)
182
217
  end
183
218
 
184
219
  # Generates all template source by combining the preamble, template, and
@@ -196,15 +231,19 @@ module Tilt
196
231
  postamble = precompiled_postamble(local_keys)
197
232
  source = String.new
198
233
 
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)
234
+ unless skip_compiled_encoding_detection?
235
+ # Ensure that our generated source code has the same encoding as the
236
+ # the source code generated by the template engine.
237
+ template_encoding = extract_encoding(template){|t| template = t}
203
238
 
204
- source.force_encoding(template_encoding)
205
- template.force_encoding(template_encoding)
239
+ if template.encoding != template_encoding
240
+ # template should never be frozen here. If it was frozen originally,
241
+ # then extract_encoding should yield a dup.
242
+ template.force_encoding(template_encoding)
243
+ end
206
244
  end
207
245
 
246
+ source.force_encoding(template.encoding)
208
247
  source << preamble << "\n" << template << "\n" << postamble
209
248
 
210
249
  [source, preamble.count("\n")+1]
@@ -232,30 +271,52 @@ module Tilt
232
271
 
233
272
  private
234
273
 
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
274
+ def process_arg(arg)
275
+ if arg
276
+ case
277
+ when arg.respond_to?(:to_str) ; @file = arg.to_str
278
+ when arg.respond_to?(:to_int) ; @line = arg.to_int
279
+ when arg.respond_to?(:to_hash) ; @options = arg.to_hash.dup
280
+ when arg.respond_to?(:path) ; @file = arg.path
281
+ when arg.respond_to?(:to_path) ; @file = arg.to_path
282
+ else raise TypeError, "Can't load the template file. Pass a string with a path " +
283
+ "or an object that responds to 'to_str', 'path' or 'to_path'"
284
+ end
240
285
  end
286
+ end
287
+
288
+ def read_template_file
289
+ data = File.binread(file)
290
+ # Set it to the default external (without verifying)
291
+ # :nocov:
292
+ data.force_encoding(Encoding.default_external) if Encoding.default_external
293
+ # :nocov:
241
294
  data
242
295
  end
243
296
 
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)
248
- end
297
+ def set_compiled_method_cache
298
+ @compiled_method = {}
249
299
  end
250
300
 
251
301
  def local_extraction(local_keys)
252
- local_keys.map do |k|
302
+ assignments = local_keys.map do |k|
253
303
  if k.to_s =~ /\A[a-z_][a-zA-Z_0-9]*\z/
254
304
  "#{k} = locals[#{k.inspect}]"
255
305
  else
256
306
  raise "invalid locals key: #{k.inspect} (keys must be variable names)"
257
307
  end
258
- end.join("\n")
308
+ end
309
+
310
+ s = "locals = locals[:locals]"
311
+ if assignments.delete(s)
312
+ # If there is a locals key itself named `locals`, delete it from the ordered keys so we can
313
+ # assign it last. This is important because the assignment of all other locals depends on the
314
+ # `locals` local variable still matching the `locals` method argument given to the method
315
+ # created in `#compile_template_method`.
316
+ assignments << s
317
+ end
318
+
319
+ assignments.join("\n")
259
320
  end
260
321
 
261
322
  def compile_template_method(local_keys, scope_class=nil)
@@ -264,39 +325,85 @@ module Tilt
264
325
 
265
326
  method_name = "__tilt_#{Thread.current.object_id.abs}"
266
327
  method_source = String.new
328
+ method_source.force_encoding(source.encoding)
267
329
 
268
- if method_source.respond_to?(:force_encoding)
269
- method_source.force_encoding(source.encoding)
330
+ if freeze_string_literals?
331
+ method_source << "# frozen-string-literal: true\n"
270
332
  end
271
333
 
272
- method_source << <<-RUBY
273
- TOPOBJECT.class_eval do
274
- def #{method_name}(locals)
275
- #{local_code}
276
- RUBY
334
+ # Don't indent method source, to avoid indentation warnings when using compiled paths
335
+ method_source << "::Tilt::TOPOBJECT.class_eval do\ndef #{method_name}(locals)\n#{local_code}\n"
336
+
277
337
  offset += method_source.count("\n")
278
338
  method_source << source
279
339
  method_source << "\nend;end;"
280
- (scope_class || Object).class_eval(method_source, eval_file, line - offset)
340
+
341
+ bind_compiled_method(method_source, offset, scope_class)
281
342
  unbind_compiled_method(method_name)
282
343
  end
283
344
 
345
+ def bind_compiled_method(method_source, offset, scope_class)
346
+ path = compiled_path
347
+ if path && scope_class.name
348
+ path = path.dup
349
+
350
+ if defined?(@compiled_path_counter)
351
+ path << '-' << @compiled_path_counter.succ!
352
+ else
353
+ @compiled_path_counter = "0".dup
354
+ end
355
+ path << ".rb"
356
+
357
+ # Wrap method source in a class block for the scope, so constant lookup works
358
+ method_source = "class #{scope_class.name}\n#{method_source}\nend"
359
+
360
+ load_compiled_method(path, method_source)
361
+ else
362
+ if path
363
+ warn "compiled_path (#{compiled_path.inspect}) ignored on template with anonymous scope_class (#{scope_class.inspect})"
364
+ end
365
+
366
+ eval_compiled_method(method_source, offset, scope_class)
367
+ end
368
+ end
369
+
370
+ def eval_compiled_method(method_source, offset, scope_class)
371
+ (scope_class || Object).class_eval(method_source, eval_file, line - offset)
372
+ end
373
+
374
+ def load_compiled_method(path, method_source)
375
+ File.binwrite(path, method_source)
376
+
377
+ # Use load and not require, so unbind_compiled_method does not
378
+ # break if the same path is used more than once.
379
+ load path
380
+ end
381
+
284
382
  def unbind_compiled_method(method_name)
285
383
  method = TOPOBJECT.instance_method(method_name)
286
384
  TOPOBJECT.class_eval { remove_method(method_name) }
287
385
  method
288
386
  end
289
387
 
290
- def extract_encoding(script)
291
- extract_magic_comment(script) || script.encoding
388
+ def extract_encoding(script, &block)
389
+ extract_magic_comment(script, &block) || script.encoding
292
390
  end
293
391
 
294
392
  def extract_magic_comment(script)
393
+ if script.frozen?
394
+ script = script.dup
395
+ yield script
396
+ end
397
+
295
398
  binary(script) do
296
399
  script[/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/n, 1]
297
400
  end
298
401
  end
299
402
 
403
+ def freeze_string_literals?
404
+ false
405
+ end
406
+
300
407
  def binary(string)
301
408
  original_encoding = string.encoding
302
409
  string.force_encoding(Encoding::BINARY)
@@ -305,4 +412,44 @@ module Tilt
305
412
  string.force_encoding(original_encoding)
306
413
  end
307
414
  end
415
+
416
+ class StaticTemplate < Template
417
+ def self.subclass(mime_type: 'text/html', &block)
418
+ Class.new(self) do
419
+ self.default_mime_type = mime_type
420
+
421
+ private
422
+
423
+ define_method(:_prepare_output, &block)
424
+ end
425
+ end
426
+
427
+ # Static templates always return the prepared output.
428
+ def render(scope=nil, locals=nil)
429
+ @output
430
+ end
431
+
432
+ # Raise NotImplementedError, since static templates
433
+ # do not support compiled methods.
434
+ def compiled_method(locals_keys, scope_class=nil)
435
+ raise NotImplementedError
436
+ end
437
+
438
+ # Static templates never allow script.
439
+ def allows_script?
440
+ false
441
+ end
442
+
443
+ protected
444
+
445
+ def prepare
446
+ @output = _prepare_output
447
+ end
448
+
449
+ private
450
+
451
+ # Do nothing, since compiled method cache is not used.
452
+ def set_compiled_method_cache
453
+ end
454
+ end
308
455
  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
@@ -1,22 +1,10 @@
1
- require 'tilt/template'
1
+ # frozen_string_literal: true
2
+ require_relative 'template'
2
3
  require 'wikicloth'
3
4
 
4
- module Tilt
5
- # WikiCloth implementation. See:
6
- # http://redcloth.org/
7
- class WikiClothTemplate < Template
8
- def prepare
9
- @parser = options.delete(:parser) || WikiCloth::Parser
10
- @engine = @parser.new options.merge(:data => data)
11
- @output = nil
12
- end
13
-
14
- def evaluate(scope, locals, &block)
15
- @output ||= @engine.to_html
16
- end
17
-
18
- def allows_script?
19
- false
20
- end
21
- end
5
+ # WikiCloth implementation. See: https://github.com/nricciar/wikicloth
6
+ Tilt::WikiClothTemplate = Tilt::StaticTemplate.subclass do
7
+ parser = @options.delete(:parser) || WikiCloth::Parser
8
+ @options[:data] = @data
9
+ parser.new(@options).to_html
22
10
  end
data/lib/tilt/yajl.rb CHANGED
@@ -1,8 +1,8 @@
1
- require 'tilt/template'
1
+ # frozen_string_literal: true
2
+ require_relative 'template'
2
3
  require 'yajl'
3
4
 
4
5
  module Tilt
5
-
6
6
  # Yajl Template implementation
7
7
  #
8
8
  # Yajl is a fast JSON parsing and encoding library for Ruby
@@ -40,14 +40,10 @@ module Tilt
40
40
  # template.render(self)
41
41
  #
42
42
  class YajlTemplate < Template
43
-
44
43
  self.default_mime_type = 'application/json'
45
44
 
46
- def prepare
47
- end
48
-
49
45
  def evaluate(scope, locals, &block)
50
- decorate super(scope, locals, &block)
46
+ decorate(super)
51
47
  end
52
48
 
53
49
  def precompiled_preamble(locals)
@@ -60,10 +56,9 @@ module Tilt
60
56
  end
61
57
 
62
58
  def precompiled_template(locals)
63
- data.to_str
59
+ @data.to_str
64
60
  end
65
61
 
66
-
67
62
  # Decorates the +json+ input according to given +options+.
68
63
  #
69
64
  # json - The json String to decorate.
@@ -71,7 +66,7 @@ module Tilt
71
66
  #
72
67
  # Returns the decorated String.
73
68
  def decorate(json)
74
- callback, variable = options[:callback], options[:variable]
69
+ callback, variable = @options[:callback], @options[:variable]
75
70
  if callback && variable
76
71
  "var #{variable} = #{json}; #{callback}(#{variable});"
77
72
  elsif variable
@@ -83,5 +78,4 @@ module Tilt
83
78
  end
84
79
  end
85
80
  end
86
-
87
81
  end