2014-01-03 22:15:02 +01:00
require 'tilt'
require 'active_support/core_ext/string/output_safety'
require 'middleman-core/template_context'
require 'middleman-core/file_renderer'
2014-07-03 04:04:34 +02:00
require 'middleman-core/contracts'
2014-01-03 22:15:02 +01:00
module Middleman
class TemplateRenderer
2014-07-05 20:17:41 +02:00
extend Forwardable
2014-07-03 04:04:34 +02:00
include Contracts
2014-07-05 20:17:41 +02:00
2014-01-03 22:15:02 +01:00
def self . cache
@_cache || = :: Tilt :: Cache . new
end
2014-07-05 20:17:41 +02:00
def_delegator :" self.class " , :cache
2014-01-03 22:15:02 +01:00
# Custom error class for handling
class TemplateNotFound < RuntimeError ; end
def initialize ( app , path )
@app = app
@path = path
end
# Render a template, with layout, given a path
#
# @param [Hash] locs
# @param [Hash] opts
# @return [String]
2014-07-03 04:04:34 +02:00
Contract Hash , Hash = > String
2014-01-04 00:49:54 +01:00
def render ( locs = { } , opts = { } )
2014-01-03 22:15:02 +01:00
path = @path . dup
2014-07-14 22:19:34 +02:00
locals = locs . dup . freeze
options = opts . dup
2014-01-03 22:15:02 +01:00
extension = File . extname ( path )
engine = extension [ 1 .. - 1 ] . to_sym
if defined? ( :: I18n )
old_locale = :: I18n . locale
2014-07-14 22:19:34 +02:00
:: I18n . locale = options [ :lang ] if options [ :lang ]
2014-01-03 22:15:02 +01:00
end
# Sandboxed class for template eval
2014-07-14 22:19:34 +02:00
context = @app . template_context_class . new ( @app , locals , options )
2014-01-03 22:15:02 +01:00
2014-04-07 21:43:16 +02:00
# TODO: Only for HAML files
2014-04-29 20:43:05 +02:00
context . init_haml_helpers if context . respond_to? ( :init_haml_helpers )
2014-01-03 22:15:02 +01:00
# Keep rendering template until we've used up all extensions. This
# handles cases like `style.css.sass.erb`
content = nil
while :: Tilt [ path ]
begin
2014-07-14 22:19:34 +02:00
options [ :template_body ] = content if content
2014-01-03 22:15:02 +01:00
content_renderer = :: Middleman :: FileRenderer . new ( @app , path )
2014-07-14 22:19:34 +02:00
content = content_renderer . render ( locals , options , context )
2014-01-03 22:15:02 +01:00
path = File . basename ( path , File . extname ( path ) )
rescue LocalJumpError
2014-02-23 03:24:40 +01:00
raise " Tried to render a layout (calls yield) at #{ path } like it was a template. Non-default layouts need to be in #{ source } / #{ @app . config [ :layouts_dir ] } . "
2014-01-03 22:15:02 +01:00
end
end
# If we need a layout and have a layout, use it
2014-07-16 03:01:45 +02:00
if layout_file = fetch_layout ( engine , options )
layout_renderer = :: Middleman :: FileRenderer . new ( @app , layout_file [ :relative_path ] . to_s )
2014-07-14 22:19:34 +02:00
content = layout_renderer . render ( locals , options , context ) { content }
2014-01-03 22:15:02 +01:00
end
# Return result
content
ensure
# Pop all the saved variables from earlier as we may be returning to a
# previous render (layouts, partials, nested layouts).
:: I18n . locale = old_locale if defined? ( :: I18n )
end
2014-04-29 19:50:21 +02:00
protected
2014-01-03 22:15:02 +01:00
# Find a layout for a given engine
#
# @param [Symbol] engine
# @param [Hash] opts
2014-07-03 04:04:34 +02:00
# @return [String, Boolean]
2014-07-16 03:01:45 +02:00
Contract Symbol , Hash = > Maybe [ IsA [ 'Middleman::SourceFile' ] ]
2014-01-03 22:15:02 +01:00
def fetch_layout ( engine , opts )
# The layout name comes from either the system default or the options
2014-04-29 19:50:21 +02:00
local_layout = opts . key? ( :layout ) ? opts [ :layout ] : @app . config [ :layout ]
2014-07-16 03:01:45 +02:00
return unless local_layout
2014-01-03 22:15:02 +01:00
# Look for engine-specific options
engine_options = @app . config . respond_to? ( engine ) ? @app . config . send ( engine ) : { }
# The engine for the layout can be set in options, engine_options or passed
# into this method
2014-04-29 19:50:21 +02:00
layout_engine = if opts . key? ( :layout_engine )
2014-01-03 22:15:02 +01:00
opts [ :layout_engine ]
2014-04-29 19:50:21 +02:00
elsif engine_options . key? ( :layout_engine )
2014-01-03 22:15:02 +01:00
engine_options [ :layout_engine ]
else
engine
end
# Automatic mode
if local_layout == :_auto_layout
# Look for :layout of any extension
# If found, use it. If not, continue
2014-07-16 03:01:45 +02:00
locate_layout ( :layout , layout_engine )
2014-01-03 22:15:02 +01:00
else
# Look for specific layout
# If found, use it. If not, error.
2014-07-16 03:01:45 +02:00
if layout_file = locate_layout ( local_layout , layout_engine )
layout_file
2014-01-03 22:15:02 +01:00
else
raise :: Middleman :: TemplateRenderer :: TemplateNotFound , " Could not locate layout: #{ local_layout } "
end
end
end
# Find a layout on-disk, optionally using a specific engine
# @param [String] name
# @param [Symbol] preferred_engine
# @return [String]
2014-07-16 03:01:45 +02:00
Contract Or [ String , Symbol ] , Symbol = > Maybe [ IsA [ 'Middleman::SourceFile' ] ]
2014-01-03 22:15:02 +01:00
def locate_layout ( name , preferred_engine = nil )
self . class . locate_layout ( @app , name , preferred_engine )
end
# Find a layout on-disk, optionally using a specific engine
# @param [String] name
# @param [Symbol] preferred_engine
# @return [String]
2014-07-16 03:01:45 +02:00
Contract IsA [ 'Middleman::Application' ] , Or [ String , Symbol ] , Symbol = > Maybe [ IsA [ 'Middleman::SourceFile' ] ]
2014-01-03 22:15:02 +01:00
def self . locate_layout ( app , name , preferred_engine = nil )
2014-02-23 03:24:40 +01:00
resolve_opts = { }
2014-04-29 19:50:21 +02:00
resolve_opts [ :preferred_engine ] = preferred_engine unless preferred_engine . nil?
2014-01-03 22:15:02 +01:00
2014-02-23 03:24:40 +01:00
# Check layouts folder
2014-07-16 03:01:45 +02:00
layout_file = resolve_template ( app , File . join ( app . config [ :layouts_dir ] , name . to_s ) , resolve_opts )
2014-01-03 22:15:02 +01:00
2014-02-23 03:24:40 +01:00
# If we didn't find it, check root
2014-07-16 03:01:45 +02:00
layout_file = resolve_template ( app , name , resolve_opts ) unless layout_file
2014-01-03 22:15:02 +01:00
# Return the path
2014-07-16 03:01:45 +02:00
layout_file
2014-01-03 22:15:02 +01:00
end
# Find a template on disk given a output path
# @param [String] request_path
# @param [Hash] options
# @return [Array<String, Symbol>, Boolean]
2014-07-03 04:04:34 +02:00
Contract String , Hash = > ArrayOf [ Or [ String , Symbol ] ]
2014-01-03 22:15:02 +01:00
def resolve_template ( request_path , options = { } )
self . class . resolve_template ( @app , request_path , options )
end
# Find a template on disk given a output path
# @param [String] request_path
2014-02-23 03:24:40 +01:00
# @option options [Boolean] :preferred_engine If set, try this engine first, then fall back to any engine.
# @return [String, Boolean] Either the path to the template, or false
2014-07-16 03:01:45 +02:00
Contract IsA [ 'Middleman::Application' ] , Or [ Symbol , String ] , Maybe [ Hash ] = > Maybe [ IsA [ 'Middleman::SourceFile' ] ]
2014-01-03 22:15:02 +01:00
def self . resolve_template ( app , request_path , options = { } )
# Find the path by searching or using the cache
2014-07-16 03:01:45 +02:00
relative_path = Util . strip_leading_slash ( request_path . to_s )
2014-01-03 22:15:02 +01:00
2014-07-20 22:37:16 +02:00
# Cache lookups in build mode only
if app . build?
2014-07-16 03:01:45 +02:00
cache . fetch ( :resolve_template , relative_path , options ) do
uncached_resolve_template ( app , relative_path , options )
2014-01-03 22:15:02 +01:00
end
2014-07-20 22:37:16 +02:00
else
2014-07-16 03:01:45 +02:00
uncached_resolve_template ( app , relative_path , options )
2014-07-20 22:37:16 +02:00
end
end
2014-01-03 22:15:02 +01:00
2014-07-11 23:24:22 +02:00
Contract IsA [ 'Middleman::Application' ] , String , Hash = > Maybe [ IsA [ 'Middleman::SourceFile' ] ]
2014-07-16 03:01:45 +02:00
def self . uncached_resolve_template ( app , relative_path , options )
2014-07-20 22:37:16 +02:00
# By default, any engine will do
2014-07-16 03:01:45 +02:00
preferred_engines = [ ]
2014-07-18 21:54:27 +02:00
2014-07-20 22:37:16 +02:00
# If we're specifically looking for a preferred engine
if options . key? ( :preferred_engine )
extension_class = :: Tilt [ options [ :preferred_engine ] ]
# Get a list of extensions for a preferred engine
2014-07-16 03:01:45 +02:00
preferred_engines += :: Tilt . mappings . select do | _ , engines |
2014-07-20 22:37:16 +02:00
engines . include? extension_class
end . keys
end
2014-01-03 22:15:02 +01:00
2014-07-16 03:01:45 +02:00
preferred_engines << '*'
preferred_engines << nil if options [ :try_static ]
2014-07-20 22:37:16 +02:00
2014-07-16 03:01:45 +02:00
found_template = nil
2014-07-20 22:37:16 +02:00
2014-07-16 03:01:45 +02:00
preferred_engines . each do | preferred_engine |
path_with_ext = relative_path . dup
path_with_ext << ( '.' + preferred_engine ) unless preferred_engine . nil?
2014-07-20 22:37:16 +02:00
2014-07-16 03:01:45 +02:00
file = app . files . find ( :source , path_with_ext , preferred_engine == '*' )
found_template = file if file && ( preferred_engine . nil? || :: Tilt [ file [ :full_path ] ] )
break if found_template
2014-07-20 22:37:16 +02:00
end
# If we found one, return it
2014-07-16 03:01:45 +02:00
found_template
2014-01-03 22:15:02 +01:00
end
end
end