123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- module Tilt
- TOPOBJECT = defined?(BasicObject) ? BasicObject : 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
- 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| 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
- # 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
- 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
- def evaluate(scope, locals, &block)
- cached_evaluate(scope, locals, &block)
- end
- # 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 cached_evaluate(scope, locals, &block)
- # Redefine itself to use method compilation the next time:
- def self.cached_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
- # 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)
- 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,
- template,
- precompiled_postamble(locals)
- ]
- [parts.join("\n"), 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, and support for template compilation when the scope object
- # allows it.
- 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 { |k,v| "#{k} = locals[#{k.inspect}]" }.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
- # Evaluate the template source in the context of the scope object.
- def evaluate_source(scope, locals, &block)
- source, offset = precompiled(locals)
- scope.instance_eval(source, eval_file, line - offset)
- end
- # JRuby doesn't allow Object#instance_eval to yield to the block it's
- # closed over. This is by design and (ostensibly) something that will
- # change in MRI, though no current MRI version tested (1.8.6 - 1.9.2)
- # exhibits the behavior. More info here:
- #
- # http://jira.codehaus.org/browse/JRUBY-2599
- #
- # We redefine evaluate_source to work around this issues.
- if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
- undef evaluate_source
- def evaluate_source(scope, locals, &block)
- source, offset = precompiled(locals)
- file, lineno = eval_file, (line - offset)
- scope.instance_eval { Kernel::eval(source, binding, file, lineno) }
- end
- end
- def compile_template_method(locals)
- source, offset = precompiled(locals)
- offset += 5
- 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
- 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_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(locals)
- source, offset = precompiled(locals)
- offset += 1
- 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
- unbind_compiled_method(method_name)
- end
- end
- end
- end
|