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)