mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Refactor template preloading. New abstractions include Renderable mixins and a refactored Template class.
This commit is contained in:
parent
30204c4e66
commit
73b34e9f75
21 changed files with 361 additions and 472 deletions
|
@ -426,7 +426,7 @@ module ActionMailer #:nodoc:
|
|||
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 @@ module ActionController
|
|||
if expected.nil?
|
||||
!@response.rendered_with_file?
|
||||
else
|
||||
expected == rendered
|
||||
rendered.match(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -431,7 +431,7 @@ module ActionController #:nodoc:
|
|||
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 @@ module ActionController #:nodoc:
|
|||
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 @@ module ActionController #:nodoc:
|
|||
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 @@ module ActionController #:nodoc:
|
|||
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 @@ module ActionController #:nodoc:
|
|||
|
||||
# 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 @@ module ActionView #: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 @@ module ActionView #:nodoc:
|
|||
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 @@ module ActionView #:nodoc:
|
|||
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 @@ module ActionView #:nodoc:
|
|||
# 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 @@ module ActionView #: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 @@ module ActionView #:nodoc:
|
|||
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 @@ module ActionView
|
|||
|
||||
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
|
||||
|
|
85
actionpack/lib/action_view/paths.rb
Normal file
85
actionpack/lib/action_view/paths.rb
Normal file
|
@ -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
|
||||
|
|
19
actionpack/lib/action_view/renderable_partial.rb
Normal file
19
actionpack/lib/action_view/renderable_partial.rb
Normal file
|
@ -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 @@ module ActionView
|
|||
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 ActionView
|
|||
module Compilable
|
||||
def self.included(base)
|
||||
base.extend ClassMethod
|
||||
|
||||
@@mutex = Mutex.new
|
||||
end
|
||||
|
||||
module ClassMethod
|
||||
|
@ -17,54 +15,6 @@ module ActionView
|
|||
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 @@ class LayoutAutoDiscoveryTest < Test::Unit::TestCase
|
|||
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 @@ module Rails
|
|||
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
|
||||
|
||||
|
|
Loading…
Reference in a new issue