aboutsummaryrefslogtreecommitdiffstats
path: root/actionview/lib/action_view
diff options
context:
space:
mode:
Diffstat (limited to 'actionview/lib/action_view')
-rw-r--r--actionview/lib/action_view/base.rb119
-rw-r--r--actionview/lib/action_view/cache_expiry.rb49
-rw-r--r--actionview/lib/action_view/digestor.rb18
-rw-r--r--actionview/lib/action_view/gem_version.rb2
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb10
-rw-r--r--actionview/lib/action_view/helpers/csp_helper.rb6
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb4
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/output_safety_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/rendering_helper.rb10
-rw-r--r--actionview/lib/action_view/helpers/tags/base.rb2
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb4
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb2
-rw-r--r--actionview/lib/action_view/layouts.rb10
-rw-r--r--actionview/lib/action_view/lookup_context.rb96
-rw-r--r--actionview/lib/action_view/path_set.rb15
-rw-r--r--actionview/lib/action_view/railtie.rb13
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb59
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb116
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb47
-rw-r--r--actionview/lib/action_view/renderer/renderer.rb20
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb4
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb40
-rw-r--r--actionview/lib/action_view/rendering.rb69
-rw-r--r--actionview/lib/action_view/template.rb141
-rw-r--r--actionview/lib/action_view/template/error.rb22
-rw-r--r--actionview/lib/action_view/template/handlers.rb28
-rw-r--r--actionview/lib/action_view/template/handlers/builder.rb4
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb10
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubi.rb6
-rw-r--r--actionview/lib/action_view/template/handlers/html.rb2
-rw-r--r--actionview/lib/action_view/template/handlers/raw.rb4
-rw-r--r--actionview/lib/action_view/template/html.rb19
-rw-r--r--actionview/lib/action_view/template/inline.rb22
-rw-r--r--actionview/lib/action_view/template/raw_file.rb28
-rw-r--r--actionview/lib/action_view/template/resolver.rb197
-rw-r--r--actionview/lib/action_view/template/sources.rb13
-rw-r--r--actionview/lib/action_view/template/sources/file.rb17
-rw-r--r--actionview/lib/action_view/template/text.rb8
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb21
-rw-r--r--actionview/lib/action_view/unbound_template.rb32
-rw-r--r--actionview/lib/action_view/view_paths.rb26
42 files changed, 888 insertions, 431 deletions
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index 6cb342a0a9..5253ef7b0c 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -3,6 +3,7 @@
require "active_support/core_ext/module/attr_internal"
require "active_support/core_ext/module/attribute_accessors"
require "active_support/ordered_options"
+require "active_support/deprecation"
require "action_view/log_subscriber"
require "action_view/helpers"
require "action_view/context"
@@ -10,10 +11,6 @@ require "action_view/template"
require "action_view/lookup_context"
module ActionView #:nodoc:
- module CompiledTemplates #:nodoc:
- # holds compiled template code
- end
-
# = Action View Base
#
# Action View templates can be written in several ways.
@@ -145,8 +142,6 @@ module ActionView #:nodoc:
class Base
include Helpers, ::ERB::Util, Context
- include CompiledTemplates
-
# Specify the proc used to decorate input tags that refer to attributes with errors.
cattr_accessor :field_error_proc, default: Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe }
@@ -185,47 +180,131 @@ module ActionView #:nodoc:
def xss_safe? #:nodoc:
true
end
+
+ def with_empty_template_cache # :nodoc:
+ subclass = Class.new(self) {
+ # We can't implement these as self.class because subclasses will
+ # share the same template cache as superclasses, so "changed?" won't work
+ # correctly.
+ define_method(:compiled_method_container) { subclass }
+ define_singleton_method(:compiled_method_container) { subclass }
+ }
+ end
+
+ def changed?(other) # :nodoc:
+ compiled_method_container != other.compiled_method_container
+ end
end
- attr_accessor :view_renderer
+ attr_reader :view_renderer, :lookup_context
attr_internal :config, :assigns
- delegate :lookup_context, to: :view_renderer
delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, to: :lookup_context
def assign(new_assigns) # :nodoc:
@_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end
- def initialize(context = nil, assigns = {}, controller = nil, formats = nil) #:nodoc:
+ # :stopdoc:
+
+ def self.build_lookup_context(context)
+ case context
+ when ActionView::Renderer
+ context.lookup_context
+ when Array
+ ActionView::LookupContext.new(context)
+ when ActionView::PathSet
+ ActionView::LookupContext.new(context)
+ when nil
+ ActionView::LookupContext.new([])
+ else
+ raise NotImplementedError, context.class.name
+ end
+ end
+
+ def self.empty
+ with_view_paths([])
+ end
+
+ def self.with_view_paths(view_paths, assigns = {}, controller = nil)
+ with_context ActionView::LookupContext.new(view_paths), assigns, controller
+ end
+
+ def self.with_context(context, assigns = {}, controller = nil)
+ new context, assigns, controller
+ end
+
+ NULL = Object.new
+
+ # :startdoc:
+
+ def initialize(lookup_context = nil, assigns = {}, controller = nil, formats = NULL) #:nodoc:
@_config = ActiveSupport::InheritableOptions.new
- if context.is_a?(ActionView::Renderer)
- @view_renderer = context
+ unless formats == NULL
+ ActiveSupport::Deprecation.warn <<~eowarn.squish
+ Passing formats to ActionView::Base.new is deprecated
+ eowarn
+ end
+
+ case lookup_context
+ when ActionView::LookupContext
+ @lookup_context = lookup_context
else
- lookup_context = context.is_a?(ActionView::LookupContext) ?
- context : ActionView::LookupContext.new(context)
- lookup_context.formats = formats if formats
- lookup_context.prefixes = controller._prefixes if controller
- @view_renderer = ActionView::Renderer.new(lookup_context)
+ ActiveSupport::Deprecation.warn <<~eowarn.squish
+ ActionView::Base instances should be constructed with a lookup context,
+ assignments, and a controller.
+ eowarn
+ @lookup_context = self.class.build_lookup_context(lookup_context)
end
+ @view_renderer = ActionView::Renderer.new @lookup_context
+ @current_template = nil
+
@cache_hit = {}
assign(assigns)
assign_controller(controller)
_prepare_context
end
- def run(method, locals, buffer, &block)
- _old_output_buffer, _old_virtual_path = @output_buffer, @virtual_path
+ def _run(method, template, locals, buffer, &block)
+ _old_output_buffer, _old_virtual_path, _old_template = @output_buffer, @virtual_path, @current_template
+ @current_template = template
@output_buffer = buffer
send(method, locals, buffer, &block)
ensure
- @output_buffer, @virtual_path = _old_output_buffer, _old_virtual_path
+ @output_buffer, @virtual_path, @current_template = _old_output_buffer, _old_virtual_path, _old_template
end
def compiled_method_container
- CompiledTemplates
+ if self.class == ActionView::Base
+ ActiveSupport::Deprecation.warn <<~eowarn.squish
+ ActionView::Base instances must implement `compiled_method_container`
+ or use the class method `with_empty_template_cache` for constructing
+ an ActionView::Base instances that has an empty cache.
+ eowarn
+ end
+
+ self.class
+ end
+
+ def in_rendering_context(options)
+ old_view_renderer = @view_renderer
+ old_lookup_context = @lookup_context
+
+ if !lookup_context.html_fallback_for_js && options[:formats]
+ formats = Array(options[:formats])
+ if formats == [:js]
+ formats << :html
+ end
+ @lookup_context = lookup_context.with_prepended_formats(formats)
+ @view_renderer = ActionView::Renderer.new @lookup_context
+ end
+
+ yield @view_renderer
+ ensure
+ @view_renderer = old_view_renderer
+ @lookup_context = old_lookup_context
end
ActiveSupport.run_load_hooks(:action_view, self)
diff --git a/actionview/lib/action_view/cache_expiry.rb b/actionview/lib/action_view/cache_expiry.rb
new file mode 100644
index 0000000000..820afc093d
--- /dev/null
+++ b/actionview/lib/action_view/cache_expiry.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module ActionView
+ class CacheExpiry
+ class Executor
+ def initialize(watcher:)
+ @cache_expiry = CacheExpiry.new(watcher: watcher)
+ end
+
+ def before(target)
+ @cache_expiry.clear_cache_if_necessary
+ end
+ end
+
+ def initialize(watcher:)
+ @watched_dirs = nil
+ @watcher_class = watcher
+ @watcher = nil
+ end
+
+ def clear_cache_if_necessary
+ watched_dirs = dirs_to_watch
+ if watched_dirs != @watched_dirs
+ @watched_dirs = watched_dirs
+ @watcher = @watcher_class.new([], watched_dirs) do
+ clear_cache
+ end
+ @watcher.execute
+ else
+ @watcher.execute_if_updated
+ end
+ end
+
+ def clear_cache
+ ActionView::LookupContext::DetailsKey.clear
+ end
+
+ private
+
+ def dirs_to_watch
+ fs_paths = all_view_paths.grep(FileSystemResolver)
+ fs_paths.map(&:path).sort.uniq
+ end
+
+ def all_view_paths
+ ActionView::ViewPaths.all_view_paths.flat_map(&:paths)
+ end
+ end
+end
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 6d2e471a44..97a6da3634 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -6,23 +6,17 @@ module ActionView
class Digestor
@@digest_mutex = Mutex.new
- module PerExecutionDigestCacheExpiry
- def self.before(target)
- ActionView::LookupContext::DetailsKey.clear
- end
- end
-
class << self
# Supported options:
#
# * <tt>name</tt> - Template name
# * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
# * <tt>dependencies</tt> - An array of dependent views
- def digest(name:, finder:, dependencies: nil)
+ def digest(name:, format:, finder:, dependencies: nil)
if dependencies.nil? || dependencies.empty?
- cache_key = "#{name}.#{finder.rendered_format}"
+ cache_key = "#{name}.#{format}"
else
- cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join(".")
+ cache_key = [ name, format, dependencies ].flatten.compact.join(".")
end
# this is a correctly done double-checked locking idiom
@@ -48,8 +42,6 @@ module ActionView
logical_name = name.gsub(%r|/_|, "/")
if template = find_template(finder, logical_name, [], partial, [])
- finder.rendered_format ||= template.formats.first
-
if node = seen[template.identifier] # handle cycles in the tree
node
else
@@ -73,9 +65,7 @@ module ActionView
private
def find_template(finder, name, prefixes, partial, keys)
finder.disable_cache do
- format = finder.rendered_format
- result = finder.find_all(name, prefixes, partial, keys, formats: [format]).first if format
- result || finder.find_all(name, prefixes, partial, keys).first
+ finder.find_all(name, prefixes, partial, keys).first
end
end
end
diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb
index 02de3eeec2..5bed37583e 100644
--- a/actionview/lib/action_view/gem_version.rb
+++ b/actionview/lib/action_view/gem_version.rb
@@ -10,7 +10,7 @@ module ActionView
MAJOR = 6
MINOR = 0
TINY = 0
- PRE = "beta1"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index b1a14250c3..020aebeea3 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -216,13 +216,13 @@ module ActionView
end
end
- def digest_path_from_virtual(virtual_path) # :nodoc:
- digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies)
+ def digest_path_from_template(template) # :nodoc:
+ digest = Digestor.digest(name: template.virtual_path, format: template.format, finder: lookup_context, dependencies: view_cache_dependencies)
if digest.present?
- "#{virtual_path}:#{digest}"
+ "#{template.virtual_path}:#{digest}"
else
- virtual_path
+ template.virtual_path
end
end
@@ -234,7 +234,7 @@ module ActionView
if virtual_path || digest_path
name = controller.url_for(name).split("://").last if name.is_a?(Hash)
- digest_path ||= digest_path_from_virtual(virtual_path)
+ digest_path ||= digest_path_from_template(@current_template)
[ digest_path, name ]
else
diff --git a/actionview/lib/action_view/helpers/csp_helper.rb b/actionview/lib/action_view/helpers/csp_helper.rb
index e2e065c218..4415018845 100644
--- a/actionview/lib/action_view/helpers/csp_helper.rb
+++ b/actionview/lib/action_view/helpers/csp_helper.rb
@@ -14,9 +14,11 @@ module ActionView
# This is used by the Rails UJS helper to create dynamically
# loaded inline <script> elements.
#
- def csp_meta_tag
+ def csp_meta_tag(**options)
if content_security_policy?
- tag("meta", name: "csp-nonce", content: content_security_policy_nonce)
+ options[:name] = "csp-nonce"
+ options[:content] = content_security_policy_nonce
+ tag("meta", options)
end
end
end
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index c5a736bfb4..5533cef249 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -739,7 +739,7 @@ module ActionView
# def labelled_form_with(**options, &block)
# form_with(**options.merge(builder: LabellingFormBuilder), &block)
# end
- def form_with(model: nil, scope: nil, url: nil, format: nil, **options)
+ def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
options[:allow_method_names_outside_object] = true
options[:skip_default_ids] = !form_with_generates_ids
@@ -752,7 +752,7 @@ module ActionView
if block_given?
builder = instantiate_builder(scope, model, options)
- output = capture(builder, &Proc.new)
+ output = capture(builder, &block)
options[:multipart] ||= builder.multipart?
html_options = html_options_for_form_with(url, model, options)
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index c0996049f0..5d4ff36425 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -24,7 +24,7 @@ module ActionView
mattr_accessor :default_enforce_utf8, default: true
- # Starts a form tag that points the action to a url configured with <tt>url_for_options</tt> just like
+ # Starts a form tag that points the action to a URL configured with <tt>url_for_options</tt> just like
# ActionController::Base#url_for. The method for the form defaults to POST.
#
# ==== Options
diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb
index 279cde5e76..52a951b2ca 100644
--- a/actionview/lib/action_view/helpers/output_safety_helper.rb
+++ b/actionview/lib/action_view/helpers/output_safety_helper.rb
@@ -38,7 +38,7 @@ module ActionView #:nodoc:
# Converts the array to a comma-separated sentence where the last element is
# joined by the connector word. This is the html_safe-aware version of
- # ActiveSupport's {Array#to_sentence}[http://api.rubyonrails.org/classes/Array.html#method-i-to_sentence].
+ # ActiveSupport's {Array#to_sentence}[https://api.rubyonrails.org/classes/Array.html#method-i-to_sentence].
#
def to_sentence(array, options = {})
options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb
index 1e12aa2736..7ead691113 100644
--- a/actionview/lib/action_view/helpers/rendering_helper.rb
+++ b/actionview/lib/action_view/helpers/rendering_helper.rb
@@ -27,10 +27,12 @@ module ActionView
def render(options = {}, locals = {}, &block)
case options
when Hash
- if block_given?
- view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
- else
- view_renderer.render(self, options)
+ in_rendering_context(options) do |renderer|
+ if block_given?
+ view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
+ else
+ view_renderer.render(self, options)
+ end
end
else
view_renderer.render_partial(self, partial: options, locals: locals, &block)
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index eef527d36f..b58e1a6680 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -138,7 +138,7 @@ module ActionView
end
def sanitized_value(value)
- value.to_s.gsub(/\s/, "_").gsub(/[^-[[:word:]]]/, "").mb_chars.downcase.to_s
+ value.to_s.gsub(/[\s\.]/, "_").gsub(/[^-[[:word:]]]/, "").downcase
end
def select_content_tag(option_tags, options, html_options)
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index ae1c93e12f..d5b0a9263f 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -114,7 +114,7 @@ module ActionView
# Delegates to <tt>I18n.localize</tt> with no additional functionality.
#
- # See http://rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
+ # See https://www.rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
# for more information.
def localize(*args)
I18n.localize(*args)
@@ -138,7 +138,7 @@ module ActionView
end
def html_safe_translation_key?(key)
- /(\b|_|\.)html$/.match?(key.to_s)
+ /(?:_|\b)html\z/.match?(key.to_s)
end
end
end
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index d63ada3890..df83dff681 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -553,7 +553,7 @@ module ActionView
url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY)
# We ignore any extra parameters in the request_uri if the
- # submitted url doesn't have any either. This lets the function
+ # submitted URL doesn't have any either. This lets the function
# work with things like ?order=asc
# the behaviour can be disabled with check_parameters: true
request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
index 3e6d352c15..08f66bf435 100644
--- a/actionview/lib/action_view/layouts.rb
+++ b/actionview/lib/action_view/layouts.rb
@@ -322,7 +322,7 @@ module ActionView
end
class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def _layout(formats)
+ def _layout(lookup_context, formats)
if _conditional_layout?
#{layout_definition}
else
@@ -388,8 +388,8 @@ module ActionView
case name
when String then _normalize_layout(name)
when Proc then name
- when true then Proc.new { |formats| _default_layout(formats, true) }
- when :default then Proc.new { |formats| _default_layout(formats, false) }
+ when true then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, true) }
+ when :default then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, false) }
when false, nil then nil
else
raise ArgumentError,
@@ -411,9 +411,9 @@ module ActionView
#
# ==== Returns
# * <tt>template</tt> - The template object for the default layout (or +nil+)
- def _default_layout(formats, require_layout = false)
+ def _default_layout(lookup_context, formats, require_layout = false)
begin
- value = _layout(formats) if action_has_layout?
+ value = _layout(lookup_context, formats) if action_has_layout?
rescue NameError => e
raise e, "Could not render layout: #{e.message}"
end
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 554d223c0e..b9a8cc129c 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -3,6 +3,7 @@
require "concurrent/map"
require "active_support/core_ext/module/remove_method"
require "active_support/core_ext/module/attribute_accessors"
+require "active_support/deprecation"
require "action_view/template/resolver"
module ActionView
@@ -15,6 +16,8 @@ module ActionView
# only once during the request, it speeds up all cache accesses.
class LookupContext #:nodoc:
attr_accessor :prefixes, :rendered_format
+ deprecate :rendered_format
+ deprecate :rendered_format=
mattr_accessor :fallbacks, default: FallbackFileSystemResolver.instances
@@ -57,21 +60,36 @@ module ActionView
alias :eql? :equal?
@details_keys = Concurrent::Map.new
+ @digest_cache = Concurrent::Map.new
- def self.get(details)
+ def self.digest_cache(details)
+ @digest_cache[details_cache_key(details)] ||= Concurrent::Map.new
+ end
+
+ def self.details_cache_key(details)
if details[:formats]
details = details.dup
details[:formats] &= Template::Types.symbols
end
- @details_keys[details] ||= Concurrent::Map.new
+ @details_keys[details] ||= Object.new
end
def self.clear
+ ActionView::ViewPaths.all_view_paths.each do |path_set|
+ path_set.each(&:clear_cache)
+ end
+ ActionView::LookupContext.fallbacks.each(&:clear_cache)
+ @view_context_class = nil
@details_keys.clear
+ @digest_cache.clear
end
def self.digest_caches
- @details_keys.values
+ @digest_cache.values
+ end
+
+ def self.view_context_class(klass)
+ @view_context_class ||= klass.with_empty_template_cache
end
end
@@ -82,7 +100,7 @@ module ActionView
# Calculate the details key. Remove the handlers from calculation to improve performance
# since the user cannot modify it explicitly.
def details_key #:nodoc:
- @details_key ||= DetailsKey.get(@details) if @cache
+ @details_key ||= DetailsKey.details_cache_key(@details) if @cache
end
# Temporary skip passing the details_key forward.
@@ -96,7 +114,8 @@ module ActionView
private
def _set_detail(key, value) # :doc:
- @details = @details.dup if @details_key
+ @details = @details.dup if @digest_cache || @details_key
+ @digest_cache = nil
@details_key = nil
@details[key] = value
end
@@ -106,20 +125,13 @@ module ActionView
module ViewPaths
attr_reader :view_paths, :html_fallback_for_js
- # Whenever setting view paths, makes a copy so that we can manipulate them in
- # instance objects as we wish.
- def view_paths=(paths)
- @view_paths = ActionView::PathSet.new(Array(paths))
- end
-
def find(name, prefixes = [], partial = false, keys = [], options = {})
@view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
end
alias :find_template :find
- def find_file(name, prefixes = [], partial = false, keys = [], options = {})
- @view_paths.find_file(*args_for_lookup(name, prefixes, partial, keys, options))
- end
+ alias :find_file :find
+ deprecate :find_file
def find_all(name, prefixes = [], partial = false, keys = [], options = {})
@view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
@@ -138,19 +150,34 @@ module ActionView
# Adds fallbacks to the view paths. Useful in cases when you are rendering
# a :file.
def with_fallbacks
- added_resolvers = 0
- self.class.fallbacks.each do |resolver|
- next if view_paths.include?(resolver)
- view_paths.push(resolver)
- added_resolvers += 1
+ view_paths = build_view_paths((@view_paths.paths + self.class.fallbacks).uniq)
+
+ if block_given?
+ ActiveSupport::Deprecation.warn <<~eowarn.squish
+ Calling `with_fallbacks` with a block is deprecated. Call methods on
+ the lookup context returned by `with_fallbacks` instead.
+ eowarn
+
+ begin
+ _view_paths = @view_paths
+ @view_paths = view_paths
+ yield
+ ensure
+ @view_paths = _view_paths
+ end
+ else
+ ActionView::LookupContext.new(view_paths, @details, @prefixes)
end
- yield
- ensure
- added_resolvers.times { view_paths.pop }
end
private
+ # Whenever setting view paths, makes a copy so that we can manipulate them in
+ # instance objects as we wish.
+ def build_view_paths(paths)
+ ActionView::PathSet.new(Array(paths))
+ end
+
def args_for_lookup(name, prefixes, partial, keys, details_options)
name, prefixes = normalize_name(name, prefixes)
details, details_key = detail_args_for(details_options)
@@ -163,7 +190,7 @@ module ActionView
user_details = @details.merge(options)
if @cache
- details_key = DetailsKey.get(user_details)
+ details_key = DetailsKey.details_cache_key(user_details)
else
details_key = nil
end
@@ -190,7 +217,7 @@ module ActionView
end
if @cache
- [details, DetailsKey.get(details)]
+ [details, DetailsKey.details_cache_key(details)]
else
[details, nil]
end
@@ -221,16 +248,23 @@ module ActionView
def initialize(view_paths, details = {}, prefixes = [])
@details_key = nil
+ @digest_cache = nil
@cache = true
@prefixes = prefixes
- @rendered_format = nil
@details = initialize_details({}, details)
- self.view_paths = view_paths
+ @view_paths = build_view_paths(view_paths)
end
def digest_cache
- details_key
+ @digest_cache ||= DetailsKey.digest_cache(@details)
+ end
+
+ def with_prepended_formats(formats)
+ details = @details.dup
+ details[:formats] = formats
+
+ self.class.new(@view_paths, details, @prefixes)
end
def initialize_details(target, details)
@@ -245,7 +279,15 @@ module ActionView
# add :html as fallback to :js.
def formats=(values)
if values
+ values = values.dup
values.concat(default_formats) if values.delete "*/*"
+ values.uniq!
+
+ invalid_values = (values - Template::Types.symbols)
+ unless invalid_values.empty?
+ raise ArgumentError, "Invalid formats: #{invalid_values.map(&:inspect).join(", ")}"
+ end
+
if values == [:js]
values << :html
@html_fallback_for_js = true
diff --git a/actionview/lib/action_view/path_set.rb b/actionview/lib/action_view/path_set.rb
index 691b53e2da..d54749d8e3 100644
--- a/actionview/lib/action_view/path_set.rb
+++ b/actionview/lib/action_view/path_set.rb
@@ -48,12 +48,11 @@ module ActionView #:nodoc:
find_all(*args).first || raise(MissingTemplate.new(self, *args))
end
- def find_file(path, prefixes = [], *args)
- _find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args))
- end
+ alias :find_file :find
+ deprecate :find_file
def find_all(path, prefixes = [], *args)
- _find_all path, prefixes, args, false
+ _find_all path, prefixes, args
end
def exists?(path, prefixes, *args)
@@ -71,15 +70,11 @@ module ActionView #:nodoc:
private
- def _find_all(path, prefixes, args, outside_app)
+ def _find_all(path, prefixes, args)
prefixes = [prefixes] if String === prefixes
prefixes.each do |prefix|
paths.each do |resolver|
- if outside_app
- templates = resolver.find_all_anywhere(path, prefix, *args)
- else
- templates = resolver.find_all(path, prefix, *args)
- end
+ templates = resolver.find_all(path, prefix, *args)
return templates unless templates.empty?
end
end
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index 12d06bf376..8bd526cdf9 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -6,11 +6,13 @@ require "rails"
module ActionView
# = Action View Railtie
class Railtie < Rails::Engine # :nodoc:
+ NULL_OPTION = Object.new
+
config.action_view = ActiveSupport::OrderedOptions.new
config.action_view.embed_authenticity_token_in_remote_forms = nil
config.action_view.debug_missing_translation = true
config.action_view.default_enforce_utf8 = nil
- config.action_view.finalize_compiled_template_methods = true
+ config.action_view.finalize_compiled_template_methods = NULL_OPTION
config.eager_load_namespaces << ActionView
@@ -48,8 +50,11 @@ module ActionView
initializer "action_view.finalize_compiled_template_methods" do |app|
ActiveSupport.on_load(:action_view) do
- ActionView::Template.finalize_compiled_template_methods =
- app.config.action_view.delete(:finalize_compiled_template_methods)
+ option = app.config.action_view.delete(:finalize_compiled_template_methods)
+
+ if option != NULL_OPTION
+ ActiveSupport::Deprecation.warn "action_view.finalize_compiled_template_methods is deprecated and has no effect"
+ end
end
end
@@ -76,7 +81,7 @@ module ActionView
initializer "action_view.per_request_digest_cache" do |app|
ActiveSupport.on_load(:action_view) do
unless ActionView::Resolver.caching?
- app.executor.to_run ActionView::Digestor::PerExecutionDigestCacheExpiry
+ app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
end
end
end
diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb
index 20b2523cac..475452f1bb 100644
--- a/actionview/lib/action_view/renderer/abstract_renderer.rb
+++ b/actionview/lib/action_view/renderer/abstract_renderer.rb
@@ -17,7 +17,7 @@ module ActionView
# that new object is called in turn. This abstracts the setup and rendering
# into a separate classes for partials and templates.
class AbstractRenderer #:nodoc:
- delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context
+ delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
def initialize(lookup_context)
@lookup_context = lookup_context
@@ -27,6 +27,53 @@ module ActionView
raise NotImplementedError
end
+ class RenderedCollection # :nodoc:
+ def self.empty(format)
+ EmptyCollection.new format
+ end
+
+ attr_reader :rendered_templates
+
+ def initialize(rendered_templates, spacer)
+ @rendered_templates = rendered_templates
+ @spacer = spacer
+ end
+
+ def body
+ @rendered_templates.map(&:body).join(@spacer.body).html_safe
+ end
+
+ def format
+ rendered_templates.first.format
+ end
+
+ class EmptyCollection
+ attr_reader :format
+
+ def initialize(format)
+ @format = format
+ end
+
+ def body; nil; end
+ end
+ end
+
+ class RenderedTemplate # :nodoc:
+ attr_reader :body, :layout, :template
+
+ def initialize(body, layout, template)
+ @body = body
+ @layout = layout
+ @template = template
+ end
+
+ def format
+ template.format
+ end
+
+ EMPTY_SPACER = Struct.new(:body).new
+ end
+
private
def extract_details(options) # :doc:
@@ -38,8 +85,6 @@ module ActionView
end
def instrument(name, **options) # :doc:
- options[:identifier] ||= (@template && @template.identifier) || @path
-
ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
yield payload
end
@@ -51,5 +96,13 @@ module ActionView
@lookup_context.formats = formats | @lookup_context.formats
end
+
+ def build_rendered_template(content, template, layout = nil)
+ RenderedTemplate.new content, layout, template
+ end
+
+ def build_rendered_collection(templates, spacer)
+ RenderedCollection.new templates, spacer
+ end
end
end
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index f175c30aa1..ed8d5cf54e 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -295,43 +295,60 @@ module ActionView
end
def render(context, options, block)
- setup(context, options, block)
- @template = find_partial
+ as = as_variable(options)
+ setup(context, options, as, block)
- @lookup_context.rendered_format ||= begin
- if @template && @template.formats.present?
- @template.formats.first
+ if @path
+ if @has_object || @collection
+ @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
+ @template_keys = retrieve_template_keys(@variable)
else
- formats.first
+ @template_keys = @locals.keys
+ end
+ template = find_partial(@path, @template_keys)
+ @variable ||= template.variable
+ else
+ if options[:cached]
+ raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
end
+ template = nil
end
if @collection
- render_collection
+ render_collection(context, template)
else
- render_partial
+ render_partial(context, template)
end
end
private
- def render_collection
- instrument(:collection, count: @collection.size) do |payload|
- return nil if @collection.blank?
+ def render_collection(view, template)
+ identifier = (template && template.identifier) || @path
+ instrument(:collection, identifier: identifier, count: @collection.size) do |payload|
+ return RenderedCollection.empty(@lookup_context.formats.first) if @collection.blank?
- if @options.key?(:spacer_template)
- spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
+ spacer = if @options.key?(:spacer_template)
+ spacer_template = find_template(@options[:spacer_template], @locals.keys)
+ build_rendered_template(spacer_template.render(view, @locals), spacer_template)
+ else
+ RenderedTemplate::EMPTY_SPACER
end
- cache_collection_render(payload) do
- @template ? collection_with_template : collection_without_template
- end.join(spacer).html_safe
+ collection_body = if template
+ cache_collection_render(payload, view, template) do
+ collection_with_template(view, template)
+ end
+ else
+ collection_without_template(view)
+ end
+ build_rendered_collection(collection_body, spacer)
end
end
- def render_partial
- instrument(:partial) do |payload|
- view, locals, block = @view, @locals, @block
+ def render_partial(view, template)
+ instrument(:partial, identifier: template.identifier) do |payload|
+ locals, block = @locals, @block
object, as = @object, @variable
if !block && (layout = @options[:layout])
@@ -341,13 +358,13 @@ module ActionView
object = locals[as] if object.nil? # Respect object when object is false
locals[as] = object if @has_object
- content = @template.render(view, locals) do |*name|
+ content = template.render(view, locals) do |*name|
view._layout_for(*name, &block)
end
content = layout.render(view, locals) { content } if layout
- payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path]
- content
+ payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
+ build_rendered_template(content, template, layout)
end
end
@@ -358,16 +375,13 @@ module ActionView
# If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
# set to that string. Otherwise, the +options[:partial]+ object must
# respond to +to_partial_path+ in order to setup the path.
- def setup(context, options, block)
- @view = context
+ def setup(context, options, as, block)
@options = options
@block = block
@locals = options[:locals] || {}
@details = extract_details(options)
- prepend_formats(options[:formats])
-
partial = options[:partial]
if String === partial
@@ -381,26 +395,26 @@ module ActionView
@collection = collection_from_object || collection_from_options
if @collection
- paths = @collection_data = @collection.map { |o| partial_path(o) }
- @path = paths.uniq.one? ? paths.first : nil
+ paths = @collection_data = @collection.map { |o| partial_path(o, context) }
+ if paths.uniq.length == 1
+ @path = paths.first
+ else
+ paths.map! { |path| retrieve_variable(path, as).unshift(path) }
+ @path = nil
+ end
else
- @path = partial_path
+ @path = partial_path(@object, context)
end
end
+ self
+ end
+
+ def as_variable(options)
if as = options[:as]
raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
- as = as.to_sym
- end
-
- if @path
- @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
- @template_keys = retrieve_template_keys
- else
- paths.map! { |path| retrieve_variable(path, as).unshift(path) }
+ as.to_sym
end
-
- self
end
def collection_from_options
@@ -414,8 +428,8 @@ module ActionView
@object.to_ary if @object.respond_to?(:to_ary)
end
- def find_partial
- find_template(@path, @template_keys) if @path
+ def find_partial(path, template_keys)
+ find_template(path, template_keys)
end
def find_template(path, locals)
@@ -423,8 +437,8 @@ module ActionView
@lookup_context.find_template(path, prefixes, true, locals, @details)
end
- def collection_with_template
- view, locals, template = @view, @locals, @template
+ def collection_with_template(view, template)
+ locals = @locals
as, counter, iteration = @variable, @variable_counter, @variable_iteration
if layout = @options[:layout]
@@ -441,12 +455,12 @@ module ActionView
content = template.render(view, locals)
content = layout.render(view, locals) { content } if layout
partial_iteration.iterate!
- content
+ build_rendered_template(content, template, layout)
end
end
- def collection_without_template
- view, locals, collection_data = @view, @locals, @collection_data
+ def collection_without_template(view)
+ locals, collection_data = @locals, @collection_data
cache = {}
keys = @locals.keys
@@ -463,7 +477,7 @@ module ActionView
template = (cache[path] ||= find_template(path, keys + [as, counter, iteration]))
content = template.render(view, locals)
partial_iteration.iterate!
- content
+ build_rendered_template(content, template)
end
end
@@ -474,7 +488,7 @@ module ActionView
#
# If +prefix_partial_path_with_controller_namespace+ is true, then this
# method will prefix the partial paths with a namespace.
- def partial_path(object = @object)
+ def partial_path(object, view)
object = object.to_model if object.respond_to?(:to_model)
path = if object.respond_to?(:to_partial_path)
@@ -483,7 +497,7 @@ module ActionView
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
end
- if @view.prefix_partial_path_with_controller_namespace
+ if view.prefix_partial_path_with_controller_namespace
prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
else
path
@@ -511,9 +525,9 @@ module ActionView
end
end
- def retrieve_template_keys
+ def retrieve_template_keys(variable)
keys = @locals.keys
- keys << @variable if @has_object || @collection
+ keys << variable
if @collection
keys << @variable_counter
keys << @variable_iteration
diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
index 5aa6f77902..ca692699a6 100644
--- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
@@ -11,19 +11,19 @@ module ActionView
end
private
- def cache_collection_render(instrumentation_payload)
+ def cache_collection_render(instrumentation_payload, view, template)
return yield unless @options[:cached]
# Result is a hash with the key represents the
# key used for cache lookup and the value is the item
# on which the partial is being rendered
- keyed_collection = collection_by_cache_keys
+ keyed_collection, ordered_keys = collection_by_cache_keys(view, template)
# Pull all partials from cache
# Result is a hash, key matches the entry in
# `keyed_collection` where the cache was retrieved and the
# value is the value that was present in the cache
- cached_partials = collection_cache.read_multi(*keyed_collection.keys)
+ cached_partials = collection_cache.read_multi(*keyed_collection.keys)
instrumentation_payload[:cache_hits] = cached_partials.size
# Extract the items for the keys that are not found
@@ -40,34 +40,38 @@ module ActionView
rendered_partials = @collection.empty? ? [] : yield
index = 0
- fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do
+ keyed_partials = fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do
# This block is called once
# for every cache miss while preserving order.
rendered_partials[index].tap { index += 1 }
end
+
+ ordered_keys.map do |key|
+ keyed_partials[key]
+ end
end
def callable_cache_key?
@options[:cached].respond_to?(:call)
end
- def collection_by_cache_keys
+ def collection_by_cache_keys(view, template)
seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
- @collection.each_with_object({}) do |item, hash|
- hash[expanded_cache_key(seed.call(item))] = item
+ digest_path = view.digest_path_from_template(template)
+
+ @collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
+ key = expanded_cache_key(seed.call(item), view, template, digest_path)
+ ordered_keys << key
+ hash[key] = item
end
end
- def expanded_cache_key(key)
- key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path, digest_path: digest_path))
+ def expanded_cache_key(key, view, template, digest_path)
+ key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: template.virtual_path, digest_path: digest_path))
key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
end
- def digest_path
- @digest_path ||= @view.digest_path_from_virtual(@template.virtual_path)
- end
-
# `order_by` is an enumerable object containing keys of the cache,
# all keys are passed in whether found already or not.
#
@@ -83,14 +87,17 @@ module ActionView
#
# If the partial is not already cached it will also be
# written back to the underlying cache store.
- def fetch_or_cache_partial(cached_partials, order_by:)
- order_by.map do |cache_key|
- cached_partials.fetch(cache_key) do
- yield.tap do |rendered_partial|
- collection_cache.write(cache_key, rendered_partial)
- end
+ def fetch_or_cache_partial(cached_partials, template, order_by:)
+ order_by.each_with_object({}) do |cache_key, hash|
+ hash[cache_key] =
+ if content = cached_partials[cache_key]
+ build_rendered_template(content, template)
+ else
+ yield.tap do |rendered_partial|
+ collection_cache.write(cache_key, rendered_partial.body)
+ end
+ end
end
- end
end
end
end
diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb
index 3f3a97529d..485eb1a5b4 100644
--- a/actionview/lib/action_view/renderer/renderer.rb
+++ b/actionview/lib/action_view/renderer/renderer.rb
@@ -19,10 +19,14 @@ module ActionView
# Main render entry point shared by Action View and Action Controller.
def render(context, options)
+ render_to_object(context, options).body
+ end
+
+ def render_to_object(context, options) # :nodoc:
if options.key?(:partial)
- render_partial(context, options)
+ render_partial_to_object(context, options)
else
- render_template(context, options)
+ render_template_to_object(context, options)
end
end
@@ -41,16 +45,24 @@ module ActionView
# Direct access to template rendering.
def render_template(context, options) #:nodoc:
- TemplateRenderer.new(@lookup_context).render(context, options)
+ render_template_to_object(context, options).body
end
# Direct access to partial rendering.
def render_partial(context, options, &block) #:nodoc:
- PartialRenderer.new(@lookup_context).render(context, options, block)
+ render_partial_to_object(context, options, &block).body
end
def cache_hits # :nodoc:
@cache_hits ||= {}
end
+
+ def render_template_to_object(context, options) #:nodoc:
+ TemplateRenderer.new(@lookup_context).render(context, options)
+ end
+
+ def render_partial_to_object(context, options, &block) #:nodoc:
+ PartialRenderer.new(@lookup_context).render(context, options, block)
+ end
end
end
diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index f414620923..19fa399e2c 100644
--- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -34,7 +34,7 @@ module ActionView
return unless logger
message = +"\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << exception.annotated_source_code.to_s if exception.respond_to?(:annotated_source_code)
message << " " << exception.backtrace.join("\n ")
logger.fatal("#{message}\n\n")
end
@@ -44,7 +44,7 @@ module ActionView
# object that responds to each. This object is initialized with a block
# that knows how to render the template.
def render_template(view, template, layout_name = nil, locals = {}) #:nodoc:
- return [super] unless layout_name && template.supports_streaming?
+ return [super.body] unless layout_name && template.supports_streaming?
locals ||= {}
layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index fb5539e0db..4aab3f913f 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -8,11 +8,9 @@ module ActionView
@details = extract_details(options)
template = determine_template(options)
- prepend_formats(template.formats)
+ prepend_formats(template.format)
- @lookup_context.rendered_format ||= (template.formats.first || formats.first)
-
- render_template(context, template, options[:layout], options[:locals])
+ render_template(context, template, options[:layout], options[:locals] || {})
end
private
@@ -28,15 +26,25 @@ module ActionView
elsif options.key?(:html)
Template::HTML.new(options[:html], formats.first)
elsif options.key?(:file)
- with_fallbacks { find_file(options[:file], nil, false, keys, @details) }
+ if File.exist?(options[:file])
+ Template::RawFile.new(options[:file])
+ else
+ ActiveSupport::Deprecation.warn "render file: should be given the absolute path to a file"
+ @lookup_context.with_fallbacks.find_template(options[:file], nil, false, keys, @details)
+ end
elsif options.key?(:inline)
handler = Template.handler_for_extension(options[:type] || "erb")
- Template.new(options[:inline], "inline template", handler, locals: keys)
+ format = if handler.respond_to?(:default_format)
+ handler.default_format
+ else
+ @lookup_context.formats.first
+ end
+ Template::Inline.new(options[:inline], "inline template", handler, locals: keys, format: format)
elsif options.key?(:template)
if options[:template].respond_to?(:render)
options[:template]
else
- find_template(options[:template], options[:prefixes], false, keys, @details)
+ @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
end
else
raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option."
@@ -45,26 +53,25 @@ module ActionView
# Renders the given template. A string representing the layout can be
# supplied as well.
- def render_template(view, template, layout_name = nil, locals = nil)
- locals ||= {}
-
- render_with_layout(view, layout_name, locals) do |layout|
+ def render_template(view, template, layout_name, locals)
+ render_with_layout(view, template, layout_name, locals) do |layout|
instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do
template.render(view, locals) { |*name| view._layout_for(*name) }
end
end
end
- def render_with_layout(view, path, locals)
+ def render_with_layout(view, template, path, locals)
layout = path && find_layout(path, locals.keys, [formats.first])
content = yield(layout)
- if layout
+ body = if layout
view.view_flow.set(:layout, content)
layout.render(view, locals) { |*name| view._layout_for(*name) }
else
content
end
+ build_rendered_template(body, template, layout)
end
# This is the method which actually finds the layout using details in the lookup
@@ -82,16 +89,17 @@ module ActionView
when String
begin
if layout.start_with?("/")
- with_fallbacks { find_template(layout, nil, false, [], details) }
+ ActiveSupport::Deprecation.warn "Rendering layouts from an absolute path is deprecated."
+ @lookup_context.with_fallbacks.find_template(layout, nil, false, [], details)
else
- find_template(layout, nil, false, [], details)
+ @lookup_context.find_template(layout, nil, false, [], details)
end
rescue ActionView::MissingTemplate
all_details = @details.merge(formats: @lookup_context.default_formats)
raise unless template_exists?(layout, nil, false, [], all_details)
end
when Proc
- resolve_layout(layout.call(formats), keys, formats)
+ resolve_layout(layout.call(@lookup_context, formats), keys, formats)
else
layout
end
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
index 205665a8c6..5a06bd9da6 100644
--- a/actionview/lib/action_view/rendering.rb
+++ b/actionview/lib/action_view/rendering.rb
@@ -26,6 +26,13 @@ module ActionView
extend ActiveSupport::Concern
include ActionView::ViewPaths
+ attr_reader :rendered_format
+
+ def initialize
+ @rendered_format = nil
+ super
+ end
+
# Overwrite process to setup I18n proxy.
def process(*) #:nodoc:
old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
@@ -35,24 +42,36 @@ module ActionView
end
module ClassMethods
- def view_context_class
- @view_context_class ||= begin
- supports_path = supports_path?
- routes = respond_to?(:_routes) && _routes
- helpers = respond_to?(:_helpers) && _helpers
-
- Class.new(ActionView::Base) do
- if routes
- include routes.url_helpers(supports_path)
- include routes.mounted_helpers
- end
-
- if helpers
- include helpers
- end
+ def _routes
+ end
+
+ def _helpers
+ end
+
+ def build_view_context_class(klass, supports_path, routes, helpers)
+ Class.new(klass) do
+ if routes
+ include routes.url_helpers(supports_path)
+ include routes.mounted_helpers
+ end
+
+ if helpers
+ include helpers
end
end
end
+
+ def view_context_class
+ klass = ActionView::LookupContext::DetailsKey.view_context_class(ActionView::Base)
+
+ @view_context_class ||= build_view_context_class(klass, supports_path?, _routes, _helpers)
+
+ if klass.changed?(@view_context_class)
+ @view_context_class = build_view_context_class(klass, supports_path?, _routes, _helpers)
+ end
+
+ @view_context_class
+ end
end
def view_context_class
@@ -70,11 +89,12 @@ module ActionView
#
# Override this method in a module to change the default behavior.
def view_context
- view_context_class.new(view_renderer, view_assigns, self)
+ view_context_class.new(lookup_context, view_assigns, self)
end
# Returns an object that is able to render templates.
def view_renderer # :nodoc:
+ # Lifespan: Per controller
@_view_renderer ||= ActionView::Renderer.new(lookup_context)
end
@@ -83,10 +103,6 @@ module ActionView
_render_template(options)
end
- def rendered_format
- Template::Types[lookup_context.rendered_format]
- end
-
private
# Find and render a template based on the options given.
@@ -96,17 +112,22 @@ module ActionView
context = view_context
context.assign assigns if assigns
- lookup_context.rendered_format = nil if options[:formats]
lookup_context.variants = variant if variant
- view_renderer.render(context, options)
+ rendered_template = context.in_rendering_context(options) do |renderer|
+ renderer.render_to_object(context, options)
+ end
+
+ rendered_format = rendered_template.format || lookup_context.formats.first
+ @rendered_format = Template::Types[rendered_format]
+
+ rendered_template.body
end
# Assign the rendered format to look up context.
def _process_format(format)
super
- lookup_context.formats = [format.to_sym]
- lookup_context.rendered_format = lookup_context.formats.first
+ lookup_context.formats = [format.to_sym] if format.to_sym
end
# Normalize args by converting render "foo" to render :action => "foo" and
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 8ce8053011..b80bf56c1b 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -2,14 +2,22 @@
require "active_support/core_ext/object/try"
require "active_support/core_ext/kernel/singleton_class"
+require "active_support/deprecation"
require "thread"
+require "delegate"
module ActionView
# = Action View Template
class Template
extend ActiveSupport::Autoload
- mattr_accessor :finalize_compiled_template_methods, default: true
+ def self.finalize_compiled_template_methods
+ ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods is deprecated and has no effect"
+ end
+
+ def self.finalize_compiled_template_methods=(_)
+ ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods= is deprecated and has no effect"
+ end
# === Encodings in ActionView::Template
#
@@ -105,44 +113,60 @@ module ActionView
eager_autoload do
autoload :Error
+ autoload :RawFile
autoload :Handlers
autoload :HTML
+ autoload :Inline
+ autoload :Sources
autoload :Text
autoload :Types
end
extend Template::Handlers
- attr_accessor :locals, :formats, :variants, :virtual_path
+ attr_reader :identifier, :handler, :original_encoding, :updated_at
+ attr_reader :variable, :format, :variant, :locals, :virtual_path
- attr_reader :source, :identifier, :handler, :original_encoding, :updated_at
-
- # This finalizer is needed (and exactly with a proc inside another proc)
- # otherwise templates leak in development.
- Finalizer = proc do |method_name, mod| # :nodoc:
- proc do
- mod.module_eval do
- remove_possible_method method_name
- end
+ def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil, updated_at: nil)
+ unless locals
+ ActiveSupport::Deprecation.warn "ActionView::Template#initialize requires a locals parameter"
+ locals = []
end
- end
-
- def initialize(source, identifier, handler, details)
- format = details[:format] || (handler.default_format if handler.respond_to?(:default_format))
@source = source
@identifier = identifier
@handler = handler
@compiled = false
- @original_encoding = nil
- @locals = details[:locals] || []
- @virtual_path = details[:virtual_path]
- @updated_at = details[:updated_at] || Time.now
- @formats = Array(format).map { |f| f.respond_to?(:ref) ? f.ref : f }
- @variants = [details[:variant]]
+ @locals = locals
+ @virtual_path = virtual_path
+
+ @variable = if @virtual_path
+ base = @virtual_path[-1] == "/" ? "" : ::File.basename(@virtual_path)
+ base =~ /\A_?(.*?)(?:\.\w+)*\z/
+ $1.to_sym
+ end
+
+ if updated_at
+ ActiveSupport::Deprecation.warn "ActionView::Template#updated_at is deprecated"
+ @updated_at = updated_at
+ else
+ @updated_at = Time.now
+ end
+ @format = format
+ @variant = variant
@compile_mutex = Mutex.new
end
+ deprecate :original_encoding
+ deprecate :updated_at
+ deprecate def virtual_path=(_); end
+ deprecate def locals=(_); end
+ deprecate def formats=(_); end
+ deprecate def formats; Array(format); end
+ deprecate def variants=(_); end
+ deprecate def variants; [variant]; end
+ deprecate def refresh(_); self; end
+
# Returns whether the underlying handler supports streaming. If so,
# a streaming buffer *may* be passed when it starts rendering.
def supports_streaming?
@@ -158,37 +182,26 @@ module ActionView
def render(view, locals, buffer = ActionView::OutputBuffer.new, &block)
instrument_render_template do
compile!(view)
- view.run(method_name, locals, buffer, &block)
+ view._run(method_name, self, locals, buffer, &block)
end
rescue => e
handle_render_error(view, e)
end
def type
- @type ||= Types[@formats.first] if @formats.first
+ @type ||= Types[format]
end
- # Receives a view object and return a template similar to self by using @virtual_path.
- #
- # This method is useful if you have a template object but it does not contain its source
- # anymore since it was already compiled. In such cases, all you need to do is to call
- # refresh passing in the view object.
- #
- # Notice this method raises an error if the template to be refreshed does not have a
- # virtual path set (true just for inline templates).
- def refresh(view)
- raise "A template needs to have a virtual path in order to be refreshed" unless @virtual_path
- lookup = view.lookup_context
- pieces = @virtual_path.split("/")
- name = pieces.pop
- partial = !!name.sub!(/^_/, "")
- lookup.disable_cache do
- lookup.find_template(name, [ pieces.join("/") ], partial, @locals)
- end
+ def short_identifier
+ @short_identifier ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "") : identifier
end
def inspect
- @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "") : identifier
+ "#<#{self.class.name} #{short_identifier} locals=#{@locals.inspect}>"
+ end
+
+ def source
+ @source.to_s
end
# This method is responsible for properly setting the encoding of the
@@ -202,7 +215,9 @@ module ActionView
# before passing the source on to the template engine, leaving a
# blank line in its stead.
def encode!
- return unless source.encoding == Encoding::BINARY
+ source = self.source
+
+ return source unless source.encoding == Encoding::BINARY
# Look for # encoding: *. If we find one, we'll encode the
# String in that encoding, otherwise, we'll use the
@@ -240,11 +255,11 @@ module ActionView
# to ensure that references to the template object can be marshalled as well. This means forgoing
# the marshalling of the compiler mutex and instantiating that again on unmarshalling.
def marshal_dump # :nodoc:
- [ @source, @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants ]
+ [ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ]
end
def marshal_load(array) # :nodoc:
- @source, @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants = *array
+ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array
@compile_mutex = Mutex.new
end
@@ -270,13 +285,19 @@ module ActionView
compile(mod)
end
- # Just discard the source if we have a virtual path. This
- # means we can get the template back.
- @source = nil if @virtual_path
@compiled = true
end
end
+ class LegacyTemplate < DelegateClass(Template) # :nodoc:
+ attr_reader :source
+
+ def initialize(template, source)
+ super(template)
+ @source = source
+ end
+ end
+
# Among other things, this method is responsible for properly setting
# the encoding of the compiled template.
#
@@ -290,11 +311,12 @@ module ActionView
# In general, this means that templates will be UTF-8 inside of Rails,
# regardless of the original source encoding.
def compile(mod)
- encode!
- code = @handler.call(self)
+ source = encode!
+ code = @handler.call(self, source)
# Make sure that the resulting String to be eval'd is in the
# encoding of the code
+ original_source = source
source = +<<-end_src
def #{method_name}(local_assigns, output_buffer)
@virtual_path = #{@virtual_path.inspect};#{locals_code};#{code}
@@ -312,12 +334,16 @@ module ActionView
# handler is valid in the default_internal. This is for handlers
# that handle encoding but screw up
unless source.valid_encoding?
- raise WrongEncodingError.new(@source, Encoding.default_internal)
+ raise WrongEncodingError.new(source, Encoding.default_internal)
end
- mod.module_eval(source, identifier, 0)
- if finalize_compiled_template_methods
- ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
+ begin
+ mod.module_eval(source, identifier, 0)
+ rescue SyntaxError
+ # Account for when code in the template is not syntactically valid; e.g. if we're using
+ # ERB and the user writes <%= foo( %>, attempting to call a helper `foo` and interpolate
+ # the result into the template, but missing an end parenthesis.
+ raise SyntaxErrorInTemplate.new(self, original_source)
end
end
@@ -326,12 +352,7 @@ module ActionView
e.sub_template_of(self)
raise e
else
- template = self
- unless template.source
- template = refresh(view)
- template.encode!
- end
- raise Template::Error.new(template)
+ raise Template::Error.new(self)
end
end
@@ -354,7 +375,7 @@ module ActionView
end
def identifier_method_name
- inspect.tr("^a-z_", "_")
+ short_identifier.tr("^a-z_", "_")
end
def instrument(action, &block) # :doc:
diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb
index 4e3c02e05e..d0ea03e228 100644
--- a/actionview/lib/action_view/template/error.rb
+++ b/actionview/lib/action_view/template/error.rb
@@ -109,7 +109,7 @@ module ActionView
end
end
- def annoted_source_code
+ def annotated_source_code
source_extract(4)
end
@@ -138,4 +138,24 @@ module ActionView
end
TemplateError = Template::Error
+
+ class SyntaxErrorInTemplate < TemplateError #:nodoc
+ def initialize(template, offending_code_string)
+ @offending_code_string = offending_code_string
+ super(template)
+ end
+
+ def message
+ <<~MESSAGE
+ Encountered a syntax error while rendering template: check #{@offending_code_string}
+ MESSAGE
+ end
+
+ def annotated_source_code
+ @offending_code_string.split("\n").map.with_index(1) { |line, index|
+ indentation = " " * 4
+ "#{index}:#{indentation}#{line}"
+ }
+ end
+ end
end
diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb
index 7ec76dcc3f..6450513003 100644
--- a/actionview/lib/action_view/template/handlers.rb
+++ b/actionview/lib/action_view/template/handlers.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/deprecation"
+
module ActionView #:nodoc:
# = Action View Template Handlers
class Template #:nodoc:
@@ -14,7 +16,7 @@ module ActionView #:nodoc:
base.register_template_handler :erb, ERB.new
base.register_template_handler :html, Html.new
base.register_template_handler :builder, Builder.new
- base.register_template_handler :ruby, :source.to_proc
+ base.register_template_handler :ruby, lambda { |_, source| source }
end
@@template_handlers = {}
@@ -24,11 +26,35 @@ module ActionView #:nodoc:
@@template_extensions ||= @@template_handlers.keys
end
+ class LegacyHandlerWrapper < SimpleDelegator # :nodoc:
+ def call(view, source)
+ __getobj__.call(ActionView::Template::LegacyTemplate.new(view, source))
+ end
+ end
+
# Register an object that knows how to handle template files with the given
# extensions. This can be used to implement new template types.
# The handler must respond to +:call+, which will be passed the template
# and should return the rendered template as a String.
def register_template_handler(*extensions, handler)
+ params = if handler.is_a?(Proc)
+ handler.parameters
+ else
+ handler.method(:call).parameters
+ end
+
+ unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2
+ ActiveSupport::Deprecation.warn <<~eowarn
+ Single arity template handlers are deprecated. Template handlers must
+ now accept two parameters, the view object and the source for the view object.
+ Change:
+ >> #{handler}.call(#{params.map(&:last).join(", ")})
+ To:
+ >> #{handler}.call(#{params.map(&:last).join(", ")}, source)
+ eowarn
+ handler = LegacyHandlerWrapper.new(handler)
+ end
+
raise(ArgumentError, "Extension is required") if extensions.empty?
extensions.each do |extension|
@@template_handlers[extension.to_sym] = handler
diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb
index 61492ce448..e5413cd2a0 100644
--- a/actionview/lib/action_view/template/handlers/builder.rb
+++ b/actionview/lib/action_view/template/handlers/builder.rb
@@ -5,11 +5,11 @@ module ActionView
class Builder
class_attribute :default_format, default: :xml
- def call(template)
+ def call(template, source)
require_engine
"xml = ::Builder::XmlMarkup.new(:indent => 2);" \
"self.output_buffer = xml.target!;" +
- template.source +
+ source +
";xml.target!;"
end
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index 7d1a6767d7..b6314a5bc3 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -28,8 +28,8 @@ module ActionView
ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
- def self.call(template)
- new.call(template)
+ def self.call(template, source)
+ new.call(template, source)
end
def supports_streaming?
@@ -40,17 +40,17 @@ module ActionView
true
end
- def call(template)
+ def call(template, source)
# First, convert to BINARY, so in case the encoding is
# wrong, we can still find an encoding tag
# (<%# encoding %>) inside the String using a regular
# expression
- template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT)
+ template_source = source.dup.force_encoding(Encoding::ASCII_8BIT)
erb = template_source.gsub(ENCODING_TAG, "")
encoding = $2
- erb.force_encoding valid_encoding(template.source.dup, encoding)
+ erb.force_encoding valid_encoding(source.dup, encoding)
# Always make sure we return a String in the default_internal
erb.encode!
diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb
index e155bae89d..e602aa117a 100644
--- a/actionview/lib/action_view/template/handlers/erb/erubi.rb
+++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb
@@ -25,9 +25,9 @@ module ActionView
src = @src
view = Class.new(ActionView::Base) {
include action_view_erb_handler_context._routes.url_helpers
- class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", @filename || "(erubi)", 0)
- }.new(action_view_erb_handler_context)
- view.run(:_template, {}, ActionView::OutputBuffer.new)
+ class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", defined?(@filename) ? @filename : "(erubi)", 0)
+ }.empty
+ view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
end
private
diff --git a/actionview/lib/action_view/template/handlers/html.rb b/actionview/lib/action_view/template/handlers/html.rb
index 27004a318c..65857d8587 100644
--- a/actionview/lib/action_view/template/handlers/html.rb
+++ b/actionview/lib/action_view/template/handlers/html.rb
@@ -3,7 +3,7 @@
module ActionView
module Template::Handlers
class Html < Raw
- def call(template)
+ def call(template, source)
"ActionView::OutputBuffer.new #{super}"
end
end
diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb
index 5cd23a0060..57ebb169fc 100644
--- a/actionview/lib/action_view/template/handlers/raw.rb
+++ b/actionview/lib/action_view/template/handlers/raw.rb
@@ -3,8 +3,8 @@
module ActionView
module Template::Handlers
class Raw
- def call(template)
- "#{template.source.inspect}.html_safe;"
+ def call(template, source)
+ "#{source.inspect}.html_safe;"
end
end
end
diff --git a/actionview/lib/action_view/template/html.rb b/actionview/lib/action_view/template/html.rb
index a262c6d9ad..ecd1c31e79 100644
--- a/actionview/lib/action_view/template/html.rb
+++ b/actionview/lib/action_view/template/html.rb
@@ -1,15 +1,21 @@
# frozen_string_literal: true
+require "active_support/deprecation"
+
module ActionView #:nodoc:
# = Action View HTML Template
class Template #:nodoc:
class HTML #:nodoc:
- attr_accessor :type
+ attr_reader :type
def initialize(string, type = nil)
+ unless type
+ ActiveSupport::Deprecation.warn "ActionView::Template::HTML#initialize requires a type parameter"
+ type = :html
+ end
+
@string = string.to_s
- @type = Types[type] || type if type
- @type ||= Types[:html]
+ @type = type
end
def identifier
@@ -26,9 +32,12 @@ module ActionView #:nodoc:
to_str
end
- def formats
- [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
+ def format
+ @type
end
+
+ def formats; Array(format); end
+ deprecate :formats
end
end
end
diff --git a/actionview/lib/action_view/template/inline.rb b/actionview/lib/action_view/template/inline.rb
new file mode 100644
index 0000000000..44658487ea
--- /dev/null
+++ b/actionview/lib/action_view/template/inline.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActionView #:nodoc:
+ class Template #:nodoc:
+ class Inline < Template #:nodoc:
+ # This finalizer is needed (and exactly with a proc inside another proc)
+ # otherwise templates leak in development.
+ Finalizer = proc do |method_name, mod| # :nodoc:
+ proc do
+ mod.module_eval do
+ remove_possible_method method_name
+ end
+ end
+ end
+
+ def compile(mod)
+ super
+ ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/raw_file.rb b/actionview/lib/action_view/template/raw_file.rb
new file mode 100644
index 0000000000..623351305f
--- /dev/null
+++ b/actionview/lib/action_view/template/raw_file.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module ActionView #:nodoc:
+ # = Action View RawFile Template
+ class Template #:nodoc:
+ class RawFile #:nodoc:
+ attr_accessor :type, :format
+
+ def initialize(filename)
+ @filename = filename.to_s
+ extname = ::File.extname(filename).delete(".")
+ @type = Template::Types[extname] || Template::Types[:text]
+ @format = @type.symbol
+ end
+
+ def identifier
+ @filename
+ end
+
+ def render(*args)
+ ::File.read(@filename)
+ end
+
+ def formats; Array(format); end
+ deprecate :formats
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index 12ae82f8c5..ce53eb046d 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -63,26 +63,11 @@ module ActionView
# Cache the templates returned by the block
def cache(key, name, prefix, partial, locals)
- if Resolver.caching?
- @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
- else
- fresh_templates = yield
- cached_templates = @data[key][name][prefix][partial][locals]
-
- if templates_have_changed?(cached_templates, fresh_templates)
- @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates)
- else
- cached_templates || NO_TEMPLATES
- end
- end
+ @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
end
def cache_query(query) # :nodoc:
- if Resolver.caching?
- @query_cache[query] ||= canonical_no_templates(yield)
- else
- yield
- end
+ @query_cache[query] ||= canonical_no_templates(yield)
end
def clear
@@ -112,19 +97,6 @@ module ActionView
def canonical_no_templates(templates)
templates.empty? ? NO_TEMPLATES : templates
end
-
- def templates_have_changed?(cached_templates, fresh_templates)
- # if either the old or new template list is empty, we don't need to (and can't)
- # compare modification times, and instead just check whether the lists are different
- if cached_templates.blank? || fresh_templates.blank?
- return fresh_templates.blank? != cached_templates.blank?
- end
-
- cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
-
- # if a template has changed, it will be now be newer than all the cached templates
- fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
- end
end
cattr_accessor :caching, default: true
@@ -143,35 +115,33 @@ module ActionView
# Normalizes the arguments and passes it on to find_templates.
def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
- cached(key, [name, prefix, partial], details, locals) do
- find_templates(name, prefix, partial, details)
- end
- end
+ locals = locals.map(&:to_s).sort!.freeze
- def find_all_anywhere(name, prefix, partial = false, details = {}, key = nil, locals = [])
cached(key, [name, prefix, partial], details, locals) do
- find_templates(name, prefix, partial, details, true)
+ _find_all(name, prefix, partial, details, key, locals)
end
end
+ alias :find_all_anywhere :find_all
+ deprecate :find_all_anywhere
+
def find_all_with_query(query) # :nodoc:
@cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
end
private
+ def _find_all(name, prefix, partial, details, key, locals)
+ find_templates(name, prefix, partial, details, locals)
+ end
+
delegate :caching?, to: :class
# This is what child classes implement. No defaults are needed
# because Resolver guarantees that the arguments are present and
# normalized.
- def find_templates(name, prefix, partial, details, outside_app_allowed = false)
- raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed = false) method"
- end
-
- # Helpers that builds a path. Useful for building virtual paths.
- def build_path(name, prefix, partial)
- Path.build(name, prefix, partial)
+ def find_templates(name, prefix, partial, details, locals = [])
+ raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, locals = []) method"
end
# Handles templates caching. If a key is given and caching is on
@@ -180,25 +150,13 @@ module ActionView
# resolver is fresher before returning it.
def cached(key, path_info, details, locals)
name, prefix, partial = path_info
- locals = locals.map(&:to_s).sort!
if key
@cache.cache(key, name, prefix, partial, locals) do
- decorate(yield, path_info, details, locals)
+ yield
end
else
- decorate(yield, path_info, details, locals)
- end
- end
-
- # Ensures all the resolver information is set in the template.
- def decorate(templates, path_info, details, locals)
- cached = nil
- templates.each do |t|
- t.locals = locals
- t.formats = details[:formats] || [:html] if t.formats.empty?
- t.variants = details[:variants] || [] if t.variants.empty?
- t.virtual_path ||= (cached ||= build_path(*path_info))
+ yield
end
end
end
@@ -209,34 +167,60 @@ module ActionView
DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
def initialize(pattern = nil)
- @pattern = pattern || DEFAULT_PATTERN
+ if pattern
+ ActiveSupport::Deprecation.warn "Specifying a custom path for #{self.class} is deprecated. Implement a custom Resolver subclass instead."
+ @pattern = pattern
+ else
+ @pattern = DEFAULT_PATTERN
+ end
+ @unbound_templates = Concurrent::Map.new
+ super()
+ end
+
+ def clear_cache
+ @unbound_templates.clear
super()
end
private
- def find_templates(name, prefix, partial, details, outside_app_allowed = false)
+ def _find_all(name, prefix, partial, details, key, locals)
path = Path.build(name, prefix, partial)
- query(path, details, details[:formats], outside_app_allowed)
+ query(path, details, details[:formats], locals, cache: !!key)
end
- def query(path, details, formats, outside_app_allowed)
+ def query(path, details, formats, locals, cache:)
template_paths = find_template_paths_from_details(path, details)
- template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
+ template_paths = reject_files_external_to_app(template_paths)
template_paths.map do |template|
- handler, format, variant = extract_handler_and_format_and_variant(template)
- contents = File.binread(template)
-
- Template.new(contents, File.expand_path(template), handler,
- virtual_path: path.virtual,
- format: format,
- variant: variant,
- updated_at: mtime(template)
- )
+ unbound_template =
+ if cache
+ @unbound_templates.compute_if_absent([template, path.virtual]) do
+ build_unbound_template(template, path.virtual)
+ end
+ else
+ build_unbound_template(template, path.virtual)
+ end
+
+ unbound_template.bind_locals(locals)
end
end
+ def build_unbound_template(template, virtual_path)
+ handler, format, variant = extract_handler_and_format_and_variant(template)
+ source = Template::Sources::File.new(template)
+
+ UnboundTemplate.new(
+ source,
+ template,
+ handler,
+ virtual_path: virtual_path,
+ format: format,
+ variant: variant,
+ )
+ end
+
def reject_files_external_to_app(files)
files.reject { |filename| !inside_path?(@path, filename) }
end
@@ -285,11 +269,6 @@ module ActionView
entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
end
- # Returns the file mtime from the filesystem.
- def mtime(p)
- File.mtime(p)
- end
-
# Extract handler, formats and variant from path. If a format cannot be found neither
# from the path, or the handler, we should return the array of formats given
# to the resolver.
@@ -301,51 +280,25 @@ module ActionView
handler = Template.handler_for_extension(extension)
format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
- format &&= Template::Types[format]
+ format = if format
+ Template::Types[format]&.ref
+ else
+ if handler.respond_to?(:default_format) # default_format can return nil
+ handler.default_format
+ else
+ nil
+ end
+ end
+ # Template::Types[format] and handler.default_format can return nil
[handler, format, variant]
end
end
- # A resolver that loads files from the filesystem. It allows setting your own
- # resolving pattern. Such pattern can be a glob string supported by some variables.
- #
- # ==== Examples
- #
- # Default pattern, loads views the same way as previous versions of rails, eg. when you're
- # looking for <tt>users/new</tt> it will produce query glob: <tt>users/new{.{en},}{.{html,js},}{.{erb,haml},}</tt>
- #
- # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
- #
- # This one allows you to keep files with different formats in separate subdirectories,
- # eg. <tt>users/new.html</tt> will be loaded from <tt>users/html/new.erb</tt> or <tt>users/new.html.erb</tt>,
- # <tt>users/new.js</tt> from <tt>users/js/new.erb</tt> or <tt>users/new.js.erb</tt>, etc.
- #
- # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
- #
- # If you don't specify a pattern then the default will be used.
- #
- # In order to use any of the customized resolvers above in a Rails application, you just need
- # to configure ActionController::Base.view_paths in an initializer, for example:
- #
- # ActionController::Base.view_paths = FileSystemResolver.new(
- # Rails.root.join("app/views"),
- # ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}",
- # )
- #
- # ==== Pattern format and variables
- #
- # Pattern has to be a valid glob string, and it allows you to use the
- # following variables:
- #
- # * <tt>:prefix</tt> - usually the controller path
- # * <tt>:action</tt> - name of the action
- # * <tt>:locale</tt> - possible locale versions
- # * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
- # * <tt>:variants</tt> - possible request variants (for example phone, tablet...)
- # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
- #
+ # A resolver that loads files from the filesystem.
class FileSystemResolver < PathResolver
+ attr_reader :path
+
def initialize(path, pattern = nil)
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
super(pattern)
@@ -365,6 +318,10 @@ module ActionView
# An Optimized resolver for Rails' most common case.
class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
+ def initialize(path)
+ super(path)
+ end
+
private
def find_template_paths_from_details(path, details)
@@ -420,12 +377,18 @@ module ActionView
# The same as FileSystemResolver but does not allow templates to store
# a virtual path since it is invalid for such resolvers.
class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
+ private_class_method :new
+
def self.instances
[new(""), new("/")]
end
- def decorate(*)
- super.each { |t| t.virtual_path = nil }
+ def build_unbound_template(template, _)
+ super(template, nil)
+ end
+
+ def reject_files_external_to_app(files)
+ files
end
end
end
diff --git a/actionview/lib/action_view/template/sources.rb b/actionview/lib/action_view/template/sources.rb
new file mode 100644
index 0000000000..8b89535741
--- /dev/null
+++ b/actionview/lib/action_view/template/sources.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActionView
+ class Template
+ module Sources
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :File
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/sources/file.rb b/actionview/lib/action_view/template/sources/file.rb
new file mode 100644
index 0000000000..2d3a7dec7f
--- /dev/null
+++ b/actionview/lib/action_view/template/sources/file.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionView
+ class Template
+ module Sources
+ class File
+ def initialize(filename)
+ @filename = filename
+ end
+
+ def to_s
+ ::File.binread @filename
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb
index f8d6c2811f..c5fd55f1b3 100644
--- a/actionview/lib/action_view/template/text.rb
+++ b/actionview/lib/action_view/template/text.rb
@@ -8,7 +8,6 @@ module ActionView #:nodoc:
def initialize(string)
@string = string.to_s
- @type = Types[:text]
end
def identifier
@@ -25,9 +24,12 @@ module ActionView #:nodoc:
to_str
end
- def formats
- [@type.ref]
+ def format
+ :text
end
+
+ def formats; Array(format); end
+ deprecate :formats
end
end
end
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index 1fad08a689..1bedf44934 100644
--- a/actionview/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -8,36 +8,37 @@ module ActionView #:nodoc:
# useful for testing extensions that have no way of knowing what the file
# system will look like at runtime.
class FixtureResolver < PathResolver
- attr_reader :hash
-
def initialize(hash = {}, pattern = nil)
super(pattern)
@hash = hash
end
+ def data
+ @hash
+ end
+
def to_s
@hash.keys.join(", ")
end
private
- def query(path, exts, _, _)
+ def query(path, exts, _, locals, cache:)
query = +""
- EXTENSIONS.each_key do |ext|
- query << "(" << exts[ext].map { |e| e && Regexp.escape(".#{e}") }.join("|") << "|)"
+ EXTENSIONS.each do |ext, prefix|
+ query << "(" << exts[ext].map { |e| e && Regexp.escape("#{prefix}#{e}") }.join("|") << "|)"
end
query = /^(#{Regexp.escape(path)})#{query}$/
templates = []
- @hash.each do |_path, array|
- source, updated_at = array
+ @hash.each do |_path, source|
next unless query.match?(_path)
handler, format, variant = extract_handler_and_format_and_variant(_path)
templates << Template.new(source, _path, handler,
virtual_path: path.virtual,
format: format,
variant: variant,
- updated_at: updated_at
+ locals: locals
)
end
@@ -46,9 +47,9 @@ module ActionView #:nodoc:
end
class NullResolver < PathResolver
- def query(path, exts, _, _)
+ def query(path, exts, _, locals, cache:)
handler, format, variant = extract_handler_and_format_and_variant(path)
- [ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant)]
+ [ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant, locals: locals)]
end
end
end
diff --git a/actionview/lib/action_view/unbound_template.rb b/actionview/lib/action_view/unbound_template.rb
new file mode 100644
index 0000000000..db69b6d016
--- /dev/null
+++ b/actionview/lib/action_view/unbound_template.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "concurrent/map"
+
+module ActionView
+ class UnboundTemplate
+ def initialize(source, identifer, handler, options)
+ @source = source
+ @identifer = identifer
+ @handler = handler
+ @options = options
+
+ @templates = Concurrent::Map.new(initial_capacity: 2)
+ end
+
+ def bind_locals(locals)
+ @templates[locals] ||= build_template(locals)
+ end
+
+ private
+
+ def build_template(locals)
+ options = @options.merge(locals: locals)
+ Template.new(
+ @source,
+ @identifer,
+ @handler,
+ options
+ )
+ end
+ end
+end
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index d5694d77f4..3ca5aedc14 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -5,13 +5,21 @@ module ActionView
extend ActiveSupport::Concern
included do
- class_attribute :_view_paths, default: ActionView::PathSet.new.freeze
+ ViewPaths.set_view_paths(self, ActionView::PathSet.new.freeze)
end
delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=,
:locale, :locale=, to: :lookup_context
module ClassMethods
+ def _view_paths
+ ViewPaths.get_view_paths(self)
+ end
+
+ def _view_paths=(paths)
+ ViewPaths.set_view_paths(self, paths)
+ end
+
def _prefixes # :nodoc:
@_prefixes ||= begin
return local_prefixes if superclass.abstract?
@@ -29,6 +37,22 @@ module ActionView
end
end
+ # :stopdoc:
+ @all_view_paths = {}
+
+ def self.get_view_paths(klass)
+ @all_view_paths[klass] || get_view_paths(klass.superclass)
+ end
+
+ def self.set_view_paths(klass, paths)
+ @all_view_paths[klass] = paths
+ end
+
+ def self.all_view_paths
+ @all_view_paths.values.uniq
+ end
+ # :startdoc:
+
# The prefixes used in render "foo" shortcuts.
def _prefixes # :nodoc:
self.class._prefixes