Refactor template preloading. New abstractions include Renderable mixins and a refactored Template class.

This commit is contained in:
Joshua Peek 2008-07-12 14:33:46 -05:00
parent 30204c4e66
commit 73b34e9f75
21 changed files with 361 additions and 472 deletions

@ -426,7 +426,7 @@ def register_template_extension(extension)
end
def template_root=(root)
write_inheritable_attribute(:template_root, ActionView::ViewLoadPaths.new(Array(root)))
write_inheritable_attribute(:template_root, ActionView::PathSet.new(Array(root)))
end
end

@ -1,5 +1,7 @@
*Edge*
* Refactor template preloading. New abstractions include Renderable mixins and a refactored Template class [Josh Peek]
* Changed ActionView::TemplateHandler#render API method signature to render(template, local_assigns = {}) [Josh Peek]
* Changed PrototypeHelper#submit_to_remote to PrototypeHelper#button_to_remote to stay consistent with link_to_remote (submit_to_remote still works as an alias) #8994 [clemens]

@ -93,7 +93,7 @@ def assert_template(expected = nil, message=nil)
if expected.nil?
!@response.rendered_with_file?
else
expected == rendered
rendered.match(expected)
end
end
end

@ -431,7 +431,7 @@ def view_paths
end
def view_paths=(value)
@view_paths = ActionView::ViewLoadPaths.new(Array(value)) if value
@view_paths = ActionView::PathSet.new(Array(value)) if value
end
# Adds a view_path to the front of the view_paths array.
@ -652,7 +652,7 @@ def view_paths
end
def view_paths=(value)
@template.view_paths = ViewLoadPaths.new(value)
@template.view_paths = PathSet.new(value)
end
# Adds a view_path to the front of the view_paths array.
@ -1248,9 +1248,8 @@ def template_public?(template_name = default_template_name)
end
def template_exempt_from_layout?(template_name = default_template_name)
extension = @template && @template.pick_template_extension(template_name)
name_with_extension = !template_name.include?('.') && extension ? "#{template_name}.#{extension}" : template_name
@@exempt_from_layout.any? { |ext| name_with_extension =~ ext }
template_name = @template.pick_template(template_name).to_s if @template
@@exempt_from_layout.any? { |ext| template_name =~ ext }
end
def default_template_name(action_name = self.action_name)

@ -304,7 +304,7 @@ def action_has_layout?
end
def layout_directory?(layout_name)
@template.view_paths.find_template_file_for_path("#{File.join('layouts', layout_name)}.#{@template.template_format}.erb") ? true : false
@template.file_exists?("#{File.join('layouts', layout_name)}.#{@template.template_format}")
end
end
end

@ -207,13 +207,9 @@ def redirect_url_match?( pattern )
# Returns the template path of the file which was used to
# render this response (or nil)
def rendered_file(with_controller=false)
unless template.first_render.nil?
unless with_controller
template.first_render
else
template.first_render.split('/').last || template.first_render
end
def rendered_file(with_controller = false)
if template.first_render
template.first_render.to_s
end
end

@ -21,14 +21,14 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
require 'action_view/template_handlers'
require 'action_view/template_file'
require 'action_view/view_load_paths'
require 'action_view/template_handlers'
require 'action_view/renderable'
require 'action_view/renderable_partial'
require 'action_view/template'
require 'action_view/partial_template'
require 'action_view/inline_template'
require 'action_view/paths'
require 'action_view/base'
require 'action_view/partials'

@ -3,6 +3,12 @@ class ActionViewError < StandardError #:nodoc:
end
class MissingTemplate < ActionViewError #:nodoc:
def initialize(paths, path, template_format = nil)
full_template_path = path.include?('.') ? path : "#{path}.erb"
display_paths = paths.join(':')
template_type = (path =~ /layouts/i) ? 'layout' : 'template'
super("Missing #{template_type} #{full_template_path} in view path #{display_paths}")
end
end
# Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb
@ -216,12 +222,14 @@ def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)
attr_reader :view_paths
def view_paths=(paths)
@view_paths = ViewLoadPaths.new(Array(paths))
@view_paths = PathSet.new(Array(paths))
end
# Renders the template present at <tt>template_path</tt> (relative to the view_paths array).
# The hash in <tt>local_assigns</tt> is made available as local variables.
def render(options = {}, local_assigns = {}, &block) #:nodoc:
local_assigns ||= {}
if options.is_a?(String)
render_file(options, nil, local_assigns)
elsif options == :update
@ -270,21 +278,40 @@ def template_format
end
def file_exists?(template_path)
view_paths.template_exists?(template_file_from_name(template_path))
pick_template(template_path) ? true : false
rescue MissingTemplate
false
end
# Gets the extension for an existing template with the given template_path.
# Returns the format with the extension if that template exists.
#
# pick_template_extension('users/show')
# # => 'html.erb'
# pick_template('users/show')
# # => 'users/show.html.erb'
#
# pick_template_extension('users/legacy')
# # => "rhtml"
# pick_template('users/legacy')
# # => 'users/legacy.rhtml'
#
def pick_template_extension(template_path)
if template = template_file_from_name(template_path)
template.extension
def pick_template(template_path)
path = template_path.sub(/^\//, '')
if m = path.match(/(.*)\.(\w+)$/)
template_file_name, template_file_extension = m[1], m[2]
else
template_file_name = path
end
# OPTIMIZE: Checks to lookup template in view path
if template = self.view_paths["#{template_file_name}.#{template_format}"]
template
elsif template = self.view_paths[template_file_name]
template
elsif first_render && template = self.view_paths["#{template_file_name}.#{first_render.extension}"]
template
elsif template_format == :js && template = self.view_paths["#{template_file_name}.html"]
@template_format = :html
template
else
Template.new(template_path, view_paths)
end
end
@ -292,6 +319,10 @@ def pick_template_extension(template_path)
# Renders the template present at <tt>template_path</tt>. The hash in <tt>local_assigns</tt>
# is made available as local variables.
def render_file(template_path, use_full_path = nil, local_assigns = {}) #:nodoc:
unless use_full_path == nil
ActiveSupport::Deprecation.warn("use_full_path option has been deprecated and has no affect.", caller)
end
if defined?(ActionMailer) && defined?(ActionMailer::Base) && controller.is_a?(ActionMailer::Base) && !template_path.include?("/")
raise ActionViewError, <<-END_ERROR
Due to changes in ActionMailer, you need to provide the mailer_name along with the template name.
@ -305,11 +336,12 @@ def render_file(template_path, use_full_path = nil, local_assigns = {}) #:nodoc:
END_ERROR
end
Template.new(self, template_path, use_full_path, local_assigns).render_template
template = pick_template(template_path)
template.render_template(self, local_assigns)
end
def render_inline(text, local_assigns = {}, type = nil)
InlineTemplate.new(self, text, local_assigns, type).render
InlineTemplate.new(text, type).render(self, local_assigns)
end
def wrap_content_for_layout(content)
@ -333,32 +365,9 @@ def assign_variables_from_controller
end
def execute(template, local_assigns = {})
send(template.method, local_assigns) do |*names|
send(template.method(local_assigns), local_assigns) do |*names|
instance_variable_get "@content_for_#{names.first || 'layout'}"
end
end
def template_file_from_name(template_name)
template_name = TemplateFile.from_path(template_name)
pick_template(template_name) unless template_name.extension
end
def pick_template(file)
if f = self.view_paths.find_template_file_for_path(file.dup_with_extension(template_format)) || file_from_first_render(file)
f
elsif template_format == :js && f = self.view_paths.find_template_file_for_path(file.dup_with_extension(:html))
@template_format = :html
f
else
nil
end
end
# Determine the template extension from the <tt>@first_render</tt> filename
def file_from_first_render(file)
if extension = File.basename(@first_render.to_s)[/^[^.]+\.(.+)$/, 1]
file.dup_with_extension(extension)
end
end
end
end

@ -2,15 +2,18 @@ module ActionView #:nodoc:
class InlineTemplate #:nodoc:
include Renderable
def initialize(view, source, locals = {}, type = nil)
@view = view
attr_reader :source, :extension, :method_segment
def initialize(source, type = nil)
@source = source
@extension = type
@locals = locals || {}
@method_segment = "inline_#{@source.hash.abs}"
@handler = Template.handler_class_for_extension(@extension).new(@view)
end
private
# Always recompile inline templates
def recompile?(local_assigns)
true
end
end
end

@ -1,69 +0,0 @@
module ActionView #:nodoc:
class PartialTemplate < Template #:nodoc:
attr_reader :variable_name, :object, :as
def initialize(view, partial_path, object = nil, locals = {}, as = nil)
@view_controller = view.controller if view.respond_to?(:controller)
@as = as
set_path_and_variable_name!(partial_path)
super(view, @path, nil, locals)
add_object_to_local_assigns!(object)
# This is needed here in order to compile template with knowledge of 'counter'
initialize_counter!
# Prepare early. This is a performance optimization for partial collections
prepare!
end
def render
ActionController::Base.benchmark("Rendered #{@path.path_without_format_and_extension}", Logger::DEBUG, false) do
super
end
end
def render_member(object)
@locals[:object] = @locals[@variable_name] = object
@locals[as] = object if as
template = render_template
@locals[@counter_name] += 1
@locals.delete(as)
@locals.delete(@variable_name)
@locals.delete(:object)
template
end
def counter=(num)
@locals[@counter_name] = num
end
private
def add_object_to_local_assigns!(object)
@locals[:object] ||=
@locals[@variable_name] ||= object || @view_controller.instance_variable_get("@#{variable_name}")
@locals[as] ||= @locals[:object] if as
end
def set_path_and_variable_name!(partial_path)
if partial_path.include?('/')
@variable_name = File.basename(partial_path)
@path = "#{File.dirname(partial_path)}/_#{@variable_name}"
elsif @view_controller
@variable_name = partial_path
@path = "#{@view_controller.class.controller_path}/_#{@variable_name}"
else
@variable_name = partial_path
@path = "_#{@variable_name}"
end
@variable_name = @variable_name.sub(/\..*$/, '').to_sym
end
def initialize_counter!
@counter_name ||= "#{@variable_name}_counter".to_sym
@locals[@counter_name] = 0
end
end
end

@ -104,10 +104,12 @@ module ActionView
module Partials
private
def render_partial(partial_path, object_assigns = nil, local_assigns = {}) #:nodoc:
local_assigns ||= {}
case partial_path
when String, Symbol, NilClass
# Render the template
ActionView::PartialTemplate.new(self, partial_path, object_assigns, local_assigns).render_template
variable_name, path = partial_pieces(partial_path)
pick_template(path).render_partial(self, variable_name, object_assigns, local_assigns)
when ActionView::Helpers::FormBuilder
builder_partial_path = partial_path.class.to_s.demodulize.underscore.sub(/_builder$/, '')
render_partial(builder_partial_path, object_assigns, (local_assigns || {}).merge(builder_partial_path.to_sym => partial_path))
@ -128,31 +130,43 @@ def render_partial_collection(partial_path, collection, partial_spacer_template
local_assigns = local_assigns ? local_assigns.clone : {}
spacer = partial_spacer_template ? render(:partial => partial_spacer_template) : ''
_partial_pieces = {}
_templates = {}
if partial_path.nil?
render_partial_collection_with_unknown_partial_path(collection, local_assigns, as)
else
render_partial_collection_with_known_partial_path(collection, partial_path, local_assigns, as)
index = 0
collection.map do |object|
_partial_path ||= partial_path || ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path)
variable_name, path = _partial_pieces[_partial_path] ||= partial_pieces(_partial_path)
template = _templates[path] ||= pick_template(path)
local_assigns["#{variable_name}_counter".to_sym] = index
local_assigns[:object] = local_assigns[variable_name] = object
local_assigns[as] = object if as
result = template.render_partial(self, variable_name, object, local_assigns)
local_assigns.delete(as)
local_assigns.delete(variable_name)
local_assigns.delete(:object)
index += 1
result
end.join(spacer)
end
def render_partial_collection_with_known_partial_path(collection, partial_path, local_assigns, as)
template = ActionView::PartialTemplate.new(self, partial_path, nil, local_assigns, as)
collection.map do |element|
template.render_member(element)
end
end
def render_partial_collection_with_unknown_partial_path(collection, local_assigns, as)
templates = Hash.new
i = 0
collection.map do |element|
partial_path = ActionController::RecordIdentifier.partial_path(element, controller.class.controller_path)
template = templates[partial_path] ||= ActionView::PartialTemplate.new(self, partial_path, nil, local_assigns, as)
template.counter = i
i += 1
template.render_member(element)
def partial_pieces(partial_path)
if partial_path.include?('/')
variable_name = File.basename(partial_path)
path = "#{File.dirname(partial_path)}/_#{variable_name}"
elsif respond_to?(:controller)
variable_name = partial_path
path = "#{controller.class.controller_path}/_#{variable_name}"
else
variable_name = partial_path
path = "_#{variable_name}"
end
variable_name = variable_name.sub(/\..*$/, '').to_sym
return variable_name, path
end
end
end

@ -0,0 +1,85 @@
module ActionView #:nodoc:
class PathSet < Array #:nodoc:
def self.type_cast(obj)
obj.is_a?(String) ? Path.new(obj) : obj
end
class Path #:nodoc:
attr_reader :path, :paths
delegate :to_s, :to_str, :inspect, :to => :path
def initialize(path)
@path = path.freeze
reload!
end
def ==(path)
to_str == path.to_str
end
def [](path)
@paths[path]
end
# Rebuild load path directory cache
def reload!
@paths = {}
templates_in_path do |template|
@paths[template.path] = template
@paths[template.path_without_extension] ||= template
end
@paths.freeze
end
private
def templates_in_path
(Dir.glob("#{@path}/**/*/**") | Dir.glob("#{@path}/**")).each do |file|
unless File.directory?(file)
template = Template.new(file.split("#{self}/").last, self)
# Eager load memoized methods and freeze cached template
template.freeze if Base.cache_template_loading
yield template
end
end
end
end
def initialize(*args)
super(*args).map! { |obj| self.class.type_cast(obj) }
end
def reload!
each { |path| path.reload! }
end
def <<(obj)
super(self.class.type_cast(obj))
end
def push(*objs)
delete_paths!(objs)
super(*objs.map { |obj| self.class.type_cast(obj) })
end
def unshift(*objs)
delete_paths!(objs)
super(*objs.map { |obj| self.class.type_cast(obj) })
end
def [](template_path)
each do |path|
if template = path[template_path]
return template
end
end
nil
end
private
def delete_paths!(paths)
paths.each { |p1| delete_if { |p2| p1.to_s == p2.to_s } }
end
end
end

@ -1,37 +1,78 @@
module ActionView
module Renderable
# TODO: Local assigns should not be tied to template instance
attr_accessor :locals
# NOTE: The template that this mixin is beening include into is frozen
# So you can not set or modify any instance variables
# TODO: These readers should be private
attr_reader :filename, :source, :handler
def render
prepare!
@handler.render(self, @locals)
def self.included(base)
@@mutex = Mutex.new
end
def method
['_run', @extension, @method_segment, local_assigns_keys].compact.join('_').to_sym
# NOTE: Exception to earlier notice. Ensure this is called before freeze
def handler
@handler ||= Template.handler_class_for_extension(extension)
end
# NOTE: Exception to earlier notice. Ensure this is called before freeze
def compiled_source
@compiled_source ||= handler.new(nil).compile(self) if handler.compilable?
end
def render(view, local_assigns = {})
view.first_render ||= self
view.send(:evaluate_assigns)
view.current_render_extension = extension
compile(local_assigns) if handler.compilable?
handler.new(view).render(self, local_assigns)
end
def method(local_assigns)
if local_assigns && local_assigns.any?
local_assigns_keys = "locals_#{local_assigns.keys.map { |k| k.to_s }.sort.join('_')}"
end
['_run', extension, method_segment, local_assigns_keys].compact.join('_').to_sym
end
private
def prepare!
unless @prepared
@view.send(:evaluate_assigns)
@view.current_render_extension = @extension
# Compile and evaluate the template's code
def compile(local_assigns)
render_symbol = method(local_assigns)
if @handler.compilable?
@handler.compile_template(self) # compile the given template, if necessary
@@mutex.synchronize do
return false unless recompile?(render_symbol)
locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join
source = <<-end_src
def #{render_symbol}(local_assigns)
old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
ensure
self.output_buffer = old_output_buffer
end
end_src
begin
file_name = respond_to?(:filename) ? filename : 'compiled-template'
ActionView::Base::CompiledTemplates.module_eval(source, file_name, 0)
rescue Exception => e # errors from template code
if logger = ActionController::Base.logger
logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
logger.debug "Function body: #{source}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end
raise ActionView::TemplateError.new(self, {}, e)
end
@prepared = true
end
end
def local_assigns_keys
if @locals && @locals.any?
"locals_#{@locals.keys.map { |k| k.to_s }.sort.join('_')}"
# Method to check whether template compilation is necessary.
# The template will be compiled if the file has not been compiled yet, or
# if local_assigns has a new key, which isn't supported by the compiled code yet.
def recompile?(symbol)
unless Base::CompiledTemplates.instance_methods.include?(symbol) && Base.cache_template_loading
true
else
false
end
end
end

@ -0,0 +1,19 @@
module ActionView
module RenderablePartial
# NOTE: The template that this mixin is beening include into is frozen
# So you can not set or modify any instance variables
def render(view, local_assigns = {})
ActionController::Base.benchmark("Rendered #{path_without_format_and_extension}", Logger::DEBUG, false) do
super
end
end
def render_partial(view, variable_name, object = nil, local_assigns = {}, as = nil)
object ||= view.controller.instance_variable_get("@#{variable_name}") if view.respond_to?(:controller)
local_assigns[:object] ||= local_assigns[variable_name] ||= object
local_assigns[as] ||= local_assigns[:object] if as
render_template(view, local_assigns)
end
end
end

@ -1,82 +1,112 @@
module ActionView #:nodoc:
class Template #:nodoc:
class Template
extend TemplateHandlers
include Renderable
attr_reader :path, :extension
attr_accessor :filename, :load_path, :base_path, :name, :format, :extension
delegate :to_s, :to => :path
def initialize(view, path, use_full_path = nil, locals = {})
unless use_full_path == nil
ActiveSupport::Deprecation.warn("use_full_path option has been deprecated and has no affect.", caller)
end
def initialize(template_path, load_paths = [])
template_path = template_path.dup
@base_path, @name, @format, @extension = split(template_path)
@base_path.to_s.gsub!(/\/$/, '') # Push to split method
@load_path, @filename = find_full_path(template_path, load_paths)
@view = view
@paths = view.view_paths
@original_path = path
@path = TemplateFile.from_path(path)
@view.first_render ||= @path.to_s
set_extension_and_file_name
@method_segment = compiled_method_name_file_path_segment
@locals = (locals && locals.dup) || {}
@handler = self.class.handler_class_for_extension(@extension).new(@view)
# Extend with partial super powers
extend RenderablePartial if @name =~ /^_/
end
def render_template
render
rescue Exception => e
raise e unless filename
if TemplateError === e
e.sub_template_of(filename)
raise e
else
raise TemplateError.new(self, @view.assigns, e)
end
def freeze
# Eager load memoized methods
format_and_extension
path
path_without_extension
path_without_format_and_extension
source
method_segment
# Eager load memoized methods from Renderable
handler
compiled_source
instance_variables.each { |ivar| ivar.freeze }
super
end
def format_and_extension
@format_and_extension ||= (extensions = [format, extension].compact.join(".")).blank? ? nil : extensions
end
def path
@path ||= [base_path, [name, format, extension].compact.join('.')].compact.join('/')
end
def path_without_extension
@path_without_extension ||= [base_path, [name, format].compact.join('.')].compact.join('/')
end
def path_without_format_and_extension
@path_without_format_and_extension ||= [base_path, name].compact.join('/')
end
def source
@source ||= File.read(@filename)
end
def base_path_for_exception
(@paths.find_load_path_for_path(@path) || @paths.first).to_s
def method_segment
unless @method_segment
segment = File.expand_path(@filename)
segment.sub!(/^#{Regexp.escape(File.expand_path(RAILS_ROOT))}/, '') if defined?(RAILS_ROOT)
segment.gsub!(/([^a-zA-Z0-9_])/) { $1.ord }
@method_segment = segment
end
@method_segment
end
def render_template(view, local_assigns = {})
render(view, local_assigns)
rescue Exception => e
raise e unless filename
if TemplateError === e
e.sub_template_of(filename)
raise e
else
raise TemplateError.new(self, view.assigns, e)
end
end
private
def set_extension_and_file_name
@extension = @path.extension
unless @extension
@path = @view.send(:template_file_from_name, @path)
raise_missing_template_exception unless @path
@extension = @path.extension
end
if p = @paths.find_template_file_for_path(path)
@path = p
@filename = @path.full_path
@extension = @path.extension
raise_missing_template_exception if @filename.blank?
else
@filename = @original_path
raise_missing_template_exception unless File.exist?(@filename)
end
def valid_extension?(extension)
Template.template_handler_extensions.include?(extension)
end
def raise_missing_template_exception
full_template_path = @original_path.include?('.') ? @original_path : "#{@original_path}.#{@view.template_format}.erb"
display_paths = @paths.join(':')
template_type = (@original_path =~ /layouts/i) ? 'layout' : 'template'
raise MissingTemplate, "Missing #{template_type} #{full_template_path} in view path #{display_paths}"
def find_full_path(path, load_paths)
load_paths = Array(load_paths) + [nil]
load_paths.each do |load_path|
file = [load_path, path].compact.join('/')
return load_path, file if File.exist?(file)
end
raise MissingTemplate.new(load_paths, path)
end
def compiled_method_name_file_path_segment
s = File.expand_path(@filename)
s.sub!(/^#{Regexp.escape(File.expand_path(RAILS_ROOT))}/, '') if defined?(RAILS_ROOT)
s.gsub!(/([^a-zA-Z0-9_])/) { $1.ord }
s
# Returns file split into an array
# [base_path, name, format, extension]
def split(file)
if m = file.match(/^(.*\/)?([^\.]+)\.?(\w+)?\.?(\w+)?\.?(\w+)?$/)
if m[5] # Mulipart formats
[m[1], m[2], "#{m[3]}.#{m[4]}", m[5]]
elsif m[4] # Single format
[m[1], m[2], m[3], m[4]]
else
if valid_extension?(m[3]) # No format
[m[1], m[2], nil, m[3]]
else # No extension
[m[1], m[2], m[3], nil]
end
end
end
end
end
end

@ -7,7 +7,7 @@ class TemplateError < ActionViewError #:nodoc:
attr_reader :original_exception
def initialize(template, assigns, original_exception)
@base_path = template.base_path_for_exception
@base_path = template.base_path
@assigns, @source, @original_exception = assigns.dup, template.source, original_exception
@file_path = template.filename
@backtrace = compute_backtrace

@ -1,88 +0,0 @@
module ActionView #:nodoc:
# TemplateFile abstracts the pattern of querying a file path for its
# path with or without its extension. The path is only the partial path
# from the load path root e.g. "hello/index.html.erb" not
# "app/views/hello/index.html.erb"
class TemplateFile
def self.from_path(path)
path.is_a?(self) ? path : new(path)
end
def self.from_full_path(load_path, full_path)
file = new(full_path.split(load_path).last)
file.load_path = load_path
file.freeze
end
attr_accessor :load_path, :base_path, :name, :format, :extension
delegate :to_s, :inspect, :to => :path
def initialize(path)
path = path.dup
# Clear the forward slash in the beginning
trim_forward_slash!(path)
@base_path, @name, @format, @extension = split(path)
end
def freeze
@load_path.freeze
@base_path.freeze
@name.freeze
@format.freeze
@extension.freeze
super
end
def format_and_extension
extensions = [format, extension].compact.join(".")
extensions.blank? ? nil : extensions
end
def full_path
if load_path
"#{load_path}/#{path}"
else
path
end
end
def path
base_path.to_s + [name, format, extension].compact.join(".")
end
def path_without_extension
base_path.to_s + [name, format].compact.join(".")
end
def path_without_format_and_extension
"#{base_path}#{name}"
end
def dup_with_extension(extension)
file = dup
file.extension = extension ? extension.to_s : nil
file
end
private
def trim_forward_slash!(path)
path.sub!(/^\//, '')
end
# Returns file split into an array
# [base_path, name, format, extension]
def split(file)
if m = file.match(/^(.*\/)?([^\.]+)\.?(\w+)?\.?(\w+)?\.?(\w+)?$/)
if m[5] # Mulipart formats
[m[1], m[2], "#{m[3]}.#{m[4]}", m[5]]
elsif m[4] # Single format
[m[1], m[2], m[3], m[4]]
else # No format
[m[1], m[2], nil, m[3]]
end
end
end
end
end

@ -3,8 +3,6 @@ module TemplateHandlers
module Compilable
def self.included(base)
base.extend ClassMethod
@@mutex = Mutex.new
end
module ClassMethod
@ -17,54 +15,6 @@ def compilable?
def render(template, local_assigns = {})
@view.send(:execute, template, local_assigns)
end
# Compile and evaluate the template's code
def compile_template(template)
return false unless recompile_template?(template)
@@mutex.synchronize do
locals_code = template.locals.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join
source = <<-end_src
def #{template.method}(local_assigns)
old_output_buffer = output_buffer;#{locals_code};#{compile(template)}
ensure
self.output_buffer = old_output_buffer
end
end_src
begin
file_name = template.filename || 'compiled-template'
ActionView::Base::CompiledTemplates.module_eval(source, file_name, 0)
rescue Exception => e # errors from template code
if logger = ActionController::Base.logger
logger.debug "ERROR: compiling #{template.method} RAISED #{e}"
logger.debug "Function body: #{source}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end
raise ActionView::TemplateError.new(template, @view.assigns, e)
end
end
end
private
# Method to check whether template compilation is necessary.
# The template will be compiled if the inline template or file has not been compiled yet,
# if local_assigns has a new key, which isn't supported by the compiled code yet.
def recompile_template?(template)
# Unless the template has been complied yet, compile
return true unless Base::CompiledTemplates.instance_methods.include?(template.method.to_s)
# If template caching is disabled, compile
return true unless Base.cache_template_loading
# Always recompile inline templates
return true if template.is_a?(InlineTemplate)
# Otherwise, use compiled method
return false
end
end
end
end

@ -1,103 +0,0 @@
module ActionView #:nodoc:
class ViewLoadPaths < Array #:nodoc:
def self.type_cast(obj)
obj.is_a?(String) ? LoadPath.new(obj) : obj
end
class LoadPath #:nodoc:
attr_reader :path, :paths
delegate :to_s, :to_str, :inspect, :to => :path
def initialize(path)
@path = path.freeze
reload!
end
def ==(path)
to_str == path.to_str
end
# Rebuild load path directory cache
def reload!
@paths = {}
files.each do |file|
@paths[file.path] = file
@paths[file.path_without_extension] ||= file
end
@paths.freeze
end
def find_template_file_for_partial_path(template_path, template_format)
@paths["#{template_path}.#{template_format}"] ||
@paths[template_path] ||
@paths[template_path.gsub(/\..*$/, '')]
end
private
# Get all the files and directories in the path
def files_in_path
Dir.glob("#{@path}/**/*/**") | Dir.glob("#{@path}/**")
end
# Create an array of all the files within the path
def files
files_in_path.map do |file|
TemplateFile.from_full_path(@path, file) unless File.directory?(file)
end.compact
end
end
def initialize(*args)
super(*args).map! { |obj| self.class.type_cast(obj) }
end
def reload!
each { |path| path.reload! }
end
def <<(obj)
super(self.class.type_cast(obj))
end
def push(*objs)
delete_paths!(objs)
super(*objs.map { |obj| self.class.type_cast(obj) })
end
def unshift(*objs)
delete_paths!(objs)
super(*objs.map { |obj| self.class.type_cast(obj) })
end
def template_exists?(file)
find_load_path_for_path(file) ? true : false
end
def find_load_path_for_path(file)
find { |path| path.paths[file.to_s] }
end
def find_template_file_for_path(template_path)
template_path_without_extension, template_extension = path_and_extension(template_path.to_s)
each do |path|
if f = path.find_template_file_for_partial_path(template_path_without_extension, template_extension)
return f
end
end
nil
end
private
def delete_paths!(paths)
paths.each { |p1| delete_if { |p2| p1.to_s == p2.to_s } }
end
# Splits the path and extension from the given template_path and returns as an array.
def path_and_extension(template_path)
template_path_without_extension = template_path.sub(/\.(\w+)$/, '')
[template_path_without_extension, $1]
end
end
end

@ -63,6 +63,7 @@ def test_controller_name_layout_name_match
end
def test_third_party_template_library_auto_discovers_layout
ThirdPartyTemplateLibraryController.view_paths.reload!
@controller = ThirdPartyTemplateLibraryController.new
get :hello
assert_equal 'layouts/third_party_template_library', @controller.active_layout

@ -137,12 +137,12 @@ def process
initialize_logger
initialize_framework_logging
initialize_framework_views
initialize_dependency_mechanism
initialize_whiny_nils
initialize_temporary_session_directory
initialize_time_zone
initialize_framework_settings
initialize_framework_views
add_support_load_paths