module Merb::InlineTemplates end module Merb::Template EXTENSIONS = {} unless defined?(EXTENSIONS) METHOD_LIST = {} unless defined?(METHOD_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. def template_name(path) path = File.expand_path(path) path.gsub(/[^\.a-zA-Z0-9]/, "__").gsub(/\./, "_") end # 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). #--- # @semipublic def load_template_io(path) File.open(path) 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. # # ==== Returns # DOC #--- # @semipublic def template_for(path, template_stack = []) path = File.expand_path(path) ret = if Merb::Config[:reload_templates] file = Dir["#{path}.{#{template_extensions.join(',')}}"].first METHOD_LIST[path] = file ? inline_template(load_template_io(file)) : nil else METHOD_LIST[path] ||= begin file = Dir["#{path}.{#{template_extensions.join(',')}}"].first file ? inline_template(load_template_io(file)) : nil end end ret end # Get all known template extensions # # ==== Returns # Array:: Extension strings. #--- # @semipublic 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) # mod:: # The module to put the compiled method into. Defaults to # Merb::InlineTemplates # # ==== Notes # Even though this method supports inlining into any module, the method # must be available to instances of AbstractController that will use it. #--- # @public def inline_template(io, mod = Merb::InlineTemplates) path = File.expand_path(io.path) METHOD_LIST[path.gsub(/\.[^\.]*$/, "")] = engine_for(path).compile_template(io, template_name(path), mod) end # Finds the engine for a particular path. # # ==== Parameters # path:: The path of the file to find an engine for. # # ==== Returns # Class:: The engine. #--- # @semipublic 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. # # ==== Example # Merb::Template.register_extensions(Merb::Template::Erubis, ["erb"]) #--- # @public 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. # mod:: The module that the compiled method will be placed into. def self.compile_template(io, name, mod) template = ::Erubis::BlockAwareEruby.new(io.read) _old_verbose, $VERBOSE = $VERBOSE, nil Merb.logger.fatal template.src template.def_method(mod, name, 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 %> def capture_erb(*args, &block) _old_buf, @_erb_buf = @_erb_buf, "" block.call(*args) ret = @_erb_buf @_erb_buf = _old_buf ret end # DOC def concat_erb(string, binding) @_erb_buf << string end end Merb::Template.register_extensions(self, %w[erb]) end end module Erubis module BlockAwareEnhancer def add_preamble(src) src << "_old_buf, @_erb_buf = @_erb_buf, ''; " src << "@_engine = 'erb'; " end def add_postamble(src) src << "\n" unless src[-1] == ?\n src << "_ret = @_erb_buf; @_erb_buf = _old_buf; _ret.to_s;\n" end def add_text(src, text) src << " @_erb_buf.concat('" << escape_text(text) << "'); " end def add_expr_escaped(src, code) src << ' @_erb_buf.concat(' << escaped_expr(code) << ');' end def add_stmt2(src, code, tailch) src << code src << " ).to_s; " if tailch == "=" src << ';' unless code[-1] == ?\n end 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