tilt 2.0.10 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  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 -71
  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 -43
  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 -64
  36. data/lib/tilt/redcloth.rb +9 -19
  37. data/lib/tilt/rst-pandoc.rb +7 -15
  38. data/lib/tilt/sass.rb +45 -28
  39. data/lib/tilt/slim.rb +5 -0
  40. data/lib/tilt/string.rb +4 -3
  41. data/lib/tilt/template.rb +231 -73
  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 -29
  46. metadata +24 -16
  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,19 +170,23 @@ 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
 
187
+ CLASS_METHOD = Kernel.instance_method(:class)
188
+ USE_BIND_CALL = RUBY_VERSION >= '2.7'
189
+
160
190
  # Execute the compiled template and return the result string. Template
161
191
  # evaluation is guaranteed to be performed in the scope object with the
162
192
  # locals specified and with support for yielding to the block.
@@ -166,8 +196,24 @@ module Tilt
166
196
  def evaluate(scope, locals, &block)
167
197
  locals_keys = locals.keys
168
198
  locals_keys.sort!{|x, y| x.to_s <=> y.to_s}
169
- method = compiled_method(locals_keys, scope.is_a?(Module) ? scope : scope.class)
170
- method.bind(scope).call(locals, &block)
199
+
200
+ case scope
201
+ when Object
202
+ scope_class = Module === scope ? scope : scope.class
203
+ else
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:
216
+ end
171
217
  end
172
218
 
173
219
  # Generates all template source by combining the preamble, template, and
@@ -185,15 +231,19 @@ module Tilt
185
231
  postamble = precompiled_postamble(local_keys)
186
232
  source = String.new
187
233
 
188
- # Ensure that our generated source code has the same encoding as the
189
- # the source code generated by the template engine.
190
- if source.respond_to?(:force_encoding)
191
- 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}
192
238
 
193
- source.force_encoding(template_encoding)
194
- 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
195
244
  end
196
245
 
246
+ source.force_encoding(template.encoding)
197
247
  source << preamble << "\n" << template << "\n" << postamble
198
248
 
199
249
  [source, preamble.count("\n")+1]
@@ -221,30 +271,52 @@ module Tilt
221
271
 
222
272
  private
223
273
 
224
- def read_template_file
225
- data = File.open(file, 'rb') { |io| io.read }
226
- if data.respond_to?(:force_encoding)
227
- # Set it to the default external (without verifying)
228
- 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
229
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:
230
294
  data
231
295
  end
232
296
 
233
- # The compiled method for the locals keys provided.
234
- def compiled_method(locals_keys, scope_class=nil)
235
- LOCK.synchronize do
236
- @compiled_method[[scope_class, locals_keys]] ||= compile_template_method(locals_keys, scope_class)
237
- end
297
+ def set_compiled_method_cache
298
+ @compiled_method = {}
238
299
  end
239
300
 
240
301
  def local_extraction(local_keys)
241
- local_keys.map do |k|
302
+ assignments = local_keys.map do |k|
242
303
  if k.to_s =~ /\A[a-z_][a-zA-Z_0-9]*\z/
243
304
  "#{k} = locals[#{k.inspect}]"
244
305
  else
245
306
  raise "invalid locals key: #{k.inspect} (keys must be variable names)"
246
307
  end
247
- 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")
248
320
  end
249
321
 
250
322
  def compile_template_method(local_keys, scope_class=nil)
@@ -253,39 +325,85 @@ module Tilt
253
325
 
254
326
  method_name = "__tilt_#{Thread.current.object_id.abs}"
255
327
  method_source = String.new
328
+ method_source.force_encoding(source.encoding)
256
329
 
257
- if method_source.respond_to?(:force_encoding)
258
- method_source.force_encoding(source.encoding)
330
+ if freeze_string_literals?
331
+ method_source << "# frozen-string-literal: true\n"
259
332
  end
260
333
 
261
- method_source << <<-RUBY
262
- TOPOBJECT.class_eval do
263
- def #{method_name}(locals)
264
- #{local_code}
265
- 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
+
266
337
  offset += method_source.count("\n")
267
338
  method_source << source
268
339
  method_source << "\nend;end;"
269
- (scope_class || Object).class_eval(method_source, eval_file, line - offset)
340
+
341
+ bind_compiled_method(method_source, offset, scope_class)
270
342
  unbind_compiled_method(method_name)
271
343
  end
272
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
+
273
382
  def unbind_compiled_method(method_name)
274
383
  method = TOPOBJECT.instance_method(method_name)
275
384
  TOPOBJECT.class_eval { remove_method(method_name) }
276
385
  method
277
386
  end
278
387
 
279
- def extract_encoding(script)
280
- extract_magic_comment(script) || script.encoding
388
+ def extract_encoding(script, &block)
389
+ extract_magic_comment(script, &block) || script.encoding
281
390
  end
282
391
 
283
392
  def extract_magic_comment(script)
393
+ if script.frozen?
394
+ script = script.dup
395
+ yield script
396
+ end
397
+
284
398
  binary(script) do
285
399
  script[/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/n, 1]
286
400
  end
287
401
  end
288
402
 
403
+ def freeze_string_literals?
404
+ false
405
+ end
406
+
289
407
  def binary(string)
290
408
  original_encoding = string.encoding
291
409
  string.force_encoding(Encoding::BINARY)
@@ -294,4 +412,44 @@ module Tilt
294
412
  string.force_encoding(original_encoding)
295
413
  end
296
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
297
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