lib/tilt/template.rb in tilt-2.0.11 vs lib/tilt/template.rb in tilt-2.1.0
- old
+ new
@@ -1,20 +1,14 @@
-require 'thread'
-
module Tilt
# @private
- TOPOBJECT = if RUBY_VERSION >= '2.0'
- # @private
- module CompiledTemplates
- self
- end
- elsif RUBY_VERSION >= '1.9'
- BasicObject
- else
- Object
+ module CompiledTemplates
end
+
# @private
+ TOPOBJECT = CompiledTemplates
+
+ # @private
LOCK = Mutex.new
# Base class for template implementations. Subclasses must implement
# the #prepare method and one of the #evaluate or #precompiled_template
# methods.
@@ -31,10 +25,16 @@
# 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
+ # A path ending in .rb that the template code will be written to, then
+ # required, instead of being evaled. This is useful for determining
+ # coverage of compiled template code, or to use static analysis tools
+ # on the compiled template code.
+ attr_reader :compiled_path
+
class << self
# An empty Hash that the template engine can populate with various
# metadata.
def metadata
@metadata ||= {}
@@ -134,21 +134,30 @@
else
self.class.metadata
end
end
+ # Set the prefix to use for compiled paths.
+ def compiled_path=(path)
+ if path
+ # Use expanded paths when loading, since that is helpful
+ # for coverage. Remove any .rb suffix, since that will
+ # be added back later.
+ path = File.expand_path(path.sub(/\.rb\z/i, ''))
+ end
+ @compiled_path = path
+ 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
+ attr_reader :default_encoding
# 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.
#
@@ -156,31 +165,35 @@
def prepare
raise NotImplementedError
end
CLASS_METHOD = Kernel.instance_method(:class)
+ USE_BIND_CALL = RUBY_VERSION >= '2.7'
# 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)
locals_keys = locals.keys
locals_keys.sort!{|x, y| x.to_s <=> y.to_s}
+
case scope
when Object
- method = compiled_method(locals_keys, Module === scope ? scope : scope.class)
+ scope_class = Module === scope ? scope : scope.class
else
- if RUBY_VERSION >= '2'
- method = compiled_method(locals_keys, CLASS_METHOD.bind(scope).call)
- else
- method = compiled_method(locals_keys, Object)
- end
+ scope_class = USE_BIND_CALL ? CLASS_METHOD.bind_call(scope) : CLASS_METHOD.bind(scope).call
end
- method.bind(scope).call(locals, &block)
+ method = compiled_method(locals_keys, scope_class)
+
+ if USE_BIND_CALL
+ method.bind_call(scope, locals, &block)
+ else
+ method.bind(scope).call(locals, &block)
+ end
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
@@ -267,22 +280,62 @@
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)
- #{local_code}
- RUBY
+ if freeze_string_literals?
+ method_source << "# frozen-string-literal: true\n"
+ end
+
+ # Don't indent method source, to avoid indentation warnings when using compiled paths
+ method_source << "::Tilt::TOPOBJECT.class_eval do\ndef #{method_name}(locals)\n#{local_code}\n"
+
offset += method_source.count("\n")
method_source << source
method_source << "\nend;end;"
- (scope_class || Object).class_eval(method_source, eval_file, line - offset)
+
+ bind_compiled_method(method_source, offset, scope_class, local_keys)
unbind_compiled_method(method_name)
end
+ def bind_compiled_method(method_source, offset, scope_class, local_keys)
+ path = compiled_path
+ if path && scope_class.name
+ path = path.dup
+
+ if defined?(@compiled_path_counter)
+ path << '-' << @compiled_path_counter.succ!
+ else
+ @compiled_path_counter = "0".dup
+ end
+ path << ".rb"
+
+ # Wrap method source in a class block for the scope, so constant lookup works
+ method_source = "class #{scope_class.name}\n#{method_source}\nend"
+
+ load_compiled_method(path, method_source)
+ else
+ if path
+ warn "compiled_path (#{compiled_path.inspect}) ignored on template with anonymous scope_class (#{scope_class.inspect})"
+ end
+
+ eval_compiled_method(method_source, offset, scope_class)
+ end
+ end
+
+ def eval_compiled_method(method_source, offset, scope_class)
+ (scope_class || Object).class_eval(method_source, eval_file, line - offset)
+ end
+
+ def load_compiled_method(path, method_source)
+ File.binwrite(path, method_source)
+
+ # Use load and not require, so unbind_compiled_method does not
+ # break if the same path is used more than once.
+ load path
+ end
+
def unbind_compiled_method(method_name)
method = TOPOBJECT.instance_method(method_name)
TOPOBJECT.class_eval { remove_method(method_name) }
method
end
@@ -293,9 +346,13 @@
def extract_magic_comment(script)
binary(script) do
script[/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/n, 1]
end
+ end
+
+ def freeze_string_literals?
+ false
end
def binary(string)
original_encoding = string.encoding
string.force_encoding(Encoding::BINARY)