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 287 288 289 290 291 292
|
module Tilt
TOPOBJECT = Object.superclass || Object
# 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
# Used to determine if this class's initialize_engine method has
# been called yet.
@engine_initialized = false
class << self
attr_accessor :engine_initialized
alias engine_initialized? engine_initialized
attr_accessor :default_mime_type
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
else raise TypeError
end
end
raise ArgumentError, "file or block required" if (@file || block).nil?
# call the initialize_engine method if this is the very first time
# an instance of this class has been created.
if !self.class.engine_initialized?
initialize_engine
self.class.engine_initialized = true
end
# 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
# 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
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
# 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)
evaluate scope, locals || {}, &block
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
# Whether or not this template engine allows executing Ruby script
# within the template. If this is false, +scope+ and +locals+ will
# generally not be used, nor will the provided block be avaiable
# via +yield+.
# This should be overridden by template subclasses.
def allows_script?
true
end
protected
# Called once and only once for each template subclass the first time
# the template class is initialized. This should be used to require the
# underlying template library and perform any initial setup.
def initialize_engine
end
# Like Kernel#require but issues a warning urging a manual require when
# running under a threaded environment.
def require_template_library(name)
if Thread.list.size > 1
warn "WARN: tilt autoloading '#{name}' in a non thread-safe way; " +
"explicit require '#{name}' suggested."
end
require name
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
if respond_to?(:compile!)
# backward compat with tilt < 0.6; just in case
warn 'Tilt::Template#compile! is deprecated; implement #prepare instead.'
compile!
else
raise NotImplementedError
end
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(locals)
preamble = precompiled_preamble(locals)
template = precompiled_template(locals)
postamble = precompiled_postamble(locals)
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
# https://github.com/rtomayko/tilt/issues/193
warn "precompiled_preamble should return String (not Array)" if preamble.is_a?(Array)
warn "precompiled_postamble should return String (not Array)" if postamble.is_a?(Array)
source << [preamble, template, postamble].join("\n")
[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(locals)
raise NotImplementedError
end
# Generates preamble code for initializing template state, and performing
# locals assignment. The default implementation performs locals
# assignment only. Lines included in the preamble are subtracted from the
# source line offset, so adding code to the preamble does not effect line
# reporting in Kernel::caller and backtraces.
def precompiled_preamble(locals)
locals.map do |k,v|
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
# Generates postamble code for the precompiled template source. The
# string returned from this method is appended to the precompiled
# template source.
def precompiled_postamble(locals)
''
end
# The compiled method for the locals keys provided.
def compiled_method(locals_keys)
@compiled_method[locals_keys] ||=
compile_template_method(locals_keys)
end
private
def compile_template_method(locals)
source, offset = precompiled(locals)
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
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
|