require'thread'moduleTilt# @privateTOPOBJECT=ifRUBY_VERSION>='2.0'# @privatemoduleCompiledTemplatesselfendelsifRUBY_VERSION>='1.9'BasicObjectelseObjectend# @privateLOCK=Mutex.new# Base class for template implementations. Subclasses must implement# the #prepare method and one of the #evaluate or #precompiled_template# methods.classTemplate# 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:optionsclass<<self# An empty Hash that the template engine can populate with various# metadata.defmetadata@metadata||={}end# @deprecated Use `.metadata[:mime_type]` instead.defdefault_mime_typemetadata[:mime_type]end# @deprecated Use `.metadata[:mime_type] = val` instead.defdefault_mime_type=(value)metadata[:mime_type]=valueendend# 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.definitialize(file=nil,line=1,options={},&block)@file,@line,@options=nil,1,{}[options,line,file].compact.eachdo|arg|casewhenarg.respond_to?(:to_str);@file=arg.to_strwhenarg.respond_to?(:to_int);@line=arg.to_intwhenarg.respond_to?(:to_hash);@options=arg.to_hash.dupwhenarg.respond_to?(:path);@file=arg.pathwhenarg.respond_to?(:to_path);@file=arg.to_pathelseraiseTypeError,"Can't load the template file. Pass a string with a path "+"or an object that responds to 'to_str', 'path' or 'to_path'"endendraiseArgumentError,"file or block required"if(@file||block).nil?# 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|read_template_file}@data=@reader.call(self)if@data.respond_to?(:force_encoding)ifdefault_encoding@data=@data.dupif@data.frozen?@data.force_encoding(default_encoding)endif!@data.valid_encoding?raiseEncoding::InvalidByteSequenceError,"#{eval_file} is not valid #{@data.encoding}"endendprepareend# 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+.defrender(scope=nil,locals={},&block)scope||=Object.newcurrent_template=Thread.current[:tilt_current_template]Thread.current[:tilt_current_template]=selfevaluate(scope,locals||{},&block)ensureThread.current[:tilt_current_template]=current_templateend# The basename of the template file.defbasename(suffix='')File.basename(file,suffix)iffileend# The template file's basename with all extensions chomped off.defnamebasename.split('.',2).firstifbasenameend# The filename used in backtraces to describe the template.defeval_filefile||'(__TEMPLATE__)'end# An empty Hash that the template engine can populate with various# metadata.defmetadataifrespond_to?(:allows_script?)self.class.metadata.merge(:allows_script=>allows_script?)elseself.class.metadataendendprotected# @!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.defdefault_encoding@default_encodingend# 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.defprepareraiseNotImplementedErrorend# 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.defevaluate(scope,locals,&block)locals_keys=locals.keyslocals_keys.sort!{|x,y|x.to_s<=>y.to_s}method=compiled_method(locals_keys)method.bind(scope).call(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.defprecompiled(local_keys)preamble=precompiled_preamble(local_keys)template=precompiled_template(local_keys)postamble=precompiled_postamble(local_keys)source=String.new# Ensure that our generated source code has the same encoding as the# the source code generated by the template engine.ifsource.respond_to?(:force_encoding)template_encoding=extract_encoding(template)source.force_encoding(template_encoding)template.force_encoding(template_encoding)endsource<<preamble<<"\n"<<template<<"\n"<<postamble[source,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, proper encoding, and support for template# compilation.defprecompiled_template(local_keys)raiseNotImplementedErrorenddefprecompiled_preamble(local_keys)''enddefprecompiled_postamble(local_keys)''end# !@endgroupprivatedefread_template_filedata=File.open(file,'rb'){|io|io.read}ifdata.respond_to?(:force_encoding)# Set it to the default external (without verifying)data.force_encoding(Encoding.default_external)ifEncoding.default_externalenddataend# The compiled method for the locals keys provided.defcompiled_method(locals_keys)LOCK.synchronizedo@compiled_method[locals_keys]||=compile_template_method(locals_keys)endenddeflocal_extraction(local_keys)local_keys.mapdo|k|ifk.to_s=~/\A[a-z_][a-zA-Z_0-9]*\z/"#{k} = locals[#{k.inspect}]"elseraise"invalid locals key: #{k.inspect} (keys must be variable names)"endend.join("\n")enddefcompile_template_method(local_keys)source,offset=precompiled(local_keys)local_code=local_extraction(local_keys)method_name="__tilt_#{Thread.current.object_id.abs}"method_source=String.newifmethod_source.respond_to?(:force_encoding)method_source.force_encoding(source.encoding)endmethod_source<<<<-RUBY
TOPOBJECT.class_eval do
def #{method_name}(locals)
Thread.current[:tilt_vars] = [self, locals]
class << self
this, locals = Thread.current[:tilt_vars]
locals = locals
this.instance_eval do
#{local_code} RUBYoffset+=method_source.count("\n")method_source<<sourcemethod_source<<"\nend;end;end;end"Object.class_eval(method_source,eval_file,line-offset)unbind_compiled_method(method_name)enddefunbind_compiled_method(method_name)method=TOPOBJECT.instance_method(method_name)TOPOBJECT.class_eval{remove_method(method_name)}methodenddefextract_encoding(script)extract_magic_comment(script)||script.encodingenddefextract_magic_comment(script)binary(script)doscript[/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/n,1]endenddefbinary(string)original_encoding=string.encodingstring.force_encoding(Encoding::BINARY)yieldensurestring.force_encoding(original_encoding)endendend