class Tilt::Template
methods.
the #prepare method and one of the #evaluate or #precompiled_template
Base class for template implementations. Subclasses must implement
def basename(suffix='')
def basename(suffix='') File.basename(@file, suffix) if @file end
def binary(string)
def binary(string) original_encoding = string.encoding string.force_encoding(Encoding::BINARY) yield ensure string.force_encoding(original_encoding) end
def bind_compiled_method(method_source, offset, scope_class)
def bind_compiled_method(method_source, offset, scope_class) 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 if freeze_string_literals? method_source_prefix = "# frozen-string-literal: true\n" method_source = method_source.sub(/\A# frozen-string-literal: true\n/, '') end method_source = "#{method_source_prefix}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 compile_template_method(local_keys, scope_class=nil)
def compile_template_method(local_keys, scope_class=nil) source, offset = precompiled(local_keys) local_code = local_extraction(local_keys) method_name = "__tilt_#{Thread.current.object_id.abs}" method_source = String.new method_source.force_encoding(source.encoding) 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;" bind_compiled_method(method_source, offset, scope_class) unbind_compiled_method(method_name) end
def compiled_method(locals_keys, scope_class=nil)
directly on the scope class, which are much faster to call than
Returns an UnboundMethod, which can be used to define methods
The compiled method for the locals keys and scope_class provided.
def compiled_method(locals_keys, scope_class=nil) key = [scope_class, locals_keys].freeze LOCK.synchronize do if meth = @compiled_method[key] return meth end end meth = compile_template_method(locals_keys, scope_class) LOCK.synchronize do @compiled_method[key] = meth end meth end
def compiled_path=(path)
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
def default_mime_type
def default_mime_type metadata[:mime_type] end
def default_mime_type=(value)
def default_mime_type=(value) metadata[:mime_type] = value end
def eval_compiled_method(method_source, offset, scope_class)
def eval_compiled_method(method_source, offset, scope_class) (scope_class || Object).class_eval(method_source, eval_file, line - offset) end
def eval_file
def eval_file @file || '(__TEMPLATE__)' end
def evaluate(scope, locals, &block)
This method is only used by source generating templates. Subclasses that
locals specified and with support for yielding to the block.
evaluation is guaranteed to be performed in the scope object with the
Execute the compiled template and return the result string. Template
def evaluate(scope, locals, &block) locals_keys = locals.keys locals_keys.sort!{|x, y| x.to_s <=> y.to_s} case scope when Object scope_class = Module === scope ? scope : scope.class else # :nocov: scope_class = USE_BIND_CALL ? CLASS_METHOD.bind_call(scope) : CLASS_METHOD.bind(scope).call # :nocov: end method = compiled_method(locals_keys, scope_class) if USE_BIND_CALL method.bind_call(scope, locals, &block) # :nocov: else method.bind(scope).call(locals, &block) # :nocov: end end
def extract_encoding(script, &block)
def extract_encoding(script, &block) extract_magic_comment(script, &block) || script.encoding end
def extract_magic_comment(script)
def extract_magic_comment(script) if script.frozen? script = script.dup yield script end binary(script) do script[/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/n, 1] end end
def freeze_string_literals?
def freeze_string_literals? false end
def initialize(file=nil, line=nil, options=nil)
a block is required.
it should read template data and return as a String. When file is nil,
default, template data is read from the file. When a block is given,
Create a new template with the file, line, and options specified. By
def initialize(file=nil, line=nil, options=nil) @file, @line, @options = nil, 1, nil process_arg(options) process_arg(line) process_arg(file) raise ArgumentError, "file or block required" unless @file || block_given? @options ||= {} set_compiled_method_cache # Force the encoding of the input data @default_encoding = @options.delete :default_encoding # Skip encoding detection from magic comments and forcing that encoding # for compiled templates @skip_compiled_encoding_detection = @options.delete :skip_compiled_encoding_detection # load template data and prepare (uses binread to avoid encoding issues) @data = block_given? ? yield(self) : read_template_file if @data.respond_to?(:force_encoding) if default_encoding @data = @data.dup if @data.frozen? @data.force_encoding(default_encoding) end if !@data.valid_encoding? raise Encoding::InvalidByteSequenceError, "#{eval_file} is not valid #{@data.encoding}" end end prepare end
def load_compiled_method(path, method_source)
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 local_extraction(local_keys)
def local_extraction(local_keys) assignments = local_keys.map do |k| if k.to_s =~ /\A[a-z_][a-zA-Z_0-9]*\z/ "#{k} = locals[#{k.inspect}]" else raise "invalid locals key: #{k.inspect} (keys must be variable names)" end end s = "locals = locals[:locals]" if assignments.delete(s) # If there is a locals key itself named `locals`, delete it from the ordered keys so we can # assign it last. This is important because the assignment of all other locals depends on the # `locals` local variable still matching the `locals` method argument given to the method # created in `#compile_template_method`. assignments << s end assignments.join("\n") end
def metadata
An empty Hash that the template engine can populate with various
def metadata @metadata ||= {} end
def metadata
An empty Hash that the template engine can populate with various
def metadata if respond_to?(:allows_script?) self.class.metadata.merge(:allows_script => allows_script?) else self.class.metadata end end
def name
def name if bname = basename bname.split('.', 2).first end end
def precompiled(local_keys)
offset. In most cases, overriding the #precompiled_template method is
control over source generation or want to adjust the default line
Template subclasses may override this method when they need complete
offset is the integer line offset where line reporting should begin.
source is the string containing (Ruby) source code for the template and
postamble and returns a two-tuple of the form: [source, offset], where
Generates all template source by combining the preamble, template, and
def precompiled(local_keys) preamble = precompiled_preamble(local_keys) template = precompiled_template(local_keys) postamble = precompiled_postamble(local_keys) source = String.new unless skip_compiled_encoding_detection? # Ensure that our generated source code has the same encoding as the # the source code generated by the template engine. template_encoding = extract_encoding(template){|t| template = t} if template.encoding != template_encoding # template should never be frozen here. If it was frozen originally, # then extract_encoding should yield a dup. template.force_encoding(template_encoding) end end source.force_encoding(template.encoding) source << preamble << "\n" << template << "\n" << postamble [source, preamble.count("\n")+1] end
def precompiled_postamble(local_keys)
def precompiled_postamble(local_keys) '' end
def precompiled_preamble(local_keys)
def precompiled_preamble(local_keys) '' end
def precompiled_template(local_keys)
support, custom scopes, proper encoding, and support for template
the base Template guarantees correct file/line handling, locals
method or the #precompiled method be overridden. When defined,
default Template#evaluate implementation requires either this
A string containing the (Ruby) source code for the template. The
def precompiled_template(local_keys) raise NotImplementedError end
def prepare
variables set in this method are available when #evaluate is called.
engine. Called immediately after template data is loaded. Instance
Do whatever preparation is necessary to setup the underlying template
def prepare end
def process_arg(arg)
def process_arg(arg) if 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 when arg.respond_to?(:path) ; @file = arg.path when arg.respond_to?(:to_path) ; @file = arg.to_path else raise TypeError, "Can't load the template file. Pass a string with a path " + "or an object that responds to 'to_str', 'path' or 'to_path'" end end end
def read_template_file
def read_template_file data = File.binread(file) # Set it to the default external (without verifying) # :nocov: data.force_encoding(Encoding.default_external) if Encoding.default_external # :nocov: data end
def render(scope=nil, locals=nil, &block)
block is given, it is typically available within the template via
Render the template in the given scope with the locals specified. If a
def render(scope=nil, locals=nil, &block) evaluate(scope || Object.new, locals || EMPTY_HASH, &block) end
def set_compiled_method_cache
def set_compiled_method_cache @compiled_method = {} end
def skip_compiled_encoding_detection?
def skip_compiled_encoding_detection? @skip_compiled_encoding_detection end
def unbind_compiled_method(method_name)
def unbind_compiled_method(method_name) method = TOPOBJECT.instance_method(method_name) TOPOBJECT.class_eval { remove_method(method_name) } method end