tilt 2.0.11 → 2.3.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 +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 +47 -81
  14. data/lib/tilt/creole.rb +9 -20
  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/erubis.rb +17 -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 +10 -25
  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 +20 -75
  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 +9 -3
  41. data/lib/tilt/template.rb +229 -82
  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 +60 -40
  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,25 @@ 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
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
101
+ def render(scope=nil, locals=nil, &block)
102
+ evaluate(scope || Object.new, locals || EMPTY_HASH, &block)
112
103
  end
113
104
 
114
105
  # The basename of the template file.
115
106
  def basename(suffix='')
116
- File.basename(file, suffix) if file
107
+ File.basename(@file, suffix) if @file
117
108
  end
118
109
 
119
110
  # The template file's basename with all extensions chomped off.
120
111
  def name
121
- basename.split('.', 2).first if basename
112
+ if bname = basename
113
+ bname.split('.', 2).first
114
+ end
122
115
  end
123
116
 
124
117
  # The filename used in backtraces to describe the template.
125
118
  def eval_file
126
- file || '(__TEMPLATE__)'
119
+ @file || '(__TEMPLATE__)'
127
120
  end
128
121
 
129
122
  # An empty Hash that the template engine can populate with various
@@ -136,6 +129,35 @@ module Tilt
136
129
  end
137
130
  end
138
131
 
132
+ # Set the prefix to use for compiled paths.
133
+ def compiled_path=(path)
134
+ if path
135
+ # Use expanded paths when loading, since that is helpful
136
+ # for coverage. Remove any .rb suffix, since that will
137
+ # be added back later.
138
+ path = File.expand_path(path.sub(/\.rb\z/i, ''))
139
+ end
140
+ @compiled_path = path
141
+ end
142
+
143
+ # The compiled method for the locals keys and scope_class provided.
144
+ # Returns an UnboundMethod, which can be used to define methods
145
+ # directly on the scope class, which are much faster to call than
146
+ # Tilt's normal rendering.
147
+ def compiled_method(locals_keys, scope_class=nil)
148
+ key = [scope_class, locals_keys].freeze
149
+ LOCK.synchronize do
150
+ if meth = @compiled_method[key]
151
+ return meth
152
+ end
153
+ end
154
+ meth = compile_template_method(locals_keys, scope_class)
155
+ LOCK.synchronize do
156
+ @compiled_method[key] = meth
157
+ end
158
+ meth
159
+ end
160
+
139
161
  protected
140
162
 
141
163
  # @!group For template implementations
@@ -144,20 +166,22 @@ module Tilt
144
166
  # default_encoding-option if present. You may override this method
145
167
  # in your template class if you have a better hint of the data's
146
168
  # encoding.
147
- def default_encoding
148
- @default_encoding
169
+ attr_reader :default_encoding
170
+
171
+ def skip_compiled_encoding_detection?
172
+ @skip_compiled_encoding_detection
149
173
  end
150
174
 
151
175
  # Do whatever preparation is necessary to setup the underlying template
152
176
  # engine. Called immediately after template data is loaded. Instance
153
177
  # variables set in this method are available when #evaluate is called.
154
178
  #
155
- # Subclasses must provide an implementation of this method.
179
+ # Empty by default as some subclasses do not need separate preparation.
156
180
  def prepare
157
- raise NotImplementedError
158
181
  end
159
182
 
160
183
  CLASS_METHOD = Kernel.instance_method(:class)
184
+ USE_BIND_CALL = RUBY_VERSION >= '2.7'
161
185
 
162
186
  # Execute the compiled template and return the result string. Template
163
187
  # evaluation is guaranteed to be performed in the scope object with the
@@ -168,17 +192,24 @@ module Tilt
168
192
  def evaluate(scope, locals, &block)
169
193
  locals_keys = locals.keys
170
194
  locals_keys.sort!{|x, y| x.to_s <=> y.to_s}
195
+
171
196
  case scope
172
197
  when Object
173
- method = compiled_method(locals_keys, Module === scope ? scope : scope.class)
198
+ scope_class = Module === scope ? scope : scope.class
174
199
  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
200
+ # :nocov:
201
+ scope_class = USE_BIND_CALL ? CLASS_METHOD.bind_call(scope) : CLASS_METHOD.bind(scope).call
202
+ # :nocov:
203
+ end
204
+ method = compiled_method(locals_keys, scope_class)
205
+
206
+ if USE_BIND_CALL
207
+ method.bind_call(scope, locals, &block)
208
+ # :nocov:
209
+ else
210
+ method.bind(scope).call(locals, &block)
211
+ # :nocov:
180
212
  end
181
- method.bind(scope).call(locals, &block)
182
213
  end
183
214
 
184
215
  # Generates all template source by combining the preamble, template, and
@@ -196,15 +227,19 @@ module Tilt
196
227
  postamble = precompiled_postamble(local_keys)
197
228
  source = String.new
198
229
 
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)
230
+ unless skip_compiled_encoding_detection?
231
+ # Ensure that our generated source code has the same encoding as the
232
+ # the source code generated by the template engine.
233
+ template_encoding = extract_encoding(template){|t| template = t}
203
234
 
204
- source.force_encoding(template_encoding)
205
- template.force_encoding(template_encoding)
235
+ if template.encoding != template_encoding
236
+ # template should never be frozen here. If it was frozen originally,
237
+ # then extract_encoding should yield a dup.
238
+ template.force_encoding(template_encoding)
239
+ end
206
240
  end
207
241
 
242
+ source.force_encoding(template.encoding)
208
243
  source << preamble << "\n" << template << "\n" << postamble
209
244
 
210
245
  [source, preamble.count("\n")+1]
@@ -232,30 +267,52 @@ module Tilt
232
267
 
233
268
  private
234
269
 
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
270
+ def process_arg(arg)
271
+ if arg
272
+ case
273
+ when arg.respond_to?(:to_str) ; @file = arg.to_str
274
+ when arg.respond_to?(:to_int) ; @line = arg.to_int
275
+ when arg.respond_to?(:to_hash) ; @options = arg.to_hash.dup
276
+ when arg.respond_to?(:path) ; @file = arg.path
277
+ when arg.respond_to?(:to_path) ; @file = arg.to_path
278
+ else raise TypeError, "Can't load the template file. Pass a string with a path " +
279
+ "or an object that responds to 'to_str', 'path' or 'to_path'"
280
+ end
240
281
  end
282
+ end
283
+
284
+ def read_template_file
285
+ data = File.binread(file)
286
+ # Set it to the default external (without verifying)
287
+ # :nocov:
288
+ data.force_encoding(Encoding.default_external) if Encoding.default_external
289
+ # :nocov:
241
290
  data
242
291
  end
243
292
 
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
293
+ def set_compiled_method_cache
294
+ @compiled_method = {}
249
295
  end
250
296
 
251
297
  def local_extraction(local_keys)
252
- local_keys.map do |k|
298
+ assignments = local_keys.map do |k|
253
299
  if k.to_s =~ /\A[a-z_][a-zA-Z_0-9]*\z/
254
300
  "#{k} = locals[#{k.inspect}]"
255
301
  else
256
302
  raise "invalid locals key: #{k.inspect} (keys must be variable names)"
257
303
  end
258
- end.join("\n")
304
+ end
305
+
306
+ s = "locals = locals[:locals]"
307
+ if assignments.delete(s)
308
+ # If there is a locals key itself named `locals`, delete it from the ordered keys so we can
309
+ # assign it last. This is important because the assignment of all other locals depends on the
310
+ # `locals` local variable still matching the `locals` method argument given to the method
311
+ # created in `#compile_template_method`.
312
+ assignments << s
313
+ end
314
+
315
+ assignments.join("\n")
259
316
  end
260
317
 
261
318
  def compile_template_method(local_keys, scope_class=nil)
@@ -264,39 +321,89 @@ module Tilt
264
321
 
265
322
  method_name = "__tilt_#{Thread.current.object_id.abs}"
266
323
  method_source = String.new
324
+ method_source.force_encoding(source.encoding)
267
325
 
268
- if method_source.respond_to?(:force_encoding)
269
- method_source.force_encoding(source.encoding)
326
+ if freeze_string_literals?
327
+ method_source << "# frozen-string-literal: true\n"
270
328
  end
271
329
 
272
- method_source << <<-RUBY
273
- TOPOBJECT.class_eval do
274
- def #{method_name}(locals)
275
- #{local_code}
276
- RUBY
330
+ # Don't indent method source, to avoid indentation warnings when using compiled paths
331
+ method_source << "::Tilt::TOPOBJECT.class_eval do\ndef #{method_name}(locals)\n#{local_code}\n"
332
+
277
333
  offset += method_source.count("\n")
278
334
  method_source << source
279
335
  method_source << "\nend;end;"
280
- (scope_class || Object).class_eval(method_source, eval_file, line - offset)
336
+
337
+ bind_compiled_method(method_source, offset, scope_class)
281
338
  unbind_compiled_method(method_name)
282
339
  end
283
340
 
341
+ def bind_compiled_method(method_source, offset, scope_class)
342
+ path = compiled_path
343
+ if path && scope_class.name
344
+ path = path.dup
345
+
346
+ if defined?(@compiled_path_counter)
347
+ path << '-' << @compiled_path_counter.succ!
348
+ else
349
+ @compiled_path_counter = "0".dup
350
+ end
351
+ path << ".rb"
352
+
353
+ # Wrap method source in a class block for the scope, so constant lookup works
354
+ if freeze_string_literals?
355
+ method_source_prefix = "# frozen-string-literal: true\n"
356
+ method_source = method_source.sub(/\A# frozen-string-literal: true\n/, '')
357
+ end
358
+ method_source = "#{method_source_prefix}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