require 'tilt'
require 'goliath/validation/standard_http_errors'
module Goliath
module Rack
# Template rendering methods. Each method takes t as a Symbol (for template
# file lookup) or as a String (to render directly), as well as an optional
# hashes giving additional options and local variables. It returns a String
# with the rendered output.
#
# This is mostly similar to the code from Sinatra (indeed, it's stolen from
# there). It does not compile or cache templates, and the find_template
# method is simpler.
#
# @author Sinatra project -- https://github.com/sinatra/sinatra/contributors
module Templates
# lets us decorate the string response with a .content_type accessor
# @private
module ContentTyped
attr_accessor :content_type
end
# Render an erb template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def erb(template, options = {}, locals = {})
render :erb, template, options, locals
end
# Render an erubis template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def erubis(template, options = {}, locals = {})
render :erubis, template, options, locals
end
# Render a haml template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def haml(template, options = {}, locals = {})
render :haml, template, options, locals
end
# Render a sass template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def sass(template, options = {}, locals = {})
options.merge! :layout => false, :default_content_type => :css
render :sass, template, options, locals
end
# Render an scss template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def scss(template, options = {}, locals = {})
options.merge! :layout => false, :default_content_type => :css
render :scss, template, options, locals
end
# Render a less template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def less(template, options = {}, locals = {})
options.merge! :layout => false, :default_content_type => :css
render :less, template, options, locals
end
# Render a builder template
#
# @example
# # produces:
# #
# #
# # Francis Albert Sinatra
# # Frank Sinatra
# #
# builder do |xml|
# xml.instruct!
# xml.person do
# xml.name "Francis Albert Sinatra", :aka => "Frank Sinatra"
# xml.email 'frank@capitolrecords.com'
# end
# end
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @yield block If the builder method is given a block, the block is called directly
# with an XmlMarkup instance and used as a template.
# @return [String] The rendered template
# @see #render
def builder(template = nil, options = {}, locals = {}, &block)
options[:default_content_type] = :xml
render_ruby(:builder, template, options, locals, &block)
end
# Render a liquid template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def liquid(template, options = {}, locals = {})
render :liquid, template, options, locals
end
# Render a markdown template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def markdown(template, options = {}, locals = {})
render :markdown, template, options, locals
end
# Render a textile template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def textile(template, options = {}, locals = {})
render :textile, template, options, locals
end
# Render an rdoc template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def rdoc(template, options = {}, locals = {})
render :rdoc, template, options, locals
end
# Render a radius template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def radius(template, options = {}, locals = {})
render :radius, template, options, locals
end
# Render a markaby template
#
# @example
# markaby do
# html do
# head { title "Sinatra With Markaby" }
# body { h1 "Markaby Is Fun!" }
# end
# end
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @yield block A block template
# @return [String] The rendered template
# @see #render
def markaby(template = nil, options = {}, locals = {}, &block)
render_ruby(:mab, template, options, locals, &block)
end
# Render a coffee template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def coffee(template, options = {}, locals = {})
options.merge! :layout => false, :default_content_type => :js
render :coffee, template, options, locals
end
# Render a nokogiri template
#
# @example
# # produces
# #
# nokogiri do |doc|
# doc.ul do
# doc.li 'hello'
# doc.li 'admin', :class => 'current' if current_user.is_admin?
# end
# end
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @yield block A block template
# @return [String] The rendered template
# @see #render
def nokogiri(template = nil, options = {}, locals = {}, &block)
options[:default_content_type] = :xml
render_ruby(:nokogiri, template, options, locals, &block)
end
# Render a slim template
#
# @param template [Symbol, String] Template Path (symbol) or contents (String)
# @param options [Hash] Rendering options -- see {#render}
# @param locals [Hash] Template-local variables -- see {#render}
# @return [String] The rendered template
# @see #render
def slim(template, options = {}, locals = {})
render :slim, template, options, locals
end
# Finds template file with same name as extension
#
# @param views [String] The view directory
# @param name [String] The template name
# @param engine [String] The template type
# @return [String | nil] Template file or nil if it doesn't exist.
def find_template(views, name, engine)
filename = ::File.join(views, "#{name}.#{engine}")
File.exist?(filename) ? filename : nil
end
# Renders a template with the given engine. Don't call this directly --
# call one of the sugar methods.
#
# @param engine [Symbol] The engine (:haml, :erb, :textile, etc) to use
# @param template [Symbol, String] Either the name or path of the template as symbol
# (Use `:'subdir/myview'` for views in subdirectories), or a string that
# will be rendered. It looks for templates in
# +Goliath::Application.root_path/views+ by default. It is not as clever
# as Sinatra: you must name your template file template_name.engine_name
# -- so 'foo.markdown', not 'foo.md'.
#
# @param options [Hash] Options for layout
# @option options :content_type [String] The MIME content type to use.
# @option options [String] :layout If false, no layout is rendered, otherwise the specified layout is used.
# @option options [String] :layout_engine Engine to use for rendering the layout.
# @option options [Hash] :locals A hash with local variables that should be available in the template
# @option options [Object] :scope If set, template is evaluate with the binding of the
# given object rather than the application instance.
# @option options [String] :views Views directory to use.
#
# You may set template-global defaults in config[:template], for example
#
# config[:template] = {
# :layout_engine => :haml,
# }
#
# and engine-specific defaults in config[:template_engines], for example
#
# config[:template_engines] = {
# :haml => {
# :escape_html => true
# }
# }
#
# @param locals [Hash] You can give a hash of local variables available to
# the template either directly in +options[:local]+ or in a
# separate hash as the last parameter.
# @return [String] The rendered template
def render(engine, data, options = {}, locals = {}, &block)
# merge app-level options
if config.has_key?(:template_engines) && config[:template_engines].has_key?(engine)
options = config[:template_engines][engine].merge(options)
end
options = config[:template].merge(options) if config.has_key?(:template)
# extract generic options
locals = options.delete(:locals) || locals || {}
views = options.delete(:views) || Goliath::Application.root_path('views')
default_layout = options.delete(:default_layout) || :layout
layout = options.delete(:layout)
layout = default_layout if layout.nil? || layout == true
content_type = options.delete(:content_type) || options.delete(:default_content_type)
layout_engine = options.delete(:layout_engine) || engine
scope = options.delete(:scope) || self
layout_filename = find_template(views, layout, layout_engine)
layout_template = if layout == false || layout_filename == nil
NullLayout
else
Tilt.new(layout_filename, nil, options)
end
# mimic sinatra behavior, if a string is given consider it as a template source
# otherwise a symbol is expected to be a template path
if data.is_a?(Symbol)
template_filename = find_template(views, data, engine)
unless template_filename
raise Goliath::Validation::InternalServerError, "Template #{data} not found in #{views} for #{engine}"
end
template = Tilt.new(template_filename, nil, options)
output = layout_template.render(scope, locals) do
template.render(scope, locals)
end
else
template = Tilt[engine].new(nil, nil, options){ data }
output = layout_template.render(scope, locals) do
template.render(scope, locals)
end
end
output.extend(ContentTyped).content_type = content_type if content_type
output
end
private
# Acts like a layout, does nothing.
# @private
class NullLayout
def self.render(*args, &block)
block.call
end
end
# logic shared between builder and nokogiri
def render_ruby(engine, template, options = {}, locals = {}, &block)
options, template = template, nil if template.is_a?(Hash)
template = Proc.new { block } if template.nil?
render engine, template, options, locals
end
end
end
end