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