module Merb::InlineTemplates end module Merb::Template EXTENSIONS = {} unless defined?(EXTENSIONS) METHOD_LIST = {} unless defined?(METHOD_LIST) SUPPORTED_LOCALS_LIST = Hash.new([].freeze) unless defined?(SUPPORTED_LOCALS_LIST) MTIMES = {} unless defined?(MTIMES) class << self # Get the template's method name from a full path. This replaces # non-alphanumeric characters with __ and "." with "_" # # Collisions are potentially possible with something like: # ~foo.bar and __foo.bar or !foo.bar. # # ==== Parameters # path:: A full path to convert to a valid Ruby method name # # ==== Returns # String:: The template name. # # # We might want to replace this with something that varies the # character replaced based on the non-alphanumeric character # to avoid edge-case collisions. # # :api: private def template_name(path) path = File.expand_path(path) path.gsub(/[^\.a-zA-Z0-9]/, "__").gsub(/\./, "_") end chainable do # For a given path, get an IO object that responds to #path. # # This is so that plugins can override this if they provide # mechanisms for specifying templates that are not just simple # files. The plugin is responsible for ensuring that the fake # path provided will work with #template_for, and thus the # RenderMixin in general. # # ==== Parameters # path:: A full path to find a template for. This is the # path that the RenderMixin assumes it should find the template # in. # # ==== Returns # IO#path:: An IO object that responds to path (File or VirtualFile). # # :api: plugin # @overridable def load_template_io(path) file = Dir["#{path}.{#{template_extensions.join(',')}}"].first File.open(file, "r") if file end end # Get the name of the template method for a particular path. # # ==== Parameters # path:: A full path to find a template method for. # template_stack:: The template stack. Not used. # locals:: The names of local variables # # ==== Returns # :: name of the method that inlines the template. # # :api: private def template_for(path, template_stack = [], locals=[]) path = File.expand_path(path) if needs_compilation?(path, locals) template_io = load_template_io(path) METHOD_LIST[path] = inline_template(template_io, locals) if template_io end METHOD_LIST[path] end # Decide if a template needs to be re/compiled. # # ==== Parameters # path:: The full path of the template to check support for. # locals:: The list of locals that need to be supported # # ==== Returns # Boolean:: Whether or not the template for the provided path needs to be recompiled # # :api: private def needs_compilation?(path, locals) return true if Merb::Config[:reload_templates] || !METHOD_LIST[path] current_locals = SUPPORTED_LOCALS_LIST[path] current_locals != locals && !(locals - current_locals).empty? end # Get all known template extensions # # ==== Returns # Array:: Extension strings. # # :api: plugin def template_extensions EXTENSIONS.keys end # Takes a template at a particular path and inlines it into a module and # adds it to the METHOD_LIST table to speed lookup later. # # ==== Parameters # io<#path>:: # An IO that responds to #path (File or VirtualFile) # locals:: # A list of local names that should be assigned in the template method # from the arguments hash. Defaults to []. # mod:: # The module to put the compiled method into. Defaults to # Merb::InlineTemplates # # ==== Returns # Symbol:: The name of the method that the template was compiled into. # # ==== Notes # Even though this method supports inlining into any module, the method # must be available to instances of AbstractController that will use it. # # :api: private def inline_template(io, locals=[], mod = Merb::InlineTemplates) full_file_path = File.expand_path(io.path) engine_neutral_path = full_file_path.gsub(/\.[^\.]*$/, "") local_list = (SUPPORTED_LOCALS_LIST[engine_neutral_path] |= locals) ret = METHOD_LIST[engine_neutral_path] = engine_for(full_file_path).compile_template(io, template_name(full_file_path), local_list, mod) io.close ret end # Finds the engine for a particular path. # # ==== Parameters # path:: The path of the file to find an engine for. # # ==== Returns # Class:: The engine. # # :api: private def engine_for(path) path = File.expand_path(path) EXTENSIONS[path.match(/\.([^\.]*)$/)[1]] end # Registers the extensions that will trigger a particular templating # engine. # # ==== Parameters # engine:: The class of the engine that is being registered # extensions:: # The list of extensions that will be registered with this templating # language # # ==== Raises # ArgumentError:: engine does not have a compile_template method. # # ==== Returns # nil # # ==== Example # Merb::Template.register_extensions(Merb::Template::Erubis, ["erb"]) # # :api: plugin def register_extensions(engine, extensions) raise ArgumentError, "The class you are registering does not have a compile_template method" unless engine.respond_to?(:compile_template) extensions.each{|ext| EXTENSIONS[ext] = engine } Merb::AbstractController.class_eval <<-HERE include #{engine}::Mixin HERE end end require 'erubis' class Erubis # ==== Parameters # io<#path>:: An IO containing the full path of the template. # name:: The name of the method that will be created. # locals:: A list of locals to assign from the args passed into the compiled template. # mod:: The module that the compiled method will be placed into. # # :api: private def self.compile_template(io, name, locals, mod) template = ::Erubis::BlockAwareEruby.new(io.read) _old_verbose, $VERBOSE = $VERBOSE, nil assigns = locals.inject([]) do |assigns, local| assigns << "#{local} = _locals[#{local.inspect}]" end.join(";") code = "def #{name}(_locals={}); #{assigns}; #{template.src}; end" mod.module_eval code, File.expand_path(io.path) $VERBOSE = _old_verbose name end module Mixin # ==== Parameters # *args:: Arguments to pass to the block. # &block:: The template block to call. # # ==== Returns # String:: The output of the block. # # ==== Examples # Capture being used in a .html.erb page: # # <% @foo = capture do %> #

Some Foo content!

# <% end %> # # :api: private def capture_erb(*args, &block) _old_buf, @_erb_buf = @_erb_buf, "" block.call(*args) ret = @_erb_buf @_erb_buf = _old_buf ret end # :api: private def concat_erb(string, binding) @_erb_buf << string end end Merb::Template.register_extensions(self, %w[erb]) end end module Erubis module BlockAwareEnhancer # :api: private def add_preamble(src) src << "_old_buf, @_erb_buf = @_erb_buf, ''; " src << "@_engine = 'erb'; " end # :api: private def add_postamble(src) src << "\n" unless src[-1] == ?\n src << "_ret = @_erb_buf; @_erb_buf = _old_buf; _ret.to_s;\n" end # :api: private def add_text(src, text) src << " @_erb_buf.concat('" << escape_text(text) << "'); " end # :api: private def add_expr_escaped(src, code) src << ' @_erb_buf.concat(' << escaped_expr(code) << ');' end # :api: private def add_stmt2(src, code, tailch) src << code src << " ).to_s; " if tailch == "=" src << ';' unless code[-1] == ?\n end # :api: private def add_expr_literal(src, code) if code =~ /(do|\{)(\s*\|[^|]*\|)?\s*\Z/ src << ' @_erb_buf.concat( ' << code << "; " else src << ' @_erb_buf.concat((' << code << ').to_s);' end end end class BlockAwareEruby < Eruby include BlockAwareEnhancer end # module RubyEvaluator # # # DOC # def def_method(object, method_name, filename=nil) # m = object.is_a?(Module) ? :module_eval : :instance_eval # setup = "@_engine = 'erb'" # object.__send__(m, "def #{method_name}(locals={}); #{setup}; #{@src}; end", filename || @filename || '(erubis)') # end # # end end