diff options
Diffstat (limited to 'actionview/lib/action_view')
29 files changed, 331 insertions, 233 deletions
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index c4565ca272..5253ef7b0c 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -242,7 +242,7 @@ module ActionView #:nodoc: @_config = ActiveSupport::InheritableOptions.new unless formats == NULL - ActiveSupport::Deprecation.warn <<~eowarn + ActiveSupport::Deprecation.warn <<~eowarn.squish Passing formats to ActionView::Base.new is deprecated eowarn end @@ -251,7 +251,7 @@ module ActionView #:nodoc: when ActionView::LookupContext @lookup_context = lookup_context else - ActiveSupport::Deprecation.warn <<~eowarn + ActiveSupport::Deprecation.warn <<~eowarn.squish ActionView::Base instances should be constructed with a lookup context, assignments, and a controller. eowarn @@ -267,7 +267,7 @@ module ActionView #:nodoc: _prepare_context end - def run(method, template, locals, buffer, &block) + 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 @@ -278,7 +278,7 @@ module ActionView #:nodoc: def compiled_method_container if self.class == ActionView::Base - ActiveSupport::Deprecation.warn <<~eowarn + 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. 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 9fa8d7eab1..97a6da3634 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -6,12 +6,6 @@ module ActionView class Digestor @@digest_mutex = Mutex.new - module PerExecutionDigestCacheExpiry - def self.before(target) - ActionView::LookupContext::DetailsKey.clear - end - end - class << self # Supported options: # diff --git a/actionview/lib/action_view/file_template.rb b/actionview/lib/action_view/file_template.rb deleted file mode 100644 index dea02176eb..0000000000 --- a/actionview/lib/action_view/file_template.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require "action_view/template" - -module ActionView - class FileTemplate < Template - def initialize(filename, handler, details) - @filename = filename - - super(nil, filename, handler, details) - end - - def source - File.binread @filename - end - - def refresh(_) - self - end - - # Exceptions are marshalled when using the parallel test runner with DRb, so we need - # 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: - [ @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ] - end - - def marshal_load(array) # :nodoc: - @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array - @compile_mutex = Mutex.new - end - end -end diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb index c799967cc5..011662d8d1 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -8,9 +8,9 @@ module ActionView module VERSION MAJOR = 6 - MINOR = 0 + MINOR = 1 TINY = 0 - PRE = "beta2" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") 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/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index 0adecf362a..39c01f3334 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -106,7 +106,7 @@ module ActionView end def tag_name(multiple = false, index = nil) - # a little duplication to construct less strings + # a little duplication to construct fewer strings case when @object_name.empty? "#{sanitized_method_name}#{multiple ? "[]" : ""}" @@ -118,7 +118,7 @@ module ActionView end def tag_id(index = nil) - # a little duplication to construct less strings + # a little duplication to construct fewer strings case when @object_name.empty? sanitized_method_name.dup @@ -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 8289c806ee..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) 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/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 10cd61bbd6..b9a8cc129c 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -130,9 +130,8 @@ module ActionView 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)) @@ -154,7 +153,7 @@ module ActionView view_paths = build_view_paths((@view_paths.paths + self.class.fallbacks).uniq) if block_given? - ActiveSupport::Deprecation.warn <<~eowarn + 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 @@ -280,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 a25e1d3d02..8bd526cdf9 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -81,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/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index ed59033e27..ca692699a6 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -17,13 +17,13 @@ module ActionView # 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(view, template) + 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,11 +40,15 @@ module ActionView rendered_partials = @collection.empty? ? [] : yield index = 0 - fetch_or_cache_partial(cached_partials, template, 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? @@ -56,8 +60,10 @@ module ActionView digest_path = view.digest_path_from_template(template) - @collection.each_with_object({}) do |item, hash| - hash[expanded_cache_key(seed.call(item), view, template, digest_path)] = item + @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 @@ -82,15 +88,16 @@ 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, template, order_by:) - order_by.map do |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 + 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/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index 279ef3c680..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 diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index 095c7fbe4e..4cd8fc5bb2 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -26,7 +26,12 @@ module ActionView elsif options.key?(:html) Template::HTML.new(options[:html], formats.first) elsif options.key?(:file) - @lookup_context.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") format = if handler.respond_to?(:default_format) @@ -49,14 +54,14 @@ module ActionView # Renders the given template. A string representing the layout can be # supplied as well. def render_template(view, template, layout_name, locals) - render_with_layout(view, layout_name, template, locals) do |layout| + 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, template, locals) + def render_with_layout(view, template, path, locals) layout = path && find_layout(path, locals.keys, [formats.first]) content = yield(layout) @@ -84,6 +89,7 @@ module ActionView when String begin if layout.start_with?("/") + ActiveSupport::Deprecation.warn "Rendering layouts from an absolute path is deprecated." @lookup_context.with_fallbacks.find_template(layout, nil, false, [], details) else @lookup_context.find_template(layout, nil, false, [], details) diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index ac861c44d4..5a06bd9da6 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -127,7 +127,7 @@ module ActionView # Assign the rendered format to look up context. def _process_format(format) super - lookup_context.formats = [format.to_sym] + 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 2c27e11b9e..b80bf56c1b 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -113,19 +113,21 @@ 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_reader :source, :identifier, :handler, :original_encoding, :updated_at + attr_reader :identifier, :handler, :original_encoding, :updated_at attr_reader :variable, :format, :variant, :locals, :virtual_path - def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil, updated_at: Time.now) + 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 = [] @@ -139,24 +141,31 @@ module ActionView @virtual_path = virtual_path @variable = if @virtual_path - base = @virtual_path[-1] == "/" ? "" : File.basename(@virtual_path) + base = @virtual_path[-1] == "/" ? "" : ::File.basename(@virtual_path) base =~ /\A_?(.*?)(?:\.\w+)*\z/ $1.to_sym end - @updated_at = updated_at + 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. @@ -173,7 +182,7 @@ module ActionView def render(view, locals, buffer = ActionView::OutputBuffer.new, &block) instrument_render_template do compile!(view) - view.run(method_name, self, locals, buffer, &block) + view._run(method_name, self, locals, buffer, &block) end rescue => e handle_render_error(view, e) @@ -183,25 +192,6 @@ module ActionView @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 - end - def short_identifier @short_identifier ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "") : identifier end @@ -210,6 +200,10 @@ module ActionView "#<#{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 # source. Until this point, we assume that the source is BINARY data. # If no additional information is supplied, we assume the encoding is @@ -291,9 +285,6 @@ 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 @@ -325,6 +316,7 @@ module ActionView # 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} @@ -345,7 +337,14 @@ module ActionView raise WrongEncodingError.new(source, Encoding.default_internal) end - mod.module_eval(source, identifier, 0) + 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 def handle_render_error(view, e) @@ -353,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 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 ddaac7a100..6450513003 100644 --- a/actionview/lib/action_view/template/handlers.rb +++ b/actionview/lib/action_view/template/handlers.rb @@ -45,12 +45,12 @@ module ActionView #:nodoc: unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2 ActiveSupport::Deprecation.warn <<~eowarn - Single arity template handlers are deprecated. Template handlers must + 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.class}#call(#{params.map(&:last).join(", ")}) + >> #{handler}.call(#{params.map(&:last).join(", ")}) To: - >> #{handler.class}#call(#{params.map(&:last).join(", ")}, source) + >> #{handler}.call(#{params.map(&:last).join(", ")}, source) eowarn handler = LegacyHandlerWrapper.new(handler) end diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb index 247b6d75d1..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) + class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", defined?(@filename) ? @filename : "(erubi)", 0) }.empty - view.run(:_template, nil, {}, ActionView::OutputBuffer.new) + view._run(:_template, nil, {}, ActionView::OutputBuffer.new) end private 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 1dc9c9919a..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 @@ -146,17 +118,12 @@ module ActionView locals = locals.map(&:to_s).sort!.freeze cached(key, [name, prefix, partial], details, locals) do - find_templates(name, prefix, partial, details, false, locals) + _find_all(name, prefix, partial, details, key, locals) end end - def find_all_anywhere(name, prefix, partial = false, details = {}, key = nil, locals = []) - locals = locals.map(&:to_s).sort!.freeze - - cached(key, [name, prefix, partial], details, locals) do - find_templates(name, prefix, partial, details, true, 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)) } @@ -164,13 +131,17 @@ module ActionView 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, locals = []) - raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed = false, locals = []) method" + 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 @@ -196,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, locals) + def _find_all(name, prefix, partial, details, key, locals) path = Path.build(name, prefix, partial) - query(path, details, details[:formats], outside_app_allowed, locals) + query(path, details, details[:formats], locals, cache: !!key) end - def query(path, details, formats, outside_app_allowed, locals) + 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) - - FileTemplate.new(File.expand_path(template), handler, - virtual_path: path.virtual, - format: format, - variant: variant, - locals: locals, - 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 @@ -272,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. @@ -303,45 +295,10 @@ module ActionView 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) @@ -361,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) @@ -416,8 +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 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/test_case.rb b/actionview/lib/action_view/test_case.rb index e14f7aaec7..3a75633743 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -284,7 +284,7 @@ module ActionView def respond_to_missing?(name, include_private = false) begin - routes = @controller.respond_to?(:_routes) && @controller._routes + routes = defined?(@controller) && @controller.respond_to?(:_routes) && @controller._routes rescue # Don't call routes, if there is an error on _routes call end diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index 3ca8420c6c..1bedf44934 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -23,24 +23,22 @@ module ActionView #:nodoc: private - def query(path, exts, _, _, locals) + 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, - locals: locals, - updated_at: updated_at + locals: locals ) end @@ -49,7 +47,7 @@ module ActionView #:nodoc: end class NullResolver < PathResolver - def query(path, exts, _, _, locals) + 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, locals: locals)] 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 |