require 'padrino-core/support_lite' unless defined?(SupportLite) module Padrino ## # Padrino enhances the Sinatra 'render' method to have support for # automatic template engine detection, enhanced layout functionality, # locale enabled rendering, among other features. # module Rendering ## # A SafeTemplate assumes that its output is safe. # # @api private module SafeTemplate def render(*) super.html_safe end end ## # Exception responsible for when an expected template did not exist. # class TemplateNotFound < RuntimeError end ## # This is an array of file patterns to ignore. If your editor add a # suffix during editing to your files please add it like: # # @example # Padrino::Rendering::IGNORE_FILE_PATTERN << /~$/ # IGNORE_FILE_PATTERN = [ /~$/ # This is for Gedit ] unless defined?(IGNORE_FILE_PATTERN) ## # Default options used in the #resolve_template-method. # DEFAULT_RENDERING_OPTIONS = { :strict_format => false, :raise_exceptions => true } unless defined?(DEFAULT_RENDERING_OPTIONS) class << self ## # Default engine configurations for Padrino::Rendering # # @return {Hash} # The configurations, keyed by engine. def engine_configurations @engine_configurations ||= {} end ## # Main class that register this extension. # def registered(app) included(app) engine_configurations.each do |engine, configs| app.set engine, configs end end def included(base) base.send(:include, InstanceMethods) base.extend(ClassMethods) end end ## # Class methods responsible for rendering templates as part of a request. # module ClassMethods ## # Use layout like rails does or if a block given then like sinatra. # If used without a block, sets the current layout for the route. # # By default, searches in your: # # +app+/+views+/+layouts+/+application+.(+haml+|+erb+|+xxx+) # +app+/+views+/+layout_name+.(+haml+|+erb+|+xxx+) # # If you define +layout+ :+custom+ then searches for your layouts in # +app+/+views+/+layouts+/+custom+.(+haml+|+erb+|+xxx+) # +app+/+views+/+custom+.(+haml+|+erb+|+xxx+) # # @param [Symbol] name (:layout) # The layout to use. # # @yield [] # def layout(name=:layout, &block) return super(name, &block) if block_given? @layout = name end ## # Returns the cached template file to render for a given url, content_type and locale. # # @param [Array] render_options # def fetch_template_file(render_options) (@_cached_templates ||= {})[render_options] end ## # Caches the template file for the given rendering options # # @param [String] template_file # The path of the template file. # # @param [Array] render_options # def cache_template_file!(template_file, render_options) (@_cached_templates ||= {})[render_options] = template_file || [] end ## # Returns the cached layout path. # # @param [Symbol, nil] given_layout # The requested layout. # def fetch_layout_path(given_layout=nil) layout_name = given_layout || @layout || :application @_cached_layout ||= {} cached_layout_path = @_cached_layout[layout_name] return cached_layout_path if cached_layout_path has_layout_at_root = Dir["#{views}/#{layout_name}.*"].any? layout_path = has_layout_at_root ? layout_name.to_sym : File.join('layouts', layout_name.to_s).to_sym @_cached_layout[layout_name] = layout_path unless reload_templates? layout_path end end # Instance methods that allow enhanced rendering to function properly in Padrino. module InstanceMethods attr_reader :current_engine ## # Get/Set the content_type # # @param [String, nil] type # The Content-Type to use. # # @param [Symbol, nil] type. # Look and parse the given symbol to the matched Content-Type. # # @param [Hash] params # Additional params to append to the Content-Type. # # @example # case content_type # when :js then do_some # when :css then do_another # end # # content_type :js # # => set the response with 'application/javascript' Content-Type # content_type 'text/html' # # # => set directly the Content-Type to 'text/html' # def content_type(type=nil, params={}) unless type.nil? super(type, params) @_content_type = type end @_content_type end private ## # Enhancing Sinatra render functionality for: # # * Using layout similar to rails # * Use render 'path/to/my/template' (without symbols) # * Use render 'path/to/my/template' (with engine lookup) # * Use render 'path/to/template.haml' (with explicit engine lookup) # * Use render 'path/to/template', :layout => false # * Use render 'path/to/template', :layout => false, :engine => 'haml' # def render(engine, data=nil, options={}, locals={}, &block) # If engine is nil, ignore engine parameter and shift up all arguments # render nil, "index", { :layout => true }, { :localvar => "foo" } engine, data, options = data, options, locals if engine.nil? && data # Data is a hash of options when no engine isn't explicit # render "index", { :layout => true }, { :localvar => "foo" } # Data is options, and options is locals in this case data, options, locals = nil, data, options if data.is_a?(Hash) # If data is unassigned then this is a likely a template to be resolved # This means that no engine was explicitly defined data, engine = *resolve_template(engine, options.dup) if data.nil? # Setup root root = settings.respond_to?(:root) ? settings.root : "" # Use @layout if it exists layout_was = options[:layout] options[:layout] = @layout if options[:layout].nil? || options[:layout] == true # Resolve layouts similar to in Rails if options[:layout].nil? && !settings.templates.has_key?(:layout) layout_path, layout_engine = *resolved_layout options[:layout] = layout_path || false # We need to force layout false so sinatra don't try to render it options[:layout] = false unless layout_engine == engine # TODO allow different layout engine options[:layout_engine] = layout_engine || engine if options[:layout] elsif options[:layout].present? options[:layout] = settings.fetch_layout_path(options[:layout] || @layout) end # Default to original layout value if none found options[:layout] ||= layout_was # Cleanup the template @current_engine, engine_was = engine, @current_engine @_out_buf, _buf_was = ActiveSupport::SafeBuffer.new, @_out_buf # Pass arguments to Sinatra render method super(engine, data, options.dup, locals, &block) ensure @current_engine = engine_was @_out_buf = _buf_was end ## # Returns the located layout tuple to be used for the rendered template # (if available). # # @example # resolve_layout # # => ["/layouts/custom", :erb] # # => [nil, nil] # def resolved_layout located_layout = resolve_template(settings.fetch_layout_path, :raise_exceptions => false, :strict_format => true) located_layout ? located_layout : [nil, nil] end ## # Returns the template path and engine that match content_type (if present), # I18n.locale. # # @param [String] template_path # The path of the template. # # @param [Hash] options # Additional options. # # @option options [Boolean] :strict_format (false) # The resolved template must match the content_type of the request. # # @option options [Boolean] :raise_exceptions (false) # Raises a {TemplateNotFound} exception if the template cannot be located. # # @return [Array] # The path and format of the template. # # @raise [TemplateNotFound] # The template could not be found. # # @example # get "/foo", :provides => [:html, :js] do; render 'path/to/foo'; end # # If you request "/foo.js" with I18n.locale == :ru => [:"/path/to/foo.ru.js", :erb] # # If you request "/foo" with I18n.locale == :de => [:"/path/to/foo.de.haml", :haml] # def resolve_template(template_path, options={}) began_at = Time.now # Fetch cached template for rendering options template_path = template_path.to_s[0] == ?/ ? template_path.to_s : "/#{template_path}" rendering_options = [template_path, content_type, locale] cached_template = settings.fetch_template_file(rendering_options) if cached_template logger.debug :cached, began_at, cached_template[0] if settings.logging? && defined?(logger) return cached_template end # Resolve view path and options options.reverse_merge!(DEFAULT_RENDERING_OPTIONS) view_path = options.delete(:views) || settings.views || "./views" target_extension = File.extname(template_path)[1..-1] || "none" # explicit template extension template_path = template_path.chomp(".#{target_extension}") # Generate potential template candidates templates = Dir[File.join(view_path, template_path) + ".*"].map do |file| template_engine = File.extname(file)[1..-1].to_sym # retrieves engine extension template_file = file.sub(view_path, '').chomp(".#{template_engine}").to_sym # retrieves template filename [template_file, template_engine] unless IGNORE_FILE_PATTERN.any? { |pattern| template_engine.to_s =~ pattern } end # Check if we have a simple content type simple_content_type = [:html, :plain].include?(content_type) # Resolve final template to render located_template = templates.find { |file, e| file.to_s == "#{template_path}.#{locale}.#{content_type}" } || templates.find { |file, e| file.to_s == "#{template_path}.#{locale}" && simple_content_type } || templates.find { |file, e| File.extname(file.to_s) == ".#{target_extension}" or e.to_s == target_extension.to_s } || templates.find { |file, e| file.to_s == "#{template_path}.#{content_type}" } || templates.find { |file, e| file.to_s == "#{template_path}" && simple_content_type } || (!options[:strict_format] && templates.first) # If not strict, fall back to the first located template raise TemplateNotFound, "Template '#{template_path}' not found in '#{view_path}'!" if !located_template && options[:raise_exceptions] settings.cache_template_file!(located_template, rendering_options) unless settings.reload_templates? logger.debug :template, began_at, located_template[0] if located_template && settings.logging? && defined?(logger) located_template end ## # Return the I18n.locale if I18n is defined. # def locale I18n.locale if defined?(I18n) end end # InstanceMethods end # Rendering end # Padrino require 'padrino-core/application/rendering/extensions/haml' require 'padrino-core/application/rendering/extensions/erubis' require 'padrino-core/application/rendering/extensions/slim'