242 lines
7.7 KiB
Ruby
242 lines
7.7 KiB
Ruby
require 'tilt'
|
|
require 'active_support/core_ext/string/output_safety'
|
|
require 'middleman-core/template_context'
|
|
require 'middleman-core/file_renderer'
|
|
require 'middleman-core/contracts'
|
|
|
|
module Middleman
|
|
class TemplateRenderer
|
|
extend Forwardable
|
|
include Contracts
|
|
|
|
class Cache
|
|
def initialize
|
|
@cache = {}
|
|
end
|
|
|
|
def fetch(*key)
|
|
@cache[key] = yield unless @cache.key?(key)
|
|
@cache[key]
|
|
end
|
|
|
|
def clear
|
|
@cache = {}
|
|
end
|
|
end
|
|
|
|
def self.cache
|
|
@_cache ||= Cache.new
|
|
end
|
|
|
|
# Find a layout on-disk, optionally using a specific engine
|
|
# @param [String] name
|
|
# @param [Symbol] preferred_engine
|
|
# @return [String]
|
|
Contract IsA['Middleman::Application'], Or[String, Symbol], Symbol => Maybe[IsA['Middleman::SourceFile']]
|
|
def self.locate_layout(app, name, preferred_engine=nil)
|
|
resolve_opts = {}
|
|
resolve_opts[:preferred_engine] = preferred_engine unless preferred_engine.nil?
|
|
|
|
# Check layouts folder
|
|
layout_file = resolve_template(app, File.join(app.config[:layouts_dir], name.to_s), resolve_opts)
|
|
|
|
# If we didn't find it, check root
|
|
layout_file = resolve_template(app, name, resolve_opts) unless layout_file
|
|
|
|
# Return the path
|
|
layout_file
|
|
end
|
|
|
|
# Find a template on disk given a output path
|
|
# @param [String] request_path
|
|
# @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
|
|
Contract IsA['Middleman::Application'], Or[Symbol, String], Maybe[Hash] => Maybe[IsA['Middleman::SourceFile']]
|
|
def self.resolve_template(app, request_path, options={})
|
|
# Find the path by searching
|
|
relative_path = Util.strip_leading_slash(request_path.to_s)
|
|
|
|
# By default, any engine will do
|
|
preferred_engines = []
|
|
|
|
# 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
|
|
preferred_engines += ::Tilt.mappings.select do |_, engines|
|
|
engines.include? extension_class
|
|
end.keys
|
|
end
|
|
|
|
preferred_engines << '*'
|
|
preferred_engines << nil if options[:try_static]
|
|
|
|
found_template = nil
|
|
|
|
preferred_engines.each do |preferred_engine|
|
|
path_with_ext = relative_path.dup
|
|
path_with_ext << ('.' + preferred_engine) unless preferred_engine.nil?
|
|
|
|
globbing = preferred_engine == '*'
|
|
|
|
# Cache lookups in build mode only
|
|
file = if app.build?
|
|
cache.fetch(path_with_ext, preferred_engine) do
|
|
app.files.find(:source, path_with_ext, globbing)
|
|
end
|
|
else
|
|
app.files.find(:source, path_with_ext, globbing)
|
|
end
|
|
|
|
found_template = file if file && (preferred_engine.nil? || ::Tilt[file[:full_path]])
|
|
break if found_template
|
|
end
|
|
|
|
# If we found one, return it
|
|
found_template
|
|
end
|
|
|
|
# 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]
|
|
Contract Hash, Hash => String
|
|
def render(locs={}, opts={}, &block)
|
|
path = @path.dup
|
|
locals = locs.dup.freeze
|
|
options = opts.dup
|
|
|
|
extension = File.extname(path)
|
|
engine = extension[1..-1].to_sym
|
|
|
|
if defined?(::I18n)
|
|
old_locale = ::I18n.locale
|
|
::I18n.locale = options[:locale] if options[:locale]
|
|
|
|
# Backwards compat
|
|
::I18n.locale = options[:lang] if options[:lang]
|
|
end
|
|
|
|
# Sandboxed class for template eval
|
|
context = @app.template_context_class.new(@app, locals, options)
|
|
|
|
# Add extension helpers to context.
|
|
@app.extensions.add_exposed_to_context(context)
|
|
|
|
content = ::Middleman::Util.instrument 'builder.output.resource.render-template', path: File.basename(path) do
|
|
_render_with_all_renderers(path, locs, context, opts, &block)
|
|
end
|
|
|
|
# If we need a layout and have a layout, use it
|
|
layout_file = fetch_layout(engine, options)
|
|
if layout_file
|
|
content = ::Middleman::Util.instrument 'builder.output.resource.render-layout', path: File.basename(layout_file[:relative_path].to_s) do
|
|
if layout_file = fetch_layout(engine, options)
|
|
layout_renderer = ::Middleman::FileRenderer.new(@app, layout_file[:relative_path].to_s)
|
|
layout_renderer.render(locals, options, context) { content }
|
|
else
|
|
content
|
|
end
|
|
end
|
|
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
|
|
|
|
protected
|
|
|
|
def _render_with_all_renderers(path, locs, context, opts, &block)
|
|
# Keep rendering template until we've used up all extensions. This
|
|
# handles cases like `style.css.sass.erb`
|
|
content = nil
|
|
|
|
while ::Tilt[path]
|
|
begin
|
|
opts[:template_body] = content if content
|
|
|
|
content_renderer = ::Middleman::FileRenderer.new(@app, path)
|
|
content = content_renderer.render(locs, opts, context, &block)
|
|
|
|
path = File.basename(path, File.extname(path))
|
|
rescue LocalJumpError
|
|
raise "Tried to render a layout (calls yield) at #{path} like it was a template. Non-default layouts need to be in #{@app.config[:source]}/#{@app.config[:layouts_dir]}."
|
|
end
|
|
end
|
|
|
|
content
|
|
end
|
|
|
|
# Find a layout for a given engine
|
|
#
|
|
# @param [Symbol] engine
|
|
# @param [Hash] opts
|
|
# @return [String, Boolean]
|
|
Contract Symbol, Hash => Maybe[IsA['Middleman::SourceFile']]
|
|
def fetch_layout(engine, opts)
|
|
# The layout name comes from either the system default or the options
|
|
local_layout = opts.key?(:layout) ? opts[:layout] : @app.config[:layout]
|
|
return unless local_layout
|
|
|
|
# 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
|
|
layout_engine = if opts.key?(:layout_engine)
|
|
opts[:layout_engine]
|
|
elsif engine_options.key?(:layout_engine)
|
|
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
|
|
locate_layout(:layout, layout_engine)
|
|
elsif layout_file = locate_layout(local_layout, layout_engine)
|
|
# Look for specific layout
|
|
# If found, use it. If not, error.
|
|
|
|
layout_file
|
|
else
|
|
raise ::Middleman::TemplateRenderer::TemplateNotFound, "Could not locate layout: #{local_layout}"
|
|
end
|
|
end
|
|
|
|
# Find a layout on-disk, optionally using a specific engine
|
|
# @param [String] name
|
|
# @param [Symbol] preferred_engine
|
|
# @return [String]
|
|
Contract Or[String, Symbol], Symbol => Maybe[IsA['Middleman::SourceFile']]
|
|
def locate_layout(name, preferred_engine=nil)
|
|
self.class.locate_layout(@app, name, preferred_engine)
|
|
end
|
|
|
|
# Find a template on disk given a output path
|
|
# @param [String] request_path
|
|
# @param [Hash] options
|
|
# @return [Array<String, Symbol>, Boolean]
|
|
Contract String, Hash => ArrayOf[Or[String, Symbol]]
|
|
def resolve_template(request_path, options={})
|
|
self.class.resolve_template(@app, request_path, options)
|
|
end
|
|
end
|
|
end
|