/
template.rb
286 lines (238 loc) · 9.18 KB
/
template.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
require 'thread'
module Tilt
# @private
TOPOBJECT = Object.superclass || Object
# @private
LOCK = Mutex.new
# Base class for template implementations. Subclasses must implement
# the #prepare method and one of the #evaluate or #precompiled_template
# methods.
class Template
# Template source; loaded from a file or given directly.
attr_reader :data
# The name of the file where the template data was loaded from.
attr_reader :file
# The line number in #file where template data was loaded from.
attr_reader :line
# A Hash of template engine specific options. This is passed directly
# to the underlying engine and is not used by the generic template
# interface.
attr_reader :options
class << self
# An empty Hash that the template engine can populate with various
# metadata.
def metadata
@metadata ||= {}
end
# @deprecated Use `.metadata[:mime_type]` instead.
def default_mime_type
metadata[:mime_type]
end
# @deprecated Use `.metadata[:mime_type] = val` instead.
def default_mime_type=(value)
metadata[:mime_type] = value
end
end
# Create a new template with the file, line, and options specified. By
# default, template data is read from the file. When a block is given,
# it should read template data and return as a String. When file is nil,
# a block is required.
#
# All arguments are optional.
def initialize(file=nil, line=1, options={}, &block)
@file, @line, @options = nil, 1, {}
[options, line, file].compact.each do |arg|
case
when arg.respond_to?(:to_str) ; @file = arg.to_str
when arg.respond_to?(:to_int) ; @line = arg.to_int
when arg.respond_to?(:to_hash) ; @options = arg.to_hash.dup
when arg.respond_to?(:path) ; @file = arg.path
when arg.respond_to?(:to_path) ; @file = arg.to_path
else raise TypeError, "Can't load the template file. Pass a string with a path " +
"or an object that responds to 'to_str', 'path' or 'to_path'"
end
end
raise ArgumentError, "file or block required" if (@file || block).nil?
# used to hold compiled template methods
@compiled_method = {}
# used on 1.9 to set the encoding if it is not set elsewhere (like a magic comment)
# currently only used if template compiles to ruby
@default_encoding = @options.delete :default_encoding
# load template data and prepare (uses binread to avoid encoding issues)
@reader = block || lambda { |t| read_template_file }
@data = @reader.call(self)
if @data.respond_to?(:force_encoding)
@data.force_encoding(default_encoding) if default_encoding
if !@data.valid_encoding?
raise Encoding::InvalidByteSequenceError, "#{eval_file} is not valid #{@data.encoding}"
end
end
prepare
end
# Render the template in the given scope with the locals specified. If a
# block is given, it is typically available within the template via
# +yield+.
def render(scope=Object.new, locals={}, &block)
current_template = Thread.current[:tilt_current_template]
Thread.current[:tilt_current_template] = self
evaluate(scope, locals || {}, &block)
ensure
Thread.current[:tilt_current_template] = current_template
end
# The basename of the template file.
def basename(suffix='')
File.basename(file, suffix) if file
end
# The template file's basename with all extensions chomped off.
def name
basename.split('.', 2).first if basename
end
# The filename used in backtraces to describe the template.
def eval_file
file || '(__TEMPLATE__)'
end
# An empty Hash that the template engine can populate with various
# metadata.
def metadata
if respond_to?(:allows_script?)
self.class.metadata.merge(:allows_script => allows_script?)
else
self.class.metadata
end
end
protected
# @!group For template implementations
# The encoding of the source data. Defaults to the
# default_encoding-option if present. You may override this method
# in your template class if you have a better hint of the data's
# encoding.
def default_encoding
@default_encoding
end
# Do whatever preparation is necessary to setup the underlying template
# engine. Called immediately after template data is loaded. Instance
# variables set in this method are available when #evaluate is called.
#
# Subclasses must provide an implementation of this method.
def prepare
raise NotImplementedError
end
# Execute the compiled template and return the result string. Template
# evaluation is guaranteed to be performed in the scope object with the
# locals specified and with support for yielding to the block.
#
# This method is only used by source generating templates. Subclasses that
# override render() may not support all features.
def evaluate(scope, locals, &block)
method = compiled_method(locals.keys)
method.bind(scope).call(locals, &block)
end
# Generates all template source by combining the preamble, template, and
# postamble and returns a two-tuple of the form: [source, offset], where
# source is the string containing (Ruby) source code for the template and
# offset is the integer line offset where line reporting should begin.
#
# Template subclasses may override this method when they need complete
# control over source generation or want to adjust the default line
# offset. In most cases, overriding the #precompiled_template method is
# easier and more appropriate.
def precompiled(local_keys)
preamble = precompiled_preamble(local_keys)
template = precompiled_template(local_keys)
postamble = precompiled_postamble(local_keys)
source = ''
# Ensure that our generated source code has the same encoding as the
# the source code generated by the template engine.
if source.respond_to?(:force_encoding)
template_encoding = extract_encoding(template)
source.force_encoding(template_encoding)
template.force_encoding(template_encoding)
end
source << preamble << "\n" << template << "\n" << postamble
[source, preamble.count("\n")+1]
end
# A string containing the (Ruby) source code for the template. The
# default Template#evaluate implementation requires either this
# method or the #precompiled method be overridden. When defined,
# the base Template guarantees correct file/line handling, locals
# support, custom scopes, proper encoding, and support for template
# compilation.
def precompiled_template(local_keys)
raise NotImplementedError
end
def precompiled_preamble(local_keys)
''
end
def precompiled_postamble(local_keys)
''
end
# !@endgroup
private
def read_template_file
data = File.open(file, 'rb') { |io| io.read }
if data.respond_to?(:force_encoding)
# Set it to the default external (without verifying)
data.force_encoding(Encoding.default_external) if Encoding.default_external
end
data
end
# The compiled method for the locals keys provided.
def compiled_method(locals_keys)
LOCK.synchronize do
@compiled_method[locals_keys] ||= compile_template_method(locals_keys)
end
end
def local_extraction(local_keys)
local_keys.map do |k|
if k.to_s =~ /\A[a-z_][a-zA-Z_0-9]*\z/
"#{k} = locals[#{k.inspect}]"
else
raise "invalid locals key: #{k.inspect} (keys must be variable names)"
end
end.join("\n")
end
def compile_template_method(local_keys)
source, offset = precompiled(local_keys)
local_code = local_extraction(local_keys)
method_name = "__tilt_#{Thread.current.object_id.abs}"
method_source = ""
if method_source.respond_to?(:force_encoding)
method_source.force_encoding(source.encoding)
end
method_source << <<-RUBY
TOPOBJECT.class_eval do
def #{method_name}(locals)
Thread.current[:tilt_vars] = [self, locals]
class << self
this, locals = Thread.current[:tilt_vars]
this.instance_eval do
#{local_code}
RUBY
offset += method_source.count("\n")
method_source << source
method_source << "\nend;end;end;end"
Object.class_eval(method_source, eval_file, line - offset)
unbind_compiled_method(method_name)
end
def unbind_compiled_method(method_name)
method = TOPOBJECT.instance_method(method_name)
TOPOBJECT.class_eval { remove_method(method_name) }
method
end
def extract_encoding(script)
extract_magic_comment(script) || script.encoding
end
def extract_magic_comment(script)
binary(script) do
script[/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/n, 1]
end
end
def binary(string)
original_encoding = string.encoding
string.force_encoding(Encoding::BINARY)
yield
ensure
string.force_encoding(original_encoding)
end
end
end