# Shutup Tilt Warnings # @private class Tilt::Template def warn(*) end end # Rendering extension # rubocop:disable UnderscorePrefixedVariableName module Middleman module CoreExtensions module Rendering # Setup extension class << self # Once registered def registered(app) # Include methods app.send :include, InstanceMethods app.define_hook :before_render app.define_hook :after_render ::Tilt.mappings.delete('html') # WTF, Tilt? ::Tilt.mappings.delete('csv') require 'active_support/core_ext/string/output_safety' # Activate custom renderers require 'middleman-core/renderers/erb' app.register Middleman::Renderers::ERb # CoffeeScript Support begin require 'middleman-core/renderers/coffee_script' app.register Middleman::Renderers::CoffeeScript rescue LoadError end # Haml Support begin require 'middleman-core/renderers/haml' app.register Middleman::Renderers::Haml rescue LoadError end # Sass Support begin require 'middleman-core/renderers/sass' app.register Middleman::Renderers::Sass rescue LoadError end # Markdown Support require 'middleman-core/renderers/markdown' app.register Middleman::Renderers::Markdown # AsciiDoc Support begin require 'middleman-core/renderers/asciidoc' app.register Middleman::Renderers::AsciiDoc rescue LoadError end # Liquid Support begin require 'middleman-core/renderers/liquid' app.register Middleman::Renderers::Liquid rescue LoadError end # Slim Support begin require 'middleman-core/renderers/slim' app.register Middleman::Renderers::Slim rescue LoadError end # Less Support begin require 'middleman-core/renderers/less' app.register Middleman::Renderers::Less rescue LoadError end # Stylus Support begin require 'middleman-core/renderers/stylus' app.register Middleman::Renderers::Stylus rescue LoadError end # Clean up missing Tilt exts app.after_configuration do Tilt.mappings.each do |key, _| begin Tilt[".#{key}"] rescue LoadError, NameError Tilt.mappings.delete(key) end end end end alias_method :included, :registered end # Custom error class for handling class TemplateNotFound < RuntimeError end # Rendering instance methods module InstanceMethods # Add or overwrite a default template extension # # @param [Hash] extension_map # @return [Hash] def template_extensions(extension_map=nil) @_template_extensions ||= {} @_template_extensions.merge!(extension_map) if extension_map @_template_extensions end # Render a template, with layout, given a path # # @param [String] path # @param [Hash] locs # @param [Hash] opts # @return [String] def render_template(path, locs={}, opts={}, blocks=[]) extension = File.extname(path) engine = extension[1..-1].to_sym if defined?(::I18n) old_locale = ::I18n.locale ::I18n.locale = opts[:lang] if opts[:lang] end # Use a dup of self as a context so that instance variables set within # the template don't persist for other templates. context = dup blocks.each do |block| context.instance_eval(&block) end # Store current locs/opts for later @current_locs = locs @current_opts = opts # 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 = render_individual_file(path, locs, opts, context) 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 #{source}/#{config[:layouts_dir]}." end end # If we need a layout and have a layout, use it if layout_path = fetch_layout(engine, opts) content = render_individual_file(layout_path, locs, opts, context) { content } 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) @content_blocks = nil @current_locs = nil @current_opts = nil end # Sinatra/Padrino compatible render method signature referenced by some view # helpers. Especially partials. # # @param [String, Symbol] engine # @param [String, Symbol] data # @param [Hash] options # @return [String] def render(_, data, options={}, &block) partial_name = data.to_s found_partial = locate_partial(partial_name, false) || locate_partial(partial_name, true) # Look in the partials_dir for the partial with the current engine unless found_partial partials_path = File.join(config[:partials_dir], partial_name) found_partial = locate_partial(partials_path, false) || locate_partial(partials_path, true) end raise ::Middleman::CoreExtensions::Rendering::TemplateNotFound, "Could not locate partial: #{data}" unless found_partial locals = options[:locals] if ::Tilt[found_partial] # Render the partial if found, otherwide throw exception render_individual_file(found_partial, locals, options, self, &block) else File.read(found_partial) end end # Partial locator. # # @param [String] partial_name # @return [String] def locate_partial(partial_name, try_static=true) resolve_opts = { try_without_underscore: true, try_static: try_static } # If the path is known to the sitemap if resource = sitemap.find_resource_by_path(current_path) current_dir = File.dirname(resource.source_file) resolve_opts[:preferred_engine] = File.extname(resource.source_file)[1..-1].to_sym # Look for partials relative to the current path relative_dir = File.join(current_dir.sub(%r{^#{Regexp.escape(source_dir)}/?}, ''), partial_name) resolve_template(relative_dir, resolve_opts) || resolve_template(partial_name, resolve_opts) else resolve_template(partial_name, resolve_opts) end end # Render an on-disk file. Used for everything, including layouts. # # @param [String, Symbol] path # @param [Hash] locs # @param [Hash] opts # @param [Class] context # @return [String] def render_individual_file(path, locs={}, opts={}, context=self, &block) path = path.to_s # Mutability is FUN! # Try to work around: https://github.com/middleman/middleman/issues/501 locs = locs.dup # Detect the remdering engine from the extension extension = File.extname(path) engine = extension[1..-1].to_sym # Store last engine for later (could be inside nested renders) context.current_engine, engine_was = engine, context.current_engine # Save current buffer for later @_out_buf, _buf_was = '', @_out_buf # Read from disk or cache the contents of the file body = if opts[:template_body] opts.delete(:template_body) else template_data_for_file(path) end # Merge per-extension options from config extension = File.extname(path) options = opts.dup.merge(options_for_ext(extension)) options[:outvar] ||= '@_out_buf' options.delete(:layout) # Overwrite with frontmatter options options = options.deep_merge(options[:renderer_options]) if options[:renderer_options] template_class = ::Tilt[path] # Allow hooks to manipulate the template before render self.class.callbacks_for_hook(:before_render).each do |callback| # Uber::Options::Value doesn't respond to call newbody = if callback.respond_to?(:call) callback.call(body, path, locs, template_class) elsif callback.respond_to?(:evaluate) callback.evaluate(self, body, path, locs, template_class) end body = newbody if newbody # Allow the callback to return nil to skip it end # Read compiled template from disk or cache template = cache.fetch(:compiled_template, extension, options, body) do ::Tilt.new(path, 1, options) { body } end # Render using Tilt content = template.render(context, locs, &block) # Allow hooks to manipulate the result after render self.class.callbacks_for_hook(:after_render).each do |callback| # Uber::Options::Value doesn't respond to call newcontent = if callback.respond_to?(:call) content = callback.call(content, path, locs, template_class) elsif callback.respond_to?(:evaluate) content = callback.evaluate(self, content, path, locs, template_class) end content = newcontent if newcontent # Allow the callback to return nil to skip it end output = ::ActiveSupport::SafeBuffer.new '' output.safe_concat content output ensure # Reset stored buffer @_out_buf = _buf_was context.current_engine = engine_was end # Get the template data from a path # @param [String] path # @return [String] def template_data_for_file(path) File.read(File.expand_path(path, source_dir)) end # Get a hash of configuration options for a given file extension, from # config.rb # # @param [String] ext # @return [Hash] def options_for_ext(ext) # Read options for extension from config/Tilt or cache cache.fetch(:options_for_ext, ext) do options = {} # Find all the engines which handle this extension in tilt. Look for # config variables of that name and merge it extension_class = ::Tilt[ext] ::Tilt.mappings.each do |mapping_ext, engines| next unless engines.include? extension_class engine_options = config[mapping_ext.to_sym] || {} options.merge!(engine_options) end options end end # Find a layout for a given engine # # @param [Symbol] engine # @param [Hash] opts # @return [String] def fetch_layout(engine, opts) # The layout name comes from either the system default or the options local_layout = opts.key?(:layout) ? opts[:layout] : config[:layout] return false unless local_layout # Look for engine-specific options engine_options = respond_to?(engine) ? 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) || false else # Look for specific layout # If found, use it. If not, error. if layout_path = locate_layout(local_layout, layout_engine) layout_path else raise ::Middleman::CoreExtensions::Rendering::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] def locate_layout(name, preferred_engine=nil) resolve_opts = {} resolve_opts[:preferred_engine] = preferred_engine unless preferred_engine.nil? # Check layouts folder layout_path = resolve_template(File.join(config[:layouts_dir], name.to_s), resolve_opts) # If we didn't find it, check root layout_path = resolve_template(name, resolve_opts) unless layout_path # Return the path layout_path end # Allow layouts to be wrapped in the contents of other layouts # @param [String, Symbol] layout_name # @return [void] def wrap_layout(layout_name, &block) # Save current buffer for later @_out_buf, _buf_was = '', @_out_buf layout_path = locate_layout(layout_name, current_engine) extension = File.extname(layout_path) engine = extension[1..-1].to_sym # Store last engine for later (could be inside nested renders) self.current_engine, engine_was = engine, current_engine begin content = if block_given? capture_html(&block) else '' end ensure # Reset stored buffer @_out_buf = _buf_was end concat_safe_content render_individual_file(layout_path, @current_locs || {}, @current_opts || {}, self) { content } ensure self.current_engine = engine_was end # The currently rendering engine # @return [Symbol, nil] def current_engine @_current_engine ||= nil end # The currently rendering engine # rubocop:disable TrivialAccessors # @return [Symbol, nil] def current_engine=(v) @_current_engine = v 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. # @option options [Boolean] :try_without_underscore # @option options [Boolean] :try_static # @return [Array, Boolean] def resolve_template(request_path, options={}) # Find the path by searching or using the cache request_path = request_path.to_s cache.fetch(:resolve_template, request_path, options) do relative_path = Util.strip_leading_slash(request_path) on_disk_path = File.expand_path(relative_path, source_dir) preferred_engines = if options[:try_static] [nil] else possible_engines = ['*'] # By default, any engine will do # 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 matched_exts = ::Tilt.mappings.select do |_, engines| engines.include? extension_class end.keys # Prefer to look for the matched extensions unless matched_exts.empty? possible_engines.unshift('{' + matched_exts.join(',') + '}') end end possible_engines end search_paths = preferred_engines.flat_map do |preferred_engine| path_with_ext = on_disk_path.dup path_with_ext << ('.' + preferred_engine) unless preferred_engine.nil? paths = [path_with_ext] if options[:try_without_underscore] paths << path_with_ext.sub(relative_path, relative_path.sub(/^_/, '').sub(/\/_/, '/')) end paths end found_path = nil search_paths.each do |path_with_ext| found_path = Dir[path_with_ext].find do |path| ::Tilt[path] end unless found_path found_path = path_with_ext if File.exist?(path_with_ext) end break if found_path end # If we found one, return it if found_path found_path elsif File.exist?(on_disk_path) on_disk_path else false end end end end end end end