require "tilt" class Roda module RodaPlugins # The render plugin adds support for template rendering using the tilt # library. Two methods are provided for template rendering, +view+ # (which uses the layout) and +render+ (which does not). # # plugin :render # # route do |r| # r.is 'foo' do # view('foo') # renders views/foo.erb inside views/layout.erb # end # # r.is 'bar' do # render('bar') # renders views/bar.erb # end # end # # You can provide options to the plugin method: # # plugin :render, :engine=>'haml', :views=>'admin_views' # # The following options are supported: # # :cache :: nil/false to not cache templates (useful for development), defaults # to true unless RACK_ENV is development to automatically use the # default template cache. # :engine :: The tilt engine to use for rendering, defaults to 'erb'. # :escape :: Use Roda's Erubis escaping support, which makes <%= %> escape output, # <%== %> not escape output, and handles postfix conditions inside # <%= %> tags. # :ext :: The file extension to assume for view files, defaults to the :engine # option. # :layout :: The base name of the layout file, defaults to 'layout'. # :layout_opts :: The options to use when rendering the layout, if different # from the default options. # :template_opts :: The tilt options used when rendering templates, defaults to # {:outvar=>'@_out_buf', :default_encoding=>Encoding.default_external}. # :views :: The directory holding the view files, defaults to 'views' in the # current directory. # # Most of these options can be overridden at runtime by passing options # to the +view+ or +render+ methods: # # view('foo', :ext=>'html.erb') # render('foo', :views=>'admin_views') # # There are a couple of additional options to +view+ and +render+ that are # available at runtime: # # :content :: Only respected by +view+, provides the content to render # inside the layout, instead of rendering a template to get # the content. # :inline :: Use the value given as the template code, instead of looking # for template code in a file. # :locals :: Hash of local variables to make available inside the template. # :path :: Use the value given as the full pathname for the file, instead # of using the :views and :ext option in combination with the # template name. # :template :: Provides the name of the template to use. This allows you # pass a single options hash to the render/view method, while # still allowing you to specify the template name. # :template_block :: Pass this block when creating the underlying template, # ignored when using :inline. # :template_class :: Provides the template class to use, inside of using # Tilt or Tilt[:engine]. # # Here's how those options are used: # # view(:inline=>'<%= @foo %>') # render(:path=>'/path/to/template.erb') # # If you pass a hash as the first argument to +view+ or +render+, it should # have either +:template+, +:inline+, +:path+, or +:content+ (for +view+) as # one of the keys. module Render OPTS={}.freeze def self.load_dependencies(app, opts=OPTS) if opts[:escape] app.plugin :_erubis_escaping end end # Setup default rendering options. See Render for details. def self.configure(app, opts=OPTS) orig_opts = opts if app.opts[:render] app.opts[:render] = app.opts[:render].merge(opts) else app.opts[:render] = opts.dup end opts = app.opts[:render] opts[:engine] ||= "erb" opts[:ext] = nil unless opts.has_key?(:ext) opts[:views] ||= File.expand_path("views", Dir.pwd) opts[:layout_opts] = (opts[:layout_opts] || {}).dup if layout = orig_opts.fetch(:layout, true) opts[:layout] = true unless opts.has_key?(:layout) case layout when Hash opts[:layout_opts].merge!(layout) when true opts[:layout_opts][:template] ||= 'layout' else opts[:layout_opts][:template] = layout end end template_opts = opts[:template_opts] = (opts[:template_opts] || {}).dup template_opts[:outvar] ||= '@_out_buf' if RUBY_VERSION >= "1.9" && !template_opts.has_key?(:default_encoding) template_opts[:default_encoding] = Encoding.default_external end if opts[:escape] template_opts[:engine_class] = ErubisEscaping::Eruby end opts[:cache] = app.thread_safe_cache if opts.fetch(:cache, ENV['RACK_ENV'] != 'development') opts[:layout_opts].freeze opts[:template_opts].freeze opts.freeze end module ClassMethods # Copy the rendering options into the subclass, duping # them as necessary to prevent changes in the subclass # affecting the parent class. def inherited(subclass) super opts = subclass.opts[:render] = subclass.opts[:render].dup opts[:cache] = thread_safe_cache if opts[:cache] opts.freeze end # Return the render options for this class. def render_opts opts[:render] end end module InstanceMethods # Render the given template. See Render for details. def render(template, opts = OPTS, &block) opts = find_template(parse_template_opts(template, opts)) cached_template(opts) do template_opts = render_opts[:template_opts] current_template_opts = opts[:template_opts] template_opts = template_opts.merge(current_template_opts) if current_template_opts opts[:template_class].new(opts[:path], 1, template_opts, &opts[:template_block]) end.render(self, (opts[:locals]||OPTS), &block) end # Return the render options for the instance's class. While this # is not currently frozen, it may be frozen in a future version, # so you should not attempt to modify it. def render_opts self.class.render_opts end # Render the given template. If there is a default layout # for the class, take the result of the template rendering # and render it inside the layout. See Render for details. def view(template, opts=OPTS) opts = parse_template_opts(template, opts) content = opts[:content] || render_template(opts) if layout_opts = view_layout_opts(opts) content = render_template(layout_opts){content} end content end private # Private alias for render. Should be used by other plugins when they want to render a template # without a layout, as plugins can override render to use a layout. alias render_template render # If caching templates, attempt to retrieve the template from the cache. Otherwise, just yield # to get the template. def cached_template(opts, &block) if cache = render_opts[:cache] key = opts[:key] unless template = cache[key] template = cache[key] = yield end template else yield end end # Given the template name and options, return the template class, template path/content, # and template block to use for the render. def find_template(opts) if content = opts[:inline] path = opts[:path] = content template_class = opts[:template_class] ||= ::Tilt[opts[:engine] || render_opts[:engine]] opts[:template_block] = Proc.new{content} else path = opts[:path] ||= template_path(opts) template_class = opts[:template_class] opts[:template_class] ||= ::Tilt end if render_opts[:cache] template_opts = opts[:template_opts] template_block = opts[:template_block] if !content key = if template_class || template_opts || template_block [path, template_class, template_opts, template_block] else path end opts[:key] = key end opts end # Return a single hash combining the template and opts arguments. def parse_template_opts(template, opts) template = {:template=>template} unless template.is_a?(Hash) opts.merge(template) end # The name to use for the template. By default, just converts the :template option to a string. def template_name(opts) opts[:template].to_s end # The template path for the given options. def template_path(opts) render_opts = render_opts() "#{opts[:views] || render_opts[:views]}/#{template_name(opts)}.#{opts[:ext] || render_opts[:ext] || render_opts[:engine]}" end # If a layout should be used, return a hash of options for # rendering the layout template. If a layout should not be # used, return nil. def view_layout_opts(opts) if layout = opts.fetch(:layout, render_opts[:layout]) layout_opts = if opts[:layout_opts] opts[:layout_opts].merge(render_opts[:layout_opts]) else render_opts[:layout_opts].dup end case layout when Hash layout_opts.merge!(layout) when true # use default layout else layout_opts[:template] = layout end layout_opts end end end end register_plugin(:render, Render) end end