lib/tilt.rb in tilt-1.1 vs lib/tilt.rb in tilt-1.2

- old
+ new

@@ -1,9 +1,8 @@ -require 'digest/md5' - module Tilt - VERSION = '1.1' + TOPOBJECT = defined?(BasicObject) ? BasicObject : Object + VERSION = '1.2' @template_mappings = {} # Hash of template path pattern => template implementation class mappings. def self.mappings @@ -40,23 +39,14 @@ pattern.sub!(/^[^.]*\.?/, '') until (pattern.empty? || registered?(pattern)) end @template_mappings[pattern] end - # Mixin allowing template compilation on scope objects. - # - # Including this module in scope objects passed to Template#render - # causes template source to be compiled to methods the first time they're - # used. This can yield significant (5x-10x) performance increases for - # templates that support it (ERB, Erubis, Builder). - # - # It's also possible (though not recommended) to include this module in - # Object to enable template compilation globally. The downside is that - # the template methods will polute the global namespace and could lead to - # unexpected behavior. + # Deprecated module. module CompileSite - def __tilt__ + def self.append_features(*) + warn "WARNING: Tilt::CompileSite is deprecated and will be removed in Tilt 2.0 (#{caller.first})." end end # Base class for template implementations. Subclasses must implement # the #prepare method and one of the #evaluate or #precompiled_template @@ -109,16 +99,19 @@ if !self.class.engine_initialized? initialize_engine self.class.engine_initialized = true end - # used to generate unique method names for template compilation - @stamp = (Time.now.to_f * 10000).to_i - @compiled_method_names = {} + # used to hold compiled template methods + @compiled_method = {} - # load template data and prepare - @reader = block || lambda { |t| File.read(@file) } + # 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| File.respond_to?(:binread) ? File.binread(@file) : File.read(@file) } @data = @reader.call(self) prepare end # Render the template in the given scope with the locals specified. If a @@ -173,29 +166,25 @@ else raise NotImplementedError end end - # Process the template and return the result. When the scope mixes in - # the Tilt::CompileSite module, the template is compiled to a method and - # reused given identical locals keys. When the scope object - # does not mix in the CompileSite module, the template source is - # evaluated with instance_eval. In any case, template executation - # is guaranteed to be performed in the scope object with the locals - # specified and with support for yielding to the block. + # Process the template and return the result. The first time this + # method is called, the template source is evaluated with instance_eval. + # On the sequential method calls it will compile the template to an + # unbound method which will lead to better performance. In any case, + # template executation is guaranteed to be performed in the scope object + # with the locals specified and with support for yielding to the block. def evaluate(scope, locals, &block) - if scope.respond_to?(:__tilt__) - method_name = compiled_method_name(locals.keys) - if scope.respond_to?(method_name) - scope.send(method_name, locals, &block) - else - compile_template_method(method_name, locals) - scope.send(method_name, locals, &block) - end - else - evaluate_source(scope, locals, &block) + # Redefine itself to use method compilation the next time: + def self.evaluate(scope, locals, &block) + method = compiled_method(locals.keys) + method.bind(scope).call(locals, &block) end + + # Use instance_eval the first time: + evaluate_source(scope, 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 @@ -205,13 +194,20 @@ # 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) + magic_comment = extract_magic_comment(template) + if magic_comment + # Magic comment e.g. "# coding: utf-8" has to be in the first line. + # So we copy the magic comment to the first line. + preamble = magic_comment + "\n" + preamble + end parts = [ preamble, - precompiled_template(locals), + template, precompiled_postamble(locals) ] [parts.join("\n"), preamble.count("\n") + 1] end @@ -239,14 +235,14 @@ # template source. def precompiled_postamble(locals) '' end - # The unique compiled method name for the locals keys provided. - def compiled_method_name(locals_keys) - @compiled_method_names[locals_keys] ||= - generate_compiled_method_name(locals_keys) + # The compiled method for the locals keys provided. + def compiled_method(locals_keys) + @compiled_method[locals_keys] ||= + compile_template_method(locals_keys) end private # Evaluate the template source in the context of the scope object. def evaluate_source(scope, locals, &block) @@ -272,66 +268,63 @@ file, lineno = eval_file, (line - offset) - 1 scope.instance_eval { Kernel::eval(source, binding, file, lineno) } end end - def generate_compiled_method_name(locals_keys) - parts = [object_id, @stamp] + locals_keys.map { |k| k.to_s }.sort - digest = Digest::MD5.hexdigest(parts.join(':')) - "__tilt_#{digest}" - end - - def compile_template_method(method_name, locals) + def compile_template_method(locals) source, offset = precompiled(locals) offset += 5 - CompileSite.class_eval <<-RUBY, eval_file, line - offset - def #{method_name}(locals) - Thread.current[:tilt_vars] = [self, locals] - class << self - this, locals = Thread.current[:tilt_vars] - this.instance_eval do - #{source} + method_name = "__tilt_#{Thread.current.object_id.abs}" + Object.class_eval <<-RUBY, eval_file, line - offset + #{extract_magic_comment source} + 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 + #{source} + end end end end RUBY - ObjectSpace.define_finalizer self, - Template.compiled_template_method_remover(CompileSite, method_name) + unbind_compiled_method(method_name) end - def self.compiled_template_method_remover(site, method_name) - proc { |oid| garbage_collect_compiled_template_method(site, method_name) } + def unbind_compiled_method(method_name) + method = TOPOBJECT.instance_method(method_name) + TOPOBJECT.class_eval { remove_method(method_name) } + method end - def self.garbage_collect_compiled_template_method(site, method_name) - site.module_eval do - begin - remove_method(method_name) - rescue NameError - # method was already removed (ruby >= 1.9) - end - end + def extract_magic_comment(script) + comment = script.slice(/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/) + return comment if comment and not %w[ascii-8bit binary].include?($1.downcase) + "# coding: #{@default_encoding}" if @default_encoding end # Special case Ruby 1.9.1's broken yield. # # http://github.com/rtomayko/tilt/commit/20c01a5 # http://redmine.ruby-lang.org/issues/show/3601 # # Remove when 1.9.2 dominates 1.9.1 installs in the wild. if RUBY_VERSION =~ /^1.9.1/ undef compile_template_method - def compile_template_method(method_name, locals) + def compile_template_method(locals) source, offset = precompiled(locals) offset += 1 - CompileSite.module_eval <<-RUBY, eval_file, line - offset - def #{method_name}(locals) - #{source} + method_name = "__tilt_#{Thread.current.object_id}" + Object.class_eval <<-RUBY, eval_file, line - offset + TOPOBJECT.class_eval do + def #{method_name}(locals) + #{source} + end end RUBY - ObjectSpace.define_finalizer self, - Template.compiled_template_method_remover(CompileSite, method_name) + unbind_compiled_method(method_name) end end end # Extremely simple template cache implementation. Calling applications @@ -633,10 +626,11 @@ def evaluate(scope, locals, &block) xml = ::Nokogiri::XML::Builder.new if data.respond_to?(:to_str) locals[:xml] = xml + block &&= proc { yield.gsub(/^<\?xml version=\"1\.0\"\?>\n?/, "") } super(scope, locals, &block) elsif data.kind_of?(Proc) data.call(xml) end xml.to_xml @@ -742,10 +736,33 @@ register 'markdown', RDiscountTemplate register 'mkd', RDiscountTemplate register 'md', RDiscountTemplate + # BlueCloth Markdown implementation. See: + # http://deveiate.org/projects/BlueCloth/ + # + # RDiscount is a simple text filter. It does not support +scope+ or + # +locals+. The +:smartypants+ and +:escape_html+ options may be set true + # to enable those flags on the underlying BlueCloth object. + class BlueClothTemplate < Template + def initialize_engine + return if defined? ::BlueCloth + require_template_library 'bluecloth' + end + + def prepare + @engine = BlueCloth.new(data, options) + @output = nil + end + + def evaluate(scope, locals, &block) + @output ||= @engine.to_html + end + end + + # RedCloth implementation. See: # http://redcloth.org/ class RedClothTemplate < Template def initialize_engine return if defined? ::RedCloth @@ -847,19 +864,23 @@ def evaluate(scope, locals, &block) builder = self.class.builder_class.new({}, scope) builder.locals = locals - if block + if data.kind_of? Proc + (class << builder; self end).send(:define_method, :__run_markaby_tilt__, &data) + else builder.instance_eval <<-CODE, __FILE__, __LINE__ def __run_markaby_tilt__ #{data} end CODE + end + if block builder.__capture_markaby_tilt__(&block) else - builder.instance_eval(data, __FILE__, __LINE__) + builder.__run_markaby_tilt__ end builder.to_s end end