diff --git a/middleman-core/lib/middleman-core/application.rb b/middleman-core/lib/middleman-core/application.rb index 4103c2b2..0b67d48f 100644 --- a/middleman-core/lib/middleman-core/application.rb +++ b/middleman-core/lib/middleman-core/application.rb @@ -1,6 +1,3 @@ -# Using Tilt for templating -require 'tilt' - # i18n Built-in require 'i18n' @@ -25,6 +22,8 @@ require 'middleman-core/configuration' require 'middleman-core/core_extensions' require 'middleman-core/config_context' +require 'middleman-core/file_renderer' +require 'middleman-core/template_renderer' # Core Middleman Class module Middleman @@ -161,17 +160,21 @@ module Middleman # Template cache attr_reader :cache + attr_reader :template_context_class + attr_reader :generic_template_context delegate :link_to, :image_tag, :to => :generic_template_context # Initialize the Middleman project def initialize(&block) - @cache = ::Tilt::Cache.new @logger = ::Middleman::Logger.singleton @template_context_class = Class.new(Middleman::TemplateContext) @generic_template_context = @template_context_class.new(self) @config_context = ConfigContext.new(self, @template_context_class) + ::Middleman::FileRenderer.cache.clear + ::Middleman::TemplateRenderer.cache.clear + # Setup the default values from calls to set before initialization self.class.config.load_settings(self.class.superclass.config.all_settings) @@ -199,6 +202,10 @@ module Middleman super end + def add_to_instance(name, &func) + self.define_singleton_method(name, &func) + end + def add_to_config_context(name, &func) @config_context.define_singleton_method(name, &func) end diff --git a/middleman-core/lib/middleman-core/core_extensions/rendering.rb b/middleman-core/lib/middleman-core/core_extensions/rendering.rb index 734b9195..12b8e734 100644 --- a/middleman-core/lib/middleman-core/core_extensions/rendering.rb +++ b/middleman-core/lib/middleman-core/core_extensions/rendering.rb @@ -99,10 +99,6 @@ module Middleman end end - # Custom error class for handling - class TemplateNotFound < RuntimeError - end - # Rendering instance methods module InstanceMethods @@ -115,289 +111,6 @@ module Middleman @_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 - - # Sandboxed class for template eval - context = @template_context_class.new(self, locs, opts) - - if context.respond_to?(:init_haml_helpers) - context.init_haml_helpers - end - - blocks.each do |block| - context.instance_eval(&block) - end - - # 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}/#{layout_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) - 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, &block) - path = path.to_s - - # 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 - _buf_was = context.save_buffer - - # 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| - newbody = callback.call(body, path, locs, template_class) - 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| - content = callback.call(content, path, locs, template_class) - end - - output = ::ActiveSupport::SafeBuffer.new '' - output.safe_concat content - output - ensure - # Reset stored buffer - context.restore_buffer(_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) - if extensions[:frontmatter] - extensions[:frontmatter].template_data_for_file(path) - else - File.read(File.expand_path(path, source_dir)) - end - 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.has_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.has_key?(:layout_engine) - opts[:layout_engine] - elsif engine_options.has_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) - # Whether we've found the layout - layout_path = false - - # If we prefer a specific engine - if !preferred_engine.nil? - # Check root - layout_path, layout_engine = resolve_template(name, :preferred_engine => preferred_engine) - - # Check layouts folder - if !layout_path - layout_path, layout_engine = resolve_template(File.join(config[:layouts_dir], name.to_s), :preferred_engine => preferred_engine) - end - end - - # Check root, no preference - if !layout_path - layout_path, layout_engine = resolve_template(name) - end - - # Check layouts folder, no preference - if !layout_path - layout_path, layout_engine = resolve_template(File.join(config[:layouts_dir], name.to_s)) - end - - # Return the path - layout_path - end - - # Find a template on disk given a output path - # @param [String] request_path - # @param [Hash] options - # @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, self.source_dir) - - # By default, any engine will do - preferred_engine = '*' - - # Unless we're specifically looking for a preferred engine - if options.has_key?(:preferred_engine) - extension_class = ::Tilt[options[:preferred_engine]] - matched_exts = [] - - # Get a list of extensions for a preferred engine - # TODO: Cache this - ::Tilt.mappings.each do |ext, engines| - next unless engines.include? extension_class - matched_exts << ext - end - - # Change the glob to only look for the matched extensions - if matched_exts.length > 0 - preferred_engine = '{' + matched_exts.join(',') + '}' - else - return false - end - end - - # Look for files that match - path_with_ext = on_disk_path + '.' + preferred_engine - - found_path = Dir[path_with_ext].find do |path| - ::Tilt[path] - end - - if !found_path && options[:try_without_underscore] && - path_no_underscore = path_with_ext. - sub(relative_path, relative_path.sub(/^_/, ''). - sub(/\/_/, '/')) - found_path = Dir[path_no_underscore].find do |path| - ::Tilt[path] - end - end - - # If we found one, return it and the found engine - if found_path || files.exists?(on_disk_path) - engine = found_path ? File.extname(found_path)[1..-1].to_sym : nil - [ found_path || on_disk_path, engine ] - else - false - end - end - end end end end diff --git a/middleman-core/lib/middleman-core/core_extensions/request.rb b/middleman-core/lib/middleman-core/core_extensions/request.rb index 1ef8b3a6..6758a7bf 100644 --- a/middleman-core/lib/middleman-core/core_extensions/request.rb +++ b/middleman-core/lib/middleman-core/core_extensions/request.rb @@ -5,6 +5,7 @@ require 'rack/lint' require 'rack/head' require 'middleman-core/util' +require 'middleman-core/template_renderer' module Middleman module CoreExtensions @@ -242,7 +243,7 @@ module Middleman res.write output # Valid content is a 200 status res.status = 200 - rescue Middleman::CoreExtensions::Rendering::TemplateNotFound => e + rescue Middleman::TemplateRenderer::TemplateNotFound => e res.write "Error: #{e.message}" res.status = 500 end diff --git a/middleman-core/lib/middleman-core/file_renderer.rb b/middleman-core/lib/middleman-core/file_renderer.rb new file mode 100644 index 00000000..0393ad83 --- /dev/null +++ b/middleman-core/lib/middleman-core/file_renderer.rb @@ -0,0 +1,119 @@ +require 'tilt' +require 'active_support/core_ext/string/output_safety' + +module Middleman + + class FileRenderer + + def self.cache + @_cache ||= ::Tilt::Cache.new + end + + delegate :cache, :to => :"self.class" + + def initialize(app, path) + @app = app + @path = path.to_s + end + + # Render an on-disk file. Used for everything, including layouts. + # + # @param [Hash] locs + # @param [Hash] opts + # @param [Class] context + # @return [String] + def render(locs = {}, opts = {}, context, &block) + path = @path.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 + _buf_was = context.save_buffer + + # Read from disk or cache the contents of the file + body = if opts[:template_body] + opts.delete(:template_body) + else + get_template_data_for_file + 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 + @app.class.callbacks_for_hook(:before_render).each do |callback| + newbody = callback.call(body, path, locs, template_class) + 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 + @app.class.callbacks_for_hook(:after_render).each do |callback| + content = callback.call(content, path, locs, template_class) + end + + output = ::ActiveSupport::SafeBuffer.new '' + output.safe_concat content + output + ensure + # Reset stored buffer + context.restore_buffer(_buf_was) + context.current_engine = engine_was + end + + # Get the template data from a path + # @param [String] path + # @return [String] + def get_template_data_for_file + if @app.extensions[:frontmatter] + @app.extensions[:frontmatter].template_data_for_file(@path) + else + File.read(File.expand_path(@path, source_dir)) + end + end + + protected + + # 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 = @app.config[mapping_ext.to_sym] || {} + options.merge!(engine_options) + end + + options + end + end + end +end \ No newline at end of file diff --git a/middleman-core/lib/middleman-core/sitemap/resource.rb b/middleman-core/lib/middleman-core/sitemap/resource.rb index d0d39483..0a2cb502 100644 --- a/middleman-core/lib/middleman-core/sitemap/resource.rb +++ b/middleman-core/lib/middleman-core/sitemap/resource.rb @@ -1,5 +1,7 @@ require 'middleman-core/sitemap/extensions/traversal' require 'middleman-core/sitemap/extensions/content_type' +require 'middleman-core/file_renderer' +require 'middleman-core/template_renderer' module Middleman @@ -106,7 +108,7 @@ module Middleman # @return [String] def render(opts={}, locs={}, &block) if !template? - return app.template_data_for_file(source_file) + return ::Middleman::FileRenderer.new(@app, source_file).get_template_data_for_file end relative_source = Pathname(source_file).relative_path_from(Pathname(app.root)) @@ -140,7 +142,8 @@ module Middleman opts[:layout] = false if %w(.js .json .css .txt).include?(self.ext) end - app.render_template(source_file, locs, opts, blocks) + renderer = ::Middleman::TemplateRenderer.new(@app, source_file) + renderer.render(locs, opts, blocks) end end diff --git a/middleman-core/lib/middleman-core/template_context.rb b/middleman-core/lib/middleman-core/template_context.rb index c2d666d8..d26e4667 100644 --- a/middleman-core/lib/middleman-core/template_context.rb +++ b/middleman-core/lib/middleman-core/template_context.rb @@ -1,3 +1,6 @@ +require 'middleman-core/file_renderer' +require 'middleman-core/template_renderer' + module Middleman class TemplateContext attr_reader :app @@ -27,7 +30,7 @@ module Middleman # Save current buffer for later _buf_was = save_buffer - layout_path = @app.locate_layout(layout_name, self.current_engine) + layout_path = ::Middleman::TemplateRenderer.locate_layout(@app, layout_name, self.current_engine) extension = File.extname(layout_path) engine = extension[1..-1].to_sym @@ -46,7 +49,8 @@ module Middleman restore_buffer(_buf_was) end - concat_safe_content @app.render_individual_file(layout_path, @current_locs || {}, @current_opts || {}, self) { content } + file_renderer = ::Middleman::FileRenderer.new(@app, layout_path) + concat_safe_content file_renderer.render(@current_locs || {}, @current_opts || {}, self) { content } ensure self.current_engine = engine_was end @@ -75,30 +79,31 @@ module Middleman relative_dir = File.join(current_dir.sub(%r{^#{Regexp.escape(self.source_dir)}/?}, ''), data) # Try to use the current engine first - found_partial, found_engine = @app.resolve_template(relative_dir, :preferred_engine => engine, :try_without_underscore => true) + found_partial, found_engine = ::Middleman::TemplateRenderer.resolve_template(@app, relative_dir, :preferred_engine => engine, :try_without_underscore => true) # Fall back to any engine available if !found_partial - found_partial, found_engine = @app.resolve_template(relative_dir, :try_without_underscore => true) + found_partial, found_engine = ::Middleman::TemplateRenderer.resolve_template(@app, relative_dir, :try_without_underscore => true) end end # Look in the partials_dir for the partial with the current engine partials_path = File.join(config[:partials_dir], data) if !found_partial && !engine.nil? - found_partial, found_engine = @app.resolve_template(partials_path, :preferred_engine => engine, :try_without_underscore => true) + found_partial, found_engine = ::Middleman::TemplateRenderer.resolve_template(@app, partials_path, :preferred_engine => engine, :try_without_underscore => true) end # Look in the root with any engine if !found_partial - found_partial, found_engine = @app.resolve_template(partials_path, :try_without_underscore => true) + found_partial, found_engine = ::Middleman::TemplateRenderer.resolve_template(@app, partials_path, :try_without_underscore => true) end # Render the partial if found, otherwide throw exception if found_partial - @app.render_individual_file(found_partial, locals, options, self, &block) + file_renderer = ::Middleman::FileRenderer.new(@app, found_partial) + file_renderer.render(locals, options, self, &block) else - raise ::Middleman::CoreExtensions::Rendering::TemplateNotFound, "Could not locate partial: #{data}" + raise ::Middleman::TemplateRenderer::TemplateNotFound, "Could not locate partial: #{data}" end end end diff --git a/middleman-core/lib/middleman-core/template_renderer.rb b/middleman-core/lib/middleman-core/template_renderer.rb new file mode 100644 index 00000000..65585c77 --- /dev/null +++ b/middleman-core/lib/middleman-core/template_renderer.rb @@ -0,0 +1,230 @@ +require 'tilt' +require 'active_support/core_ext/string/output_safety' +require 'middleman-core/template_context' +require 'middleman-core/file_renderer' + +module Middleman + + class TemplateRenderer + + def self.cache + @_cache ||= ::Tilt::Cache.new + end + + delegate :cache, :to => :"self.class" + + # 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] + def render(locs={}, opts={}, blocks=[]) + path = @path.dup + 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 + + # Sandboxed class for template eval + context = @app.template_context_class.new(@app, locs, opts) + + if context.respond_to?(:init_haml_helpers) + context.init_haml_helpers + end + + blocks.each do |block| + context.instance_eval(&block) + end + + # 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) + + 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}/#{layout_dir}." + end + end + + # If we need a layout and have a layout, use it + if layout_path = fetch_layout(engine, opts) + layout_renderer = ::Middleman::FileRenderer.new(@app, layout_path) + content = layout_renderer.render(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) + end + + protected + + # 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.has_key?(:layout) ? opts[:layout] : @app.config[:layout] + return false 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.has_key?(:layout_engine) + opts[:layout_engine] + elsif engine_options.has_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::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] + 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] + def self.locate_layout(app, name, preferred_engine=nil) + # Whether we've found the layout + layout_path = false + + # If we prefer a specific engine + if !preferred_engine.nil? + # Check root + layout_path, layout_engine = resolve_template(app, name, :preferred_engine => preferred_engine) + + # Check layouts folder + if !layout_path + layout_path, layout_engine = resolve_template(app, File.join(app.config[:layouts_dir], name.to_s), :preferred_engine => preferred_engine) + end + end + + # Check root, no preference + if !layout_path + layout_path, layout_engine = resolve_template(app, name) + end + + # Check layouts folder, no preference + if !layout_path + layout_path, layout_engine = resolve_template(app, File.join(app.config[:layouts_dir], name.to_s)) + end + + # Return the path + layout_path + end + + # Find a template on disk given a output path + # @param [String] request_path + # @param [Hash] options + # @return [Array, Boolean] + 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 + # @param [Hash] options + # @return [Array, Boolean] + def self.resolve_template(app, 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, app.source_dir) + + # By default, any engine will do + preferred_engine = '*' + + # Unless we're specifically looking for a preferred engine + if options.has_key?(:preferred_engine) + extension_class = ::Tilt[options[:preferred_engine]] + matched_exts = [] + + # Get a list of extensions for a preferred engine + # TODO: Cache this + ::Tilt.mappings.each do |ext, engines| + next unless engines.include? extension_class + matched_exts << ext + end + + # Change the glob to only look for the matched extensions + if matched_exts.length > 0 + preferred_engine = '{' + matched_exts.join(',') + '}' + else + return false + end + end + + # Look for files that match + path_with_ext = on_disk_path + '.' + preferred_engine + + found_path = Dir[path_with_ext].find do |path| + ::Tilt[path] + end + + if !found_path && options[:try_without_underscore] && + path_no_underscore = path_with_ext. + sub(relative_path, relative_path.sub(/^_/, ''). + sub(/\/_/, '/')) + found_path = Dir[path_no_underscore].find do |path| + ::Tilt[path] + end + end + + # If we found one, return it and the found engine + if found_path || app.files.exists?(on_disk_path) + engine = found_path ? File.extname(found_path)[1..-1].to_sym : nil + [ found_path || on_disk_path, engine ] + else + false + end + end + end + end +end