diff options
Diffstat (limited to 'actionview/lib')
105 files changed, 4300 insertions, 2317 deletions
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index 0a87500a52..f398ba0ff0 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + #-- -# Copyright (c) 2004-2016 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -21,9 +23,9 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -require 'active_support' -require 'active_support/rails' -require 'action_view/version' +require "active_support" +require "active_support/rails" +require "action_view/version" module ActionView extend ActiveSupport::Autoload @@ -33,7 +35,6 @@ module ActionView eager_autoload do autoload :Base autoload :Context - autoload :CompiledTemplates, "action_view/context" autoload :Digestor autoload :Helpers autoload :LookupContext @@ -43,6 +44,7 @@ module ActionView autoload :Rendering autoload :RoutingUrlFor autoload :Template + autoload :FileTemplate autoload :ViewPaths autoload_under "renderer" do @@ -74,12 +76,12 @@ module ActionView autoload :MissingTemplate autoload :ActionViewError autoload :EncodingError - autoload :MissingRequestError autoload :TemplateError autoload :WrongEncodingError end end + autoload :CacheExpiry autoload :TestCase def self.eager_load! @@ -89,8 +91,8 @@ module ActionView end end -require 'active_support/core_ext/string/output_safety' +require "active_support/core_ext/string/output_safety" ActiveSupport.on_load(:i18n) do - I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en.yml" + I18n.load_path << File.expand_path("action_view/locale/en.yml", __dir__) end diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index ad1cb1a4be..5253ef7b0c 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -1,23 +1,26 @@ -require 'active_support/core_ext/module/attr_internal' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/ordered_options' -require 'action_view/log_subscriber' -require 'action_view/helpers' -require 'action_view/context' -require 'action_view/template' -require 'action_view/lookup_context' +# frozen_string_literal: true + +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" +require "action_view/template" +require "action_view/lookup_context" module ActionView #:nodoc: # = Action View Base # # Action View templates can be written in several ways. - # If the template file has a <tt>.erb</tt> extension, then it uses the erubis[https://rubygems.org/gems/erubis] + # If the template file has a <tt>.erb</tt> extension, then it uses the erubi[https://rubygems.org/gems/erubi] # template system which can embed Ruby into an HTML document. # If the template file has a <tt>.builder</tt> extension, then Jim Weirich's Builder::XmlMarkup library is used. # # == ERB # - # You trigger ERB by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the + # You trigger ERB by using embeddings such as <tt><% %></tt>, <tt><% -%></tt>, and <tt><%= %></tt>. The <tt><%= %></tt> tag set is used when you want output. Consider the # following loop for names: # # <b>Names of all the people</b> @@ -25,7 +28,7 @@ module ActionView #:nodoc: # Name: <%= person.name %><br/> # <% end %> # - # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this + # The loop is setup in regular embedding tags <tt><% %></tt>, and the name is written using the output embedding tag <tt><%= %></tt>. Note that this # is not just a usage suggestion. Regular output functions like print or puts won't work with ERB templates. So this would be wrong: # # <%# WRONG %> @@ -33,9 +36,9 @@ module ActionView #:nodoc: # # If you absolutely must write from within a function use +concat+. # - # When on a line that only contains whitespaces except for the tag, <% %> suppress leading and trailing whitespace, - # including the trailing newline. <% %> and <%- -%> are the same. - # Note however that <%= %> and <%= -%> are different: only the latter removes trailing whitespaces. + # When on a line that only contains whitespaces except for the tag, <tt><% %></tt> suppresses leading and trailing whitespace, + # including the trailing newline. <tt><% %></tt> and <tt><%- -%></tt> are the same. + # Note however that <tt><%= %></tt> and <tt><%= -%></tt> are different: only the latter removes trailing whitespaces. # # === Using sub templates # @@ -110,7 +113,7 @@ module ActionView #:nodoc: # <p>A product of Danish Design during the Winter of '79...</p> # </div> # - # A full-length RSS example actually used on Basecamp: + # Here is a full-length RSS example actually used on Basecamp: # # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do # xml.channel do @@ -140,36 +143,31 @@ module ActionView #:nodoc: include Helpers, ::ERB::Util, Context # Specify the proc used to decorate input tags that refer to attributes with errors. - cattr_accessor :field_error_proc - @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe } + cattr_accessor :field_error_proc, default: Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe } # How to complete the streaming when an exception occurs. # This is our best guess: first try to close the attribute, then the tag. - cattr_accessor :streaming_completion_on_exception - @@streaming_completion_on_exception = %("><script>window.location = "/500.html"</script></html>) + cattr_accessor :streaming_completion_on_exception, default: %("><script>window.location = "/500.html"</script></html>) # Specify whether rendering within namespaced controllers should prefix # the partial paths for ActiveModel objects with the namespace. # (e.g., an Admin::PostsController would render @post using /admin/posts/_post.erb) - cattr_accessor :prefix_partial_path_with_controller_namespace - @@prefix_partial_path_with_controller_namespace = true + cattr_accessor :prefix_partial_path_with_controller_namespace, default: true # Specify default_formats that can be rendered. cattr_accessor :default_formats # Specify whether an error should be raised for missing translations - cattr_accessor :raise_on_missing_translations - @@raise_on_missing_translations = false + cattr_accessor :raise_on_missing_translations, default: false # Specify whether submit_tag should automatically disable on click - cattr_accessor :automatically_disable_submit_tag - @@automatically_disable_submit_tag = true + cattr_accessor :automatically_disable_submit_tag, default: true class_attribute :_routes class_attribute :logger class << self - delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB' + delegate :erb_trim_mode=, to: "ActionView::Template::Handlers::ERB" def cache_template_loading ActionView::Resolver.caching? @@ -182,36 +180,133 @@ 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 + 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, 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, @current_template = _old_output_buffer, _old_virtual_path, _old_template + end + + def compiled_method_container + 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) end end diff --git a/actionview/lib/action_view/buffers.rb b/actionview/lib/action_view/buffers.rb index be5d86b1dc..18eaee5d79 100644 --- a/actionview/lib/action_view/buffers.rb +++ b/actionview/lib/action_view/buffers.rb @@ -1,6 +1,23 @@ -require 'active_support/core_ext/string/output_safety' +# frozen_string_literal: true + +require "active_support/core_ext/string/output_safety" module ActionView + # Used as a buffer for views + # + # The main difference between this and ActiveSupport::SafeBuffer + # is for the methods `<<` and `safe_expr_append=` the inputs are + # checked for nil before they are assigned and `to_s` is called on + # the input. For example: + # + # obuf = ActionView::OutputBuffer.new "hello" + # obuf << 5 + # puts obuf # => "hello5" + # + # sbuf = ActiveSupport::SafeBuffer.new "hello" + # sbuf << 5 + # puts sbuf # => "hello\u0005" + # class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc: def initialize(*) super 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/context.rb b/actionview/lib/action_view/context.rb index ee263df484..2b22c30a3a 100644 --- a/actionview/lib/action_view/context.rb +++ b/actionview/lib/action_view/context.rb @@ -1,23 +1,20 @@ -module ActionView - module CompiledTemplates #:nodoc: - # holds compiled template code - end +# frozen_string_literal: true +module ActionView # = Action View Context # # Action View contexts are supplied to Action Controller to render a template. # The default Action View context is ActionView::Base. # - # In order to work with ActionController, a Context must just include this module. - # The initialization of the variables used by the context (@output_buffer, @view_flow, - # and @virtual_path) is responsibility of the object that includes this module - # (although you can call _prepare_context defined below). + # In order to work with Action Controller, a Context must just include this + # module. The initialization of the variables used by the context + # (@output_buffer, @view_flow, and @virtual_path) is responsibility of the + # object that includes this module (although you can call _prepare_context + # defined below). module Context - include CompiledTemplates attr_accessor :output_buffer, :view_flow # Prepares the context by setting the appropriate instance variables. - # :api: plugin def _prepare_context @view_flow = OutputFlow.new @output_buffer = nil @@ -27,8 +24,7 @@ module ActionView # Encapsulates the interaction with the view flow so it # returns the correct buffer on +yield+. This is usually # overwritten by helpers to add more behavior. - # :api: plugin - def _layout_for(name=nil) + def _layout_for(name = nil) name ||= :layout view_flow.get(name).html_safe end diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index 7731773040..182f6e2eef 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -1,5 +1,7 @@ -require 'concurrent/map' -require 'action_view/path_set' +# frozen_string_literal: true + +require "concurrent/map" +require "action_view/path_set" module ActionView class DependencyTracker # :nodoc: @@ -105,7 +107,6 @@ module ActionView attr_reader :name, :template private :name, :template - private def source template.source @@ -142,7 +143,7 @@ module ActionView def add_static_dependency(dependencies, dependency) if dependency - if dependency.include?('/') + if dependency.include?("/") dependencies << dependency else dependencies << "#{directory}/#{dependency}" @@ -163,7 +164,7 @@ module ActionView def explicit_dependencies dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq - wildcards, explicits = dependencies.partition { |dependency| dependency[-1] == '*' } + wildcards, explicits = dependencies.partition { |dependency| dependency[-1] == "*" } (explicits + resolve_directories(wildcards)).uniq end diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index b99d1af998..97a6da3634 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -1,17 +1,10 @@ -require 'concurrent/map' -require 'action_view/dependency_tracker' -require 'monitor' +# frozen_string_literal: true + +require "action_view/dependency_tracker" module ActionView class Digestor - @@digest_mutex = Mutex.new - - class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc: - def call(env) - ActionView::LookupContext::DetailsKey.clear - app.call(env) - end - end + @@digest_mutex = Mutex.new class << self # Supported options: @@ -19,10 +12,12 @@ module ActionView # * <tt>name</tt> - Template name # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt> # * <tt>dependencies</tt> - An array of dependent views - # * <tt>partial</tt> - Specifies whether the template is a partial - def digest(name:, finder:, dependencies: []) - dependencies ||= [] - cache_key = ([ name ].compact + dependencies).join('.') + def digest(name:, format:, finder:, dependencies: nil) + if dependencies.nil? || dependencies.empty? + cache_key = "#{name}.#{format}" + else + cache_key = [ name, format, dependencies ].flatten.compact.join(".") + end # this is a correctly done double-checked locking idiom # (Concurrent::Map's lookups have volatile semantics) @@ -32,7 +27,7 @@ module ActionView root = tree(name, finder, partial) dependencies.each do |injected_dep| root.children << Injected.new(injected_dep, nil, nil) - end + end if dependencies finder.digest_cache[cache_key] = root.digest(finder) end end @@ -46,9 +41,7 @@ module ActionView def tree(name, finder, partial = false, seen = {}) logical_name = name.gsub(%r|/_|, "/") - if finder.disable_cache { finder.exists?(logical_name, [], partial) } - template = finder.disable_cache { finder.find(logical_name, [], partial) } - + if template = find_template(finder, logical_name, [], partial, []) if node = seen[template.identifier] # handle cycles in the tree node else @@ -61,11 +54,20 @@ module ActionView node end else - logger.error " '#{name}' file doesn't exist, so no dependencies" - logger.error " Couldn't find template for digesting: #{name}" + unless name.include?("#") # Dynamic template partial names can never be tracked + logger.error " Couldn't find template for digesting: #{name}" + end + seen[name] ||= Missing.new(name, logical_name, nil) end end + + private + def find_template(finder, name, prefixes, partial, keys) + finder.disable_cache do + finder.find_all(name, prefixes, partial, keys).first + end + end end class Node @@ -84,7 +86,7 @@ module ActionView end def digest(finder, stack = []) - Digest::MD5.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}") + ActiveSupport::Digest.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}") end def dependency_digest(finder, stack) @@ -108,7 +110,7 @@ module ActionView class Partial < Node; end class Missing < Node - def digest(finder, _ = []) '' end + def digest(finder, _ = []) "" end end class Injected < Node diff --git a/actionview/lib/action_view/file_template.rb b/actionview/lib/action_view/file_template.rb new file mode 100644 index 0000000000..dea02176eb --- /dev/null +++ b/actionview/lib/action_view/file_template.rb @@ -0,0 +1,33 @@ +# 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/flows.rb b/actionview/lib/action_view/flows.rb index bc61920848..ff44fa6619 100644 --- a/actionview/lib/action_view/flows.rb +++ b/actionview/lib/action_view/flows.rb @@ -1,11 +1,13 @@ -require 'active_support/core_ext/string/output_safety' +# frozen_string_literal: true + +require "active_support/core_ext/string/output_safety" module ActionView class OutputFlow #:nodoc: attr_reader :content def initialize - @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } + @content = Hash.new { |h, k| h[k] = ActiveSupport::SafeBuffer.new } end # Called by _layout_for to read stored values. @@ -23,7 +25,6 @@ module ActionView @content[key] << value end alias_method :append!, :append - end class StreamingFlow < OutputFlow #:nodoc: @@ -37,9 +38,8 @@ module ActionView end # Try to get stored content. If the content - # is not available and we are inside the layout - # fiber, we set that we are waiting for the given - # key and yield. + # is not available and we're inside the layout fiber, + # then it will begin waiting for the given key and yield. def get(key) return super if @content.key?(key) @@ -60,8 +60,8 @@ module ActionView end # Appends the contents for the given key. This is called - # by provides and resumes back to the fiber if it is - # the key it is waiting for. + # by providing and resuming back to the fiber, + # if that's the key it's waiting for. def append!(key, value) super @fiber.resume if @waiting_for == key @@ -69,8 +69,8 @@ module ActionView private - def inside_fiber? - Fiber.current.object_id != @root - end + def inside_fiber? + Fiber.current.object_id != @root + end end end diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb index efb565bf59..5bed37583e 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView # Returns the version of the currently loaded Action View as a <tt>Gem::Version</tt> def self.gem_version @@ -5,7 +7,7 @@ module ActionView end module VERSION - MAJOR = 5 + MAJOR = 6 MINOR = 0 TINY = 0 PRE = "beta3" diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb index 787e9d67b2..0d77f74171 100644 --- a/actionview/lib/action_view/helpers.rb +++ b/actionview/lib/action_view/helpers.rb @@ -1,4 +1,6 @@ -require 'active_support/benchmarkable' +# frozen_string_literal: true + +require "active_support/benchmarkable" module ActionView #:nodoc: module Helpers #:nodoc: @@ -11,6 +13,7 @@ module ActionView #:nodoc: autoload :CacheHelper autoload :CaptureHelper autoload :ControllerHelper + autoload :CspHelper autoload :CsrfHelper autoload :DateHelper autoload :DebugHelper @@ -20,7 +23,6 @@ module ActionView #:nodoc: autoload :JavaScriptHelper, "action_view/helpers/javascript_helper" autoload :NumberHelper autoload :OutputSafetyHelper - autoload :RecordTagHelper autoload :RenderingHelper autoload :SanitizeHelper autoload :TagHelper @@ -44,6 +46,7 @@ module ActionView #:nodoc: include CacheHelper include CaptureHelper include ControllerHelper + include CspHelper include CsrfHelper include DateHelper include DebugHelper @@ -53,7 +56,6 @@ module ActionView #:nodoc: include JavaScriptHelper include NumberHelper include OutputSafetyHelper - include RecordTagHelper include RenderingHelper include SanitizeHelper include TagHelper diff --git a/actionview/lib/action_view/helpers/active_model_helper.rb b/actionview/lib/action_view/helpers/active_model_helper.rb index d5222e3616..e41a95d2ce 100644 --- a/actionview/lib/action_view/helpers/active_model_helper.rb +++ b/actionview/lib/action_view/helpers/active_model_helper.rb @@ -1,9 +1,11 @@ -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/enumerable' +# frozen_string_literal: true + +require "active_support/core_ext/module/attribute_accessors" +require "active_support/core_ext/enumerable" module ActionView # = Active Model Helpers - module Helpers + module Helpers #:nodoc: module ActiveModelHelper end @@ -15,8 +17,8 @@ module ActionView end end - def content_tag(*) - error_wrapping(super) + def content_tag(type, options, *) + select_markup_helper?(type) ? super : error_wrapping(super) end def tag(type, options, *) @@ -37,13 +39,17 @@ module ActionView private - def object_has_errors? - object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present? - end + def object_has_errors? + object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present? + end - def tag_generate_errors?(options) - options['type'] != 'hidden' - end + def select_markup_helper?(type) + ["optgroup", "option"].include?(type) + end + + def tag_generate_errors?(options) + options["type"] != "hidden" + end end end end diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 413c35954c..59d70a1dc4 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -1,7 +1,11 @@ -require 'active_support/core_ext/array/extract_options' -require 'active_support/core_ext/hash/keys' -require 'action_view/helpers/asset_url_helper' -require 'action_view/helpers/tag_helper' +# frozen_string_literal: true + +require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/hash/keys" +require "active_support/core_ext/object/inclusion" +require "active_support/core_ext/object/try" +require "action_view/helpers/asset_url_helper" +require "action_view/helpers/tag_helper" module ActionView # = Action View Asset Tag Helpers @@ -11,7 +15,7 @@ module ActionView # the assets exist before linking to them: # # image_tag("rails.png") - # # => <img alt="Rails" src="/assets/rails.png" /> + # # => <img src="/assets/rails.png" /> # stylesheet_link_tag("application") # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" /> module AssetTagHelper @@ -35,33 +39,71 @@ module ActionView # When the Asset Pipeline is enabled, you can pass the name of your manifest as # source, and include other JavaScript or CoffeeScript files inside the manifest. # + # If the server supports Early Hints header links for these assets will be + # automatically pushed. + # + # ==== Options + # + # When the last parameter is a hash you can add HTML attributes using that + # parameter. The following options are supported: + # + # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension + # already exists. This only applies for relative URLs. + # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only + # applies when a relative URL and +host+ options are provided. + # * <tt>:host</tt> - When a relative URL is provided the host is added to the + # that path. + # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline + # when it is set to true. + # * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if + # you have Content Security Policy enabled. + # + # ==== Examples + # # javascript_include_tag "xmlhr" - # # => <script src="/assets/xmlhr.js?1284139606"></script> + # # => <script src="/assets/xmlhr.debug-1284139606.js"></script> + # + # javascript_include_tag "xmlhr", host: "localhost", protocol: "https" + # # => <script src="https://localhost/assets/xmlhr.debug-1284139606.js"></script> # # javascript_include_tag "template.jst", extname: false - # # => <script src="/assets/template.jst?1284139606"></script> + # # => <script src="/assets/template.debug-1284139606.jst"></script> # # javascript_include_tag "xmlhr.js" - # # => <script src="/assets/xmlhr.js?1284139606"></script> + # # => <script src="/assets/xmlhr.debug-1284139606.js"></script> # # javascript_include_tag "common.javascript", "/elsewhere/cools" - # # => <script src="/assets/common.javascript?1284139606"></script> - # # <script src="/elsewhere/cools.js?1423139606"></script> + # # => <script src="/assets/common.javascript.debug-1284139606.js"></script> + # # <script src="/elsewhere/cools.debug-1284139606.js"></script> # # javascript_include_tag "http://www.example.com/xmlhr" # # => <script src="http://www.example.com/xmlhr"></script> # # javascript_include_tag "http://www.example.com/xmlhr.js" # # => <script src="http://www.example.com/xmlhr.js"></script> + # + # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true + # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script> def javascript_include_tag(*sources) options = sources.extract_options!.stringify_keys - path_options = options.extract!('protocol', 'extname', 'host').symbolize_keys - sources.uniq.map { |source| + path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys + early_hints_links = [] + + sources_tags = sources.uniq.map { |source| + href = path_to_javascript(source, path_options) + early_hints_links << "<#{href}>; rel=preload; as=script" tag_options = { - "src" => path_to_javascript(source, path_options) + "src" => href }.merge!(options) - content_tag("script".freeze, "", tag_options) + if tag_options["nonce"] == true + tag_options["nonce"] = content_security_policy_nonce + end + content_tag("script", "", tag_options) }.join("\n").html_safe + + request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request + + sources_tags end # Returns a stylesheet link tag for the sources specified as arguments. If @@ -71,6 +113,9 @@ module ActionView # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to # apply to all media types. # + # If the server supports Early Hints header links for these assets will be + # automatically pushed. + # # stylesheet_link_tag "style" # # => <link href="/assets/style.css" media="screen" rel="stylesheet" /> # @@ -91,22 +136,29 @@ module ActionView # # <link href="/css/stylish.css" media="screen" rel="stylesheet" /> def stylesheet_link_tag(*sources) options = sources.extract_options!.stringify_keys - path_options = options.extract!('protocol', 'host').symbolize_keys + path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys + early_hints_links = [] - sources.uniq.map { |source| + sources_tags = sources.uniq.map { |source| + href = path_to_stylesheet(source, path_options) + early_hints_links << "<#{href}>; rel=preload; as=style" tag_options = { "rel" => "stylesheet", "media" => "screen", - "href" => path_to_stylesheet(source, path_options) + "href" => href }.merge!(options) tag(:link, tag_options) }.join("\n").html_safe + + request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request + + sources_tags end # Returns a link tag that browsers and feed readers can use to auto-detect - # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or - # <tt>:atom</tt>. Control the link options in url_for format using the - # +url_options+. You can modify the LINK tag itself in +tag_options+. + # an RSS, Atom, or JSON feed. The +type+ can be <tt>:rss</tt> (default), + # <tt>:atom</tt>, or <tt>:json</tt>. Control the link options in url_for format + # using the +url_options+. You can modify the LINK tag itself in +tag_options+. # # ==== Options # @@ -120,6 +172,8 @@ module ActionView # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" /> # auto_discovery_link_tag(:atom) # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" /> + # auto_discovery_link_tag(:json) + # # => <link rel="alternate" type="application/json" title="JSON" href="http://www.currenthost.com/controller/action" /> # auto_discovery_link_tag(:rss, {action: "feed"}) # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" /> # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"}) @@ -129,8 +183,8 @@ module ActionView # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"}) # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" /> def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) - if !(type == :rss || type == :atom) && tag_options[:type].blank? - raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.") + if !(type == :rss || type == :atom || type == :json) && tag_options[:type].blank? + raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss, :atom, or :json.") end tag( @@ -138,7 +192,7 @@ module ActionView "rel" => tag_options[:rel] || "alternate", "type" => tag_options[:type] || Template::Types[type].to_s, "title" => tag_options[:title] || type.to_s.upcase, - "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options + "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(only_path: false)) : url_options ) end @@ -169,94 +223,156 @@ module ActionView # # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png' # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" /> - def favicon_link_tag(source='favicon.ico', options={}) - tag('link', { - :rel => 'shortcut icon', - :type => 'image/x-icon', - :href => path_to_image(source) + def favicon_link_tag(source = "favicon.ico", options = {}) + tag("link", { + rel: "shortcut icon", + type: "image/x-icon", + href: path_to_image(source, skip_pipeline: options.delete(:skip_pipeline)) }.merge!(options.symbolize_keys)) end + # Returns a link tag that browsers can use to preload the +source+. + # The +source+ can be the path of a resource managed by asset pipeline, + # a full path, or an URI. + # + # ==== Options + # + # * <tt>:type</tt> - Override the auto-generated mime type, defaults to the mime type for +source+ extension. + # * <tt>:as</tt> - Override the auto-generated value for as attribute, calculated using +source+ extension and mime type. + # * <tt>:crossorigin</tt> - Specify the crossorigin attribute, required to load cross-origin resources. + # * <tt>:nopush</tt> - Specify if the use of server push is not desired for the resource. Defaults to +false+. + # + # ==== Examples + # + # preload_link_tag("custom_theme.css") + # # => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" /> + # + # preload_link_tag("/videos/video.webm") + # # => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" /> + # + # preload_link_tag(post_path(format: :json), as: "fetch") + # # => <link rel="preload" href="/posts.json" as="fetch" type="application/json" /> + # + # preload_link_tag("worker.js", as: "worker") + # # => <link rel="preload" href="/assets/worker.js" as="worker" type="text/javascript" /> + # + # preload_link_tag("//example.com/font.woff2") + # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/> + # + # preload_link_tag("//example.com/font.woff2", crossorigin: "use-credentials") + # # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" /> + # + # preload_link_tag("/media/audio.ogg", nopush: true) + # # => <link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" /> + # + def preload_link_tag(source, options = {}) + href = asset_path(source, skip_pipeline: options.delete(:skip_pipeline)) + extname = File.extname(source).downcase.delete(".") + mime_type = options.delete(:type) || Template::Types[extname].try(:to_s) + as_type = options.delete(:as) || resolve_link_as(extname, mime_type) + crossorigin = options.delete(:crossorigin) + crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font") + nopush = options.delete(:nopush) || false + + link_tag = tag.link({ + rel: "preload", + href: href, + as: as_type, + type: mime_type, + crossorigin: crossorigin + }.merge!(options.symbolize_keys)) + + early_hints_link = "<#{href}>; rel=preload; as=#{as_type}" + early_hints_link += "; type=#{mime_type}" if mime_type + early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin + early_hints_link += "; nopush" if nopush + + request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request + + link_tag + end + # Returns an HTML image tag for the +source+. The +source+ can be a full - # path or a file. + # path, a file, or an Active Storage attachment. # # ==== Options # # You can add HTML attributes using the +options+. The +options+ supports - # two additional keys for convenience and conformance: + # additional keys for convenience and conformance: # - # * <tt>:alt</tt> - If no alt text is given, the file name part of the - # +source+ is used (capitalized and without the extension) # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes # width="30" and height="45", and "50" becomes width="50" and height="50". # <tt>:size</tt> will be ignored if the value is not in the correct format. + # * <tt>:srcset</tt> - If supplied as a hash or array of <tt>[source, descriptor]</tt> + # pairs, each image path will be expanded before the list is formatted as a string. # # ==== Examples # + # Assets (images that are part of your app): + # # image_tag("icon") - # # => <img alt="Icon" src="/assets/icon" /> + # # => <img src="/assets/icon" /> # image_tag("icon.png") - # # => <img alt="Icon" src="/assets/icon.png" /> + # # => <img src="/assets/icon.png" /> # image_tag("icon.png", size: "16x10", alt: "Edit Entry") # # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" /> # image_tag("/icons/icon.gif", size: "16") - # # => <img src="/icons/icon.gif" width="16" height="16" alt="Icon" /> + # # => <img src="/icons/icon.gif" width="16" height="16" /> # image_tag("/icons/icon.gif", height: '32', width: '32') - # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" /> + # # => <img height="32" src="/icons/icon.gif" width="32" /> # image_tag("/icons/icon.gif", class: "menu_icon") - # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" /> + # # => <img class="menu_icon" src="/icons/icon.gif" /> # image_tag("/icons/icon.gif", data: { title: 'Rails Application' }) # # => <img data-title="Rails Application" src="/icons/icon.gif" /> - def image_tag(source, options={}) + # image_tag("icon.png", srcset: { "icon_2x.png" => "2x", "icon_4x.png" => "4x" }) + # # => <img src="/assets/icon.png" srcset="/assets/icon_2x.png 2x, /assets/icon_4x.png 4x"> + # image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw") + # # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw"> + # + # Active Storage blobs (images that are uploaded by the users of your app): + # + # image_tag(user.avatar) + # # => <img src="/rails/active_storage/blobs/.../tiger.jpg" /> + # image_tag(user.avatar.variant(resize_to_limit: [100, 100])) + # # => <img src="/rails/active_storage/representations/.../tiger.jpg" /> + # image_tag(user.avatar.variant(resize_to_limit: [100, 100]), size: '100') + # # => <img width="100" height="100" src="/rails/active_storage/representations/.../tiger.jpg" /> + def image_tag(source, options = {}) options = options.symbolize_keys check_for_image_tag_errors(options) + skip_pipeline = options.delete(:skip_pipeline) - src = options[:src] = path_to_image(source) + options[:src] = resolve_image_source(source, skip_pipeline) - unless src =~ /^(?:cid|data):/ || src.blank? - options[:alt] = options.fetch(:alt){ image_alt(src) } + if options[:srcset] && !options[:srcset].is_a?(String) + options[:srcset] = options[:srcset].map do |src_path, size| + src_path = path_to_image(src_path, skip_pipeline: skip_pipeline) + "#{src_path} #{size}" + end.join(", ") end options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size] tag("img", options) end - # Returns a string suitable for an HTML image tag alt attribute. - # The +src+ argument is meant to be an image file path. - # The method removes the basename of the file path and the digest, - # if any. It also removes hyphens and underscores from file names and - # replaces them with spaces, returning a space-separated, titleized - # string. - # - # ==== Examples - # - # image_alt('rails.png') - # # => Rails - # - # image_alt('hyphenated-file-name.png') - # # => Hyphenated file name - # - # image_alt('underscored_file_name.png') - # # => Underscored file name - def image_alt(src) - File.basename(src, '.*'.freeze).sub(/-[[:xdigit:]]{32,64}\z/, ''.freeze).tr('-_'.freeze, ' '.freeze).capitalize - end - # Returns an HTML video tag for the +sources+. If +sources+ is a string, # a single video tag will be returned. If +sources+ is an array, a video # tag with nested source tags for each source will be returned. The - # +sources+ can be full paths or files that exists in your public videos + # +sources+ can be full paths or files that exist in your public videos # directory. # # ==== Options - # You can add HTML attributes using the +options+. The +options+ supports - # two additional keys for convenience and conformance: + # + # When the last parameter is a hash you can add HTML attributes using that + # parameter. The following options are supported: # # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown # before the video loads. The path is calculated like the +src+ of +image_tag+. # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes # width="30" and height="45", and "50" becomes width="50" and height="50". # <tt>:size</tt> will be ignored if the value is not in the correct format. + # * <tt>:poster_skip_pipeline</tt> will bypass the asset pipeline when using + # the <tt>:poster</tt> option instead using an asset in the public folder. # # ==== Examples # @@ -264,10 +380,12 @@ module ActionView # # => <video src="/videos/trailer"></video> # video_tag("trailer.ogg") # # => <video src="/videos/trailer.ogg"></video> - # video_tag("trailer.ogg", controls: true, autobuffer: true) - # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" ></video> + # video_tag("trailer.ogg", controls: true, preload: 'none') + # # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video> # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png") # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video> + # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true) + # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="screenshot.png"></video> # video_tag("/trailers/hd.avi", size: "16x16") # # => <video src="/trailers/hd.avi" width="16" height="16"></video> # video_tag("/trailers/hd.avi", size: "16") @@ -281,15 +399,23 @@ module ActionView # video_tag(["trailer.ogg", "trailer.flv"], size: "160x120") # # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> def video_tag(*sources) - multiple_sources_tag('video', sources) do |options| - options[:poster] = path_to_image(options[:poster]) if options[:poster] - options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size] + options = sources.extract_options!.symbolize_keys + public_poster_folder = options.delete(:poster_skip_pipeline) + sources << options + multiple_sources_tag_builder("video", sources) do |tag_options| + tag_options[:poster] = path_to_image(tag_options[:poster], skip_pipeline: public_poster_folder) if tag_options[:poster] + tag_options[:width], tag_options[:height] = extract_dimensions(tag_options.delete(:size)) if tag_options[:size] end end - # Returns an HTML audio tag for the +source+. - # The +source+ can be full path or file that exists in - # your public audios directory. + # Returns an HTML audio tag for the +sources+. If +sources+ is a string, + # a single audio tag will be returned. If +sources+ is an array, an audio + # tag with nested source tags for each source will be returned. The + # +sources+ can be full paths or files that exist in your public audios + # directory. + # + # When the last parameter is a hash you can add HTML attributes using that + # parameter. # # audio_tag("sound") # # => <audio src="/audios/sound"></audio> @@ -300,31 +426,42 @@ module ActionView # audio_tag("sound.wav", "sound.mid") # # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio> def audio_tag(*sources) - multiple_sources_tag('audio', sources) + multiple_sources_tag_builder("audio", sources) end private - def multiple_sources_tag(type, sources) - options = sources.extract_options!.symbolize_keys + def multiple_sources_tag_builder(type, sources) + options = sources.extract_options!.symbolize_keys + skip_pipeline = options.delete(:skip_pipeline) sources.flatten! yield options if block_given? if sources.size > 1 content_tag(type, options) do - safe_join sources.map { |source| tag("source", :src => send("path_to_#{type}", source)) } + safe_join sources.map { |source| tag("source", src: send("path_to_#{type}", source, skip_pipeline: skip_pipeline)) } end else - options[:src] = send("path_to_#{type}", sources.first) + options[:src] = send("path_to_#{type}", sources.first, skip_pipeline: skip_pipeline) content_tag(type, nil, options) end end + def resolve_image_source(source, skip_pipeline) + if source.is_a?(Symbol) || source.is_a?(String) + path_to_image(source, skip_pipeline: skip_pipeline) + else + polymorphic_url(source) + end + rescue NoMethodError => e + raise ArgumentError, "Can't resolve image into URL: #{e}" + end + def extract_dimensions(size) size = size.to_s - if size =~ %r{\A\d+x\d+\z} - size.split('x') - elsif size =~ %r{\A\d+\z} + if /\A\d+x\d+\z/.match?(size) + size.split("x") + elsif /\A\d+\z/.match?(size) [size, size] end end @@ -334,6 +471,18 @@ module ActionView raise ArgumentError, "Cannot pass a :size option with a :height or :width option" end end + + def resolve_link_as(extname, mime_type) + if extname == "js" + "script" + elsif extname == "css" + "style" + elsif extname == "vtt" + "track" + elsif (type = mime_type.to_s.split("/")[0]) && type.in?(%w(audio video font)) + type + end + end end end end diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index 717b326740..cc62783d60 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -1,10 +1,12 @@ -require 'zlib' +# frozen_string_literal: true + +require "zlib" module ActionView # = Action View Asset URL Helpers - module Helpers + module Helpers #:nodoc: # This module provides methods for generating asset paths and - # urls. + # URLs. # # image_path("rails.png") # # => "/assets/rails.png" @@ -27,7 +29,7 @@ module ActionView # Helpers take that into account: # # image_tag("rails.png") - # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" /> + # # => <img src="http://assets.example.com/assets/rails.png" /> # stylesheet_link_tag("application") # # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" /> # @@ -36,11 +38,11 @@ module ActionView # some asset downloads to wait for previous assets to finish before they can # begin. You can use the <tt>%d</tt> wildcard in the +asset_host+ to # distribute the requests over four hosts. For example, - # <tt>assets%d.example.com<tt> will spread the asset requests over + # <tt>assets%d.example.com</tt> will spread the asset requests over # "assets0.example.com", ..., "assets3.example.com". # # image_tag("rails.png") - # # => <img alt="Rails" src="http://assets0.example.com/assets/rails.png" /> + # # => <img src="http://assets0.example.com/assets/rails.png" /> # stylesheet_link_tag("application") # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" /> # @@ -55,8 +57,8 @@ module ActionView # You can read more about setting up your DNS CNAME records from your ISP. # # Note: This is purely a browser performance optimization and is not meant - # for server load balancing. See http://www.die.net/musings/page_load_time/ - # for background and http://www.browserscope.org/?category=network for + # for server load balancing. See https://www.die.net/musings/page_load_time/ + # for background and https://www.browserscope.org/?category=network for # connection limit data. # # Alternatively, you can exert more control over the asset host by setting @@ -66,7 +68,7 @@ module ActionView # "http://assets#{Digest::MD5.hexdigest(source).to_i(16) % 2 + 1}.example.com" # } # image_tag("rails.png") - # # => <img alt="Rails" src="http://assets1.example.com/assets/rails.png" /> + # # => <img src="http://assets1.example.com/assets/rails.png" /> # stylesheet_link_tag("application") # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" /> # @@ -85,7 +87,7 @@ module ActionView # end # } # image_tag("rails.png") - # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" /> + # # => <img src="http://assets.example.com/assets/rails.png" /> # stylesheet_link_tag("application") # # => <link href="http://stylesheets.example.com/assets/application.css" media="screen" rel="stylesheet" /> # @@ -95,9 +97,10 @@ module ActionView # still sending assets for plain HTTP requests from asset hosts. If you don't # have SSL certificates for each of the asset hosts this technique allows you # to avoid warnings in the client about mixed media. - # Note that the request parameter might not be supplied, e.g. when the assets - # are precompiled via a Rake task. Make sure to use a Proc instead of a lambda, - # since a Proc allows missing parameters and sets them to nil. + # Note that the +request+ parameter might not be supplied, e.g. when the assets + # are precompiled with the command `rails assets:precompile`. Make sure to use a + # +Proc+ instead of a lambda, since a +Proc+ allows missing parameters and sets them + # to +nil+. # # config.action_controller.asset_host = Proc.new { |source, request| # if request && request.ssl? @@ -117,31 +120,86 @@ module ActionView module AssetUrlHelper URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}i - # Computes the path to asset in public directory. If :type - # options is set, a file extension will be appended and scoped - # to the corresponding public directory. + # This is the entry point for all assets. + # When using the asset pipeline (i.e. sprockets and sprockets-rails), the + # behavior is "enhanced". You can bypass the asset pipeline by passing in + # <tt>skip_pipeline: true</tt> to the options. # # All other asset *_path helpers delegate through this method. # - # asset_path "application.js" # => /assets/application.js - # asset_path "application", type: :javascript # => /assets/application.js - # asset_path "application", type: :stylesheet # => /assets/application.css - # asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js + # === With the asset pipeline + # + # All options passed to +asset_path+ will be passed to +compute_asset_path+ + # which is implemented by sprockets-rails. + # + # asset_path("application.js") # => "/assets/application-60aa4fdc5cea14baf5400fba1abf4f2a46a5166bad4772b1effe341570f07de9.js" + # + # === Without the asset pipeline (<tt>skip_pipeline: true</tt>) + # + # Accepts a <tt>type</tt> option that can specify the asset's extension. No error + # checking is done to verify the source passed into +asset_path+ is valid + # and that the file exists on disk. + # + # asset_path("application.js", skip_pipeline: true) # => "application.js" + # asset_path("filedoesnotexist.png", skip_pipeline: true) # => "filedoesnotexist.png" + # asset_path("application", type: :javascript, skip_pipeline: true) # => "/javascripts/application.js" + # asset_path("application", type: :stylesheet, skip_pipeline: true) # => "/stylesheets/application.css" + # + # === Options applying to all assets + # + # Below lists scenarios that apply to +asset_path+ whether or not you're + # using the asset pipeline. + # + # - All fully qualified URLs are returned immediately. This bypasses the + # asset pipeline and all other behavior described. + # + # asset_path("http://www.example.com/js/xmlhr.js") # => "http://www.example.com/js/xmlhr.js" + # + # - All assets that begin with a forward slash are assumed to be full + # URLs and will not be expanded. This will bypass the asset pipeline. + # + # asset_path("/foo.png") # => "/foo.png" + # + # - All blank strings will be returned immediately. This bypasses the + # asset pipeline and all other behavior described. + # + # asset_path("") # => "" + # + # - If <tt>config.relative_url_root</tt> is specified, all assets will have that + # root prepended. + # + # Rails.application.config.relative_url_root = "bar" + # asset_path("foo.js", skip_pipeline: true) # => "bar/foo.js" + # + # - A different asset host can be specified via <tt>config.action_controller.asset_host</tt> + # this is commonly used in conjunction with a CDN. + # + # Rails.application.config.action_controller.asset_host = "assets.example.com" + # asset_path("foo.js", skip_pipeline: true) # => "http://assets.example.com/foo.js" + # + # - An extension name can be specified manually with <tt>extname</tt>. + # + # asset_path("foo", skip_pipeline: true, extname: ".js") # => "/foo.js" + # asset_path("foo.css", skip_pipeline: true, extname: ".js") # => "/foo.css.js" def asset_path(source, options = {}) raise ArgumentError, "nil is not a valid asset source" if source.nil? source = source.to_s - return "" unless source.present? - return source if source =~ URI_REGEXP + return "" if source.blank? + return source if URI_REGEXP.match?(source) - tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, ''.freeze) + tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, "") if extname = compute_asset_extname(source, options) source = "#{source}#{extname}" end if source[0] != ?/ - source = compute_asset_path(source, options) + if options[:skip_pipeline] + source = public_compute_asset_path(source, options) + else + source = compute_asset_path(source, options) + end end relative_url_root = defined?(config.relative_url_root) && config.relative_url_root @@ -168,31 +226,35 @@ module ActionView # asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js # def asset_url(source, options = {}) - path_to_asset(source, options.merge(:protocol => :request)) + path_to_asset(source, options.merge(protocol: :request)) end alias_method :url_to_asset, :asset_url # aliased to avoid conflicts with an asset_url named route ASSET_EXTENSIONS = { - javascript: '.js', - stylesheet: '.css' + javascript: ".js", + stylesheet: ".css" } - # Compute extname to append to asset path. Returns nil if + # Compute extname to append to asset path. Returns +nil+ if # nothing should be added. def compute_asset_extname(source, options = {}) return if options[:extname] == false extname = options[:extname] || ASSET_EXTENSIONS[options[:type]] - extname if extname && File.extname(source) != extname + if extname && File.extname(source) != extname + extname + else + nil + end end # Maps asset types to public directory. ASSET_PUBLIC_DIRECTORIES = { - audio: '/audios', - font: '/fonts', - image: '/images', - javascript: '/javascripts', - stylesheet: '/stylesheets', - video: '/videos' + audio: "/audios", + font: "/fonts", + image: "/images", + javascript: "/javascripts", + stylesheet: "/stylesheets", + video: "/videos" } # Computes asset path to public directory. Plugins and @@ -202,6 +264,7 @@ module ActionView dir = ASSET_PUBLIC_DIRECTORIES[options[:type]] || "" File.join(dir, source) end + alias :public_compute_asset_path :compute_asset_path # Pick an asset host for this source. Returns +nil+ if no host is set, # the host if no wildcard is set, the host interpolated with the @@ -213,19 +276,21 @@ module ActionView host = options[:host] host ||= config.asset_host if defined? config.asset_host - if host.respond_to?(:call) - arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity - args = [source] - args << request if request && (arity > 1 || arity < 0) - host = host.call(*args) - elsif host =~ /%d/ - host = host % (Zlib.crc32(source) % 4) + if host + if host.respond_to?(:call) + arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity + args = [source] + args << request if request && (arity > 1 || arity < 0) + host = host.call(*args) + elsif host.include?("%d") + host = host % (Zlib.crc32(source) % 4) + end end host ||= request.base_url if request && options[:protocol] == :request return unless host - if host =~ URI_REGEXP + if URI_REGEXP.match?(host) host else protocol = options[:protocol] || config.default_asset_host_protocol || (request ? :request : :relative) @@ -251,7 +316,7 @@ module ActionView # javascript_path "http://www.example.com/js/xmlhr" # => http://www.example.com/js/xmlhr # javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js def javascript_path(source, options = {}) - path_to_asset(source, {type: :javascript}.merge!(options)) + path_to_asset(source, { type: :javascript }.merge!(options)) end alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route @@ -260,10 +325,10 @@ module ActionView # Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/dir/xmlhr.js + # javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/js/xmlhr.js # def javascript_url(source, options = {}) - url_to_asset(source, {type: :javascript}.merge!(options)) + url_to_asset(source, { type: :javascript }.merge!(options)) end alias_method :url_to_javascript, :javascript_url # aliased to avoid conflicts with a javascript_url named route @@ -278,7 +343,7 @@ module ActionView # stylesheet_path "http://www.example.com/css/style" # => http://www.example.com/css/style # stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css def stylesheet_path(source, options = {}) - path_to_asset(source, {type: :stylesheet}.merge!(options)) + path_to_asset(source, { type: :stylesheet }.merge!(options)) end alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route @@ -287,10 +352,10 @@ module ActionView # Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/css/style.css + # stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/assets/css/style.css # def stylesheet_url(source, options = {}) - url_to_asset(source, {type: :stylesheet}.merge!(options)) + url_to_asset(source, { type: :stylesheet }.merge!(options)) end alias_method :url_to_stylesheet, :stylesheet_url # aliased to avoid conflicts with a stylesheet_url named route @@ -308,7 +373,7 @@ module ActionView # The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and # plugin authors are encouraged to do so. def image_path(source, options = {}) - path_to_asset(source, {type: :image}.merge!(options)) + path_to_asset(source, { type: :image }.merge!(options)) end alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route @@ -317,10 +382,10 @@ module ActionView # Since +image_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/edit.png + # image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/assets/edit.png # def image_url(source, options = {}) - url_to_asset(source, {type: :image}.merge!(options)) + url_to_asset(source, { type: :image }.merge!(options)) end alias_method :url_to_image, :image_url # aliased to avoid conflicts with an image_url named route @@ -334,7 +399,7 @@ module ActionView # video_path("/trailers/hd.avi") # => /trailers/hd.avi # video_path("http://www.example.com/vid/hd.avi") # => http://www.example.com/vid/hd.avi def video_path(source, options = {}) - path_to_asset(source, {type: :video}.merge!(options)) + path_to_asset(source, { type: :video }.merge!(options)) end alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route @@ -343,12 +408,12 @@ module ActionView # Since +video_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/hd.avi + # video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/videos/hd.avi # def video_url(source, options = {}) - url_to_asset(source, {type: :video}.merge!(options)) + url_to_asset(source, { type: :video }.merge!(options)) end - alias_method :url_to_video, :video_url # aliased to avoid conflicts with an video_url named route + alias_method :url_to_video, :video_url # aliased to avoid conflicts with a video_url named route # Computes the path to an audio asset in the public audios directory. # Full paths from the document root will be passed through. @@ -360,7 +425,7 @@ module ActionView # audio_path("/sounds/horse.wav") # => /sounds/horse.wav # audio_path("http://www.example.com/sounds/horse.wav") # => http://www.example.com/sounds/horse.wav def audio_path(source, options = {}) - path_to_asset(source, {type: :audio}.merge!(options)) + path_to_asset(source, { type: :audio }.merge!(options)) end alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route @@ -369,10 +434,10 @@ module ActionView # Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/horse.wav + # audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/audios/horse.wav # def audio_url(source, options = {}) - url_to_asset(source, {type: :audio}.merge!(options)) + url_to_asset(source, { type: :audio }.merge!(options)) end alias_method :url_to_audio, :audio_url # aliased to avoid conflicts with an audio_url named route @@ -385,21 +450,21 @@ module ActionView # font_path("/dir/font.ttf") # => /dir/font.ttf # font_path("http://www.example.com/dir/font.ttf") # => http://www.example.com/dir/font.ttf def font_path(source, options = {}) - path_to_asset(source, {type: :font}.merge!(options)) + path_to_asset(source, { type: :font }.merge!(options)) end - alias_method :path_to_font, :font_path # aliased to avoid conflicts with an font_path named route + alias_method :path_to_font, :font_path # aliased to avoid conflicts with a font_path named route # Computes the full URL to a font asset. # This will use +font_path+ internally, so most of their behaviors will be the same. # Since +font_url+ is based on +asset_url+ method you can set :host options. If :host # options is set, it overwrites global +config.action_controller.asset_host+ setting. # - # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/font.ttf + # font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/fonts/font.ttf # def font_url(source, options = {}) - url_to_asset(source, {type: :font}.merge!(options)) + url_to_asset(source, { type: :font }.merge!(options)) end - alias_method :url_to_font, :font_url # aliased to avoid conflicts with an font_url named route + alias_method :url_to_font, :font_url # aliased to avoid conflicts with a font_url named route end end end diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb index c875f5870f..e6b9878271 100644 --- a/actionview/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -1,8 +1,10 @@ -require 'set' +# frozen_string_literal: true + +require "set" module ActionView # = Action View Atom Feed Helpers - module Helpers + module Helpers #:nodoc: module AtomFeedHelper # Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERB or any other # template languages). @@ -103,7 +105,7 @@ module ActionView xml = options.delete(:xml) || eval("xml", block.binding) xml.instruct! if options[:instruct] - options[:instruct].each do |target,attrs| + options[:instruct].each do |target, attrs| if attrs.respond_to?(:keys) xml.instruct!(target, attrs) elsif attrs.respond_to?(:each) @@ -112,13 +114,13 @@ module ActionView end end - feed_opts = {"xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom'} - feed_opts.merge!(options).reject!{|k,v| !k.to_s.match(/^xml/)} + feed_opts = { "xml:lang" => options[:language] || "en-US", "xmlns" => "http://www.w3.org/2005/Atom" } + feed_opts.merge!(options).reject! { |k, v| !k.to_s.match(/^xml/) } xml.feed(feed_opts) do xml.id(options[:id] || "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}") - xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:root_url] || (request.protocol + request.host_with_port)) - xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:url] || request.url) + xml.link(rel: "alternate", type: "text/html", href: options[:root_url] || (request.protocol + request.host_with_port)) + xml.link(rel: "self", type: "application/atom+xml", href: options[:url] || request.url) yield AtomFeedBuilder.new(xml, self, options) end @@ -138,7 +140,7 @@ module ActionView def method_missing(method, *arguments, &block) if xhtml_block?(method, arguments) @xml.__send__(method, *arguments) do - @xml.div(:xmlns => 'http://www.w3.org/1999/xhtml') do |xhtml| + @xml.div(xmlns: "http://www.w3.org/1999/xhtml") do |xhtml| block.call(xhtml) end end @@ -153,7 +155,7 @@ module ActionView def xhtml_block?(method, arguments) if XHTML_TAG_NAMES.include?(method.to_s) last = arguments.last - last.is_a?(Hash) && last[:type].to_s == 'xhtml' + last.is_a?(Hash) && last[:type].to_s == "xhtml" end end end @@ -163,7 +165,7 @@ module ActionView @xml, @view, @feed_options = xml, view, feed_options end - # Accepts a Date or Time object and inserts it in the proper format. If nil is passed, current time in UTC is used. + # Accepts a Date or Time object and inserts it in the proper format. If +nil+ is passed, current time in UTC is used. def updated(date_or_time = nil) @xml.updated((date_or_time || Time.now.utc).xmlschema) end @@ -174,7 +176,7 @@ module ActionView # # * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists. # * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists. - # * <tt>:url</tt>: The URL for this entry or false or nil for not having a link tag. Defaults to the polymorphic_url for the record. + # * <tt>:url</tt>: The URL for this entry or +false+ or +nil+ for not having a link tag. Defaults to the +polymorphic_url+ for the record. # * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}" # * <tt>:type</tt>: The TYPE for this entry. Defaults to "text/html". def entry(record, options = {}) @@ -189,16 +191,15 @@ module ActionView @xml.updated((options[:updated] || record.updated_at).xmlschema) end - type = options.fetch(:type, 'text/html') + type = options.fetch(:type, "text/html") url = options.fetch(:url) { @view.polymorphic_url(record) } - @xml.link(:rel => 'alternate', :type => type, :href => url) if url + @xml.link(rel: "alternate", type: type, href: url) if url yield AtomBuilder.new(@xml) end end end - end end end diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 4c7c4b91c6..020aebeea3 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActionView # = Action View Cache Helper - module Helpers + module Helpers #:nodoc: module CacheHelper # This helper exposes a method for caching fragments of a view # rather than an entire action or page. This technique is useful @@ -8,10 +10,9 @@ module ActionView # fragments, and so on. This method takes a block that contains # the content you wish to cache. # - # The best way to use this is by doing key-based cache expiration - # on top of a cache store like Memcached that'll automatically - # kick out old entries. For more on key-based expiration, see: - # http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works + # The best way to use this is by doing recyclable key-based cache expiration + # on top of a cache store like Memcached or Redis that'll automatically + # kick out old entries. # # When using this method, you list the cache dependency as the name of the cache, like so: # @@ -23,10 +24,14 @@ module ActionView # This approach will assume that when a new topic is added, you'll touch # the project. The cache key generated from this call will be something like: # - # views/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9 - # ^class ^id ^updated_at ^template tree digest + # views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123 + # ^template path ^template tree digest ^class ^id # - # The cache is thus automatically bumped whenever the project updated_at is touched. + # This cache key is stable, but it's combined with a cache version derived from the project + # record. When the project updated_at is touched, the #cache_version changes, even + # if the key stays stable. This means that unlike a traditional key-based cache expiration + # approach, you won't be generating cache trash, unused keys, simply because the dependent + # record is updated. # # If your template cache depends on multiple sources (try to avoid this to keep things simple), # you can name all these dependencies as part of an array: @@ -41,11 +46,11 @@ module ActionView # # ==== \Template digest # - # The template digest that's added to the cache key is computed by taking an md5 of the + # The template digest that's added to the cache key is computed by taking an MD5 of the # contents of the entire template file. This ensures that your caches will automatically # expire when you change the template file. # - # Note that the md5 is taken of the entire template file, not just what's within the + # Note that the MD5 is taken of the entire template file, not just what's within the # cache do/end call. So it's possible that changing something outside of that call will # still expire the cache. # @@ -69,11 +74,11 @@ module ActionView # render 'comments/comments' # render('comments/comments') # - # render "header" => render("comments/header") + # render "header" translates to render("comments/header") # - # render(@topic) => render("topics/topic") - # render(topics) => render("topics/topic") - # render(message.topics) => render("topics/topic") + # render(@topic) translates to render("topics/topic") + # render(topics) translates to render("topics/topic") + # render(message.topics) translates to render("topics/topic") # # It's not possible to derive all render calls like that, though. # Here are a few examples of things that can't be derived: @@ -88,7 +93,7 @@ module ActionView # # === Explicit dependencies # - # Some times you'll have template dependencies that can't be derived at all. This is typically + # Sometimes you'll have template dependencies that can't be derived at all. This is typically # the case when you have template rendering that happens in helpers. Here's an example: # # <%= render_sortable_todolists @project.todolists %> @@ -106,9 +111,9 @@ module ActionView # <%= render_categorizable_events @person.events %> # # This marks every template in the directory as a dependency. To find those - # templates, the wildcard path must be absolutely defined from app/views or paths + # templates, the wildcard path must be absolutely defined from <tt>app/views</tt> or paths # otherwise added with +prepend_view_path+ or +append_view_path+. - # This way the wildcard for `app/views/recordings/events` would be `recordings/events/*` etc. + # This way the wildcard for <tt>app/views/recordings/events</tt> would be <tt>recordings/events/*</tt> etc. # # The pattern used to match explicit dependencies is <tt>/# Template Dependency: (\S+)/</tt>, # so it's important that you type it out just so. @@ -118,7 +123,7 @@ module ActionView # # If you use a helper method, for example, inside a cached block and # you then update that helper, you'll have to bump the cache as well. - # It doesn't really matter how you do it, but the md5 of the template file + # It doesn't really matter how you do it, but the MD5 of the template file # must change. One recommendation is to simply be explicit in a comment, like: # # <%# Helper Dependency Updated: May 6, 2012 at 6pm %> @@ -128,13 +133,14 @@ module ActionView # # === Collection Caching # - # When rendering a collection of objects that each use the same partial, a `cached` + # When rendering a collection of objects that each use the same partial, a <tt>:cached</tt> # option can be passed. + # # For collections rendered such: # - # <%= render partial: 'notifications/notification', collection: @notifications, cached: true %> + # <%= render partial: 'projects/project', collection: @projects, cached: true %> # - # The `cached: true` will make Action View's rendering read several templates + # The <tt>cached: true</tt> will make Action View's rendering read several templates # from cache at once instead of one call per template. # # Templates in the collection not already cached are written to cache. @@ -142,13 +148,21 @@ module ActionView # Works great alongside individual template fragment caching. # For instance if the template the collection renders is cached like: # - # # notifications/_notification.html.erb - # <% cache notification do %> + # # projects/_project.html.erb + # <% cache project do %> # <%# ... %> # <% end %> # # Any collection renders will find those cached templates when attempting # to read multiple templates at once. + # + # If your collection cache depends on multiple sources (try to avoid this to keep things simple), + # you can name all these dependencies as part of a block that returns an array: + # + # <%= render partial: 'projects/project', collection: @projects, cached: -> project { [ project, current_user ] } %> + # + # This will include both records as part of the cache key and updating either of them will + # expire the cache. def cache(name = {}, options = {}, &block) if controller.respond_to?(:perform_caching) && controller.perform_caching name_options = options.slice(:skip_digest, :virtual_path) @@ -187,46 +201,62 @@ module ActionView end # This helper returns the name of a cache key for a given fragment cache - # call. By supplying +skip_digest:+ true to cache, the digestion of cache + # call. By supplying <tt>skip_digest: true</tt> to cache, the digestion of cache # fragments can be manually bypassed. This is useful when cache fragments # cannot be manually expired unless you know the exact key which is the # case when using memcached. # # The digest will be generated using +virtual_path:+ if it is provided. # - def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil) + def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil, digest_path: nil) if skip_digest name else - fragment_name_with_digest(name, virtual_path) + fragment_name_with_digest(name, virtual_path, digest_path) + end + end + + 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? + "#{template.virtual_path}:#{digest}" + else + template.virtual_path end end private - def fragment_name_with_digest(name, virtual_path) #:nodoc: + def fragment_name_with_digest(name, virtual_path, digest_path) virtual_path ||= @virtual_path - if virtual_path - name = controller.url_for(name).split("://").last if name.is_a?(Hash) - digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies - [ name, digest ] + + if virtual_path || digest_path + name = controller.url_for(name).split("://").last if name.is_a?(Hash) + + digest_path ||= digest_path_from_template(@current_template) + + [ digest_path, name ] else name end end - # TODO: Create an object that has caching read/write on it - def fragment_for(name = {}, options = nil, &block) #:nodoc: - read_fragment_for(name, options) || write_fragment_for(name, options, &block) + def fragment_for(name = {}, options = nil, &block) + if content = read_fragment_for(name, options) + @view_renderer.cache_hits[@virtual_path] = :hit if defined?(@view_renderer) + content + else + @view_renderer.cache_hits[@virtual_path] = :miss if defined?(@view_renderer) + write_fragment_for(name, options, &block) + end end - def read_fragment_for(name, options) #:nodoc: + def read_fragment_for(name, options) controller.read_fragment(name, options) end - def write_fragment_for(name, options) #:nodoc: - # VIEW TODO: Make #capture usable outside of ERB - # This dance is needed because Builder can't use capture + def write_fragment_for(name, options) pos = output_buffer.length yield output_safe = output_buffer.html_safe? diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb index df8d0affd0..c87c212cc7 100644 --- a/actionview/lib/action_view/helpers/capture_helper.rb +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -1,13 +1,15 @@ -require 'active_support/core_ext/string/output_safety' +# frozen_string_literal: true + +require "active_support/core_ext/string/output_safety" module ActionView # = Action View Capture Helper - module Helpers + module Helpers #:nodoc: # CaptureHelper exposes methods to let you extract generated markup which # can be used in other parts of a template or layout file. # # It provides a method to capture blocks into variables through capture and - # a way to capture a block of markup for use in a layout through content_for. + # a way to capture a block of markup for use in a layout through {content_for}[rdoc-ref:ActionView::Helpers::CaptureHelper#content_for]. module CaptureHelper # The capture method extracts part of a template as a String object. # You can then use this object anywhere in your templates, layout, or helpers. @@ -34,15 +36,19 @@ module ActionView # </body> # </html> # + # The return of capture is the string generated by the block. For Example: + # + # @greeting # => "Welcome to my shiny new web page! The date and time is 2018-09-06 11:09:16 -0500" + # def capture(*args) value = nil buffer = with_output_buffer { value = yield(*args) } - if string = buffer.presence || value and string.is_a?(String) + if (string = buffer.presence || value) && string.is_a?(String) ERB::Util.html_escape string end end - # Calling content_for stores a block of markup in an identifier for later use. + # Calling <tt>content_for</tt> stores a block of markup in an identifier for later use. # In order to access this stored content in other templates, helper modules # or the layout, you would pass the identifier as an argument to <tt>content_for</tt>. # @@ -108,7 +114,7 @@ module ActionView # That will place +script+ tags for your default set of JavaScript files on the page; # this technique is useful if you'll only be using these scripts in a few views. # - # Note that content_for concatenates (default) the blocks it is given for a particular + # Note that <tt>content_for</tt> concatenates (default) the blocks it is given for a particular # identifier in order. For example: # # <% content_for :navigation do %> @@ -125,7 +131,7 @@ module ActionView # # <ul><%= content_for :navigation %></ul> # - # If the flush parameter is true content_for replaces the blocks it is given. For example: + # If the flush parameter is +true+ <tt>content_for</tt> replaces the blocks it is given. For example: # # <% content_for :navigation do %> # <li><%= link_to 'Home', action: 'index' %></li> @@ -145,7 +151,7 @@ module ActionView # # <% content_for :script, javascript_include_tag(:defaults) %> # - # WARNING: content_for is ignored in caches. So you shouldn't use it for elements that will be fragment cached. + # WARNING: <tt>content_for</tt> is ignored in caches. So you shouldn't use it for elements that will be fragment cached. def content_for(name, content = nil, options = {}, &block) if content || block_given? if block_given? @@ -172,7 +178,7 @@ module ActionView result unless content end - # content_for? checks whether any content has been captured yet using `content_for`. + # <tt>content_for?</tt> checks whether any content has been captured yet using <tt>content_for</tt>. # Useful to render parts of your layout differently based on what is in your views. # # <%# This is the layout %> diff --git a/actionview/lib/action_view/helpers/controller_helper.rb b/actionview/lib/action_view/helpers/controller_helper.rb index 3569fba8c6..79cf86c7d1 100644 --- a/actionview/lib/action_view/helpers/controller_helper.rb +++ b/actionview/lib/action_view/helpers/controller_helper.rb @@ -1,14 +1,19 @@ -require 'active_support/core_ext/module/attr_internal' +# frozen_string_literal: true + +require "active_support/core_ext/module/attr_internal" module ActionView - module Helpers + module Helpers #:nodoc: # This module keeps all methods and behavior in ActionView # that simply delegates to the controller. module ControllerHelper #:nodoc: attr_internal :controller, :request - delegate :request_forgery_protection_token, :params, :session, :cookies, :response, :headers, - :flash, :action_name, :controller_name, :controller_path, :to => :controller + CONTROLLER_DELEGATES = [:request_forgery_protection_token, :params, + :session, :cookies, :response, :headers, :flash, :action_name, + :controller_name, :controller_path] + + delegate(*CONTROLLER_DELEGATES, to: :controller) def assign_controller(controller) if @_controller = controller @@ -21,6 +26,11 @@ module ActionView def logger controller.logger if controller.respond_to?(:logger) end + + def respond_to?(method_name, include_private = false) + return controller.respond_to?(method_name) if CONTROLLER_DELEGATES.include?(method_name.to_sym) + super + end end end end diff --git a/actionview/lib/action_view/helpers/csp_helper.rb b/actionview/lib/action_view/helpers/csp_helper.rb new file mode 100644 index 0000000000..4415018845 --- /dev/null +++ b/actionview/lib/action_view/helpers/csp_helper.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ActionView + # = Action View CSP Helper + module Helpers #:nodoc: + module CspHelper + # Returns a meta tag "csp-nonce" with the per-session nonce value + # for allowing inline <script> tags. + # + # <head> + # <%= csp_meta_tag %> + # </head> + # + # This is used by the Rails UJS helper to create dynamically + # loaded inline <script> elements. + # + def csp_meta_tag(**options) + if content_security_policy? + options[:name] = "csp-nonce" + options[:content] = content_security_policy_nonce + tag("meta", options) + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/csrf_helper.rb b/actionview/lib/action_view/helpers/csrf_helper.rb index 5af92c4ff2..c0422c6ff5 100644 --- a/actionview/lib/action_view/helpers/csrf_helper.rb +++ b/actionview/lib/action_view/helpers/csrf_helper.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActionView # = Action View CSRF Helper - module Helpers + module Helpers #:nodoc: module CsrfHelper # Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site # request forgery protection parameter and token, respectively. @@ -14,14 +16,14 @@ module ActionView # # You don't need to use these tags for regular forms as they generate their own hidden fields. # - # For AJAX requests other than GETs, extract the "csrf-token" from the meta-tag and send as the - # "X-CSRF-Token" HTTP header. If you are using jQuery with jquery-rails this happens automatically. + # For AJAX requests other than GETs, extract the "csrf-token" from the meta-tag and send as the + # "X-CSRF-Token" HTTP header. If you are using rails-ujs this happens automatically. # def csrf_meta_tags - if protect_against_forgery? + if defined?(protect_against_forgery?) && protect_against_forgery? [ - tag('meta', :name => 'csrf-param', :content => request_forgery_protection_token), - tag('meta', :name => 'csrf-token', :content => form_authenticity_token) + tag("meta", name: "csrf-param", content: request_forgery_protection_token), + tag("meta", name: "csrf-token", content: form_authenticity_token) ].join("\n").html_safe end end diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 233e613e97..9d5e5eaba3 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -1,12 +1,15 @@ -require 'date' -require 'action_view/helpers/tag_helper' -require 'active_support/core_ext/array/extract_options' -require 'active_support/core_ext/date/conversions' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/object/with_options' +# frozen_string_literal: true + +require "date" +require "action_view/helpers/tag_helper" +require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/date/conversions" +require "active_support/core_ext/hash/slice" +require "active_support/core_ext/object/acts_like" +require "active_support/core_ext/object/with_options" module ActionView - module Helpers + module Helpers #:nodoc: # = Action View Date Helpers # # The Date Helper primarily creates select/option tags for different kinds of dates and times or date and time @@ -94,66 +97,62 @@ module ActionView scope: :'datetime.distance_in_words' }.merge!(options) - from_time = from_time.to_time if from_time.respond_to?(:to_time) - to_time = to_time.to_time if to_time.respond_to?(:to_time) + from_time = normalize_distance_of_time_argument_to_time(from_time) + to_time = normalize_distance_of_time_argument_to_time(to_time) from_time, to_time = to_time, from_time if from_time > to_time - distance_in_minutes = ((to_time - from_time)/60.0).round + distance_in_minutes = ((to_time - from_time) / 60.0).round distance_in_seconds = (to_time - from_time).round - I18n.with_options :locale => options[:locale], :scope => options[:scope] do |locale| + I18n.with_options locale: options[:locale], scope: options[:scope] do |locale| case distance_in_minutes - when 0..1 - return distance_in_minutes == 0 ? - locale.t(:less_than_x_minutes, :count => 1) : - locale.t(:x_minutes, :count => distance_in_minutes) unless options[:include_seconds] - - case distance_in_seconds - when 0..4 then locale.t :less_than_x_seconds, :count => 5 - when 5..9 then locale.t :less_than_x_seconds, :count => 10 - when 10..19 then locale.t :less_than_x_seconds, :count => 20 - when 20..39 then locale.t :half_a_minute - when 40..59 then locale.t :less_than_x_minutes, :count => 1 - else locale.t :x_minutes, :count => 1 - end - - when 2...45 then locale.t :x_minutes, :count => distance_in_minutes - when 45...90 then locale.t :about_x_hours, :count => 1 + when 0..1 + return distance_in_minutes == 0 ? + locale.t(:less_than_x_minutes, count: 1) : + locale.t(:x_minutes, count: distance_in_minutes) unless options[:include_seconds] + + case distance_in_seconds + when 0..4 then locale.t :less_than_x_seconds, count: 5 + when 5..9 then locale.t :less_than_x_seconds, count: 10 + when 10..19 then locale.t :less_than_x_seconds, count: 20 + when 20..39 then locale.t :half_a_minute + when 40..59 then locale.t :less_than_x_minutes, count: 1 + else locale.t :x_minutes, count: 1 + end + + when 2...45 then locale.t :x_minutes, count: distance_in_minutes + when 45...90 then locale.t :about_x_hours, count: 1 # 90 mins up to 24 hours - when 90...1440 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round + when 90...1440 then locale.t :about_x_hours, count: (distance_in_minutes.to_f / 60.0).round # 24 hours up to 42 hours - when 1440...2520 then locale.t :x_days, :count => 1 + when 1440...2520 then locale.t :x_days, count: 1 # 42 hours up to 30 days - when 2520...43200 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round + when 2520...43200 then locale.t :x_days, count: (distance_in_minutes.to_f / 1440.0).round # 30 days up to 60 days - when 43200...86400 then locale.t :about_x_months, :count => (distance_in_minutes.to_f / 43200.0).round + when 43200...86400 then locale.t :about_x_months, count: (distance_in_minutes.to_f / 43200.0).round # 60 days up to 365 days - when 86400...525600 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round + when 86400...525600 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round + else + from_year = from_time.year + from_year += 1 if from_time.month >= 3 + to_year = to_time.year + to_year -= 1 if to_time.month < 3 + leap_years = (from_year > to_year) ? 0 : (from_year..to_year).count { |x| Date.leap?(x) } + minute_offset_for_leap_year = leap_years * 1440 + # Discount the leap year days when calculating year distance. + # e.g. if there are 20 leap year days between 2 dates having the same day + # and month then the based on 365 days calculation + # the distance in years will come out to over 80 years when in written + # English it would read better as about 80 years. + minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year + remainder = (minutes_with_offset % MINUTES_IN_YEAR) + distance_in_years = (minutes_with_offset.div MINUTES_IN_YEAR) + if remainder < MINUTES_IN_QUARTER_YEAR + locale.t(:about_x_years, count: distance_in_years) + elsif remainder < MINUTES_IN_THREE_QUARTERS_YEAR + locale.t(:over_x_years, count: distance_in_years) else - if from_time.acts_like?(:time) && to_time.acts_like?(:time) - fyear = from_time.year - fyear += 1 if from_time.month >= 3 - tyear = to_time.year - tyear -= 1 if to_time.month < 3 - leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count{|x| Date.leap?(x)} - minute_offset_for_leap_year = leap_years * 1440 - # Discount the leap year days when calculating year distance. - # e.g. if there are 20 leap year days between 2 dates having the same day - # and month then the based on 365 days calculation - # the distance in years will come out to over 80 years when in written - # English it would read better as about 80 years. - minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year - else - minutes_with_offset = distance_in_minutes - end - remainder = (minutes_with_offset % MINUTES_IN_YEAR) - distance_in_years = (minutes_with_offset.div MINUTES_IN_YEAR) - if remainder < MINUTES_IN_QUARTER_YEAR - locale.t(:about_x_years, :count => distance_in_years) - elsif remainder < MINUTES_IN_THREE_QUARTERS_YEAR - locale.t(:over_x_years, :count => distance_in_years) - else - locale.t(:almost_x_years, :count => distance_in_years + 1) - end + locale.t(:almost_x_years, count: distance_in_years + 1) + end end end end @@ -206,6 +205,7 @@ module ActionView # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Date.today.year + 5</tt> if # you are creating new record. While editing existing record, <tt>:end_year</tt> defaults to # the current selected year plus 5. + # * <tt>:year_format</tt> - Set format of years for year select. Lambda should be passed. # * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the # first of the given month in order to not create invalid dates like 31 February. @@ -219,15 +219,17 @@ module ActionView # the respective locale (e.g. [:year, :month, :day] in the en locale that ships with Rails). # * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty # dates. - # * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil. + # * <tt>:default</tt> - Set a default date if the affected date isn't set or is +nil+. # * <tt>:selected</tt> - Set a date that overrides the actual value. # * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled. # * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings # for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>. # Setting this option prepends a select option with a generic prompt (Day, Month, Year, Hour, Minute, Seconds) # or the given prompt string. - # * <tt>:with_css_classes</tt> - Set to true if you want assign different styles for 'select' tags. This option - # automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second' for your 'select' tags. + # * <tt>:with_css_classes</tt> - Set to true or a hash of strings. Use true if you want to assign generic styles for + # select tags. This automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second'. A hash of + # strings for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt>, <tt>:second</tt> + # will extend the select type with the given value. Use +html_options+ to modify every select tag in the set. # * <tt>:use_hidden</tt> - Set to true if you only want to generate hidden input tags. # # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set. @@ -264,7 +266,7 @@ module ActionView # date_select("article", "written_on", default: 3.days.from_now) # # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute - # # which is set in the form with todays date, regardless of the value in the Active Record object. + # # which is set in the form with today's date, regardless of the value in the Active Record object. # date_select("article", "written_on", selected: Date.today) # # # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute @@ -274,6 +276,9 @@ module ActionView # # Generates a date select with custom prompts. # date_select("article", "written_on", prompt: { day: 'Select day', month: 'Select month', year: 'Select year' }) # + # # Generates a date select with custom year format. + # date_select("article", "written_on", year_format: ->(year) { "Heisei #{year - 1988}" }) + # # The selects are prepared for multi-parameter assignment to an Active Record object. # # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that @@ -300,16 +305,16 @@ module ActionView # # the sunrise attribute. # time_select("article", "start_time", include_seconds: true) # - # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30 and 45. - # time_select 'game', 'game_time', {minute_step: 15} + # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30, and 45. + # time_select 'game', 'game_time', { minute_step: 15 } # # # Creates a time select tag with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # time_select("article", "written_on", prompt: {hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds'}) - # time_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours + # time_select("article", "written_on", prompt: { hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds' }) + # time_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours # time_select("article", "written_on", prompt: true) # generic prompts for all # # # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM. - # time_select 'game', 'game_time', {ampm: true} + # time_select 'game', 'game_time', { ampm: true } # # The selects are prepared for multi-parameter assignment to an Active Record object. # @@ -345,8 +350,8 @@ module ActionView # datetime_select("article", "written_on", discard_type: true) # # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # datetime_select("article", "written_on", prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # datetime_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours + # datetime_select("article", "written_on", prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # datetime_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours # datetime_select("article", "written_on", prompt: true) # generic prompts for all # # The selects are prepared for multi-parameter assignment to an Active Record object. @@ -396,8 +401,8 @@ module ActionView # select_datetime(my_date_time, prefix: 'payday') # # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # select_datetime(my_date_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_datetime(my_date_time, prompt: {hour: true}) # generic prompt for hours + # select_datetime(my_date_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_datetime(my_date_time, prompt: { hour: true }) # generic prompt for hours # select_datetime(my_date_time, prompt: true) # generic prompts for all def select_datetime(datetime = Time.current, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_datetime @@ -435,8 +440,8 @@ module ActionView # select_date(my_date, prefix: 'payday') # # # Generates a date select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # select_date(my_date, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_date(my_date, prompt: {hour: true}) # generic prompt for hours + # select_date(my_date, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_date(my_date, prompt: { hour: true }) # generic prompt for hours # select_date(my_date, prompt: true) # generic prompts for all def select_date(date = Date.current, options = {}, html_options = {}) DateTimeSelector.new(date, options, html_options).select_date @@ -475,8 +480,8 @@ module ActionView # select_time(my_time, start_hour: 2, end_hour: 14) # # # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts. - # select_time(my_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_time(my_time, prompt: {hour: true}) # generic prompt for hours + # select_time(my_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_time(my_time, prompt: { hour: true }) # generic prompt for hours # select_time(my_time, prompt: true) # generic prompts for all def select_time(datetime = Time.current, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_time @@ -667,8 +672,6 @@ module ActionView # <time datetime="2010-11-04T17:55:45+01:00">November 04, 2010 17:55</time> # time_tag Date.yesterday, 'Yesterday' # => # <time datetime="2010-11-03">Yesterday</time> - # time_tag Date.today, pubdate: true # => - # <time datetime="2010-11-04" pubdate="pubdate">November 04, 2010</time> # time_tag Date.today, datetime: Date.today.strftime('%G-W%V') # => # <time datetime="2010-W44">November 04, 2010</time> # @@ -679,19 +682,30 @@ module ActionView def time_tag(date_or_time, *args, &block) options = args.extract_options! format = options.delete(:format) || :long - content = args.first || I18n.l(date_or_time, :format => format) - datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601 + content = args.first || I18n.l(date_or_time, format: format) - content_tag("time".freeze, content, options.reverse_merge(:datetime => datetime), &block) + content_tag("time", content, options.reverse_merge(datetime: date_or_time.iso8601), &block) end + + private + + def normalize_distance_of_time_argument_to_time(value) + if value.is_a?(Numeric) + Time.at(value) + elsif value.respond_to?(:to_time) + value.to_time + else + raise ArgumentError, "#{value.inspect} can't be converted to a Time value" + end + end end class DateTimeSelector #:nodoc: include ActionView::Helpers::TagHelper - DEFAULT_PREFIX = 'date'.freeze + DEFAULT_PREFIX = "date" POSITION = { - :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 + year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6 }.freeze AMPM_TRANSLATION = Hash[ @@ -707,8 +721,8 @@ module ActionView @options = options.dup @html_options = html_options.dup @datetime = datetime - @options[:datetime_separator] ||= ' — ' - @options[:time_separator] ||= ' : ' + @options[:datetime_separator] ||= " — " + @options[:time_separator] ||= " : " end def select_datetime @@ -778,7 +792,7 @@ module ActionView if @options[:use_hidden] || @options[:discard_minute] build_hidden(:minute, min) else - build_options_and_select(:minute, min, :step => @options[:minute_step]) + build_options_and_select(:minute, min, step: @options[:minute_step]) end end @@ -798,7 +812,7 @@ module ActionView if @options[:use_hidden] || @options[:discard_day] build_hidden(:day, day || 1) else - build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false, :use_two_digit_numbers => @options[:use_two_digit_numbers]) + build_options_and_select(:day, day, start: 1, end: 31, leading_zeros: false, use_two_digit_numbers: @options[:use_two_digit_numbers]) end end @@ -808,9 +822,9 @@ module ActionView else month_options = [] 1.upto(12) do |month_number| - options = { :value => month_number } + options = { value: month_number } options[:selected] = "selected" if month == month_number - month_options << content_tag("option".freeze, month_name(month_number), options) + "\n" + month_options << content_tag("option", month_name(month_number), options) + "\n" end build_select(:month, month_options.join) end @@ -818,7 +832,7 @@ module ActionView def select_year if !@datetime || @datetime == 0 - val = '1' + val = "1" middle_year = Date.today.year else val = middle_year = year @@ -838,7 +852,7 @@ module ActionView raise ArgumentError, "There are too many years options to be built. Are you sure you haven't mistyped something? You can provide the :max_years_allowed parameter." end - build_options_and_select(:year, val, options) + build_select(:year, build_year_options(val, options)) end end @@ -858,12 +872,12 @@ module ActionView # valid. Otherwise, February 31st or February 29th, 2011 can be selected, which are invalid. def set_day_if_discarded if @datetime && @options[:discard_day] - @datetime = @datetime.change(:day => 1) + @datetime = @datetime.change(day: 1) end end # Returns translated month names, but also ensures that a custom month - # name array has a leading nil element. + # name array has a leading +nil+ element. def month_names @month_names ||= begin month_names = @options[:use_month_names] || translated_month_names @@ -883,7 +897,7 @@ module ActionView # "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] def translated_month_names key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names' - I18n.translate(key, :locale => @options[:locale]) + I18n.translate(key, locale: @options[:locale]) end # Looks up month names by number (1-based): @@ -911,22 +925,37 @@ module ActionView if @options[:use_month_numbers] number elsif @options[:use_two_digit_numbers] - '%02d' % number + "%02d" % number elsif @options[:add_month_numbers] "#{number} - #{month_names[number]}" elsif format_string = @options[:month_format_string] - format_string % {number: number, name: month_names[number]} + format_string % { number: number, name: month_names[number] } else month_names[number] end end + # Looks up year names by number. + # + # year_name(1998) # => 1998 + # + # If the <tt>:year_format</tt> option is passed: + # + # year_name(1998) # => "Heisei 10" + def year_name(number) + if year_format_lambda = @options[:year_format] + year_format_lambda.call(number) + else + number + end + end + def date_order @date_order ||= @options[:order] || translated_date_order end def translated_date_order - date_order = I18n.translate(:'date.order', :locale => @options[:locale], :default => []) + date_order = I18n.translate(:'date.order', locale: @options[:locale], default: []) date_order = date_order.map(&:to_sym) forbidden_elements = date_order - [:year, :month, :day] @@ -973,11 +1002,39 @@ module ActionView select_options = [] start.step(stop, step) do |i| value = leading_zeros ? sprintf("%02d", i) : i - tag_options = { :value => value } + tag_options = { value: value } tag_options[:selected] = "selected" if selected == i text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value text = options[:ampm] ? AMPM_TRANSLATION[i] : text - select_options << content_tag("option".freeze, text, tag_options) + select_options << content_tag("option", text, tag_options) + end + + (select_options.join("\n") + "\n").html_safe + end + + # Build select option HTML for year. + # If <tt>year_format</tt> option is not passed + # build_year_options(1998, start: 1998, end: 2000) + # => "<option value="1998" selected="selected">1998</option> + # <option value="1999">1999</option> + # <option value="2000">2000</option>" + # + # If <tt>year_format</tt> option is passed + # build_year_options(1998, start: 1998, end: 2000, year_format: ->year { "Heisei #{ year - 1988 }" }) + # => "<option value="1998" selected="selected">Heisei 10</option> + # <option value="1999">Heisei 11</option> + # <option value="2000">Heisei 12</option>" + def build_year_options(selected, options = {}) + start = options.delete(:start) + stop = options.delete(:end) + step = options.delete(:step) + + select_options = [] + start.step(stop, step) do |value| + tag_options = { value: value } + tag_options[:selected] = "selected" if selected == value + text = year_name(value) + select_options << content_tag("option", text, tag_options) end (select_options.join("\n") + "\n").html_safe @@ -990,35 +1047,51 @@ module ActionView # </select>" def build_select(type, select_options_as_html) select_options = { - :id => input_id_from_type(type), - :name => input_name_from_type(type) + id: input_id_from_type(type), + name: input_name_from_type(type) }.merge!(@html_options) - select_options[:disabled] = 'disabled' if @options[:disabled] - select_options[:class] = [select_options[:class], type].compact.join(' ') if @options[:with_css_classes] + select_options[:disabled] = "disabled" if @options[:disabled] + select_options[:class] = css_class_attribute(type, select_options[:class], @options[:with_css_classes]) if @options[:with_css_classes] - select_html = "\n" - select_html << content_tag("option".freeze, '', :value => '') + "\n" if @options[:include_blank] + select_html = +"\n" + select_html << content_tag("option", "", value: "") + "\n" if @options[:include_blank] select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt] select_html << select_options_as_html - (content_tag("select".freeze, select_html.html_safe, select_options) + "\n").html_safe + (content_tag("select", select_html.html_safe, select_options) + "\n").html_safe + end + + # Builds the css class value for the select element + # css_class_attribute(:year, 'date optional', { year: 'my-year' }) + # => "date optional my-year" + def css_class_attribute(type, html_options_class, options) # :nodoc: + css_class = \ + case options + when Hash + options[type.to_sym] + else + type + end + + [html_options_class, css_class].compact.join(" ") end # Builds a prompt option tag with supplied options or from default options. # prompt_option_tag(:month, prompt: 'Select month') # => "<option value="">Select month</option>" def prompt_option_tag(type, options) - prompt = case options + prompt = \ + case options when Hash - default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false} + default_options = { year: false, month: false, day: false, hour: false, minute: false, second: false } default_options.merge!(options)[type.to_sym] when String options else - I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale]) - end + I18n.translate(:"datetime.prompts.#{type}", locale: @options[:locale]) + end - prompt ? content_tag("option".freeze, prompt, :value => '') : '' + prompt ? content_tag("option", prompt, value: "") : "" end # Builds hidden input tag for date part and value. @@ -1026,12 +1099,12 @@ module ActionView # => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />" def build_hidden(type, value) select_options = { - :type => "hidden", - :id => input_id_from_type(type), - :name => input_name_from_type(type), - :value => value + type: "hidden", + id: input_id_from_type(type), + name: input_name_from_type(type), + value: value }.merge!(@html_options.slice(:disabled)) - select_options[:disabled] = 'disabled' if @options[:disabled] + select_options[:disabled] = "disabled" if @options[:disabled] tag(:input, select_options) + "\n".html_safe end @@ -1042,7 +1115,7 @@ module ActionView prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX prefix += "[#{@options[:index]}]" if @options.has_key?(:index) - field_name = @options[:field_name] || type + field_name = @options[:field_name] || type.to_s if @options[:include_position] field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)" end @@ -1053,8 +1126,8 @@ module ActionView # Returns the id attribute for the input tag. # => "post_written_on_1i" def input_id_from_type(type) - id = input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') - id = @options[:namespace] + '_' + id if @options[:namespace] + id = input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, "_").gsub(/[\]\)]/, "") + id = @options[:namespace] + "_" + id if @options[:namespace] id end @@ -1062,7 +1135,7 @@ module ActionView # Given an ordering of datetime components, create the selection HTML # and join them with their appropriate separators. def build_selects_from_types(order) - select = '' + select = +"" first_visible = order.find { |type| !@options[:"discard_#{type}"] } order.reverse_each do |type| separator = separator(type) unless type == first_visible # don't add before first visible field @@ -1076,12 +1149,12 @@ module ActionView return "" if @options[:use_hidden] case type - when :year, :month, :day - @options[:"discard_#{type}"] ? "" : @options[:date_separator] - when :hour - (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator] - when :minute, :second - @options[:"discard_#{type}"] ? "" : @options[:time_separator] + when :year, :month, :day + @options[:"discard_#{type}"] ? "" : @options[:date_separator] + when :hour + (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator] + when :minute, :second + @options[:"discard_#{type}"] ? "" : @options[:time_separator] end end end diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb index e9dccbad1c..88ceba414b 100644 --- a/actionview/lib/action_view/helpers/debug_helper.rb +++ b/actionview/lib/action_view/helpers/debug_helper.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true + module ActionView # = Action View Debug Helper # # Provides a set of methods for making it easier to debug Rails objects. - module Helpers + module Helpers #:nodoc: module DebugHelper - include TagHelper # Returns a YAML representation of +object+ wrapped with <pre> and </pre>. @@ -23,12 +24,12 @@ module ActionView # created_at: # </pre> def debug(object) - Marshal::dump(object) + Marshal.dump(object) object = ERB::Util.html_escape(object.to_yaml) - content_tag(:pre, object, :class => "debug_dump") + content_tag(:pre, object, class: "debug_dump") rescue # errors from Marshal or YAML # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback - content_tag(:code, object.inspect, :class => "debug_dump") + content_tag(:code, object.inspect, class: "debug_dump") end end end diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 7ced37572e..5533cef249 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1,23 +1,25 @@ -require 'cgi' -require 'action_view/helpers/date_helper' -require 'action_view/helpers/tag_helper' -require 'action_view/helpers/form_tag_helper' -require 'action_view/helpers/active_model_helper' -require 'action_view/model_naming' -require 'action_view/record_identifier' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/string/output_safety' -require 'active_support/core_ext/string/inflections' +# frozen_string_literal: true + +require "cgi" +require "action_view/helpers/date_helper" +require "action_view/helpers/tag_helper" +require "action_view/helpers/form_tag_helper" +require "action_view/helpers/active_model_helper" +require "action_view/model_naming" +require "action_view/record_identifier" +require "active_support/core_ext/module/attribute_accessors" +require "active_support/core_ext/hash/slice" +require "active_support/core_ext/string/output_safety" +require "active_support/core_ext/string/inflections" module ActionView # = Action View Form Helpers - module Helpers + module Helpers #:nodoc: # Form helpers are designed to make working with resources much easier # compared to using vanilla HTML. # # Typically, a form designed to create or update a resource reflects the - # identity of the resource in several ways: (i) the url that the form is + # identity of the resource in several ways: (i) the URL that the form is # sent to (the form element's +action+ attribute) should result in a request # being routed to the appropriate controller action (with the appropriate <tt>:id</tt> # parameter in the case of an existing resource), (ii) input fields should @@ -164,7 +166,7 @@ module ActionView # So for example you may use a named route directly. When the model is # represented by a string or symbol, as in the example above, if the # <tt>:url</tt> option is not specified, by default the form will be - # sent back to the current url (We will describe below an alternative + # sent back to the current URL (We will describe below an alternative # resource-oriented usage of +form_for+ in which the URL does not need # to be specified explicitly). # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of @@ -201,9 +203,9 @@ module ActionView # <%= f.submit %> # <% end %> # - # This also works for the methods in FormOptionHelper and DateHelper that + # This also works for the methods in FormOptionsHelper and DateHelper that # are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === #form_for with a model object # @@ -416,13 +418,13 @@ module ActionView # # To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter # - # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| + # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %> # ... # <% end %> # # If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>: # - # <%= form_for @invoice, url: external_url, authenticity_token: false do |f| + # <%= form_for @invoice, url: external_url, authenticity_token: false do |f| %> # ... # <% end %> def form_for(record, options = {}, &block) @@ -467,13 +469,300 @@ module ActionView ) options[:url] ||= if options.key?(:format) - polymorphic_path(record, format: options.delete(:format)) - else - polymorphic_path(record, {}) - end + polymorphic_path(record, format: options.delete(:format)) + else + polymorphic_path(record, {}) + end end private :apply_form_for_options! + mattr_accessor :form_with_generates_remote_forms, default: true + + mattr_accessor :form_with_generates_ids, default: false + + # Creates a form tag based on mixing URLs, scopes, or models. + # + # # Using just a URL: + # <%= form_with url: posts_path do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts" method="post" data-remote="true"> + # <input type="text" name="title"> + # </form> + # + # # Adding a scope prefixes the input field names: + # <%= form_with scope: :post, url: posts_path do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts" method="post" data-remote="true"> + # <input type="text" name="post[title]"> + # </form> + # + # # Using a model infers both the URL and scope: + # <%= form_with model: Post.new do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts" method="post" data-remote="true"> + # <input type="text" name="post[title]"> + # </form> + # + # # An existing model makes an update form and fills out field values: + # <%= form_with model: Post.first do |form| %> + # <%= form.text_field :title %> + # <% end %> + # # => + # <form action="/posts/1" method="post" data-remote="true"> + # <input type="hidden" name="_method" value="patch"> + # <input type="text" name="post[title]" value="<the title of the post>"> + # </form> + # + # # Though the fields don't have to correspond to model attributes: + # <%= form_with model: Cat.new do |form| %> + # <%= form.text_field :cats_dont_have_gills %> + # <%= form.text_field :but_in_forms_they_can %> + # <% end %> + # # => + # <form action="/cats" method="post" data-remote="true"> + # <input type="text" name="cat[cats_dont_have_gills]"> + # <input type="text" name="cat[but_in_forms_they_can]"> + # </form> + # + # The parameters in the forms are accessible in controllers according to + # their name nesting. So inputs named +title+ and <tt>post[title]</tt> are + # accessible as <tt>params[:title]</tt> and <tt>params[:post][:title]</tt> + # respectively. + # + # By default +form_with+ attaches the <tt>data-remote</tt> attribute + # submitting the form via an XMLHTTPRequest in the background if an + # Unobtrusive JavaScript driver, like rails-ujs, is used. See the + # <tt>:local</tt> option for more. + # + # For ease of comparison the examples above left out the submit button, + # as well as the auto generated hidden fields that enable UTF-8 support + # and adds an authenticity token needed for cross site request forgery + # protection. + # + # === Resource-oriented style + # + # In many of the examples just shown, the +:model+ passed to +form_with+ + # is a _resource_. It corresponds to a set of RESTful routes, most likely + # defined via +resources+ in <tt>config/routes.rb</tt>. + # + # So when passing such a model record, Rails infers the URL and method. + # + # <%= form_with model: @post do |form| %> + # ... + # <% end %> + # + # is then equivalent to something like: + # + # <%= form_with scope: :post, url: post_path(@post), method: :patch do |form| %> + # ... + # <% end %> + # + # And for a new record + # + # <%= form_with model: Post.new do |form| %> + # ... + # <% end %> + # + # is equivalent to something like: + # + # <%= form_with scope: :post, url: posts_path do |form| %> + # ... + # <% end %> + # + # ==== +form_with+ options + # + # * <tt>:url</tt> - The URL the form submits to. Akin to values passed to + # +url_for+ or +link_to+. For example, you may use a named route + # directly. When a <tt>:scope</tt> is passed without a <tt>:url</tt> the + # form just submits to the current URL. + # * <tt>:method</tt> - The method to use when submitting the form, usually + # either "get" or "post". If "patch", "put", "delete", or another verb + # is used, a hidden input named <tt>_method</tt> is added to + # simulate the verb over post. + # * <tt>:format</tt> - The format of the route the form submits to. + # Useful when submitting to another resource type, like <tt>:json</tt>. + # Skipped if a <tt>:url</tt> is passed. + # * <tt>:scope</tt> - The scope to prefix input field names with and + # thereby how the submitted parameters are grouped in controllers. + # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of + # id attributes on form elements. The namespace attribute will be prefixed + # with underscore on the generated HTML id. + # * <tt>:model</tt> - A model object to infer the <tt>:url</tt> and + # <tt>:scope</tt> by, plus fill out input field values. + # So if a +title+ attribute is set to "Ahoy!" then a +title+ input + # field's value would be "Ahoy!". + # If the model is a new record a create form is generated, if an + # existing record, however, an update form is generated. + # Pass <tt>:scope</tt> or <tt>:url</tt> to override the defaults. + # E.g. turn <tt>params[:post]</tt> into <tt>params[:article]</tt>. + # * <tt>:authenticity_token</tt> - Authenticity token to use in the form. + # Override with a custom authenticity token or pass <tt>false</tt> to + # skip the authenticity token field altogether. + # Useful when submitting to an external resource like a payment gateway + # that might limit the valid fields. + # Remote forms may omit the embedded authenticity token by setting + # <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>. + # This is helpful when fragment-caching the form. Remote forms + # get the authenticity token from the <tt>meta</tt> tag, so embedding is + # unnecessary unless you support browsers without JavaScript. + # * <tt>:local</tt> - By default form submits are remote and unobtrusive XHRs. + # Disable remote submits with <tt>local: true</tt>. + # * <tt>:skip_enforcing_utf8</tt> - If set to true, a hidden input with name + # utf8 is not output. + # * <tt>:builder</tt> - Override the object used to build the form. + # * <tt>:id</tt> - Optional HTML id attribute. + # * <tt>:class</tt> - Optional HTML class attribute. + # * <tt>:data</tt> - Optional HTML data attributes. + # * <tt>:html</tt> - Other optional HTML attributes for the form tag. + # + # === Examples + # + # When not passing a block, +form_with+ just generates an opening form tag. + # + # <%= form_with(model: @post, url: super_posts_path) %> + # <%= form_with(model: @post, scope: :article) %> + # <%= form_with(model: @post, format: :json) %> + # <%= form_with(model: @post, authenticity_token: false) %> # Disables the token. + # + # For namespaced routes, like +admin_post_url+: + # + # <%= form_with(model: [ :admin, @post ]) do |form| %> + # ... + # <% end %> + # + # If your resource has associations defined, for example, you want to add comments + # to the document given that the routes are set correctly: + # + # <%= form_with(model: [ @document, Comment.new ]) do |form| %> + # ... + # <% end %> + # + # Where <tt>@document = Document.find(params[:id])</tt>. + # + # === Mixing with other form helpers + # + # While +form_with+ uses a FormBuilder object it's possible to mix and + # match the stand-alone FormHelper methods and methods + # from FormTagHelper: + # + # <%= form_with scope: :person do |form| %> + # <%= form.text_field :first_name %> + # <%= form.text_field :last_name %> + # + # <%= text_area :person, :biography %> + # <%= check_box_tag "person[admin]", "1", @person.company.admin? %> + # + # <%= form.submit %> + # <% end %> + # + # Same goes for the methods in FormOptionsHelper and DateHelper designed + # to work with an object as a base, like + # FormOptionsHelper#collection_select and DateHelper#datetime_select. + # + # === Setting the method + # + # You can force the form to use the full array of HTTP verbs by setting + # + # method: (:get|:post|:patch|:put|:delete) + # + # in the options hash. If the verb is not GET or POST, which are natively + # supported by HTML forms, the form will be set to POST and a hidden input + # called _method will carry the intended verb for the server to interpret. + # + # === Setting HTML options + # + # You can set data attributes directly in a data hash, but HTML options + # besides id and class must be wrapped in an HTML key: + # + # <%= form_with(model: @post, data: { behavior: "autosave" }, html: { name: "go" }) do |form| %> + # ... + # <% end %> + # + # generates + # + # <form action="/posts/123" method="post" data-behavior="autosave" name="go"> + # <input name="_method" type="hidden" value="patch" /> + # ... + # </form> + # + # === Removing hidden model id's + # + # The +form_with+ method automatically includes the model id as a hidden field in the form. + # This is used to maintain the correlation between the form data and its associated model. + # Some ORM systems do not use IDs on nested models so in this case you want to be able + # to disable the hidden id. + # + # In the following example the Post model has many Comments stored within it in a NoSQL database, + # thus there is no primary key for comments. + # + # <%= form_with(model: @post) do |form| %> + # <%= form.fields(:comments, skip_id: true) do |fields| %> + # ... + # <% end %> + # <% end %> + # + # === Customized form builders + # + # You can also build forms using a customized FormBuilder class. Subclass + # FormBuilder and override or define some more helpers, then use your + # custom builder. For example, let's say you made a helper to + # automatically add labels to form inputs. + # + # <%= form_with model: @person, url: { action: "create" }, builder: LabellingFormBuilder do |form| %> + # <%= form.text_field :first_name %> + # <%= form.text_field :last_name %> + # <%= form.text_area :biography %> + # <%= form.check_box :admin %> + # <%= form.submit %> + # <% end %> + # + # In this case, if you use: + # + # <%= render form %> + # + # The rendered template is <tt>people/_labelling_form</tt> and the local + # variable referencing the form builder is called + # <tt>labelling_form</tt>. + # + # The custom FormBuilder class is automatically merged with the options + # of a nested +fields+ call, unless it's explicitly set. + # + # In many cases you will want to wrap the above in another helper, so you + # could do something like the following: + # + # 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, &block) + options[:allow_method_names_outside_object] = true + options[:skip_default_ids] = !form_with_generates_ids + + if model + url ||= polymorphic_path(model, format: format) + + model = model.last if model.is_a?(Array) + scope ||= model_name_from_record_or_class(model).param_key + end + + if block_given? + builder = instantiate_builder(scope, model, options) + output = capture(builder, &block) + options[:multipart] ||= builder.multipart? + + html_options = html_options_for_form_with(url, model, options) + form_tag_with_body(html_options, output) + else + html_options = html_options_for_form_with(url, model, options) + form_tag_html(html_options) + end + end + # Creates a scope around a specific model object like form_for, but # doesn't create the form tags themselves. This makes fields_for suitable # for specifying additional model objects in the same form. @@ -531,9 +820,9 @@ module ActionView # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is # of class +Permission+, the field will still be named <tt>permission[admin]</tt>. # - # Note: This also works for the methods in FormOptionHelper and + # Note: This also works for the methods in FormOptionsHelper and # DateHelper that are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === Nested Attributes Examples # @@ -720,6 +1009,63 @@ module ActionView capture(builder, &block) end + # Scopes input fields with either an explicit scope or model. + # Like +form_with+ does with <tt>:scope</tt> or <tt>:model</tt>, + # except it doesn't output the form tags. + # + # # Using a scope prefixes the input field names: + # <%= fields :comment do |fields| %> + # <%= fields.text_field :body %> + # <% end %> + # # => <input type="text" name="comment[body]"> + # + # # Using a model infers the scope and assigns field values: + # <%= fields model: Comment.new(body: "full bodied") do |fields| %> + # <%= fields.text_field :body %> + # <% end %> + # # => <input type="text" name="comment[body]" value="full bodied"> + # + # # Using +fields+ with +form_with+: + # <%= form_with model: @post do |form| %> + # <%= form.text_field :title %> + # + # <%= form.fields :comment do |fields| %> + # <%= fields.text_field :body %> + # <% end %> + # <% end %> + # + # Much like +form_with+ a FormBuilder instance associated with the scope + # or model is yielded, so any generated field names are prefixed with + # either the passed scope or the scope inferred from the <tt>:model</tt>. + # + # === Mixing with other form helpers + # + # While +form_with+ uses a FormBuilder object it's possible to mix and + # match the stand-alone FormHelper methods and methods + # from FormTagHelper: + # + # <%= fields model: @comment do |fields| %> + # <%= fields.text_field :body %> + # + # <%= text_area :commenter, :biography %> + # <%= check_box_tag "comment[all_caps]", "1", @comment.commenter.hulk_mode? %> + # <% end %> + # + # Same goes for the methods in FormOptionsHelper and DateHelper designed + # to work with an object as a base, like + # FormOptionsHelper#collection_select and DateHelper#datetime_select. + def fields(scope = nil, model: nil, **options, &block) + options[:allow_method_names_outside_object] = true + options[:skip_default_ids] = !form_with_generates_ids + + if model + scope ||= model_name_from_record_or_class(model).param_key + end + + builder = instantiate_builder(scope, model, options) + capture(builder, &block) + end + # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly. @@ -784,6 +1130,9 @@ module ActionView # text_field(:post, :title, class: "create_input") # # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" /> # + # text_field(:post, :title, maxlength: 30, class: "title_input") + # # => <input type="text" id="post_title" name="post[title]" maxlength="30" size="30" value="#{@post.title}" class="title_input" /> + # # text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }") # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }"/> # @@ -860,26 +1209,8 @@ module ActionView # # file_field(:attachment, :file, class: 'file_input') # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> - # - # ==== Gotcha - # - # The HTML specification says that when a file field is empty, web browsers - # do not send any value to the server. Unfortunately this introduces a - # gotcha: if a +User+ model has an +avatar+ field, and no file is selected, - # then the +avatar+ parameter is empty. Thus, any mass-assignment idiom like - # - # @user.update(params[:user]) - # - # wouldn't update the +avatar+ field. - # - # To prevent this, the helper generates an auxiliary hidden field before - # every file field. The hidden field has the same name as the file one and - # a blank value. - # - # In case you don't want the helper to generate this hidden field you can - # specify the <tt>include_hidden: false</tt> option. def file_field(object_name, method, options = {}) - Tags::FileField.new(object_name, method, self, options).render + Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render end # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+) @@ -983,6 +1314,7 @@ module ActionView # # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" /> # # <input type="radio" id="post_category_java" name="post[category]" value="java" /> # + # # Let's say that @user.receive_newsletter returns "no": # radio_button("user", "receive_newsletter", "yes") # radio_button("user", "receive_newsletter", "no") # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" /> @@ -1066,7 +1398,7 @@ module ActionView # Returns a text_field of type "time". # # The default value is generated by trying to call +strftime+ with "%T.%L" - # on the objects's value. It is still possible to override that + # on the object's value. It is still possible to override that # by passing the "value" option. # # === Options @@ -1092,42 +1424,9 @@ module ActionView Tags::TimeField.new(object_name, method, self, options).render end - # Returns a text_field of type "datetime". - # - # datetime_field("user", "born_on") - # # => <input id="user_born_on" name="user[born_on]" type="datetime" /> - # - # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T.%L%z" - # on the object's value, which makes it behave as expected for instances - # of DateTime and ActiveSupport::TimeWithZone. - # - # @user.born_on = Date.new(1984, 1, 12) - # datetime_field("user", "born_on") - # # => <input id="user_born_on" name="user[born_on]" type="datetime" value="1984-01-12T00:00:00.000+0000" /> - # - # You can create values for the "min" and "max" attributes by passing - # instances of Date or Time to the options hash. - # - # datetime_field("user", "born_on", min: Date.today) - # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> - # - # Alternatively, you can pass a String formatted as an ISO8601 datetime - # with UTC offset as the values for "min" and "max." - # - # datetime_field("user", "born_on", min: "2014-05-20T00:00:00+0000") - # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> - # - def datetime_field(object_name, method, options = {}) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - datetime_field is deprecated and will be removed in Rails 5.1. - Use datetime_local_field instead. - MESSAGE - Tags::DatetimeField.new(object_name, method, self, options).render - end - # Returns a text_field of type "datetime-local". # - # datetime_local_field("user", "born_on") + # datetime_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" /> # # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T" @@ -1135,25 +1434,27 @@ module ActionView # of DateTime and ActiveSupport::TimeWithZone. # # @user.born_on = Date.new(1984, 1, 12) - # datetime_local_field("user", "born_on") + # datetime_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" /> # # You can create values for the "min" and "max" attributes by passing # instances of Date or Time to the options hash. # - # datetime_local_field("user", "born_on", min: Date.today) + # datetime_field("user", "born_on", min: Date.today) # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" /> # # Alternatively, you can pass a String formatted as an ISO8601 datetime as # the values for "min" and "max." # - # datetime_local_field("user", "born_on", min: "2014-05-20T00:00:00") + # datetime_field("user", "born_on", min: "2014-05-20T00:00:00") # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" /> # - def datetime_local_field(object_name, method, options = {}) + def datetime_field(object_name, method, options = {}) Tags::DatetimeLocalField.new(object_name, method, self, options).render end + alias datetime_local_field datetime_field + # Returns a text_field of type "month". # # month_field("user", "born_on") @@ -1223,6 +1524,34 @@ module ActionView end private + def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms, + skip_enforcing_utf8: nil, **options) + html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html) + html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? + html_options[:enforce_utf8] = !skip_enforcing_utf8 unless skip_enforcing_utf8.nil? + + html_options[:enctype] = "multipart/form-data" if html_options.delete(:multipart) + + # The following URL is unescaped, this is just a hash of options, and it is the + # responsibility of the caller to escape all the values. + html_options[:action] = url_for(url_for_options || {}) + html_options[:"accept-charset"] = "UTF-8" + html_options[:"data-remote"] = true unless local + + html_options[:authenticity_token] = options.delete(:authenticity_token) + + if !local && html_options[:authenticity_token].blank? + html_options[:authenticity_token] = embed_authenticity_token_in_remote_forms + end + + if html_options[:authenticity_token] == true + # Include the default authenticity_token, which is only generated when it's set to nil, + # but we needed the true value to override the default of no authenticity_token on data-remote. + html_options[:authenticity_token] = nil + end + + html_options.stringify_keys! + end def instantiate_builder(record_name, record_object, options) case record_name @@ -1231,7 +1560,7 @@ module ActionView object_name = record_name else object = record_name - object_name = model_name_from_record_or_class(object).param_key + object_name = model_name_from_record_or_class(object).param_key if object end builder = options[:builder] || default_form_builder_class @@ -1257,7 +1586,7 @@ module ActionView # In the above block, a +FormBuilder+ object is yielded as the # +person_form+ variable. This allows you to generate the +text_field+ # and +check_box+ fields by specifying their eponymous methods, which - # modify the underlying template and associates the +@person+ model object + # modify the underlying template and associates the <tt>@person</tt> model object # with the form. # # The +FormBuilder+ object can be thought of as serving as a proxy for the @@ -1296,14 +1625,15 @@ module ActionView include ModelNaming # The methods which wrap a form helper call. - class_attribute :field_helpers - self.field_helpers = [:fields_for, :label, :text_field, :password_field, - :hidden_field, :file_field, :text_area, :check_box, - :radio_button, :color_field, :search_field, - :telephone_field, :phone_field, :date_field, - :time_field, :datetime_field, :datetime_local_field, - :month_field, :week_field, :url_field, :email_field, - :number_field, :range_field] + class_attribute :field_helpers, default: [ + :fields_for, :fields, :label, :text_field, :password_field, + :hidden_field, :file_field, :text_area, :check_box, + :radio_button, :color_field, :search_field, + :telephone_field, :phone_field, :date_field, + :time_field, :datetime_field, :datetime_local_field, + :month_field, :week_field, :url_field, :email_field, + :number_field, :range_field + ] attr_accessor :object_name, :object, :options @@ -1319,7 +1649,7 @@ module ActionView end def self._to_partial_path - @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, '') + @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, "") end def to_partial_path @@ -1333,19 +1663,245 @@ module ActionView def initialize(object_name, object, template, options) @nested_child_index = {} @object_name, @object, @template, @options = object_name, object, template, options - @default_options = @options ? @options.slice(:index, :namespace) : {} + @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {} + @default_html_options = @default_options.except(:skip_default_ids, :allow_method_names_outside_object) + + convert_to_legacy_options(@options) + if @object_name.to_s.match(/\[\]$/) - if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param) + if (object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param) @auto_index = object.to_param else raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" end end + @multipart = nil @index = options[:index] || options[:child_index] end - (field_helpers - [:label, :check_box, :radio_button, :fields_for, :hidden_field, :file_field]).each do |selector| + ## + # :method: text_field + # + # :call-seq: text_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#text_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.text_field :name %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: password_field + # + # :call-seq: password_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#password_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.password_field :password %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: text_area + # + # :call-seq: text_area(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#text_area for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.text_area :detail %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: color_field + # + # :call-seq: color_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#color_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.color_field :favorite_color %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: search_field + # + # :call-seq: search_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#search_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.search_field :name %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: telephone_field + # + # :call-seq: telephone_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#telephone_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.telephone_field :phone %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: phone_field + # + # :call-seq: phone_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#phone_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.phone_field :phone %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: date_field + # + # :call-seq: date_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#date_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.date_field :born_on %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: time_field + # + # :call-seq: time_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#time_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.time_field :borned_at %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: datetime_field + # + # :call-seq: datetime_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#datetime_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.datetime_field :graduation_day %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: datetime_local_field + # + # :call-seq: datetime_local_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#datetime_local_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.datetime_local_field :graduation_day %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: month_field + # + # :call-seq: month_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#month_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.month_field :birthday_month %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: week_field + # + # :call-seq: week_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#week_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.week_field :birthday_week %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: url_field + # + # :call-seq: url_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#url_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.url_field :homepage %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: email_field + # + # :call-seq: email_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#email_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.email_field :address %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: number_field + # + # :call-seq: number_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#number_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.number_field :age %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: range_field + # + # :call-seq: range_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#range_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.range_field :age %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + (field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]).each do |selector| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{selector}(method, options = {}) # def text_field(method, options = {}) @template.send( # @template.send( @@ -1414,9 +1970,9 @@ module ActionView # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is # of class +Permission+, the field will still be named <tt>permission[admin]</tt>. # - # Note: This also works for the methods in FormOptionHelper and + # Note: This also works for the methods in FormOptionsHelper and # DateHelper that are designed to work with an object as base, like - # FormOptionHelper#collection_select and DateHelper#datetime_select. + # FormOptionsHelper#collection_select and DateHelper#datetime_select. # # === Nested Attributes Examples # @@ -1614,26 +2170,37 @@ module ActionView record_name = model_name_from_record_or_class(record_object).param_key end + object_name = @object_name index = if options.has_key?(:index) options[:index] elsif defined?(@auto_index) - self.object_name = @object_name.to_s.sub(/\[\]$/,"") + object_name = object_name.to_s.sub(/\[\]$/, "") @auto_index end record_name = if index - "#{object_name}[#{index}][#{record_name}]" - elsif record_name.to_s.end_with?('[]') - record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]") - "#{object_name}#{record_name}" - else - "#{object_name}[#{record_name}]" - end + "#{object_name}[#{index}][#{record_name}]" + elsif record_name.to_s.end_with?("[]") + record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]") + "#{object_name}#{record_name}" + else + "#{object_name}[#{record_name}]" + end fields_options[:child_index] = index @template.fields_for(record_name, record_object, fields_options, &block) end + # See the docs for the <tt>ActionView::FormHelper.fields</tt> helper method. + def fields(scope = nil, model: nil, **options, &block) + options[:allow_method_names_outside_object] = true + options[:skip_default_ids] = !FormHelper.form_with_generates_ids + + convert_to_legacy_options(options) + + fields_for(scope || model, model, options, &block) + end + # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly. @@ -1760,7 +2327,7 @@ module ActionView # # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" /> # # <input type="radio" id="post_category_java" name="post[category]" value="java" /> # - # # Let's say that @user.category returns "no": + # # Let's say that @user.receive_newsletter returns "no": # radio_button("receive_newsletter", "yes") # radio_button("receive_newsletter", "no") # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" /> @@ -1837,11 +2404,11 @@ module ActionView # <%= f.submit %> # <% end %> # - # In the example above, if @post is a new record, it will use "Create Post" as - # submit button label, otherwise, it uses "Update Post". + # In the example above, if <tt>@post</tt> is a new record, it will use "Create Post" as + # submit button label; otherwise, it uses "Update Post". # - # Those labels can be customized using I18n, under the helpers.submit key and accept - # the %{model} as translation interpolation: + # Those labels can be customized using I18n under the +helpers.submit+ key and using + # <tt>%{model}</tt> for translation interpolation: # # en: # helpers: @@ -1849,7 +2416,7 @@ module ActionView # create: "Create a %{model}" # update: "Confirm changes to %{model}" # - # It also searches for a key specific for the given object: + # It also searches for a key specific to the given object: # # en: # helpers: @@ -1857,7 +2424,7 @@ module ActionView # post: # create: "Add %{model}" # - def submit(value=nil, options={}) + def submit(value = nil, options = {}) value, options = nil, value if value.is_a?(Hash) value ||= submit_default_value @template.submit_tag(value, options) @@ -1870,11 +2437,11 @@ module ActionView # <%= f.button %> # <% end %> # - # In the example above, if @post is a new record, it will use "Create Post" as - # button label, otherwise, it uses "Update Post". + # In the example above, if <tt>@post</tt> is a new record, it will use "Create Post" as + # button label; otherwise, it uses "Update Post". # - # Those labels can be customized using I18n, under the helpers.submit key - # (the same as submit helper) and accept the %{model} as translation interpolation: + # Those labels can be customized using I18n under the +helpers.submit+ key + # (the same as submit helper) and using <tt>%{model}</tt> for translation interpolation: # # en: # helpers: @@ -1882,7 +2449,7 @@ module ActionView # create: "Create a %{model}" # update: "Confirm changes to %{model}" # - # It also searches for a key specific for the given object: + # It also searches for a key specific to the given object: # # en: # helpers: @@ -1907,7 +2474,7 @@ module ActionView @template.button_tag(value, options, &block) end - def emitted_hidden_id? + def emitted_hidden_id? # :nodoc: @emitted_hidden_id ||= nil end @@ -1927,7 +2494,12 @@ module ActionView end defaults = [] - defaults << :"helpers.submit.#{object_name}.#{key}" + # Object is a model and it is not overwritten by as and scope option. + if object.respond_to?(:model_name) && object_name.to_s == model.downcase + defaults << :"helpers.submit.#{object.model_name.i18n_key}.#{key}" + else + defaults << :"helpers.submit.#{object_name}.#{key}" + end defaults << :"helpers.submit.#{key}" defaults << "#{key.to_s.humanize} #{model}" @@ -1982,12 +2554,16 @@ module ActionView @nested_child_index[name] ||= -1 @nested_child_index[name] += 1 end + + def convert_to_legacy_options(options) + if options.key?(:skip_id) + options[:include_id] = !options.delete(:skip_id) + end + end end end ActiveSupport.on_load(:action_view) do - cattr_accessor(:default_form_builder, instance_writer: false, instance_reader: false) do - ::ActionView::Helpers::FormBuilder - end + cattr_accessor :default_form_builder, instance_writer: false, instance_reader: false, default: ::ActionView::Helpers::FormBuilder end end diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 430051379d..a7747456a4 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -1,20 +1,22 @@ -require 'cgi' -require 'erb' -require 'action_view/helpers/form_helper' -require 'active_support/core_ext/string/output_safety' -require 'active_support/core_ext/array/extract_options' -require 'active_support/core_ext/array/wrap' +# frozen_string_literal: true + +require "cgi" +require "erb" +require "action_view/helpers/form_helper" +require "active_support/core_ext/string/output_safety" +require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/array/wrap" module ActionView # = Action View Form Option Helpers - module Helpers + module Helpers #:nodoc: # Provides a number of methods for turning different kinds of containers into a set of option tags. # # The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash: # # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. # - # select("post", "category", Post::CATEGORIES, {include_blank: true}) + # select("post", "category", Post::CATEGORIES, { include_blank: true }) # # could become: # @@ -28,7 +30,7 @@ module ActionView # # Example with <tt>@post.person_id => 2</tt>: # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'}) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: 'None' }) # # could become: # @@ -41,7 +43,7 @@ module ActionView # # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'}) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { prompt: 'Select Person' }) # # could become: # @@ -67,7 +69,7 @@ module ActionView # # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output. # - # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'}) + # select("post", "category", Post::CATEGORIES, { disabled: 'restricted' }) # # could become: # @@ -80,7 +82,7 @@ module ActionView # # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. # - # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: -> (category) { category.archived? }}) + # collection_select(:post, :category_id, Category.all, :id, :name, { disabled: -> (category) { category.archived? } }) # # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: # <select name="post[category_id]" id="post_category_id"> @@ -105,7 +107,7 @@ module ActionView # # For example: # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true }) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true }) # # would become: # @@ -212,9 +214,13 @@ module ActionView # * +method+ - The attribute of +object+ corresponding to the select tag # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags. # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an - # array of child objects representing the <tt><option></tt> tags. + # array of child objects representing the <tt><option></tt> tags. It can also be any object that responds + # to +call+, such as a +proc+, that will be called for each member of the +collection+ to retrieve the + # value. # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a - # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. + # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. It can also be any object + # that responds to +call+, such as a +proc+, that will be called for each member of the +collection+ to + # retrieve the label. # * +option_key_method+ - The name of a method which, when called on a child object of a member of # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag. # * +option_value_method+ - The name of a method which, when called on a child object of a member of @@ -268,25 +274,26 @@ module ActionView # for more information.) # # You can also supply an array of ActiveSupport::TimeZone objects - # as +priority_zones+, so that they will be listed above the rest of the - # (long) list. (You can use ActiveSupport::TimeZone.us_zones as a convenience - # for obtaining a list of the US time zones, or a Regexp to select the zones - # of your choice) + # as +priority_zones+ so that they will be listed above the rest of the + # (long) list. You can use ActiveSupport::TimeZone.us_zones for a list + # of US time zones, ActiveSupport::TimeZone.country_zones(country_code) + # for another country's time zones, or a Regexp to select the zones of + # your choice. # # Finally, this method supports a <tt>:default</tt> option, which selects # a default ActiveSupport::TimeZone if the object's time zone is +nil+. # - # time_zone_select( "user", "time_zone", nil, include_blank: true) + # time_zone_select("user", "time_zone", nil, include_blank: true) # - # time_zone_select( "user", "time_zone", nil, default: "Pacific Time (US & Canada)" ) + # time_zone_select("user", "time_zone", nil, default: "Pacific Time (US & Canada)") # - # time_zone_select( "user", 'time_zone', ActiveSupport::TimeZone.us_zones, default: "Pacific Time (US & Canada)") + # time_zone_select("user", 'time_zone', ActiveSupport::TimeZone.us_zones, default: "Pacific Time (US & Canada)") # - # time_zone_select( "user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ]) + # time_zone_select("user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ]) # - # time_zone_select( "user", 'time_zone', /Australia/) + # time_zone_select("user", 'time_zone', /Australia/) # - # time_zone_select( "user", "time_zone", ActiveSupport::TimeZone.all.sort, model: ActiveSupport::TimeZone) + # time_zone_select("user", "time_zone", ActiveSupport::TimeZone.all.sort, model: ActiveSupport::TimeZone) def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {}) Tags::TimeZoneSelect.new(object, method, self, priority_zones, options, html_options).render end @@ -316,12 +323,12 @@ module ActionView # # You can optionally provide HTML attributes as the last element of the array. # - # options_for_select([ "Denmark", ["USA", {class: 'bold'}], "Sweden" ], ["USA", "Sweden"]) + # options_for_select([ "Denmark", ["USA", { class: 'bold' }], "Sweden" ], ["USA", "Sweden"]) # # => <option value="Denmark">Denmark</option> # # => <option value="USA" class="bold" selected="selected">USA</option> # # => <option value="Sweden" selected="selected">Sweden</option> # - # options_for_select([["Dollar", "$", {class: "bold"}], ["Kroner", "DKK", {onclick: "alert('HI');"}]]) + # options_for_select([["Dollar", "$", { class: "bold" }], ["Kroner", "DKK", { onclick: "alert('HI');" }]]) # # => <option value="$" class="bold">Dollar</option> # # => <option value="DKK" onclick="alert('HI');">Kroner</option> # @@ -362,7 +369,7 @@ module ActionView html_attributes[:disabled] ||= disabled && option_value_selected?(value, disabled) html_attributes[:value] = value - content_tag_string(:option, text, html_attributes) + tag_builder.content_tag_string(:option, text, html_attributes) end.join("\n").html_safe end @@ -454,9 +461,9 @@ module ActionView def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil) collection.map do |group| option_tags = options_from_collection_for_select( - group.send(group_method), option_key_method, option_value_method, selected_key) + value_for_collection(group, group_method), option_key_method, option_value_method, selected_key) - content_tag("optgroup".freeze, option_tags, label: group.send(group_label_method)) + content_tag("optgroup", option_tags, label: value_for_collection(group, group_label_method)) end.join.html_safe end @@ -528,7 +535,7 @@ module ActionView body = "".html_safe if prompt - body.safe_concat content_tag("option".freeze, prompt_text(prompt), value: "") + body.safe_concat content_tag("option", prompt_text(prompt), value: "") end grouped_options.each do |container| @@ -541,7 +548,7 @@ module ActionView end html_attributes = { label: label }.merge!(html_attributes) - body.safe_concat content_tag("optgroup".freeze, options_for_select(container, selected_key), html_attributes) + body.safe_concat content_tag("optgroup", options_for_select(container, selected_key), html_attributes) end body @@ -577,7 +584,7 @@ module ActionView end zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected) - zone_options.safe_concat content_tag("option".freeze, '-------------', value: '', disabled: true) + zone_options.safe_concat content_tag("option", "-------------", value: "", disabled: true) zone_options.safe_concat "\n" zones = zones - priority_zones @@ -647,15 +654,15 @@ module ActionView # # ==== Gotcha # - # The HTML specification says when nothing is select on a collection of radio buttons + # The HTML specification says when nothing is selected on a collection of radio buttons # web browsers do not send any value to server. # Unfortunately this introduces a gotcha: - # if a +User+ model has a +category_id+ field, and in the form none category is selected no +category_id+ parameter is sent. So, - # any strong parameters idiom like + # if a +User+ model has a +category_id+ field and in the form no category is selected, no +category_id+ parameter is sent. So, + # any strong parameters idiom like: # # params.require(:user).permit(...) # - # will raise an error since no +{user: ...}+ will be present. + # will raise an error since no <tt>{user: ...}</tt> will be present. # # To prevent this the helper generates an auxiliary hidden field before # every collection of radio buttons. The hidden field has the same name as collection radio button and blank value. @@ -787,7 +794,7 @@ module ActionView def extract_values_from_collection(collection, value_method, selected) if selected.is_a?(Proc) collection.map do |element| - element.send(value_method) if selected.call(element) + public_or_deprecated_send(element, value_method) if selected.call(element) end.compact else selected @@ -795,11 +802,19 @@ module ActionView end def value_for_collection(item, value) - value.respond_to?(:call) ? value.call(item) : item.send(value) + value.respond_to?(:call) ? value.call(item) : public_or_deprecated_send(item, value) + end + + def public_or_deprecated_send(item, value) + item.public_send(value) + rescue NoMethodError + raise unless item.respond_to?(value, true) && !item.respond_to?(value) + ActiveSupport::Deprecation.warn "Using private methods from view helpers is deprecated (calling private #{item.class}##{value})" + item.send(value) end def prompt_text(prompt) - prompt.kind_of?(String) ? prompt : I18n.translate('helpers.select.prompt', default: 'Please select') + prompt.kind_of?(String) ? prompt : I18n.translate("helpers.select.prompt", default: "Please select") end end @@ -813,7 +828,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def select(method, choices = nil, options = {}, html_options = {}, &block) - @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options), &block) + @template.select(@object_name, method, choices, objectify_options(options), @default_html_options.merge(html_options), &block) end # Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders: @@ -825,7 +840,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) - @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) + @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options)) end # Wraps ActionView::Helpers::FormOptionsHelper#grouped_collection_select for form builders: @@ -837,7 +852,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {}) - @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_options.merge(html_options)) + @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_html_options.merge(html_options)) end # Wraps ActionView::Helpers::FormOptionsHelper#time_zone_select for form builders: @@ -849,7 +864,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) - @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options)) + @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_html_options.merge(html_options)) end # Wraps ActionView::Helpers::FormOptionsHelper#collection_check_boxes for form builders: @@ -861,7 +876,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block) - @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) + @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block) end # Wraps ActionView::Helpers::FormOptionsHelper#collection_radio_buttons for form builders: @@ -873,7 +888,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block) - @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) + @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block) end end end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index cfff0bef5d..5d4ff36425 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -1,11 +1,13 @@ -require 'cgi' -require 'action_view/helpers/tag_helper' -require 'active_support/core_ext/string/output_safety' -require 'active_support/core_ext/module/attribute_accessors' +# frozen_string_literal: true + +require "cgi" +require "action_view/helpers/tag_helper" +require "active_support/core_ext/string/output_safety" +require "active_support/core_ext/module/attribute_accessors" module ActionView # = Action View Form Tag Helpers - module Helpers + module Helpers #:nodoc: # Provides a number of methods for creating form tags that don't rely on an Active Record object assigned to the template like # FormHelper does. Instead, you provide the names and values manually. # @@ -18,9 +20,11 @@ module ActionView include TextHelper mattr_accessor :embed_authenticity_token_in_remote_forms - self.embed_authenticity_token_in_remote_forms = false + self.embed_authenticity_token_in_remote_forms = nil + + 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 @@ -113,7 +117,7 @@ module ActionView # # <option>Write</option></select> # # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true - # # => <select id="people" name="people"><option value=""></option><option value="1">David</option></select> + # # => <select id="people" name="people"><option value="" label=" "></option><option value="1">David</option></select> # # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: "All" # # => <select id="people" name="people"><option value="">All</option><option value="1">David</option></select> @@ -134,21 +138,23 @@ module ActionView if options.include?(:include_blank) include_blank = options.delete(:include_blank) + options_for_blank_options_tag = { value: "" } if include_blank == true - include_blank = '' + include_blank = "" + options_for_blank_options_tag[:label] = " " end if include_blank - option_tags = content_tag("option".freeze, include_blank, value: '').safe_concat(option_tags) + option_tags = content_tag("option", include_blank, options_for_blank_options_tag).safe_concat(option_tags) end end if prompt = options.delete(:prompt) - option_tags = content_tag("option".freeze, prompt, value: '').safe_concat(option_tags) + option_tags = content_tag("option", prompt, value: "").safe_concat(option_tags) end - content_tag "select".freeze, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) + content_tag "select", option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) end # Creates a standard text field; use these text fields to input smaller chunks of text like a username @@ -270,7 +276,7 @@ module ActionView # file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html' # # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" /> def file_field_tag(name, options = {}) - text_field_tag(name, nil, options.merge(type: :file)) + text_field_tag(name, nil, convert_direct_upload_option_to_url(options.merge(type: :file))) end # Creates a password field, a masked text field that will hide the users input behind a mask character. @@ -383,14 +389,14 @@ module ActionView # * Any other key creates standard HTML options for the tag. # # ==== Examples - # radio_button_tag 'gender', 'male' - # # => <input id="gender_male" name="gender" type="radio" value="male" /> + # radio_button_tag 'favorite_color', 'maroon' + # # => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" /> # # radio_button_tag 'receive_updates', 'no', true # # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" /> # # radio_button_tag 'time_slot', "3:00 p.m.", false, disabled: true - # # => <input disabled="disabled" id="time_slot_300_pm" name="time_slot" type="radio" value="3:00 p.m." /> + # # => <input disabled="disabled" id="time_slot_3:00_p.m." name="time_slot" type="radio" value="3:00 p.m." /> # # radio_button_tag 'color', "green", true, class: "color_input" # # => <input checked="checked" class="color_input" id="color_green" name="color" type="radio" value="green" /> @@ -440,31 +446,19 @@ module ActionView # # => <input name='commit' type='submit' value='Save' data-disable-with="Save" data-confirm="Are you sure?" /> # def submit_tag(value = "Save changes", options = {}) - options = options.stringify_keys + options = options.deep_stringify_keys tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options) - - if ActionView::Base.automatically_disable_submit_tag - unless tag_options["data-disable-with"] == false || (tag_options["data"] && tag_options["data"][:disable_with] == false) - disable_with_text = tag_options["data-disable-with"] - disable_with_text ||= tag_options["data"][:disable_with] if tag_options["data"] - disable_with_text ||= value.to_s.clone - tag_options.deep_merge!("data" => { "disable_with" => disable_with_text }) - else - tag_options["data"].delete(:disable_with) if tag_options["data"] - end - tag_options.delete("data-disable-with") - end - + set_default_disable_with value, tag_options tag :input, tag_options end # Creates a button element that defines a <tt>submit</tt> button, - # <tt>reset</tt>button or a generic button which can be used in + # <tt>reset</tt> button or a generic button which can be used in # JavaScript, for example. You can use the button tag as a regular # submit tag but it isn't supported in legacy browsers. However, # the button tag does allow for richer labels such as images and emphasis, # so this helper will also accept a block. By default, it will create - # a button tag with type `submit`, if type is not given. + # a button tag with type <tt>submit</tt>, if type is not given. # # ==== Options # * <tt>:data</tt> - This option can be used to add custom data attributes. @@ -516,12 +510,12 @@ module ActionView options ||= {} end - options = { 'name' => 'button', 'type' => 'submit' }.merge!(options.stringify_keys) + options = { "name" => "button", "type" => "submit" }.merge!(options.stringify_keys) if block_given? content_tag :button, options, &block else - content_tag :button, content_or_options || 'Button', options + content_tag :button, content_or_options || "Button", options end end @@ -542,22 +536,23 @@ module ActionView # # ==== Examples # image_submit_tag("login.png") - # # => <input alt="Login" src="/assets/login.png" type="image" /> + # # => <input src="/assets/login.png" type="image" /> # # image_submit_tag("purchase.png", disabled: true) - # # => <input alt="Purchase" disabled="disabled" src="/assets/purchase.png" type="image" /> + # # => <input disabled="disabled" src="/assets/purchase.png" type="image" /> # # image_submit_tag("search.png", class: 'search_button', alt: 'Find') - # # => <input alt="Find" class="search_button" src="/assets/search.png" type="image" /> + # # => <input class="search_button" src="/assets/search.png" type="image" /> # # image_submit_tag("agree.png", disabled: true, class: "agree_disagree_button") - # # => <input alt="Agree" class="agree_disagree_button" disabled="disabled" src="/assets/agree.png" type="image" /> + # # => <input class="agree_disagree_button" disabled="disabled" src="/assets/agree.png" type="image" /> # # image_submit_tag("save.png", data: { confirm: "Are you sure?" }) - # # => <input alt="Save" src="/assets/save.png" data-confirm="Are you sure?" type="image" /> + # # => <input src="/assets/save.png" data-confirm="Are you sure?" type="image" /> def image_submit_tag(source, options = {}) options = options.stringify_keys - tag :input, { "alt" => image_alt(source), "type" => "image", "src" => path_to_image(source) }.update(options) + src = path_to_image(source, skip_pipeline: options.delete("skip_pipeline")) + tag :input, { "type" => "image", "src" => src }.update(options) end # Creates a field set for grouping HTML form elements. @@ -582,7 +577,7 @@ module ActionView # # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset> def field_set_tag(legend = nil, options = nil, &block) output = tag(:fieldset, options, true) - output.safe_concat(content_tag("legend".freeze, legend)) unless legend.blank? + output.safe_concat(content_tag("legend", legend)) unless legend.blank? output.concat(capture(&block)) if block_given? output.safe_concat("</fieldset>") end @@ -683,7 +678,7 @@ module ActionView text_field_tag(name, value, options.merge(type: :time)) end - # Creates a text field of type "datetime". + # Creates a text field of type "datetime-local". # # === Options # * <tt>:min</tt> - The minimum acceptable value. @@ -691,23 +686,10 @@ module ActionView # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. def datetime_field_tag(name, value = nil, options = {}) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - datetime_field_tag is deprecated and will be removed in Rails 5.1. - Use datetime_local_field_tag instead. - MESSAGE - text_field_tag(name, value, options.merge(type: :datetime)) + text_field_tag(name, value, options.merge(type: "datetime-local")) end - # Creates a text field of type "datetime-local". - # - # === Options - # * <tt>:min</tt> - The minimum acceptable value. - # * <tt>:max</tt> - The maximum acceptable value. - # * <tt>:step</tt> - The acceptable value granularity. - # * Otherwise accepts the same options as text_field_tag. - def datetime_local_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.merge(type: 'datetime-local')) - end + alias datetime_local_field_tag datetime_field_tag # Creates a text field of type "month". # @@ -868,11 +850,12 @@ module ActionView authenticity_token = html_options.delete("authenticity_token") method = html_options.delete("method").to_s.downcase - method_tag = case method - when 'get' + method_tag = \ + case method + when "get" html_options["method"] = "get" - '' - when 'post', '' + "" + when "post", "" html_options["method"] = "post" token_tag(authenticity_token, form_options: { action: html_options["action"], @@ -884,9 +867,9 @@ module ActionView action: html_options["action"], method: method }) - end + end - if html_options.delete("enforce_utf8") { true } + if html_options.delete("enforce_utf8") { default_enforce_utf8 } utf8_enforcer_tag + method_tag else method_tag @@ -906,7 +889,30 @@ module ActionView # see http://www.w3.org/TR/html4/types.html#type-name def sanitize_to_id(name) - name.to_s.delete(']').tr('^-a-zA-Z0-9:.', "_") + name.to_s.delete("]").tr("^-a-zA-Z0-9:.", "_") + end + + def set_default_disable_with(value, tag_options) + return unless ActionView::Base.automatically_disable_submit_tag + data = tag_options["data"] + + unless tag_options["data-disable-with"] == false || (data && data["disable_with"] == false) + disable_with_text = tag_options["data-disable-with"] + disable_with_text ||= data["disable_with"] if data + disable_with_text ||= value.to_s.clone + tag_options.deep_merge!("data" => { "disable_with" => disable_with_text }) + else + data.delete("disable_with") if data + end + + tag_options.delete("data-disable-with") + end + + def convert_direct_upload_option_to_url(options) + if options.delete(:direct_upload) && respond_to?(:rails_direct_uploads_url) + options["data-direct-upload-url"] = rails_direct_uploads_url + end + options end end end diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb index ed7e882c94..b680cb1bd3 100644 --- a/actionview/lib/action_view/helpers/javascript_helper.rb +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -1,11 +1,13 @@ -require 'action_view/helpers/tag_helper' +# frozen_string_literal: true + +require "action_view/helpers/tag_helper" module ActionView - module Helpers + module Helpers #:nodoc: module JavaScriptHelper JS_ESCAPE_MAP = { '\\' => '\\\\', - '</' => '<\/', + "</" => '<\/', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', @@ -13,8 +15,8 @@ module ActionView "'" => "\\'" } - JS_ESCAPE_MAP["\342\200\250".force_encoding(Encoding::UTF_8).encode!] = '
' - JS_ESCAPE_MAP["\342\200\251".force_encoding(Encoding::UTF_8).encode!] = '
' + JS_ESCAPE_MAP[(+"\342\200\250").force_encoding(Encoding::UTF_8).encode!] = "
" + JS_ESCAPE_MAP[(+"\342\200\251").force_encoding(Encoding::UTF_8).encode!] = "
" # Escapes carriage returns and single and double quotes for JavaScript segments. # @@ -23,12 +25,13 @@ module ActionView # # $('some_element').replaceWith('<%= j render 'some/element_template' %>'); def escape_javascript(javascript) - if javascript - result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] } - javascript.html_safe? ? result.html_safe : result + javascript = javascript.to_s + if javascript.empty? + result = "" else - '' + result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) { |match| JS_ESCAPE_MAP[match] } end + javascript.html_safe? ? result.html_safe : result end alias_method :j, :escape_javascript @@ -61,6 +64,13 @@ module ActionView # <%= javascript_tag defer: 'defer' do -%> # alert('All is good') # <% end -%> + # + # If you have a content security policy enabled then you can add an automatic + # nonce value by passing <tt>nonce: true</tt> as part of +html_options+. Example: + # + # <%= javascript_tag nonce: true do -%> + # alert('All is good') + # <% end -%> def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block) content = if block_given? @@ -70,7 +80,11 @@ module ActionView content_or_options_with_block end - content_tag("script".freeze, javascript_cdata_section(content), html_options) + if html_options[:nonce] == true + html_options[:nonce] = content_security_policy_nonce + end + + content_tag("script", javascript_cdata_section(content), html_options) end def javascript_cdata_section(content) #:nodoc: diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 161aa031c6..35206b7e48 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -1,11 +1,12 @@ -require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/string/output_safety' -require 'active_support/number_helper' +# frozen_string_literal: true + +require "active_support/core_ext/hash/keys" +require "active_support/core_ext/string/output_safety" +require "active_support/number_helper" module ActionView # = Action View Number Helpers module Helpers #:nodoc: - # Provides methods for converting numbers into formatted strings. # Methods are provided for phone numbers, currency, percentage, # precision, positional notation, file size and pretty printing. @@ -13,7 +14,6 @@ module ActionView # Most methods expect a +number+ argument, and will return it # unchanged if can't be converted into a valid number. module NumberHelper - # Raised when argument +number+ param given to the helpers is invalid and # the option :raise is set to +true+. class InvalidNumberError < StandardError @@ -23,7 +23,7 @@ module ActionView end end - # Formats a +number+ into a US phone number (e.g., (555) + # Formats a +number+ into a phone number (US by default e.g., (555) # 123-9876). You can customize the format in the +options+ hash. # # ==== Options @@ -35,6 +35,8 @@ module ActionView # end of the generated number. # * <tt>:country_code</tt> - Sets the country code for the phone # number. + # * <tt>:pattern</tt> - Specifies how the number is divided into three + # groups with the custom regexp to override the default format. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when # the argument is invalid. # @@ -52,6 +54,11 @@ module ActionView # # number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: ".") # # => +1.123.555.1234 x 1343 + # + # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true) + # # => "(755) 6123-4567" + # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/)) + # # => "133-1234-5678" def number_to_phone(number, options = {}) return unless number options = options.symbolize_keys @@ -87,12 +94,15 @@ module ActionView # (defaults to "%u%n"). Fields are <tt>%u</tt> for the # currency, and <tt>%n</tt> for the number. # * <tt>:negative_format</tt> - Sets the format for negative - # numbers (defaults to prepending an hyphen to the formatted + # numbers (defaults to prepending a hyphen to the formatted # number given by <tt>:format</tt>). Accepts the same fields # than <tt>:format</tt>, except <tt>%n</tt> is here the # absolute value of the number. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when # the argument is invalid. + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). # # ==== Examples # @@ -110,6 +120,8 @@ module ActionView # # => R$1234567890,50 # number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "", format: "%n %u") # # => 1234567890,50 R$ + # number_to_currency(1234567890.50, strip_insignificant_zeros: true) + # # => "$1,234,567,890.5" def number_to_currency(number, options = {}) delegate_number_helper_method(:number_to_currency, number, options) end @@ -166,6 +178,9 @@ module ActionView # to ","). # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). + # * <tt>:delimiter_pattern</tt> - Sets a custom regular expression used for + # deriving the placement of delimiter. Helpful when using currency formats + # like INR. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when # the argument is invalid. # @@ -182,6 +197,9 @@ module ActionView # number_with_delimiter(98765432.98, delimiter: " ", separator: ",") # # => 98 765 432,98 # + # number_with_delimiter("123456.78", + # delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/) # => "1,23,456.78" + # # number_with_delimiter("112a", raise: true) # => raise InvalidNumberError def number_with_delimiter(number, options = {}) delegate_number_helper_method(:number_to_delimited, number, options) @@ -256,8 +274,6 @@ module ActionView # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes # insignificant zeros after the decimal separator (defaults to # +true+) - # * <tt>:prefix</tt> - If +:si+ formats the number using the SI - # prefix (defaults to :binary) # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when # the argument is invalid. # @@ -388,53 +404,53 @@ module ActionView private - def delegate_number_helper_method(method, number, options) - return unless number - options = escape_unsafe_options(options.symbolize_keys) + def delegate_number_helper_method(method, number, options) + return unless number + options = escape_unsafe_options(options.symbolize_keys) - wrap_with_output_safety_handling(number, options.delete(:raise)) { - ActiveSupport::NumberHelper.public_send(method, number, options) - } - end + wrap_with_output_safety_handling(number, options.delete(:raise)) { + ActiveSupport::NumberHelper.public_send(method, number, options) + } + end - def escape_unsafe_options(options) - options[:format] = ERB::Util.html_escape(options[:format]) if options[:format] - options[:negative_format] = ERB::Util.html_escape(options[:negative_format]) if options[:negative_format] - options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator] - options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter] - options[:unit] = ERB::Util.html_escape(options[:unit]) if options[:unit] && !options[:unit].html_safe? - options[:units] = escape_units(options[:units]) if options[:units] && Hash === options[:units] - options - end + def escape_unsafe_options(options) + options[:format] = ERB::Util.html_escape(options[:format]) if options[:format] + options[:negative_format] = ERB::Util.html_escape(options[:negative_format]) if options[:negative_format] + options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator] + options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter] + options[:unit] = ERB::Util.html_escape(options[:unit]) if options[:unit] && !options[:unit].html_safe? + options[:units] = escape_units(options[:units]) if options[:units] && Hash === options[:units] + options + end - def escape_units(units) - Hash[units.map do |k, v| - [k, ERB::Util.html_escape(v)] - end] - end + def escape_units(units) + Hash[units.map do |k, v| + [k, ERB::Util.html_escape(v)] + end] + end - def wrap_with_output_safety_handling(number, raise_on_invalid, &block) - valid_float = valid_float?(number) - raise InvalidNumberError, number if raise_on_invalid && !valid_float + def wrap_with_output_safety_handling(number, raise_on_invalid, &block) + valid_float = valid_float?(number) + raise InvalidNumberError, number if raise_on_invalid && !valid_float - formatted_number = yield + formatted_number = yield - if valid_float || number.html_safe? - formatted_number.html_safe - else - formatted_number + if valid_float || number.html_safe? + formatted_number.html_safe + else + formatted_number + end end - end - def valid_float?(number) - !parse_float(number, false).nil? - end + def valid_float?(number) + !parse_float(number, false).nil? + end - def parse_float(number, raise_error) - Float(number) - rescue ArgumentError, TypeError - raise InvalidNumberError, number if raise_error - end + def parse_float(number, raise_error) + Float(number) + rescue ArgumentError, TypeError + raise InvalidNumberError, number if raise_error + end end end end diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb index c0fc3b820f..52a951b2ca 100644 --- a/actionview/lib/action_view/helpers/output_safety_helper.rb +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -1,4 +1,6 @@ -require 'active_support/core_ext/string/output_safety' +# frozen_string_literal: true + +require "active_support/core_ext/string/output_safety" module ActionView #:nodoc: # = Action View Raw Output Helper @@ -25,14 +27,44 @@ module ActionView #:nodoc: # safe_join([raw("<p>foo</p>"), "<p>bar</p>"], "<br />") # # => "<p>foo</p><br /><p>bar</p>" # - # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />") + # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />")) # # => "<p>foo</p><br /><p>bar</p>" # - def safe_join(array, sep=$,) + def safe_join(array, sep = $,) sep = ERB::Util.unwrapped_html_escape(sep) array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe end + + # 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}[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) + + default_connectors = { + words_connector: ", ", + two_words_connector: " and ", + last_word_connector: ", and " + } + if defined?(I18n) + i18n_connectors = I18n.translate(:'support.array', locale: options[:locale], default: {}) + default_connectors.merge!(i18n_connectors) + end + options = default_connectors.merge!(options) + + case array.length + when 0 + "".html_safe + when 1 + ERB::Util.html_escape(array[0]) + when 2 + safe_join([array[0], array[1]], options[:two_words_connector]) + else + safe_join([safe_join(array[0...-1], options[:words_connector]), options[:last_word_connector], array[-1]], nil) + end + end end end end diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb deleted file mode 100644 index f7ee573035..0000000000 --- a/actionview/lib/action_view/helpers/record_tag_helper.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ActionView - module Helpers - module RecordTagHelper - def div_for(*) - raise NoMethodError, "The `div_for` method has been removed from " \ - "Rails. To continue using it, add the `record_tag_helper` gem to " \ - "your Gemfile:\n" \ - " gem 'record_tag_helper', '~> 1.0'\n" \ - "Consult the Rails upgrade guide for details." - end - - def content_tag_for(*) - raise NoMethodError, "The `content_tag_for` method has been removed from " \ - "Rails. To continue using it, add the `record_tag_helper` gem to " \ - "your Gemfile:\n" \ - " gem 'record_tag_helper', '~> 1.0'\n" \ - "Consult the Rails upgrade guide for details." - end - end - end -end diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index c98f2d74a8..7ead691113 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module ActionView - module Helpers + module Helpers #:nodoc: # = Action View Rendering # # Implements methods that allow rendering from a view context. @@ -11,7 +13,6 @@ module ActionView # * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt>. # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those. # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. - # * <tt>:text</tt> - Renders the text passed in out. # * <tt>:plain</tt> - Renders the text passed in out. Setting the content # type as <tt>text/plain</tt>. # * <tt>:html</tt> - Renders the HTML safe string passed in out, otherwise @@ -26,13 +27,15 @@ 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) + view_renderer.render_partial(self, partial: options, locals: locals, &block) end end diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index f9784c3483..f4fa133f55 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -1,18 +1,21 @@ -require 'active_support/core_ext/object/try' -require 'rails-html-sanitizer' +# frozen_string_literal: true + +require "active_support/core_ext/object/try" +require "rails-html-sanitizer" module ActionView # = Action View Sanitize Helpers - module Helpers + module Helpers #:nodoc: # The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements. # These helper methods extend Action View making them callable within your template files. module SanitizeHelper extend ActiveSupport::Concern - # Sanitizes HTML input, stripping all tags and attributes that aren't whitelisted. + # Sanitizes HTML input, stripping all but known-safe tags and attributes. # # It also strips href/src attributes with unsafe protocols like # <tt>javascript:</tt>, while also protecting against attempts to use Unicode, # ASCII, and hex character references to work around these protocol filters. + # All special characters will be escaped. # # The default sanitizer is Rails::Html::WhiteListSanitizer. See {Rails HTML # Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information. @@ -20,8 +23,7 @@ module ActionView # Custom sanitization rules can also be provided. # # Please note that sanitizing user-provided text does not guarantee that the - # resulting markup is valid or even well-formed. For example, the output may still - # contain unescaped characters like <tt><</tt>, <tt>></tt>, or <tt>&</tt>. + # resulting markup is valid or even well-formed. # # ==== Options # @@ -38,24 +40,22 @@ module ActionView # # <%= sanitize @comment.body %> # - # Providing custom whitelisted tags and attributes: + # Providing custom lists of permitted tags and attributes: # # <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %> # # Providing a custom Rails::Html scrubber: # # class CommentScrubber < Rails::Html::PermitScrubber - # def allowed_node?(node) - # !%w(form script comment blockquote).include?(node.name) + # def initialize + # super + # self.tags = %w( form script comment blockquote ) + # self.attributes = %w( style ) # end # # def skip_node?(node) # node.text? # end - # - # def scrub_attribute?(name) - # name == 'style' - # end # end # # <%= sanitize @comment.body, scrubber: CommentScrubber.new %> @@ -88,7 +88,7 @@ module ActionView self.class.white_list_sanitizer.sanitize_css(style) end - # Strips all HTML tags from +html+, including comments. + # Strips all HTML tags from +html+, including comments and special characters. # # strip_tags("Strip <i>these</i> tags!") # # => Strip these tags! @@ -98,8 +98,11 @@ module ActionView # # strip_tags("<div id='top-bar'>Welcome to my website!</div>") # # => Welcome to my website! + # + # strip_tags("> A quote from Smith & Wesson") + # # => > A quote from Smith & Wesson def strip_tags(html) - self.class.full_sanitizer.sanitize(html, encode_special_chars: false) + self.class.full_sanitizer.sanitize(html) end # Strips all link tags from +html+ leaving just the link text. @@ -112,6 +115,9 @@ module ActionView # # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.') # # => Blog: Visit. + # + # strip_links('<<a href="https://example.org">malformed & link</a>') + # # => <malformed & link def strip_links(html) self.class.link_sanitizer.sanitize(html) end @@ -120,7 +126,7 @@ module ActionView attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer # Vendors the full, link and white list sanitizers. - # Provided strictly for compatibility and can be removed in Rails 5.1. + # Provided strictly for compatibility and can be removed in Rails 6. def sanitizer_vendor Rails::Html::Sanitizer end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 42e7358a1d..3979721d34 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -1,39 +1,208 @@ -require 'active_support/core_ext/string/output_safety' -require 'set' +# frozen_string_literal: true + +require "active_support/core_ext/string/output_safety" +require "set" module ActionView # = Action View Tag Helpers module Helpers #:nodoc: - # Provides methods to generate HTML tags programmatically when you can't use - # a Builder. By default, they output XHTML compliant tags. + # Provides methods to generate HTML tags programmatically both as a modern + # HTML5 compliant builder style and legacy XHTML compliant tags. module TagHelper extend ActiveSupport::Concern include CaptureHelper include OutputSafetyHelper - BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer - autoplay controls loop selected hidden scoped async - defer reversed ismap seamless muted required - autofocus novalidate formnovalidate open pubdate - itemscope allowfullscreen default inert sortable - truespeed typemustmatch).to_set + BOOLEAN_ATTRIBUTES = %w(allowfullscreen async autofocus autoplay checked + compact controls declare default defaultchecked + defaultmuted defaultselected defer disabled + enabled formnovalidate hidden indeterminate inert + ismap itemscope loop multiple muted nohref + noresize noshade novalidate nowrap open + pauseonexit readonly required reversed scoped + seamless selected sortable truespeed typemustmatch + visible).to_set BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym)) - TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set + TAG_PREFIXES = ["aria", "data", :aria, :data].to_set - PRE_CONTENT_STRINGS = Hash.new { "".freeze } + PRE_CONTENT_STRINGS = Hash.new { "" } PRE_CONTENT_STRINGS[:textarea] = "\n" PRE_CONTENT_STRINGS["textarea"] = "\n" + class TagBuilder #:nodoc: + include CaptureHelper + include OutputSafetyHelper + + VOID_ELEMENTS = %i(area base br col embed hr img input keygen link meta param source track wbr).to_set + + def initialize(view_context) + @view_context = view_context + end + + def tag_string(name, content = nil, escape_attributes: true, **options, &block) + content = @view_context.capture(self, &block) if block_given? + if VOID_ELEMENTS.include?(name) && content.nil? + "<#{name.to_s.dasherize}#{tag_options(options, escape_attributes)}>".html_safe + else + content_tag_string(name.to_s.dasherize, content || "", options, escape_attributes) + end + end + + def content_tag_string(name, content, options, escape = true) + tag_options = tag_options(options, escape) if options + content = ERB::Util.unwrapped_html_escape(content) if escape + "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe + end + + def tag_options(options, escape = true) + return if options.blank? + output = +"" + sep = " " + options.each_pair do |key, value| + if TAG_PREFIXES.include?(key) && value.is_a?(Hash) + value.each_pair do |k, v| + next if v.nil? + output << sep + output << prefix_tag_option(key, k, v, escape) + end + elsif BOOLEAN_ATTRIBUTES.include?(key) + if value + output << sep + output << boolean_tag_option(key) + end + elsif !value.nil? + output << sep + output << tag_option(key, value, escape) + end + end + output unless output.empty? + end + + def boolean_tag_option(key) + %(#{key}="#{key}") + end + + def tag_option(key, value, escape) + if value.is_a?(Array) + value = escape ? safe_join(value, " ") : value.join(" ") + else + value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s.dup + end + value.gsub!('"', """) + %(#{key}="#{value}") + end + + private + def prefix_tag_option(prefix, key, value, escape) + key = "#{prefix}-#{key.to_s.dasherize}" + unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal) + value = value.to_json + end + tag_option(key, value, escape) + end + + def respond_to_missing?(*args) + true + end + + def method_missing(called, *args, &block) + tag_string(called, *args, &block) + end + end - # Returns an empty HTML tag of type +name+ which by default is XHTML + # Returns an HTML tag. + # + # === Building HTML tags + # + # Builds HTML5 compliant tags with a tag proxy. Every tag can be built with: + # + # tag.<tag name>(optional content, options) + # + # where tag name can be e.g. br, div, section, article, or any tag really. + # + # ==== Passing content + # + # Tags can pass content to embed within it: + # + # tag.h1 'All titles fit to print' # => <h1>All titles fit to print</h1> + # + # tag.div tag.p('Hello world!') # => <div><p>Hello world!</p></div> + # + # Content can also be captured with a block, which is useful in templates: + # + # <%= tag.p do %> + # The next great American novel starts here. + # <% end %> + # # => <p>The next great American novel starts here.</p> + # + # ==== Options + # + # Use symbol keyed options to add attributes to the generated tag. + # + # tag.section class: %w( kitties puppies ) + # # => <section class="kitties puppies"></section> + # + # tag.section id: dom_id(@post) + # # => <section id="<generated dom id>"></section> + # + # Pass +true+ for any attributes that can render with no values, like +disabled+ and +readonly+. + # + # tag.input type: 'text', disabled: true + # # => <input type="text" disabled="disabled"> + # + # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key + # pointing to a hash of sub-attributes. + # + # To play nicely with JavaScript conventions, sub-attributes are dasherized. + # + # tag.article data: { user_id: 123 } + # # => <article data-user-id="123"></article> + # + # Thus <tt>data-user-id</tt> can be accessed as <tt>dataset.userId</tt>. + # + # Data attribute values are encoded to JSON, with the exception of strings, symbols and + # BigDecimals. + # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt> + # from 1.4.3. + # + # tag.div data: { city_state: %w( Chicago IL ) } + # # => <div data-city-state="["Chicago","IL"]"></div> + # + # The generated attributes are escaped by default. This can be disabled using + # +escape_attributes+. + # + # tag.img src: 'open & shut.png' + # # => <img src="open & shut.png"> + # + # tag.img src: 'open & shut.png', escape_attributes: false + # # => <img src="open & shut.png"> + # + # The tag builder respects + # {HTML5 void elements}[https://www.w3.org/TR/html5/syntax.html#void-elements] + # if no content is passed, and omits closing tags for those elements. + # + # # A standard element: + # tag.div # => <div></div> + # + # # A void element: + # tag.br # => <br> + # + # === Legacy syntax + # + # The following format is for legacy syntax support. It will be deprecated in future versions of Rails. + # + # tag(name, options = nil, open = false, escape = true) + # + # It returns an empty HTML tag of type +name+ which by default is XHTML # compliant. Set +open+ to true to create an open tag compatible # with HTML 4.0 and below. Add HTML attributes by passing an attributes # hash to +options+. Set +escape+ to false to disable attribute value # escaping. # # ==== Options + # # You can use symbols or strings for the attribute names. # # Use +true+ with boolean attributes that can render with no value, like @@ -42,16 +211,8 @@ module ActionView # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key # pointing to a hash of sub-attributes. # - # To play nicely with JavaScript conventions sub-attributes are dasherized. - # For example, a key +user_id+ would render as <tt>data-user-id</tt> and - # thus accessed as <tt>dataset.userId</tt>. - # - # Values are encoded to JSON, with the exception of strings, symbols and - # BigDecimals. - # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt> - # from 1.4.3. - # # ==== Examples + # # tag("br") # # => <br /> # @@ -67,13 +228,17 @@ module ActionView # tag("img", src: "open & shut.png") # # => <img src="open & shut.png" /> # - # tag("img", {src: "open & shut.png"}, false, false) + # tag("img", { src: "open & shut.png" }, false, false) # # => <img src="open & shut.png" /> # - # tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)}) + # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) }) # # => <div data-name="Stephen" data-city-state="["Chicago","IL"]" /> - def tag(name, options = nil, open = false, escape = true) - "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe + def tag(name = nil, options = nil, open = false, escape = true) + if name.nil? + tag_builder + else + "<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe + end end # Returns an HTML block tag of type +name+ surrounding the +content+. Add @@ -81,6 +246,7 @@ module ActionView # Instead of passing the content as an argument, you can also use a block # in which case, you pass your +options+ as the second parameter. # Set escape to false to disable attribute value escaping. + # Note: this is legacy syntax, see +tag+ method description for details. # # ==== Options # The +options+ hash can be used with attributes with no value like (<tt>disabled</tt> and @@ -104,9 +270,9 @@ module ActionView def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block) if block_given? options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) - content_tag_string(name, capture(&block), options, escape) + tag_builder.content_tag_string(name, capture(&block), options, escape) else - content_tag_string(name, content_or_options_with_block, options, escape) + tag_builder.content_tag_string(name, content_or_options_with_block, options, escape) end end @@ -124,7 +290,7 @@ module ActionView # cdata_section("hello]]>world") # # => <![CDATA[hello]]]]><![CDATA[>world]]> def cdata_section(content) - splitted = content.to_s.gsub(/\]\]\>/, ']]]]><![CDATA[>') + splitted = content.to_s.gsub(/\]\]\>/, "]]]]><![CDATA[>") "<![CDATA[#{splitted}]]>".html_safe end @@ -140,56 +306,8 @@ module ActionView end private - - def content_tag_string(name, content, options, escape = true) - tag_options = tag_options(options, escape) if options - content = ERB::Util.unwrapped_html_escape(content) if escape - "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe - end - - def tag_options(options, escape = true) - return if options.blank? - output = "" - sep = " ".freeze - options.each_pair do |key, value| - if TAG_PREFIXES.include?(key) && value.is_a?(Hash) - value.each_pair do |k, v| - next if v.nil? - output << sep - output << prefix_tag_option(key, k, v, escape) - end - elsif BOOLEAN_ATTRIBUTES.include?(key) - if value - output << sep - output << boolean_tag_option(key) - end - elsif !value.nil? - output << sep - output << tag_option(key, value, escape) - end - end - output unless output.empty? - end - - def prefix_tag_option(prefix, key, value, escape) - key = "#{prefix}-#{key.to_s.dasherize}" - unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal) - value = value.to_json - end - tag_option(key, value, escape) - end - - def boolean_tag_option(key) - %(#{key}="#{key}") - end - - def tag_option(key, value, escape) - if value.is_a?(Array) - value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze) - else - value = escape ? ERB::Util.unwrapped_html_escape(value) : value - end - %(#{key}="#{value}") + def tag_builder + @tag_builder ||= TagBuilder.new(self) end end end diff --git a/actionview/lib/action_view/helpers/tags.rb b/actionview/lib/action_view/helpers/tags.rb index a4f6eb0150..566668b958 100644 --- a/actionview/lib/action_view/helpers/tags.rb +++ b/actionview/lib/action_view/helpers/tags.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module ActionView - module Helpers + module Helpers #:nodoc: module Tags #:nodoc: extend ActiveSupport::Autoload diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index d57f26ba4f..b58e1a6680 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: @@ -11,10 +13,19 @@ module ActionView @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup @template_object = template_object - @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]") + @object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]") @object = retrieve_object(options.delete(:object)) + @skip_default_ids = options.delete(:skip_default_ids) + @allow_method_names_outside_object = options.delete(:allow_method_names_outside_object) @options = options - @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil + + if Regexp.last_match + @generate_indexed_names = true + @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) + else + @generate_indexed_names = false + @auto_index = nil + end end # This is what child classes implement. @@ -24,136 +35,161 @@ module ActionView private - def value(object) - object.public_send @method_name if object - end + def value + if @allow_method_names_outside_object + object.public_send @method_name if object && object.respond_to?(@method_name) + else + object.public_send @method_name if object + end + end - def value_before_type_cast(object) - unless object.nil? - method_before_type_cast = @method_name + "_before_type_cast" + def value_before_type_cast + unless object.nil? + method_before_type_cast = @method_name + "_before_type_cast" - if value_came_from_user?(object) && object.respond_to?(method_before_type_cast) - object.public_send(method_before_type_cast) - else - value(object) + if value_came_from_user? && object.respond_to?(method_before_type_cast) + object.public_send(method_before_type_cast) + else + value + end end end - end - def value_came_from_user?(object) - method_name = "#{@method_name}_came_from_user?" - !object.respond_to?(method_name) || object.public_send(method_name) - end + def value_came_from_user? + method_name = "#{@method_name}_came_from_user?" + !object.respond_to?(method_name) || object.public_send(method_name) + end - def retrieve_object(object) - if object - object - elsif @template_object.instance_variable_defined?("@#{@object_name}") - @template_object.instance_variable_get("@#{@object_name}") + def retrieve_object(object) + if object + object + elsif @template_object.instance_variable_defined?("@#{@object_name}") + @template_object.instance_variable_get("@#{@object_name}") + end + rescue NameError + # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil. + nil end - rescue NameError - # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil. - nil - end - def retrieve_autoindex(pre_match) - object = self.object || @template_object.instance_variable_get("@#{pre_match}") - if object && object.respond_to?(:to_param) - object.to_param - else - raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" + def retrieve_autoindex(pre_match) + object = self.object || @template_object.instance_variable_get("@#{pre_match}") + if object && object.respond_to?(:to_param) + object.to_param + else + raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" + end end - end - def add_default_name_and_id_for_value(tag_value, options) - if tag_value.nil? - add_default_name_and_id(options) - else - specified_id = options["id"] - add_default_name_and_id(options) + def add_default_name_and_id_for_value(tag_value, options) + if tag_value.nil? + add_default_name_and_id(options) + else + specified_id = options["id"] + add_default_name_and_id(options) - if specified_id.blank? && options["id"].present? - options["id"] += "_#{sanitized_value(tag_value)}" + if specified_id.blank? && options["id"].present? + options["id"] += "_#{sanitized_value(tag_value)}" + end end end - end - def add_default_name_and_id(options) - index = name_and_id_index(options) - options["name"] = options.fetch("name"){ tag_name(options["multiple"], index) } - options["id"] = options.fetch("id"){ tag_id(index) } - if namespace = options.delete("namespace") - options['id'] = options['id'] ? "#{namespace}_#{options['id']}" : namespace + def add_default_name_and_id(options) + index = name_and_id_index(options) + options["name"] = options.fetch("name") { tag_name(options["multiple"], index) } + + if generate_ids? + options["id"] = options.fetch("id") { tag_id(index) } + if namespace = options.delete("namespace") + options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace + end + end end - end - def tag_name(multiple = false, index = nil) - # a little duplication to construct less strings - if index - "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" - else - "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}" + def tag_name(multiple = false, index = nil) + # a little duplication to construct less strings + case + when @object_name.empty? + "#{sanitized_method_name}#{multiple ? "[]" : ""}" + when index + "#{@object_name}[#{index}][#{sanitized_method_name}]#{multiple ? "[]" : ""}" + else + "#{@object_name}[#{sanitized_method_name}]#{multiple ? "[]" : ""}" + end end - end - def tag_id(index = nil) - # a little duplication to construct less strings - if index - "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" - else - "#{sanitized_object_name}_#{sanitized_method_name}" + def tag_id(index = nil) + # a little duplication to construct less strings + case + when @object_name.empty? + sanitized_method_name.dup + when index + "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" + else + "#{sanitized_object_name}_#{sanitized_method_name}" + end end - end - def sanitized_object_name - @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") - end + def sanitized_object_name + @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") + end - def sanitized_method_name - @sanitized_method_name ||= @method_name.sub(/\?$/,"") - end + def sanitized_method_name + @sanitized_method_name ||= @method_name.sub(/\?$/, "") + end - def sanitized_value(value) - value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase - end + def sanitized_value(value) + value.to_s.gsub(/[\s\.]/, "_").gsub(/[^-[[:word:]]]/, "").downcase + end - def select_content_tag(option_tags, options, html_options) - html_options = html_options.stringify_keys - add_default_name_and_id(html_options) + def select_content_tag(option_tags, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) - if placeholder_required?(html_options) - raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false - options[:include_blank] ||= true unless options[:prompt] - end + if placeholder_required?(html_options) + raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false + options[:include_blank] ||= true unless options[:prompt] + end - value = options.fetch(:selected) { value(object) } - select = content_tag("select", add_options(option_tags, options, value), html_options) + value = options.fetch(:selected) { value() } + select = content_tag("select", add_options(option_tags, options, value), html_options) - if html_options["multiple"] && options.fetch(:include_hidden, true) - tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select - else - select + if html_options["multiple"] && options.fetch(:include_hidden, true) + tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "") + select + else + select + end end - end - def placeholder_required?(html_options) - # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required - html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1 - end + def placeholder_required?(html_options) + # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required + html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1 + end - def add_options(option_tags, options, value = nil) - if options[:include_blank] - option_tags = content_tag_string('option', options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, :value => '') + "\n" + option_tags + def add_options(option_tags, options, value = nil) + if options[:include_blank] + option_tags = tag_builder.content_tag_string("option", options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, value: "") + "\n" + option_tags + end + if value.blank? && options[:prompt] + tag_options = { value: "" }.tap do |prompt_opts| + prompt_opts[:disabled] = true if options[:disabled] == "" + prompt_opts[:selected] = true if options[:selected] == "" + end + option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags + end + option_tags end - if value.blank? && options[:prompt] - option_tags = content_tag_string('option', prompt_text(options[:prompt]), :value => '') + "\n" + option_tags + + def name_and_id_index(options) + if options.key?("index") + options.delete("index") || "" + elsif @generate_indexed_names + @auto_index || "" + end end - option_tags - end - def name_and_id_index(options) - options.key?("index") ? options.delete("index") || "" : @auto_index - end + def generate_ids? + !@skip_default_ids + end end end end diff --git a/actionview/lib/action_view/helpers/tags/check_box.rb b/actionview/lib/action_view/helpers/tags/check_box.rb index 6d51f2629a..4327e07cae 100644 --- a/actionview/lib/action_view/helpers/tags/check_box.rb +++ b/actionview/lib/action_view/helpers/tags/check_box.rb @@ -1,4 +1,6 @@ -require 'action_view/helpers/tags/checkable' +# frozen_string_literal: true + +require "action_view/helpers/tags/checkable" module ActionView module Helpers @@ -16,7 +18,7 @@ module ActionView options = @options.stringify_keys options["type"] = "checkbox" options["value"] = @checked_value - options["checked"] = "checked" if input_checked?(object, options) + options["checked"] = "checked" if input_checked?(options) if options["multiple"] add_default_name_and_id_for_value(@checked_value, options) @@ -38,26 +40,26 @@ module ActionView private - def checked?(value) - case value - when TrueClass, FalseClass - value == !!@checked_value - when NilClass - false - when String - value == @checked_value - else - if value.respond_to?(:include?) - value.include?(@checked_value) + def checked?(value) + case value + when TrueClass, FalseClass + value == !!@checked_value + when NilClass + false + when String + value == @checked_value else - value.to_i == @checked_value.to_i + if value.respond_to?(:include?) + value.include?(@checked_value) + else + value.to_i == @checked_value.to_i + end end end - end - def hidden_field_for_checkbox(options) - @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe - end + def hidden_field_for_checkbox(options) + @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe + end end end end diff --git a/actionview/lib/action_view/helpers/tags/checkable.rb b/actionview/lib/action_view/helpers/tags/checkable.rb index 052e9df662..776fefe778 100644 --- a/actionview/lib/action_view/helpers/tags/checkable.rb +++ b/actionview/lib/action_view/helpers/tags/checkable.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: module Checkable # :nodoc: - def input_checked?(object, options) + def input_checked?(options) if options.has_key?("checked") checked = options.delete "checked" checked == true || checked == "checked" else - checked?(value(object)) + checked?(value) end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 3dda47a458..455442178e 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -1,4 +1,6 @@ -require 'action_view/helpers/tags/collection_helpers' +# frozen_string_literal: true + +require "action_view/helpers/tags/collection_helpers" module ActionView module Helpers @@ -7,9 +9,10 @@ module ActionView include CollectionHelpers class CheckBoxBuilder < Builder # :nodoc: - def check_box(extra_html_options={}) + def check_box(extra_html_options = {}) html_options = extra_html_options.merge(@input_html_options) html_options[:multiple] = true + html_options[:skip_default_ids] = false @template_object.check_box(@object_name, @method_name, html_options, @value, nil) end end @@ -20,13 +23,13 @@ module ActionView private - def render_component(builder) - builder.check_box + builder.label - end + def render_component(builder) + builder.check_box + builder.label + end - def hidden_field_name #:nodoc: - "#{super}[]" - end + def hidden_field_name + "#{super}[]" + end end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb index fb51460c8e..e1ad11bff8 100644 --- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: @@ -17,7 +19,7 @@ module ActionView @input_html_options = input_html_options end - def label(label_html_options={}, &block) + def label(label_html_options = {}, &block) html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options) html_options[:for] ||= @input_html_options[:id] if @input_html_options[:id] @@ -36,81 +38,81 @@ module ActionView private - def instantiate_builder(builder_class, item, value, text, html_options) - builder_class.new(@template_object, @object_name, @method_name, item, - sanitize_attribute_name(value), text, value, html_options) - end - - # Generate default options for collection helpers, such as :checked and - # :disabled. - def default_html_options_for_collection(item, value) #:nodoc: - html_options = @html_options.dup - - [:checked, :selected, :disabled, :readonly].each do |option| - current_value = @options[option] - next if current_value.nil? + def instantiate_builder(builder_class, item, value, text, html_options) + builder_class.new(@template_object, @object_name, @method_name, item, + sanitize_attribute_name(value), text, value, html_options) + end - accept = if current_value.respond_to?(:call) - current_value.call(item) - else - Array(current_value).map(&:to_s).include?(value.to_s) + # Generate default options for collection helpers, such as :checked and + # :disabled. + def default_html_options_for_collection(item, value) + html_options = @html_options.dup + + [:checked, :selected, :disabled, :readonly].each do |option| + current_value = @options[option] + next if current_value.nil? + + accept = if current_value.respond_to?(:call) + current_value.call(item) + else + Array(current_value).map(&:to_s).include?(value.to_s) + end + + if accept + html_options[option] = true + elsif option == :checked + html_options[option] = false + end end - if accept - html_options[option] = true - elsif option == :checked - html_options[option] = false - end + html_options[:object] = @object + html_options end - html_options[:object] = @object - html_options - end + def sanitize_attribute_name(value) + "#{sanitized_method_name}_#{sanitized_value(value)}" + end - def sanitize_attribute_name(value) #:nodoc: - "#{sanitized_method_name}_#{sanitized_value(value)}" - end + def render_collection + @collection.map do |item| + value = value_for_collection(item, @value_method) + text = value_for_collection(item, @text_method) + default_html_options = default_html_options_for_collection(item, value) + additional_html_options = option_html_attributes(item) - def render_collection #:nodoc: - @collection.map do |item| - value = value_for_collection(item, @value_method) - text = value_for_collection(item, @text_method) - default_html_options = default_html_options_for_collection(item, value) - additional_html_options = option_html_attributes(item) + yield item, value, text, default_html_options.merge(additional_html_options) + end.join.html_safe + end - yield item, value, text, default_html_options.merge(additional_html_options) - end.join.html_safe - end + def render_collection_for(builder_class, &block) + options = @options.stringify_keys + rendered_collection = render_collection do |item, value, text, default_html_options| + builder = instantiate_builder(builder_class, item, value, text, default_html_options) - def render_collection_for(builder_class, &block) #:nodoc: - options = @options.stringify_keys - rendered_collection = render_collection do |item, value, text, default_html_options| - builder = instantiate_builder(builder_class, item, value, text, default_html_options) + if block_given? + @template_object.capture(builder, &block) + else + render_component(builder) + end + end - if block_given? - @template_object.capture(builder, &block) + # Prepend a hidden field to make sure something will be sent back to the + # server if all radio buttons are unchecked. + if options.fetch("include_hidden", true) + hidden_field + rendered_collection else - render_component(builder) + rendered_collection end end - # Prepend a hidden field to make sure something will be sent back to the - # server if all radio buttons are unchecked. - if options.fetch('include_hidden', true) - hidden_field + rendered_collection - else - rendered_collection + def hidden_field + hidden_name = @html_options[:name] || hidden_field_name + @template_object.hidden_field_tag(hidden_name, "", id: nil) end - end - def hidden_field #:nodoc: - hidden_name = @html_options[:name] || hidden_field_name - @template_object.hidden_field_tag(hidden_name, "", id: nil) - end - - def hidden_field_name #:nodoc: - "#{tag_name(false, @options[:index])}" - end + def hidden_field_name + "#{tag_name(false, @options[:index])}" + end end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb index 21aaf122f8..16d37134e5 100644 --- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb +++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -1,4 +1,6 @@ -require 'action_view/helpers/tags/collection_helpers' +# frozen_string_literal: true + +require "action_view/helpers/tags/collection_helpers" module ActionView module Helpers @@ -7,8 +9,9 @@ module ActionView include CollectionHelpers class RadioButtonBuilder < Builder # :nodoc: - def radio_button(extra_html_options={}) + def radio_button(extra_html_options = {}) html_options = extra_html_options.merge(@input_html_options) + html_options[:skip_default_ids] = false @template_object.radio_button(@object_name, @method_name, @value, html_options) end end diff --git a/actionview/lib/action_view/helpers/tags/collection_select.rb b/actionview/lib/action_view/helpers/tags/collection_select.rb index 6cb2b2e0d3..6a3af1b256 100644 --- a/actionview/lib/action_view/helpers/tags/collection_select.rb +++ b/actionview/lib/action_view/helpers/tags/collection_select.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: @@ -13,8 +15,8 @@ module ActionView def render option_tags_options = { - :selected => @options.fetch(:selected) { value(@object) }, - :disabled => @options[:disabled] + selected: @options.fetch(:selected) { value }, + disabled: @options[:disabled] } select_content_tag( diff --git a/actionview/lib/action_view/helpers/tags/color_field.rb b/actionview/lib/action_view/helpers/tags/color_field.rb index b4bbe92746..39ab1285c3 100644 --- a/actionview/lib/action_view/helpers/tags/color_field.rb +++ b/actionview/lib/action_view/helpers/tags/color_field.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: class ColorField < TextField # :nodoc: def render options = @options.stringify_keys - options["value"] ||= validate_color_string(value(object)) + options["value"] ||= validate_color_string(value) @options = options super end @@ -13,7 +15,7 @@ module ActionView def validate_color_string(string) regex = /#[0-9a-fA-F]{6}/ - if regex.match(string) + if regex.match?(string) string.downcase else "#000000" diff --git a/actionview/lib/action_view/helpers/tags/date_field.rb b/actionview/lib/action_view/helpers/tags/date_field.rb index c22be0db29..b17a907651 100644 --- a/actionview/lib/action_view/helpers/tags/date_field.rb +++ b/actionview/lib/action_view/helpers/tags/date_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/date_select.rb b/actionview/lib/action_view/helpers/tags/date_select.rb index 0c4ac40070..fe4e3914d7 100644 --- a/actionview/lib/action_view/helpers/tags/date_select.rb +++ b/actionview/lib/action_view/helpers/tags/date_select.rb @@ -1,4 +1,6 @@ -require 'active_support/core_ext/time/calculations' +# frozen_string_literal: true + +require "active_support/core_ext/time/calculations" module ActionView module Helpers @@ -16,56 +18,56 @@ module ActionView class << self def select_type - @select_type ||= self.name.split("::").last.sub("Select", "").downcase + @select_type ||= name.split("::").last.sub("Select", "").downcase end end private - def select_type - self.class.select_type - end + def select_type + self.class.select_type + end - def datetime_selector(options, html_options) - datetime = options.fetch(:selected) { value(object) || default_datetime(options) } - @auto_index ||= nil + def datetime_selector(options, html_options) + datetime = options.fetch(:selected) { value || default_datetime(options) } + @auto_index ||= nil - options = options.dup - options[:field_name] = @method_name - options[:include_position] = true - options[:prefix] ||= @object_name - options[:index] = @auto_index if @auto_index && !options.has_key?(:index) + options = options.dup + options[:field_name] = @method_name + options[:include_position] = true + options[:prefix] ||= @object_name + options[:index] = @auto_index if @auto_index && !options.has_key?(:index) - DateTimeSelector.new(datetime, options, html_options) - end + DateTimeSelector.new(datetime, options, html_options) + end - def default_datetime(options) - return if options[:include_blank] || options[:prompt] + def default_datetime(options) + return if options[:include_blank] || options[:prompt] - case options[:default] - when nil - Time.current - when Date, Time - options[:default] - else - default = options[:default].dup + case options[:default] + when nil + Time.current + when Date, Time + options[:default] + else + default = options[:default].dup - # Rename :minute and :second to :min and :sec - default[:min] ||= default[:minute] - default[:sec] ||= default[:second] + # Rename :minute and :second to :min and :sec + default[:min] ||= default[:minute] + default[:sec] ||= default[:second] - time = Time.current + time = Time.current - [:year, :month, :day, :hour, :min, :sec].each do |key| - default[key] ||= time.send(key) - end + [:year, :month, :day, :hour, :min, :sec].each do |key| + default[key] ||= time.send(key) + end - Time.utc( - default[:year], default[:month], default[:day], - default[:hour], default[:min], default[:sec] - ) + Time.utc( + default[:year], default[:month], default[:day], + default[:hour], default[:min], default[:sec] + ) + end end - end end end end diff --git a/actionview/lib/action_view/helpers/tags/datetime_field.rb b/actionview/lib/action_view/helpers/tags/datetime_field.rb index b2cee9d198..5d9b639b1b 100644 --- a/actionview/lib/action_view/helpers/tags/datetime_field.rb +++ b/actionview/lib/action_view/helpers/tags/datetime_field.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: class DatetimeField < TextField # :nodoc: def render options = @options.stringify_keys - options["value"] ||= format_date(value(object)) + options["value"] ||= format_date(value) options["min"] = format_date(datetime_value(options["min"])) options["max"] = format_date(datetime_value(options["max"])) @options = options @@ -14,7 +16,7 @@ module ActionView private def format_date(value) - value.try(:strftime, "%Y-%m-%dT%T.%L%z") + raise NotImplementedError end def datetime_value(value) diff --git a/actionview/lib/action_view/helpers/tags/datetime_local_field.rb b/actionview/lib/action_view/helpers/tags/datetime_local_field.rb index b4a74185d1..d8f8fd00d1 100644 --- a/actionview/lib/action_view/helpers/tags/datetime_local_field.rb +++ b/actionview/lib/action_view/helpers/tags/datetime_local_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/datetime_select.rb b/actionview/lib/action_view/helpers/tags/datetime_select.rb index 563de1840e..dc5570931d 100644 --- a/actionview/lib/action_view/helpers/tags/datetime_select.rb +++ b/actionview/lib/action_view/helpers/tags/datetime_select.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/email_field.rb b/actionview/lib/action_view/helpers/tags/email_field.rb index 7ce3ccb9bf..0c3b9224fa 100644 --- a/actionview/lib/action_view/helpers/tags/email_field.rb +++ b/actionview/lib/action_view/helpers/tags/email_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/file_field.rb b/actionview/lib/action_view/helpers/tags/file_field.rb index e6a1d9c62d..0b1d9bb778 100644 --- a/actionview/lib/action_view/helpers/tags/file_field.rb +++ b/actionview/lib/action_view/helpers/tags/file_field.rb @@ -1,22 +1,9 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: class FileField < TextField # :nodoc: - - def render - options = @options.stringify_keys - - if options.fetch("include_hidden", true) - add_default_name_and_id(options) - options[:type] = "file" - tag("input", name: options["name"], type: "hidden", value: "") + tag("input", options) - else - options.delete("include_hidden") - @options = options - - super - end - end end end end diff --git a/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb b/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb index 2ed4712dac..f24cb4beea 100644 --- a/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb +++ b/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: @@ -15,8 +17,8 @@ module ActionView def render option_tags_options = { - :selected => @options.fetch(:selected) { value(@object) }, - :disabled => @options[:disabled] + selected: @options.fetch(:selected) { value }, + disabled: @options[:disabled] } select_content_tag( diff --git a/actionview/lib/action_view/helpers/tags/hidden_field.rb b/actionview/lib/action_view/helpers/tags/hidden_field.rb index c3757c2461..e014bd3aef 100644 --- a/actionview/lib/action_view/helpers/tags/hidden_field.rb +++ b/actionview/lib/action_view/helpers/tags/hidden_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb index b31d5fda66..02bd099784 100644 --- a/actionview/lib/action_view/helpers/tags/label.rb +++ b/actionview/lib/action_view/helpers/tags/label.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/month_field.rb b/actionview/lib/action_view/helpers/tags/month_field.rb index 4c0fb846ee..93b2bf11f0 100644 --- a/actionview/lib/action_view/helpers/tags/month_field.rb +++ b/actionview/lib/action_view/helpers/tags/month_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/number_field.rb b/actionview/lib/action_view/helpers/tags/number_field.rb index 4f95b1b4de..41c696423c 100644 --- a/actionview/lib/action_view/helpers/tags/number_field.rb +++ b/actionview/lib/action_view/helpers/tags/number_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/password_field.rb b/actionview/lib/action_view/helpers/tags/password_field.rb index 6099fa6f19..9f10f5236e 100644 --- a/actionview/lib/action_view/helpers/tags/password_field.rb +++ b/actionview/lib/action_view/helpers/tags/password_field.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: class PasswordField < TextField # :nodoc: def render - @options = {:value => nil}.merge!(@options) + @options = { value: nil }.merge!(@options) super end end diff --git a/actionview/lib/action_view/helpers/tags/placeholderable.rb b/actionview/lib/action_view/helpers/tags/placeholderable.rb index cf7b117614..e9f7601e57 100644 --- a/actionview/lib/action_view/helpers/tags/placeholderable.rb +++ b/actionview/lib/action_view/helpers/tags/placeholderable.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/radio_button.rb b/actionview/lib/action_view/helpers/tags/radio_button.rb index 4849c537a5..621db2b1b5 100644 --- a/actionview/lib/action_view/helpers/tags/radio_button.rb +++ b/actionview/lib/action_view/helpers/tags/radio_button.rb @@ -1,4 +1,6 @@ -require 'action_view/helpers/tags/checkable' +# frozen_string_literal: true + +require "action_view/helpers/tags/checkable" module ActionView module Helpers @@ -15,16 +17,16 @@ module ActionView options = @options.stringify_keys options["type"] = "radio" options["value"] = @tag_value - options["checked"] = "checked" if input_checked?(object, options) + options["checked"] = "checked" if input_checked?(options) add_default_name_and_id_for_value(@tag_value, options) tag("input", options) end private - def checked?(value) - value.to_s == @tag_value.to_s - end + def checked?(value) + value.to_s == @tag_value.to_s + end end end end diff --git a/actionview/lib/action_view/helpers/tags/range_field.rb b/actionview/lib/action_view/helpers/tags/range_field.rb index f98ae88043..66d1bbac5b 100644 --- a/actionview/lib/action_view/helpers/tags/range_field.rb +++ b/actionview/lib/action_view/helpers/tags/range_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/search_field.rb b/actionview/lib/action_view/helpers/tags/search_field.rb index a848aeabfa..f209348904 100644 --- a/actionview/lib/action_view/helpers/tags/search_field.rb +++ b/actionview/lib/action_view/helpers/tags/search_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb index 180900cc8d..790721a0b7 100644 --- a/actionview/lib/action_view/helpers/tags/select.rb +++ b/actionview/lib/action_view/helpers/tags/select.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: @@ -13,8 +15,8 @@ module ActionView def render option_tags_options = { - :selected => @options.fetch(:selected) { value(@object) }, - :disabled => @options[:disabled] + selected: @options.fetch(:selected) { value }, + disabled: @options[:disabled] } option_tags = if grouped_choices? @@ -28,13 +30,13 @@ module ActionView private - # Grouped choices look like this: - # - # [nil, []] - # { nil => [] } - def grouped_choices? - !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last - end + # Grouped choices look like this: + # + # [nil, []] + # { nil => [] } + def grouped_choices? + !@choices.blank? && @choices.first.respond_to?(:last) && Array === @choices.first.last + end end end end diff --git a/actionview/lib/action_view/helpers/tags/tel_field.rb b/actionview/lib/action_view/helpers/tags/tel_field.rb index 987bb9e67a..ab1caaac48 100644 --- a/actionview/lib/action_view/helpers/tags/tel_field.rb +++ b/actionview/lib/action_view/helpers/tags/tel_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb index 69038c1498..4519082ff6 100644 --- a/actionview/lib/action_view/helpers/tags/text_area.rb +++ b/actionview/lib/action_view/helpers/tags/text_area.rb @@ -1,4 +1,6 @@ -require 'action_view/helpers/tags/placeholderable' +# frozen_string_literal: true + +require "action_view/helpers/tags/placeholderable" module ActionView module Helpers @@ -14,7 +16,7 @@ module ActionView options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split) end - content_tag("textarea", options.delete("value") { value_before_type_cast(object) }, options) + content_tag("textarea", options.delete("value") { value_before_type_cast }, options) end end end diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb index 5c576a20ca..d92967e212 100644 --- a/actionview/lib/action_view/helpers/tags/text_field.rb +++ b/actionview/lib/action_view/helpers/tags/text_field.rb @@ -1,4 +1,6 @@ -require 'action_view/helpers/tags/placeholderable' +# frozen_string_literal: true + +require "action_view/helpers/tags/placeholderable" module ActionView module Helpers @@ -10,22 +12,22 @@ module ActionView options = @options.stringify_keys options["size"] = options["maxlength"] unless options.key?("size") options["type"] ||= field_type - options["value"] = options.fetch("value") { value_before_type_cast(object) } unless field_type == "file" + options["value"] = options.fetch("value") { value_before_type_cast } unless field_type == "file" add_default_name_and_id(options) tag("input", options) end class << self def field_type - @field_type ||= self.name.split("::").last.sub("Field", "").downcase + @field_type ||= name.split("::").last.sub("Field", "").downcase end end private - def field_type - self.class.field_type - end + def field_type + self.class.field_type + end end end end diff --git a/actionview/lib/action_view/helpers/tags/time_field.rb b/actionview/lib/action_view/helpers/tags/time_field.rb index 0e90a3aed7..9384a83a3e 100644 --- a/actionview/lib/action_view/helpers/tags/time_field.rb +++ b/actionview/lib/action_view/helpers/tags/time_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/time_select.rb b/actionview/lib/action_view/helpers/tags/time_select.rb index 0b06311d25..ba3dcb64e3 100644 --- a/actionview/lib/action_view/helpers/tags/time_select.rb +++ b/actionview/lib/action_view/helpers/tags/time_select.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/time_zone_select.rb b/actionview/lib/action_view/helpers/tags/time_zone_select.rb index 80d165ec7e..1d06096096 100644 --- a/actionview/lib/action_view/helpers/tags/time_zone_select.rb +++ b/actionview/lib/action_view/helpers/tags/time_zone_select.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: @@ -11,7 +13,7 @@ module ActionView def render select_content_tag( - time_zone_options_for_select(value(@object) || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options + time_zone_options_for_select(value || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options ) end end diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb index 8b6655481d..e81ca3aef0 100644 --- a/actionview/lib/action_view/helpers/tags/translator.rb +++ b/actionview/lib/action_view/helpers/tags/translator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: @@ -14,26 +16,23 @@ module ActionView translated_attribute || human_attribute_name end - protected - - attr_reader :object_name, :method_and_value, :scope, :model - private + attr_reader :object_name, :method_and_value, :scope, :model - def i18n_default - if model - key = model.model_name.i18n_key - ["#{key}.#{method_and_value}".to_sym, ""] - else - "" + def i18n_default + if model + key = model.model_name.i18n_key + ["#{key}.#{method_and_value}".to_sym, ""] + else + "" + end end - end - def human_attribute_name - if model && model.class.respond_to?(:human_attribute_name) - model.class.human_attribute_name(method_and_value) + def human_attribute_name + if model && model.class.respond_to?(:human_attribute_name) + model.class.human_attribute_name(method_and_value) + end end - end end end end diff --git a/actionview/lib/action_view/helpers/tags/url_field.rb b/actionview/lib/action_view/helpers/tags/url_field.rb index d76340178d..395fec67e7 100644 --- a/actionview/lib/action_view/helpers/tags/url_field.rb +++ b/actionview/lib/action_view/helpers/tags/url_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/tags/week_field.rb b/actionview/lib/action_view/helpers/tags/week_field.rb index 835d1667d7..572535d1d6 100644 --- a/actionview/lib/action_view/helpers/tags/week_field.rb +++ b/actionview/lib/action_view/helpers/tags/week_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Helpers module Tags # :nodoc: diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 58ce042f12..c282505e13 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -1,5 +1,7 @@ -require 'active_support/core_ext/string/filters' -require 'active_support/core_ext/array/extract_options' +# frozen_string_literal: true + +require "active_support/core_ext/string/filters" +require "active_support/core_ext/array/extract_options" module ActionView # = Action View Text Helpers @@ -11,9 +13,9 @@ module ActionView # # ==== Sanitization # - # Most text helpers by default sanitize the given content, but do not escape it. - # This means HTML tags will appear in the page but all malicious code will be removed. - # Let's look at some examples using the +simple_format+ method: + # Most text helpers that generate HTML output sanitize the given input by default, + # but do not escape it. This means HTML tags will appear in the page but all malicious + # code will be removed. Let's look at some examples using the +simple_format+ method: # # simple_format('<a href="http://example.com/">Example</a>') # # => "<p><a href=\"http://example.com/\">Example</a></p>" @@ -126,7 +128,7 @@ module ActionView # # => You searched for: <a href="search?q=rails">rails</a> # # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false) - # # => "<a>ruby</a> on <mark>rails</mark>" + # # => <a href="javascript:alert('no!')">ruby</a> on <mark>rails</mark> def highlight(text, phrases, options = {}) text = sanitize(text) if options.fetch(:sanitize, true) @@ -135,7 +137,7 @@ module ActionView else match = Array(phrases).map do |p| Regexp === p ? p.to_s : Regexp.escape(p) - end.join('|') + end.join("|") if block_given? text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found } @@ -151,7 +153,7 @@ module ActionView # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+, # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. Use the # <tt>:separator</tt> option to choose the delimitation. The resulting string will be stripped in any case. If the +phrase+ - # isn't found, nil is returned. + # isn't found, +nil+ is returned. # # excerpt('This is an example', 'an', radius: 5) # # => ...s is an exam... @@ -186,8 +188,8 @@ module ActionView unless separator.empty? text.split(separator).each do |value| - if value.match(regex) - regex = phrase = value + if value.match?(regex) + phrase = value break end end @@ -225,15 +227,8 @@ module ActionView # # pluralize(2, 'Person', locale: :de) # # => 2 Personen - def pluralize(count, singular, deprecated_plural = nil, plural: nil, locale: I18n.locale) - if deprecated_plural - ActiveSupport::Deprecation.warn("Passing plural as a positional argument " \ - "is deprecated and will be removed in Rails 5.1. Use e.g. " \ - "pluralize(1, 'person', plural: 'people') instead.") - plural ||= deprecated_plural - end - - word = if (count == 1 || count =~ /^1(\.0+)?$/) + def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale) + word = if count == 1 || count.to_s =~ /^1(\.0+)?$/ singular else plural || singular.pluralize(locale) @@ -264,15 +259,16 @@ module ActionView # # => Once\r\nupon\r\na\r\ntime def word_wrap(text, line_width: 80, break_sequence: "\n") text.split("\n").collect! do |line| - line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line + line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line end * break_sequence end # Returns +text+ transformed into HTML using simple formatting rules. - # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a - # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is - # considered as a linebreak and a <tt><br /></tt> tag is appended. This - # method does not remove the newlines from the +text+. + # Two or more consecutive newlines(<tt>\n\n</tt> or <tt>\r\n\r\n</tt>) are + # considered a paragraph and wrapped in <tt><p></tt> tags. One newline + # (<tt>\n</tt> or <tt>\r\n</tt>) is considered a linebreak and a + # <tt><br /></tt> tag is appended. This method does not remove the + # newlines from the +text+. # # You can pass any HTML attributes into <tt>html_options</tt>. These # will be added to all created paragraphs. @@ -357,7 +353,7 @@ module ActionView # <% end %> def cycle(first_value, *values) options = values.extract_options! - name = options.fetch(:name, 'default') + name = options.fetch(:name, "default") values.unshift(*first_value) @@ -426,22 +422,22 @@ module ActionView def to_s value = @values[@index].to_s @index = next_index - return value + value end private - def next_index - step_index(1) - end + def next_index + step_index(1) + end - def previous_index - step_index(-1) - end + def previous_index + step_index(-1) + end - def step_index(n) - (@index + n) % @values.size - end + def step_index(n) + (@index + n) % @values.size + end end private @@ -450,7 +446,7 @@ module ActionView # uses an instance variable of ActionView::Base. def get_cycle(name) @_cycles = Hash.new unless defined?(@_cycles) - return @_cycles[name] + @_cycles[name] end def set_cycle(name, cycle_object) diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 152e1b1211..d5b0a9263f 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -1,18 +1,19 @@ -require 'action_view/helpers/tag_helper' -require 'active_support/core_ext/string/access' -require 'i18n/exceptions' +# frozen_string_literal: true + +require "action_view/helpers/tag_helper" +require "active_support/core_ext/string/access" +require "i18n/exceptions" module ActionView # = Action View Translation Helpers - module Helpers + module Helpers #:nodoc: module TranslationHelper extend ActiveSupport::Concern include TagHelper included do - mattr_accessor :debug_missing_translation - self.debug_missing_translation = true + mattr_accessor :debug_missing_translation, default: true end # Delegates to <tt>I18n#translate</tt> but also performs three additional @@ -58,11 +59,9 @@ module ActionView # they can provide HTML values for. def translate(key, options = {}) options = options.dup - has_default = options.has_key?(:default) - remaining_defaults = Array(options.delete(:default)).compact - - if has_default && !remaining_defaults.first.kind_of?(Symbol) - options[:default] = remaining_defaults + if options.has_key?(:default) + remaining_defaults = Array(options.delete(:default)).compact + options[:default] = remaining_defaults unless remaining_defaults.first.kind_of?(Symbol) end # If the user has explicitly decided to NOT raise errors, pass that option to I18n. @@ -84,8 +83,11 @@ module ActionView end end translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise)) - - translation.respond_to?(:html_safe) ? translation.html_safe : translation + if translation.respond_to?(:map) + translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element } + else + translation.respond_to?(:html_safe) ? translation.html_safe : translation + end else I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise)) end @@ -96,23 +98,23 @@ module ActionView raise e if raise_error keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) - title = "translation missing: #{keys.join('.')}" + title = +"translation missing: #{keys.join('.')}" interpolations = options.except(:default, :scope) if interpolations.any? - title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(', ') + title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ") end return title unless ActionView::Base.debug_missing_translation - content_tag('span', keys.last.to_s.titleize, class: 'translation_missing', title: title) + content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title) end end alias :t :translate # 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) @@ -121,9 +123,12 @@ module ActionView private def scope_key_by_partial(key) - if key.to_s.first == "." + stringified_key = key.to_s + if stringified_key.first == "." if @virtual_path - @virtual_path.gsub(%r{/_?}, ".") + key.to_s + @_scope_key_by_partial_cache ||= {} + @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".") + "#{@_scope_key_by_partial_cache[@virtual_path]}#{stringified_key}" else raise "Cannot use t(#{key.inspect}) shortcut because path is not available" end @@ -133,7 +138,7 @@ module ActionView end def html_safe_translation_key?(key) - key.to_s =~ /(\b|_|\.)html$/ + /(?:_|\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 11c7daf4da..df83dff681 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -1,7 +1,9 @@ -require 'action_view/helpers/javascript_helper' -require 'active_support/core_ext/array/access' -require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/string/output_safety' +# frozen_string_literal: true + +require "action_view/helpers/javascript_helper" +require "active_support/core_ext/array/access" +require "active_support/core_ext/hash/keys" +require "active_support/core_ext/string/output_safety" module ActionView # = Action View URL Helpers @@ -35,20 +37,20 @@ module ActionView when :back _back_url else - raise ArgumentError, "arguments passed to url_for can't be handled. Please require " + + raise ArgumentError, "arguments passed to url_for can't be handled. Please require " \ "routes or provide your own implementation" end end def _back_url # :nodoc: - _filtered_referrer || 'javascript:history.back()' + _filtered_referrer || "javascript:history.back()" end protected :_back_url def _filtered_referrer # :nodoc: if controller.respond_to?(:request) referrer = controller.request.env["HTTP_REFERER"] - if referrer && URI(referrer).scheme != 'javascript' + if referrer && URI(referrer).scheme != "javascript" referrer end end @@ -105,10 +107,9 @@ module ActionView # driver to prompt with the question specified (in this case, the # resulting text would be <tt>question?</tt>. If the user accepts, the # link is processed normally, otherwise no action is taken. - # * <tt>:disable_with</tt> - Value of this parameter will be - # used as the value for a disabled version of the submit - # button when the form is submitted. This feature is provided - # by the unobtrusive JavaScript driver. + # * <tt>:disable_with</tt> - Value of this parameter will be used as the + # name for a disabled version of the link. This feature is provided by + # the unobtrusive JavaScript driver. # # ==== Examples # Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments @@ -138,6 +139,11 @@ module ActionView # link_to "Profiles", controller: "profiles" # # => <a href="/profiles">Profiles</a> # + # When name is +nil+ the href is presented instead + # + # link_to nil, "http://example.com" + # # => <a href="http://www.example.com">http://www.example.com</a> + # # You can use a block as well if your link target is hard to fit into the name parameter. ERB example: # # <%= link_to(@profile) do %> @@ -194,9 +200,9 @@ module ActionView html_options = convert_options_to_data_attributes(options, html_options) url = url_for(options) - html_options["href".freeze] ||= url + html_options["href"] ||= url - content_tag("a".freeze, name || url, html_options, &block) + content_tag("a", name || url, html_options, &block) end # Generates a form containing a single button that submits to the URL created @@ -298,34 +304,34 @@ module ActionView html_options = html_options.stringify_keys url = options.is_a?(String) ? options : url_for(options) - remote = html_options.delete('remote') - params = html_options.delete('params') + remote = html_options.delete("remote") + params = html_options.delete("params") - method = html_options.delete('method').to_s - method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.freeze.html_safe + method = html_options.delete("method").to_s + method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe - form_method = method == 'get' ? 'get' : 'post' - form_options = html_options.delete('form') || {} - form_options[:class] ||= html_options.delete('form_class') || 'button_to' + form_method = method == "get" ? "get" : "post" + form_options = html_options.delete("form") || {} + form_options[:class] ||= html_options.delete("form_class") || "button_to" form_options[:method] = form_method form_options[:action] = url form_options[:'data-remote'] = true if remote - request_token_tag = if form_method == 'post' - request_method = method.empty? ? 'post' : method + request_token_tag = if form_method == "post" + request_method = method.empty? ? "post" : method token_tag(nil, form_options: { action: url, method: request_method }) else - ''.freeze + "" end html_options = convert_options_to_data_attributes(options, html_options) - html_options['type'] = 'submit' + html_options["type"] = "submit" button = if block_given? - content_tag('button', html_options, &block) + content_tag("button", html_options, &block) else - html_options['value'] = name || url - tag('input', html_options) + html_options["value"] = name || url + tag("input", html_options) end inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag) @@ -334,7 +340,7 @@ module ActionView inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value]) end end - content_tag('form', inner_tags, form_options) + content_tag("form", inner_tags, form_options) end # Creates a link tag of the given +name+ using a URL created by the set of @@ -481,12 +487,12 @@ module ActionView option = html_options.delete(item).presence || next "#{item.dasherize}=#{ERB::Util.url_encode(option)}" }.compact - extras = extras.empty? ? ''.freeze : '?' + extras.join('&') + extras = extras.empty? ? "" : "?" + extras.join("&") encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@") html_options["href"] = "mailto:#{encoded_email_address}#{extras}" - content_tag("a".freeze, name || email_address, html_options, &block) + content_tag("a", name || email_address, html_options, &block) end # True if the current request URI was generated by the given +options+. @@ -518,6 +524,9 @@ module ActionView # current_page?('http://www.example.com/shop/checkout') # # => true # + # current_page?('http://www.example.com/shop/checkout', check_parameters: true) + # # => false + # # current_page?('/shop/checkout') # # => true # @@ -531,7 +540,7 @@ module ActionView # # We can also pass in the symbol arguments instead of strings. # - def current_page?(options) + def current_page?(options, check_parameters: false) unless request raise "You cannot use helpers that need to determine the current " \ "page unless your view context provides a Request object " \ @@ -540,15 +549,22 @@ module ActionView return false unless request.get? || request.head? + check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters) 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 - request_uri = url_string.index("?") ? request.fullpath : request.path + # the behaviour can be disabled with check_parameters: true + request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY) - if url_string =~ /^\w+:\/\// + if url_string.start_with?("/") && url_string != "/" + url_string.chomp!("/") + request_uri.chomp!("/") + end + + if %r{^\w+://}.match?(url_string) url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}" else url_string == request_uri @@ -559,51 +575,68 @@ module ActionView def convert_options_to_data_attributes(options, html_options) if html_options html_options = html_options.stringify_keys - html_options['data-remote'] = 'true'.freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options) + html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options) - method = html_options.delete('method'.freeze) + method = html_options.delete("method") add_method_to_attributes!(html_options, method) if method html_options else - link_to_remote_options?(options) ? {'data-remote' => 'true'.freeze} : {} + link_to_remote_options?(options) ? { "data-remote" => "true" } : {} end end def link_to_remote_options?(options) if options.is_a?(Hash) - options.delete('remote'.freeze) || options.delete(:remote) + options.delete("remote") || options.delete(:remote) end end def add_method_to_attributes!(html_options, method) - if method && method.to_s.downcase != "get".freeze && html_options["rel".freeze] !~ /nofollow/ - html_options["rel".freeze] = "#{html_options["rel".freeze]} nofollow".lstrip + if method_not_get_method?(method) && html_options["rel"] !~ /nofollow/ + if html_options["rel"].blank? + html_options["rel"] = "nofollow" + else + html_options["rel"] = "#{html_options["rel"]} nofollow" + end end - html_options["data-method".freeze] = method + html_options["data-method"] = method + end + + STRINGIFIED_COMMON_METHODS = { + get: "get", + delete: "delete", + patch: "patch", + post: "post", + put: "put", + }.freeze + + def method_not_get_method?(method) + return false unless method + (STRINGIFIED_COMMON_METHODS[method] || method.to_s.downcase) != "get" end - def token_tag(token=nil, form_options: {}) - if token != false && protect_against_forgery? + def token_tag(token = nil, form_options: {}) + if token != false && defined?(protect_against_forgery?) && protect_against_forgery? token ||= form_authenticity_token(form_options: form_options) tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token) else - ''.freeze + "" end end def method_tag(method) - tag('input', type: 'hidden', name: '_method', value: method.to_s) + tag("input", type: "hidden", name: "_method", value: method.to_s) end # Returns an array of hashes each containing :name and :value keys # suitable for use as the names and values of form input fields: # # to_form_params(name: 'David', nationality: 'Danish') - # # => [{name: :name, value: 'David'}, {name: 'nationality', value: 'Danish'}] + # # => [{name: 'name', value: 'David'}, {name: 'nationality', value: 'Danish'}] # - # to_form_params(country: {name: 'Denmark'}) + # to_form_params(country: { name: 'Denmark' }) # # => [{name: 'country[name]', value: 'Denmark'}] # # to_form_params(countries: ['Denmark', 'Sweden']}) @@ -613,7 +646,13 @@ module ActionView # # to_form_params({ name: 'Denmark' }, 'country') # # => [{name: 'country[name]', value: 'Denmark'}] - def to_form_params(attribute, namespace = nil) # :nodoc: + def to_form_params(attribute, namespace = nil) + attribute = if attribute.respond_to?(:permitted?) + attribute.to_h + else + attribute + end + params = [] case attribute when Hash @@ -627,7 +666,7 @@ module ActionView params.push(*to_form_params(value, array_prefix)) end else - params << { name: namespace, value: attribute.to_param } + params << { name: namespace.to_s, value: attribute.to_param } end params.sort_by { |pair| pair[:name] } diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index a74a5e05f3..08f66bf435 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require "action_view/rendering" -require "active_support/core_ext/module/remove_method" +require "active_support/core_ext/module/redefine_method" module ActionView # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in @@ -91,16 +93,16 @@ module ActionView # layout false # # In these examples, we have three implicit lookup scenarios: - # * The BankController uses the "bank" layout. - # * The ExchangeController uses the "exchange" layout. - # * The CurrencyController inherits the layout from BankController. + # * The +BankController+ uses the "bank" layout. + # * The +ExchangeController+ uses the "exchange" layout. + # * The +CurrencyController+ inherits the layout from BankController. # # However, when a layout is explicitly set, the explicitly set layout wins: - # * The InformationController uses the "information" layout, explicitly set. - # * The TellerController also uses the "information" layout, because the parent explicitly set it. - # * The EmployeeController uses the "employee" layout, because it set the layout to nil, resetting the parent configuration. - # * The VaultController chooses a layout dynamically by calling the <tt>access_level_layout</tt> method. - # * The TillController does not use a layout at all. + # * The +InformationController+ uses the "information" layout, explicitly set. + # * The +TellerController+ also uses the "information" layout, because the parent explicitly set it. + # * The +EmployeeController+ uses the "employee" layout, because it set the layout to +nil+, resetting the parent configuration. + # * The +VaultController+ chooses a layout dynamically by calling the <tt>access_level_layout</tt> method. + # * The +TillController+ does not use a layout at all. # # == Types of layouts # @@ -148,8 +150,8 @@ module ActionView # The template will be looked always in <tt>app/views/layouts/</tt> folder. But you can point # <tt>layouts</tt> folder direct also. <tt>layout "layouts/demo"</tt> is the same as <tt>layout "demo"</tt>. # - # Setting the layout to nil forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists. - # Setting it to nil is useful to re-enable template lookup overriding a previous configuration set in the parent: + # Setting the layout to +nil+ forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists. + # Setting it to +nil+ is useful to re-enable template lookup overriding a previous configuration set in the parent: # # class ApplicationController < ActionController::Base # layout "application" @@ -204,9 +206,9 @@ module ActionView include ActionView::Rendering included do - class_attribute :_layout, :_layout_conditions, :instance_accessor => false - self._layout = nil - self._layout_conditions = {} + class_attribute :_layout, instance_accessor: false + class_attribute :_layout_conditions, instance_accessor: false, default: {} + _write_layout_method end @@ -223,36 +225,39 @@ module ActionView module LayoutConditions # :nodoc: private - # Determines whether the current action has a layout definition by - # checking the action name against the :only and :except conditions - # set by the <tt>layout</tt> method. - # - # ==== Returns - # * <tt>Boolean</tt> - True if the action has a layout definition, false otherwise. - def _conditional_layout? - return unless super - - conditions = _layout_conditions - - if only = conditions[:only] - only.include?(action_name) - elsif except = conditions[:except] - !except.include?(action_name) - else - true + # Determines whether the current action has a layout definition by + # checking the action name against the :only and :except conditions + # set by the <tt>layout</tt> method. + # + # ==== Returns + # * <tt>Boolean</tt> - True if the action has a layout definition, false otherwise. + def _conditional_layout? + return unless super + + conditions = _layout_conditions + + if only = conditions[:only] + only.include?(action_name) + elsif except = conditions[:except] + !except.include?(action_name) + else + true + end end - end end # Specify the layout to use for this class. # # If the specified layout is a: # String:: the String is the template name - # Symbol:: call the method specified by the symbol, which will return the template name + # Symbol:: call the method specified by the symbol + # Proc:: call the passed Proc # false:: There is no layout # true:: raise an ArgumentError # nil:: Force default layout behavior with inheritance # + # Return value of +Proc+ and +Symbol+ arguments should be +String+, +false+, +true+ or +nil+ + # with the same meaning as described above. # ==== Parameters # * <tt>layout</tt> - The layout to use. # @@ -262,7 +267,7 @@ module ActionView def layout(layout, conditions = {}) include LayoutConditions unless conditions.empty? - conditions.each {|k, v| conditions[k] = Array(v).map(&:to_s) } + conditions.each { |k, v| conditions[k] = Array(v).map(&:to_s) } self._layout_conditions = conditions self._layout = layout @@ -274,9 +279,9 @@ module ActionView # If a layout is not explicitly mentioned then look for a layout with the controller's name. # if nothing is found then try same procedure to find super class's layout. def _write_layout_method # :nodoc: - remove_possible_method(:_layout) + silence_redefinition_of_method(:_layout) - prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"] + prefixes = /\blayouts/.match?(_implied_layout_name) ? [] : ["layouts"] default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}, false, [], { formats: formats }).first || super" name_clause = if name default_behavior @@ -286,7 +291,8 @@ module ActionView RUBY end - layout_definition = case _layout + layout_definition = \ + case _layout when String _layout.inspect when Symbol @@ -313,10 +319,10 @@ module ActionView raise ArgumentError, "Layouts must be specified as a String, Symbol, Proc, false, or nil" when nil name_clause - end + end - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _layout(formats) + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _layout(lookup_context, formats) if _conditional_layout? #{layout_definition} else @@ -329,14 +335,14 @@ module ActionView private - # If no layout is supplied, look for a template named the return - # value of this method. - # - # ==== Returns - # * <tt>String</tt> - A template name - def _implied_layout_name # :nodoc: - controller_path - end + # If no layout is supplied, look for a template named the return + # value of this method. + # + # ==== Returns + # * <tt>String</tt> - A template name + def _implied_layout_name + controller_path + end end def _normalize_options(options) # :nodoc: @@ -382,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, @@ -400,14 +406,14 @@ module ActionView # # ==== Parameters # * <tt>formats</tt> - The formats accepted to this layout - # * <tt>require_layout</tt> - If set to true and layout is not found, - # an +ArgumentError+ exception is raised (defaults to false) + # * <tt>require_layout</tt> - If set to +true+ and layout is not found, + # an +ArgumentError+ exception is raised (defaults to +false+) # # ==== Returns - # * <tt>template</tt> - The template object for the default layout (or nil) - def _default_layout(formats, require_layout = false) + # * <tt>template</tt> - The template object for the default layout (or +nil+) + 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 @@ -421,7 +427,7 @@ module ActionView end def _include_layout?(options) - (options.keys & [:body, :text, :plain, :html, :inline, :partial]).empty? || options.key?(:layout) + (options.keys & [:body, :plain, :html, :inline, :partial]).empty? || options.key?(:layout) end end end diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index 5a29c68214..227f025385 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -1,4 +1,6 @@ -require 'active_support/log_subscriber' +# frozen_string_literal: true + +require "active_support/log_subscriber" module ActionView # = Action View Log Subscriber @@ -14,19 +16,28 @@ module ActionView def render_template(event) info do - message = " Rendered #{from_rails_root(event.payload[:identifier])}" + message = +" Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << " (#{event.duration.round(1)}ms)" + message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" + end + end + + def render_partial(event) + info do + message = +" Rendered #{from_rails_root(event.payload[:identifier])}" + message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] + message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" + message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil? + message end end - alias :render_partial :render_template def render_collection(event) - identifier = event.payload[:identifier] || 'templates' + identifier = event.payload[:identifier] || "templates" info do " Rendered collection of #{from_rails_root(identifier)}" \ - " #{render_count(event.payload)} (#{event.duration.round(1)}ms)" + " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" end end @@ -42,20 +53,20 @@ module ActionView ActionView::Base.logger end - protected + private - EMPTY = '' - def from_rails_root(string) + EMPTY = "" + def from_rails_root(string) # :doc: string = string.sub(rails_root, EMPTY) string.sub!(VIEWS_PATTERN, EMPTY) string end - def rails_root + def rails_root # :doc: @root ||= "#{Rails.root}/" end - def render_count(payload) + def render_count(payload) # :doc: if payload[:cache_hits] "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]" else @@ -63,11 +74,18 @@ module ActionView end end - private + def cache_message(payload) # :doc: + case payload[:cache_hit] + when :hit + "[cache hit]" + when :miss + "[cache miss]" + end + end def log_rendering_start(payload) info do - message = " Rendering #{from_rails_root(payload[:identifier])}" + message = +" Rendering #{from_rails_root(payload[:identifier])}" message << " within #{from_rails_root(payload[:layout])}" if payload[:layout] message end diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 626c4b8f5e..fd3d025cbf 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -1,7 +1,10 @@ -require 'concurrent/map' -require 'active_support/core_ext/module/remove_method' -require 'active_support/core_ext/module/attribute_accessors' -require 'action_view/template/resolver' +# frozen_string_literal: true + +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 # = Action View Lookup Context @@ -13,18 +16,18 @@ 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 - @@fallbacks = FallbackFileSystemResolver.instances + mattr_accessor :fallbacks, default: FallbackFileSystemResolver.instances - mattr_accessor :registered_details - self.registered_details = [] + mattr_accessor :registered_details, default: [] def self.register_detail(name, &block) - self.registered_details << name + registered_details << name Accessors::DEFAULT_PROCS[name] = block - Accessors.send :define_method, :"default_#{name}", &block + Accessors.define_method(:"default_#{name}", &block) Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{name} @details.fetch(:#{name}, []) @@ -57,27 +60,36 @@ module ActionView alias :eql? :equal? @details_keys = Concurrent::Map.new + @digest_cache = Concurrent::Map.new + + def self.digest_cache(details) + @digest_cache[details_cache_key(details)] ||= Concurrent::Map.new + end - def self.get(details) + def self.details_cache_key(details) if details[:formats] details = details.dup details[:formats] &= Template::Types.symbols end - @details_keys[details] ||= 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.map(&:digest_cache) + @digest_cache.values end - attr_reader :digest_cache - - def initialize - @digest_cache = Concurrent::Map.new + def self.view_context_class(klass) + @view_context_class ||= klass.with_empty_template_cache end end @@ -88,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. @@ -99,10 +111,11 @@ module ActionView @cache = old_value end - protected + private - def _set_detail(key, value) - @details = @details.dup if @details_key + def _set_detail(key, value) # :doc: + @details = @details.dup if @digest_cache || @details_key + @digest_cache = nil @details_key = nil @details[key] = value end @@ -112,12 +125,6 @@ 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 @@ -144,32 +151,47 @@ 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 - protected + 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) #:nodoc: + def args_for_lookup(name, prefixes, partial, keys, details_options) name, prefixes = normalize_name(name, prefixes) details, details_key = detail_args_for(details_options) [name, prefixes, partial || false, details, details_key, keys] end # Compute details hash and key according to user options (e.g. passed from #render). - def detail_args_for(options) + def detail_args_for(options) # :doc: return @details, details_key if options.empty? # most common path. 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 @@ -177,13 +199,13 @@ module ActionView [user_details, details_key] end - def args_for_any(name, prefixes, partial) # :nodoc: + def args_for_any(name, prefixes, partial) name, prefixes = normalize_name(name, prefixes) details, details_key = detail_args_for_any [name, prefixes, partial || false, details, details_key] end - def detail_args_for_any # :nodoc: + def detail_args_for_any @detail_args_for_any ||= begin details = {} @@ -196,7 +218,7 @@ module ActionView end if @cache - [details, DetailsKey.get(details)] + [details, DetailsKey.details_cache_key(details)] else [details, nil] end @@ -206,15 +228,15 @@ module ActionView # Support legacy foo.erb names even though we now ignore .erb # as well as incorrectly putting part of the path in the template # name instead of the prefix. - def normalize_name(name, prefixes) #:nodoc: + def normalize_name(name, prefixes) prefixes = prefixes.presence - parts = name.to_s.split('/'.freeze) + parts = name.to_s.split("/") parts.shift if parts.first.empty? - name = parts.pop + name = parts.pop return name, prefixes || [""] if parts.empty? - parts = parts.join('/'.freeze) + parts = parts.join("/") prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts] return name, prefixes @@ -227,16 +249,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 + @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) @@ -251,7 +280,15 @@ module ActionView # add :html as fallback to :js. def formats=(values) if values - values.concat(default_formats) if values.delete "*/*".freeze + 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/model_naming.rb b/actionview/lib/action_view/model_naming.rb index b6ed13424e..23cca8d607 100644 --- a/actionview/lib/action_view/model_naming.rb +++ b/actionview/lib/action_view/model_naming.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module ModelNaming #:nodoc: # Converts the given object to an ActiveModel compliant one. diff --git a/actionview/lib/action_view/path_set.rb b/actionview/lib/action_view/path_set.rb index f68d2a77ed..691b53e2da 100644 --- a/actionview/lib/action_view/path_set.rb +++ b/actionview/lib/action_view/path_set.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView #:nodoc: # = Action View PathSet # @@ -69,30 +71,30 @@ module ActionView #:nodoc: private - def _find_all(path, prefixes, args, outside_app) - 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) + def _find_all(path, prefixes, args, outside_app) + 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 + return templates unless templates.empty? end - return templates unless templates.empty? end + [] end - [] - end - def typecast(paths) - paths.map do |path| - case path - when Pathname, String - OptimizedFileSystemResolver.new path.to_s - else - path + def typecast(paths) + paths.map do |path| + case path + when Pathname, String + OptimizedFileSystemResolver.new path.to_s + else + path + end end end - end end end diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index df14ae09f4..8bd526cdf9 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -1,12 +1,18 @@ +# frozen_string_literal: true + require "action_view" require "rails" module ActionView # = Action View Railtie - class Railtie < Rails::Railtie # :nodoc: + class Railtie < Rails::Engine # :nodoc: + NULL_OPTION = Object.new + config.action_view = ActiveSupport::OrderedOptions.new - config.action_view.embed_authenticity_token_in_remote_forms = false + 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 = NULL_OPTION config.eager_load_namespaces << ActionView @@ -17,13 +23,48 @@ module ActionView end end + initializer "action_view.form_with_generates_remote_forms" do |app| + ActiveSupport.on_load(:action_view) do + form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms) + ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms + end + end + + initializer "action_view.form_with_generates_ids" do |app| + ActiveSupport.on_load(:action_view) do + form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids) + unless form_with_generates_ids.nil? + ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids + end + end + end + + initializer "action_view.default_enforce_utf8" do |app| + ActiveSupport.on_load(:action_view) do + default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8) + unless default_enforce_utf8.nil? + ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8 + end + end + end + + initializer "action_view.finalize_compiled_template_methods" do |app| + ActiveSupport.on_load(:action_view) do + 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 + initializer "action_view.logger" do ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger } end initializer "action_view.set_configs" do |app| ActiveSupport.on_load(:action_view) do - app.config.action_view.each do |k,v| + app.config.action_view.each do |k, v| send "#{k}=", v end end @@ -37,16 +78,10 @@ module ActionView end end - initializer "action_view.collection_caching" do |app| - ActiveSupport.on_load(:action_controller) do - PartialRenderer.collection_cache = app.config.action_controller.cache_store - end - end - initializer "action_view.per_request_digest_cache" do |app| ActiveSupport.on_load(:action_view) do - if app.config.consider_all_requests_local - app.middleware.use ActionView::Digestor::PerRequestDigestCacheExpiry + unless ActionView::Resolver.caching? + app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher) end end end @@ -57,6 +92,10 @@ module ActionView end end + initializer "action_view.collection_caching", after: "action_controller.set_configs" do |app| + PartialRenderer.collection_cache = app.config.action_controller.cache_store + end + rake_tasks do |app| unless app.config.api_only load "action_view/tasks/cache_digests.rake" diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb index 4a2547b0fb..ee39b6050d 100644 --- a/actionview/lib/action_view/record_identifier.rb +++ b/actionview/lib/action_view/record_identifier.rb @@ -1,5 +1,7 @@ -require 'active_support/core_ext/module' -require 'action_view/model_naming' +# frozen_string_literal: true + +require "active_support/core_ext/module" +require "action_view/model_naming" module ActionView # RecordIdentifier encapsulates methods used by various ActionView helpers @@ -57,8 +59,8 @@ module ActionView include ModelNaming - JOIN = '_'.freeze - NEW = 'new'.freeze + JOIN = "_" + NEW = "new" # The DOM class convention is to use the singular form of an object or class. # @@ -92,7 +94,7 @@ module ActionView end end - protected + private # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id. # This can be overwritten to customize the default generated string representation if desired. @@ -102,7 +104,7 @@ module ActionView # overwritten version of the method. By default, this implementation passes the key string through a # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to # make sure yourself that your dom ids are valid, in case you overwrite this method. - def record_key_for_dom_id(record) + def record_key_for_dom_id(record) # :doc: key = convert_to_model(record).to_key key ? key.join(JOIN) : key end diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index 1dddf53df0..475452f1bb 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView # This class defines the interface for a renderer. Each class that # subclasses +AbstractRenderer+ is used by the base +Renderer+ class to @@ -15,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 @@ -25,29 +27,82 @@ module ActionView raise NotImplementedError end - protected + class RenderedCollection # :nodoc: + def self.empty(format) + EmptyCollection.new format + end - def extract_details(options) - @lookup_context.registered_details.each_with_object({}) do |key, details| - value = options[key] + attr_reader :rendered_templates - details[key] = Array(value) if value + def initialize(rendered_templates, spacer) + @rendered_templates = rendered_templates + @spacer = spacer end - end - def instrument(name, **options) - options[:identifier] ||= (@template && @template.identifier) || @path + def body + @rendered_templates.map(&:body).join(@spacer.body).html_safe + end - ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| - yield payload + def format + rendered_templates.first.format + end + + class EmptyCollection + attr_reader :format + + def initialize(format) + @format = format + end + + def body; nil; end end end - def prepend_formats(formats) - formats = Array(formats) - return if formats.empty? || @lookup_context.html_fallback_for_js + 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 - @lookup_context.formats = formats | @lookup_context.formats + EMPTY_SPACER = Struct.new(:body).new end + + private + + def extract_details(options) # :doc: + @lookup_context.registered_details.each_with_object({}) do |key, details| + value = options[key] + + details[key] = Array(value) if value + end + end + + def instrument(name, **options) # :doc: + ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| + yield payload + end + end + + def prepend_formats(formats) # :doc: + formats = Array(formats) + return if formats.empty? || @lookup_context.html_fallback_for_js + + @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 13b4ec6133..ed8d5cf54e 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -1,5 +1,7 @@ -require 'action_view/renderer/partial_renderer/collection_caching' -require 'concurrent/map' +# frozen_string_literal: true + +require "concurrent/map" +require "action_view/renderer/partial_renderer/collection_caching" module ActionView class PartialIteration @@ -50,12 +52,12 @@ module ActionView # <%= render partial: "ad", locals: { ad: ad } %> # <% end %> # - # This would first render "advertiser/_account.html.erb" with @buyer passed in as the local variable +account+, then - # render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. + # This would first render <tt>advertiser/_account.html.erb</tt> with <tt>@buyer</tt> passed in as the local variable +account+, then + # render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display. # # == The :as and :object options # - # By default <tt>ActionView::PartialRenderer</tt> doesn't have any local variables. + # By default ActionView::PartialRenderer doesn't have any local variables. # The <tt>:object</tt> option can be used to pass an object to the partial. For instance: # # <%= render partial: "account", object: @buyer %> @@ -83,7 +85,7 @@ module ActionView # # <%= render partial: "ad", collection: @advertisements %> # - # This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An + # This will render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display. An # iteration object will automatically be made available to the template with a name of the form # +partial_name_iteration+. The iteration object has knowledge about which index the current object has in # the collection and the total size of the collection. The iteration object also has two convenience methods, @@ -98,8 +100,8 @@ module ActionView # # <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %> # - # If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil. This will allow you - # to specify a text which will displayed instead by using this form: + # If the given <tt>:collection</tt> is +nil+ or empty, <tt>render</tt> will return +nil+. This will allow you + # to specify a text which will be displayed instead by using this form: # # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %> # @@ -112,18 +114,18 @@ module ActionView # # <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %> # - # This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from. + # This will render the partial <tt>advertisement/_ad.html.erb</tt> regardless of which controller this is being called from. # - # == \Rendering objects that respond to `to_partial_path` + # == \Rendering objects that respond to +to_partial_path+ # # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work - # and pick the proper path by checking `to_partial_path` method. + # and pick the proper path by checking +to_partial_path+ method. # # # @account.to_partial_path returns 'accounts/account', so it can be used to replace: # # <%= render partial: "accounts/account", locals: { account: @account} %> # <%= render partial: @account %> # - # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`, + # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+, # # that's why we can replace: # # <%= render partial: "posts/post", collection: @posts %> # <%= render partial: @posts %> @@ -143,7 +145,7 @@ module ActionView # # <%= render partial: "accounts/account", locals: { account: @account} %> # <%= render @account %> # - # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`, + # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+, # # that's why we can replace: # # <%= render partial: "posts/post", collection: @posts %> # <%= render @posts %> @@ -293,257 +295,272 @@ 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 - instrument(:partial) do - render_partial - end + render_partial(context, template) end end private - def render_collection - instrument(:collection, count: @collection.size) do |payload| - return nil if @collection.blank? - - if @options.key?(:spacer_template) - spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) + 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? + + 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 + + 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 - - cache_collection_render(payload) do - @template ? collection_with_template : collection_without_template - end.join(spacer).html_safe end - end - def render_partial - view, locals, block = @view, @locals, @block - object, as = @object, @variable + 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]) - layout = find_template(layout.to_s, @template_keys) - end + if !block && (layout = @options[:layout]) + layout = find_template(layout.to_s, @template_keys) + end - object = locals[as] if object.nil? # Respect object when object is false - locals[as] = object if @has_object + 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| - view._layout_for(*name, &block) - end - - content = layout.render(view, locals){ content } if layout - content - end + content = template.render(view, locals) do |*name| + view._layout_for(*name, &block) + end - # Sets up instance variables needed for rendering a partial. This method - # finds the options and details and extracts them. The method also contains - # logic that handles the type of object passed in as the partial. - # - # If +options[:partial]+ is a string, then the +@path+ 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 - @options = options - @block = block - - @locals = options[:locals] || {} - @details = extract_details(options) - - prepend_formats(options[:formats]) - - partial = options[:partial] - - if String === partial - @has_object = options.key?(:object) - @object = options[:object] - @collection = collection_from_options - @path = partial - else - @has_object = true - @object = partial - @collection = collection_from_object || collection_from_options + content = layout.render(view, locals) { content } if layout + payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path] + build_rendered_template(content, template, layout) + end + end - if @collection - paths = @collection_data = @collection.map { |o| partial_path(o) } - @path = paths.uniq.one? ? paths.first : nil + # Sets up instance variables needed for rendering a partial. This method + # finds the options and details and extracts them. The method also contains + # logic that handles the type of object passed in as the partial. + # + # 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, as, block) + @options = options + @block = block + + @locals = options[:locals] || {} + @details = extract_details(options) + + partial = options[:partial] + + if String === partial + @has_object = options.key?(:object) + @object = options[:object] + @collection = collection_from_options + @path = partial else - @path = partial_path + @has_object = true + @object = partial + @collection = collection_from_object || collection_from_options + + if @collection + 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(@object, context) + end end - end - if as = options[:as] - raise_invalid_option_as(as) unless as.to_s =~ /\A[a-z_]\w*\z/ - as = as.to_sym + self 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) } + def as_variable(options) + if as = options[:as] + raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s) + as.to_sym + end end - self - end - - def collection_from_options - if @options.key?(:collection) - collection = @options[:collection] - collection.respond_to?(:to_ary) ? collection.to_ary : [] + def collection_from_options + if @options.key?(:collection) + collection = @options[:collection] + collection ? collection.to_a : [] + end end - end - def collection_from_object - @object.to_ary if @object.respond_to?(:to_ary) - end + def collection_from_object + @object.to_ary if @object.respond_to?(:to_ary) + end - def find_partial - find_template(@path, @template_keys) if @path - end + def find_partial(path, template_keys) + find_template(path, template_keys) + end - def find_template(path, locals) - prefixes = path.include?(?/) ? [] : @lookup_context.prefixes - @lookup_context.find_template(path, prefixes, true, locals, @details) - end + def find_template(path, locals) + prefixes = path.include?(?/) ? [] : @lookup_context.prefixes + @lookup_context.find_template(path, prefixes, true, locals, @details) + end - def collection_with_template - view, locals, template = @view, @locals, @template - as, counter, iteration = @variable, @variable_counter, @variable_iteration + def collection_with_template(view, template) + locals = @locals + as, counter, iteration = @variable, @variable_counter, @variable_iteration - if layout = @options[:layout] - layout = find_template(layout, @template_keys) - end + if layout = @options[:layout] + layout = find_template(layout, @template_keys) + end - partial_iteration = PartialIteration.new(@collection.size) - locals[iteration] = partial_iteration + partial_iteration = PartialIteration.new(@collection.size) + locals[iteration] = partial_iteration - @collection.map do |object| - locals[as] = object - locals[counter] = partial_iteration.index + @collection.map do |object| + locals[as] = object + locals[counter] = partial_iteration.index - content = template.render(view, locals) - content = layout.render(view, locals) { content } if layout - partial_iteration.iterate! - content + content = template.render(view, locals) + content = layout.render(view, locals) { content } if layout + partial_iteration.iterate! + build_rendered_template(content, template, layout) + end end - end - def collection_without_template - view, locals, collection_data = @view, @locals, @collection_data - cache = {} - keys = @locals.keys + def collection_without_template(view) + locals, collection_data = @locals, @collection_data + cache = {} + keys = @locals.keys - partial_iteration = PartialIteration.new(@collection.size) + partial_iteration = PartialIteration.new(@collection.size) - @collection.map do |object| - index = partial_iteration.index - path, as, counter, iteration = collection_data[index] + @collection.map do |object| + index = partial_iteration.index + path, as, counter, iteration = collection_data[index] - locals[as] = object - locals[counter] = index - locals[iteration] = partial_iteration + locals[as] = object + locals[counter] = index + locals[iteration] = partial_iteration - template = (cache[path] ||= find_template(path, keys + [as, counter])) - content = template.render(view, locals) - partial_iteration.iterate! - content + template = (cache[path] ||= find_template(path, keys + [as, counter, iteration])) + content = template.render(view, locals) + partial_iteration.iterate! + build_rendered_template(content, template) + end end - end - # Obtains the path to where the object's partial is located. If the object - # responds to +to_partial_path+, then +to_partial_path+ will be called and - # will provide the path. If the object does not respond to +to_partial_path+, - # then an +ArgumentError+ is raised. - # - # 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) - object = object.to_model if object.respond_to?(:to_model) - - path = if object.respond_to?(:to_partial_path) - object.to_partial_path - else - raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.") + # Obtains the path to where the object's partial is located. If the object + # responds to +to_partial_path+, then +to_partial_path+ will be called and + # will provide the path. If the object does not respond to +to_partial_path+, + # then an +ArgumentError+ is raised. + # + # If +prefix_partial_path_with_controller_namespace+ is true, then this + # method will prefix the partial paths with a namespace. + def partial_path(object, view) + object = object.to_model if object.respond_to?(:to_model) + + path = if object.respond_to?(:to_partial_path) + object.to_partial_path + else + 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 + prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup) + else + path + end end - if @view.prefix_partial_path_with_controller_namespace - prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup) - else - path + def prefixed_partial_names + @prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix] end - end - def prefixed_partial_names - @prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix] - end + def merge_prefix_into_object_path(prefix, object_path) + if prefix.include?(?/) && object_path.include?(?/) + prefixes = [] + prefix_array = File.dirname(prefix).split("/") + object_path_array = object_path.split("/")[0..-3] # skip model dir & partial - def merge_prefix_into_object_path(prefix, object_path) - if prefix.include?(?/) && object_path.include?(?/) - prefixes = [] - prefix_array = File.dirname(prefix).split('/') - object_path_array = object_path.split('/')[0..-3] # skip model dir & partial + prefix_array.each_with_index do |dir, index| + break if dir == object_path_array[index] + prefixes << dir + end - prefix_array.each_with_index do |dir, index| - break if dir == object_path_array[index] - prefixes << dir + (prefixes << object_path).join("/") + else + object_path end - - (prefixes << object_path).join("/") - else - object_path end - end - def retrieve_template_keys - keys = @locals.keys - keys << @variable if @has_object || @collection - if @collection - keys << @variable_counter - keys << @variable_iteration + def retrieve_template_keys(variable) + keys = @locals.keys + keys << variable + if @collection + keys << @variable_counter + keys << @variable_iteration + end + keys end - keys - end - def retrieve_variable(path, as) - variable = as || begin - base = path[-1] == "/".freeze ? "".freeze : File.basename(path) - raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/ - $1.to_sym - end - if @collection - variable_counter = :"#{variable}_counter" - variable_iteration = :"#{variable}_iteration" + def retrieve_variable(path, as) + variable = as || begin + base = path[-1] == "/" ? "" : File.basename(path) + raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/ + $1.to_sym + end + if @collection + variable_counter = :"#{variable}_counter" + variable_iteration = :"#{variable}_iteration" + end + [variable, variable_counter, variable_iteration] end - [variable, variable_counter, variable_iteration] - end - IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " + - "make sure your partial name starts with underscore." + IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \ + "make sure your partial name starts with underscore." - OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " + - "make sure it starts with lowercase letter, " + - "and is followed by any combination of letters, numbers and underscores." + OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \ + "make sure it starts with lowercase letter, " \ + "and is followed by any combination of letters, numbers and underscores." - def raise_invalid_identifier(path) - raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path)) - end + def raise_invalid_identifier(path) + raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path)) + end - def raise_invalid_option_as(as) - raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as)) - end + def raise_invalid_option_as(as) + raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as)) + 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 f7deba94ce..ed59033e27 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module CollectionCaching # :nodoc: extend ActiveSupport::Concern @@ -5,42 +7,87 @@ module ActionView included do # Fallback cache store if Action View is used without Rails. # Otherwise overridden in Railtie to use Rails.cache. - mattr_accessor(:collection_cache) { ActiveSupport::Cache::MemoryStore.new } + mattr_accessor :collection_cache, default: ActiveSupport::Cache::MemoryStore.new end private - def cache_collection_render(instrumentation_payload) + def cache_collection_render(instrumentation_payload, view, template) return yield unless @options[:cached] - keyed_collection = collection_by_cache_keys + # 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) + + # 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) instrumentation_payload[:cache_hits] = cached_partials.size + # Extract the items for the keys that are not found + # Set the uncached values to instance variable @collection + # which is used by the caller @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values + + # If all elements are already in cache then + # rendered partials will be an empty array + # + # If the cache is missing elements then + # the block will be called against the remaining items + # in the @collection. rendered_partials = @collection.empty? ? [] : yield index = 0 - fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do + 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 end - def collection_by_cache_keys + def callable_cache_key? + @options[:cached].respond_to?(:call) + end + + def collection_by_cache_keys(view, template) + seed = callable_cache_key? ? @options[:cached] : ->(i) { i } + + digest_path = view.digest_path_from_template(template) + @collection.each_with_object({}) do |item, hash| - hash[expanded_cache_key(item)] = item + hash[expanded_cache_key(seed.call(item), view, template, digest_path)] = item end end - def expanded_cache_key(key) - key = @view.fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_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 fetch_or_cache_partial(cached_partials, order_by:) + # `order_by` is an enumerable object containing keys of the cache, + # all keys are passed in whether found already or not. + # + # `cached_partials` is a hash. If the value exists + # it represents the rendered partial from the cache + # otherwise `Hash#fetch` will take the value of its block. + # + # This method expects a block that will return the rendered + # partial. An example is to render all results + # for each element that was not found in the cache and store it as an array. + # Order it so that the first empty cache element in `cached_partials` + # corresponds to the first element in `rendered_partials`. + # + # 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| - cached_partials.fetch(cache_key) do + if content = cached_partials[cache_key] + build_rendered_template(content, template) + else yield.tap do |rendered_partial| - collection_cache.write(cache_key, rendered_partial) + collection_cache.write(cache_key, rendered_partial.body) end end end diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb index 2a3b89aebf..485eb1a5b4 100644 --- a/actionview/lib/action_view/renderer/renderer.rb +++ b/actionview/lib/action_view/renderer/renderer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView # This is the main entry point for rendering. It basically delegates # to other objects like TemplateRenderer and PartialRenderer which @@ -17,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 @@ -39,11 +45,23 @@ 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: + 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 diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index f38e2764d0..19fa399e2c 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -1,10 +1,11 @@ -require 'fiber' +# frozen_string_literal: true + +require "fiber" module ActionView # == TODO # # * Support streaming from child templates, partials and so on. - # * Integrate exceptions with exceptron # * Rack::Cache needs to support streaming bodies class StreamingTemplateRenderer < TemplateRenderer #:nodoc: # A valid Rack::Body (i.e. it responds to each). @@ -27,77 +28,78 @@ module ActionView private - # This is the same logging logic as in ShowExceptions middleware. - # TODO Once "exceptron" is in, refactor this piece to simply re-use exceptron. - def log_error(exception) #:nodoc: - logger = ActionView::Base.logger - return unless logger + # This is the same logging logic as in ShowExceptions middleware. + def log_error(exception) + logger = ActionView::Base.logger + 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.backtrace.join("\n ") - logger.fatal("#{message}\n\n") - end + message = +"\n#{exception.class} (#{exception.message}):\n" + 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 end # For streaming, instead of rendering a given a template, we return a Body # object that responds to each. This object is initialized with a block # that knows how to render the template. - def render_template(template, layout_name = nil, locals = {}) #:nodoc: - return [super] unless layout_name && template.supports_streaming? + def render_template(view, template, layout_name = nil, locals = {}) #:nodoc: + return [super.body] unless layout_name && template.supports_streaming? locals ||= {} layout = layout_name && find_layout(layout_name, locals.keys, [formats.first]) Body.new do |buffer| - delayed_render(buffer, template, layout, @view, locals) + delayed_render(buffer, template, layout, view, locals) end end private - def delayed_render(buffer, template, layout, view, locals) - # Wrap the given buffer in the StreamingBuffer and pass it to the - # underlying template handler. Now, every time something is concatenated - # to the buffer, it is not appended to an array, but streamed straight - # to the client. - output = ActionView::StreamingBuffer.new(buffer) - yielder = lambda { |*name| view._layout_for(*name) } - - instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do - fiber = Fiber.new do - if layout - layout.render(view, locals, output, &yielder) - else - # If you don't have a layout, just render the thing - # and concatenate the final result. This is the same - # as a layout with just <%= yield %> - output.safe_concat view._layout_for + def delayed_render(buffer, template, layout, view, locals) + # Wrap the given buffer in the StreamingBuffer and pass it to the + # underlying template handler. Now, every time something is concatenated + # to the buffer, it is not appended to an array, but streamed straight + # to the client. + output = ActionView::StreamingBuffer.new(buffer) + yielder = lambda { |*name| view._layout_for(*name) } + + instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do + outer_config = I18n.config + fiber = Fiber.new do + I18n.config = outer_config + if layout + layout.render(view, locals, output, &yielder) + else + # If you don't have a layout, just render the thing + # and concatenate the final result. This is the same + # as a layout with just <%= yield %> + output.safe_concat view._layout_for + end end - end - # Set the view flow to support streaming. It will be aware - # when to stop rendering the layout because it needs to search - # something in the template and vice-versa. - view.view_flow = StreamingFlow.new(view, fiber) + # Set the view flow to support streaming. It will be aware + # when to stop rendering the layout because it needs to search + # something in the template and vice-versa. + view.view_flow = StreamingFlow.new(view, fiber) - # Yo! Start the fiber! - fiber.resume + # Yo! Start the fiber! + fiber.resume - # If the fiber is still alive, it means we need something - # from the template, so start rendering it. If not, it means - # the layout exited without requiring anything from the template. - if fiber.alive? - content = template.render(view, locals, &yielder) + # If the fiber is still alive, it means we need something + # from the template, so start rendering it. If not, it means + # the layout exited without requiring anything from the template. + if fiber.alive? + content = template.render(view, locals, &yielder) - # Once rendering the template is done, sets its content in the :layout key. - view.view_flow.set(:layout, content) + # Once rendering the template is done, sets its content in the :layout key. + view.view_flow.set(:layout, content) - # In case the layout continues yielding, we need to resume - # the fiber until all yields are handled. - fiber.resume while fiber.alive? + # In case the layout continues yielding, we need to resume + # the fiber until all yields are handled. + fiber.resume while fiber.alive? + end end end - end end end diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index 9d15bbfca7..1faef9ca81 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -1,102 +1,108 @@ -require 'active_support/core_ext/object/try' +# frozen_string_literal: true + +require "active_support/core_ext/object/try" module ActionView class TemplateRenderer < AbstractRenderer #:nodoc: def render(context, options) - @view = context @details = extract_details(options) template = determine_template(options) - prepend_formats(template.formats) - - @lookup_context.rendered_format ||= (template.formats.first || formats.first) + prepend_formats(template.format) - render_template(template, options[:layout], options[:locals]) + render_template(context, template, options[:layout], options[:locals] || {}) end private - # Determine the template to be rendered using the given options. - def determine_template(options) - keys = options.has_key?(:locals) ? options[:locals].keys : [] + # Determine the template to be rendered using the given options. + def determine_template(options) + keys = options.has_key?(:locals) ? options[:locals].keys : [] - if options.key?(:body) - Template::Text.new(options[:body]) - elsif options.key?(:text) - Template::Text.new(options[:text], formats.first) - elsif options.key?(:plain) - Template::Text.new(options[:plain]) - 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) } - elsif options.key?(:inline) - handler = Template.handler_for_extension(options[:type] || "erb") - Template.new(options[:inline], "inline template", handler, :locals => keys) - elsif options.key?(:template) - if options[:template].respond_to?(:render) - options[:template] + if options.key?(:body) + Template::Text.new(options[:body]) + elsif options.key?(:plain) + Template::Text.new(options[:plain]) + elsif options.key?(:html) + Template::HTML.new(options[:html], formats.first) + elsif options.key?(:file) + 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_file(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) + 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 + @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details) + end else - find_template(options[:template], options[:prefixes], false, keys, @details) + raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option." end - else - raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html, :text or :body option." end - end - - # Renders the given template. A string representing the layout can be - # supplied as well. - def render_template(template, layout_name = nil, locals = nil) #:nodoc: - view, locals = @view, locals || {} - render_with_layout(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) } + # 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, 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 - end - def render_with_layout(path, locals) #:nodoc: - layout = path && find_layout(path, locals.keys, [formats.first]) - content = yield(layout) + def render_with_layout(view, template, path, locals) + layout = path && find_layout(path, locals.keys, [formats.first]) + content = yield(layout) - if layout - view = @view - view.view_flow.set(:layout, content) - layout.render(view, locals){ |*name| view._layout_for(*name) } - else - content + 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 - end - # This is the method which actually finds the layout using details in the lookup - # context object. If no layout is found, it checks if at least a layout with - # the given name exists across all details before raising the error. - def find_layout(layout, keys, formats) - resolve_layout(layout, keys, formats) - end + # This is the method which actually finds the layout using details in the lookup + # context object. If no layout is found, it checks if at least a layout with + # the given name exists across all details before raising the error. + def find_layout(layout, keys, formats) + resolve_layout(layout, keys, formats) + end - def resolve_layout(layout, keys, formats) - details = @details.dup - details[:formats] = formats + def resolve_layout(layout, keys, formats) + details = @details.dup + details[:formats] = formats - case layout - when String - begin - if layout =~ /^\// - with_fallbacks { find_template(layout, nil, false, keys, details) } - else - find_template(layout, nil, false, keys, details) + case layout + 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) + end + rescue ActionView::MissingTemplate + all_details = @details.merge(formats: @lookup_context.default_formats) + raise unless template_exists?(layout, nil, false, [], all_details) end - rescue ActionView::MissingTemplate - all_details = @details.merge(:formats => @lookup_context.default_formats) - raise unless template_exists?(layout, nil, false, keys, all_details) + when Proc + resolve_layout(layout.call(@lookup_context, formats), keys, formats) + else + layout end - when Proc - resolve_layout(layout.call(formats), keys, formats) - else - layout end - end end end diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 3ca7f9d220..5a06bd9da6 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "action_view/view_paths" module ActionView @@ -24,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) @@ -33,48 +42,59 @@ 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 - end - attr_internal_writer :view_context_class + 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 - @_view_context_class ||= self.class.view_context_class + self.class.view_context_class end # An instance of a view class. The default view class is ActionView::Base. # # The view class must have the following methods: - # View.new[lookup_context, assigns, controller] - # Create a new ActionView instance for a controller and we can also pass the arguments. - # View#render(option) - # Returns String with the rendered template + # + # * <tt>View.new(lookup_context, assigns, controller)</tt> — Create a new + # ActionView instance for a controller and we can also pass the arguments. + # + # * <tt>View#render(option)</tt> — Returns String with the rendered template. # # 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. - # :api: private - def view_renderer + def view_renderer # :nodoc: + # Lifespan: Per controller @_view_renderer ||= ActionView::Renderer.new(lookup_context) end @@ -83,37 +103,36 @@ 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. - # :api: private - def _render_template(options) #:nodoc: + def _render_template(options) variant = options.delete(:variant) assigns = options.delete(:assigns) 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) #:nodoc: + 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 # render "foo/bar" to render :template => "foo/bar". - # :api: private - def _normalize_args(action=nil, options={}) + def _normalize_args(action = nil, options = {}) options = super(action, options) case action when NilClass @@ -124,14 +143,17 @@ module ActionView key = action.include?(?/) ? :template : :action options[key] = action else - options[:partial] = action + if action.respond_to?(:permitted?) && action.permitted? + options = action + else + options[:partial] = action + end end options end # Normalize options. - # :api: private def _normalize_options(options) options = super(options) if options[:partial] == true diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index 45e78d1ad9..f8ea3aa770 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -1,8 +1,9 @@ -require 'action_dispatch/routing/polymorphic_routes' +# frozen_string_literal: true + +require "action_dispatch/routing/polymorphic_routes" module ActionView module RoutingUrlFor - # Returns the URL for the set of +options+ provided. This takes the # same options as +url_for+ in Action Controller (see the # documentation for <tt>ActionController::Base#url_for</tt>). Note that by default @@ -83,25 +84,24 @@ module ActionView super(only_path: _generate_paths_by_default) when Hash options = options.symbolize_keys - unless options.key?(:only_path) - options[:only_path] = only_path?(options[:host]) - end + ensure_only_path_option(options) super(options) when ActionController::Parameters - unless options.key?(:only_path) - options[:only_path] = only_path?(options[:host]) - end + ensure_only_path_option(options) super(options) when :back _back_url when Array components = options.dup - if _generate_paths_by_default - polymorphic_path(components, components.extract_options!) + options = components.extract_options! + ensure_only_path_option(options) + + if options[:only_path] + polymorphic_path(components, options) else - polymorphic_url(components, components.extract_options!) + polymorphic_url(components, options) end else method = _generate_paths_by_default ? :path : :url @@ -123,25 +123,24 @@ module ActionView controller.url_options end - def _routes_context #:nodoc: - controller - end - protected :_routes_context - - def optimize_routes_generation? #:nodoc: - controller.respond_to?(:optimize_routes_generation?, true) ? - controller.optimize_routes_generation? : super - end - protected :optimize_routes_generation? - private + def _routes_context + controller + end + + def optimize_routes_generation? + controller.respond_to?(:optimize_routes_generation?, true) ? + controller.optimize_routes_generation? : super + end def _generate_paths_by_default true end - def only_path?(host) - _generate_paths_by_default unless host + def ensure_only_path_option(options) + unless options.key?(:only_path) + options[:only_path] = _generate_paths_by_default unless options[:host] + end end end end diff --git a/actionview/lib/action_view/tasks/cache_digests.rake b/actionview/lib/action_view/tasks/cache_digests.rake index 045bdf5691..dd8e94bd88 100644 --- a/actionview/lib/action_view/tasks/cache_digests.rake +++ b/actionview/lib/action_view/tasks/cache_digests.rake @@ -1,19 +1,21 @@ +# frozen_string_literal: true + namespace :cache_digests do - desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)' - task :nested_dependencies => :environment do - abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? + desc "Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)" + task nested_dependencies: :environment do + abort "You must provide TEMPLATE for the task to run" unless ENV["TEMPLATE"].present? puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:to_dep_map) end - desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)' - task :dependencies => :environment do - abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? + desc "Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)" + task dependencies: :environment do + abort "You must provide TEMPLATE for the task to run" unless ENV["TEMPLATE"].present? puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:name) end class CacheDigests def self.template_name - ENV['TEMPLATE'].split('.', 2).first + ENV["TEMPLATE"].split(".", 2).first end def self.finder diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 169ee55fdc..94f8a194a0 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -1,12 +1,24 @@ -require 'active_support/core_ext/object/try' -require 'active_support/core_ext/kernel/singleton_class' -require 'thread' +# frozen_string_literal: true + +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 + 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 # # ActionView::Template is one of a few sources of potential @@ -65,8 +77,7 @@ module ActionView # If you want to provide an alternate mechanism for # specifying encodings (like ERB does via <%# encoding: ... %>), # you may indicate that you will handle encodings yourself - # by implementing <tt>self.handles_encoding?</tt> - # on your handler. + # by implementing <tt>handles_encoding?</tt> on your handler. # # If you do, Rails will not try to encode the String # into the default_internal, passing you the unaltered @@ -102,46 +113,60 @@ module ActionView eager_autoload do autoload :Error + autoload :RawFile autoload :Handlers autoload :HTML + autoload :Inline autoload :Text autoload :Types end extend Template::Handlers - attr_accessor :locals, :formats, :variants, :virtual_path - attr_reader :source, :identifier, :handler, :original_encoding, :updated_at + attr_reader :variable, :format, :variant, :locals, :virtual_path - # 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 + # Returns whether the underlying handler supports streaming. If so, - # a streaming buffer *may* be passed when it start rendering. + # a streaming buffer *may* be passed when it starts rendering. def supports_streaming? handler.respond_to?(:supports_streaming?) && handler.supports_streaming? end @@ -152,17 +177,17 @@ module ActionView # This method is instrumented as "!render_template.action_view". Notice that # we use a bang in this instrumentation because you don't want to # consume this in production. This is only slow if it's being listened to. - def render(view, locals, buffer=nil, &block) - instrument("!render_template".freeze) do + def render(view, locals, buffer = ActionView::OutputBuffer.new, &block) + instrument_render_template do compile!(view) - view.send(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. @@ -180,12 +205,16 @@ module ActionView name = pieces.pop partial = !!name.sub!(/^_/, "") lookup.disable_cache do - lookup.find_template(name, [ pieces.join('/') ], partial, @locals) + lookup.find_template(name, [ pieces.join("/") ], partial, @locals) end 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}/", ''.freeze) : identifier + "#<#{self.class.name} #{short_identifier} locals=#{@locals.inspect}>" end # This method is responsible for properly setting the encoding of the @@ -199,12 +228,14 @@ 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 # default external encoding. - if source.sub!(/\A#{ENCODING_FLAG}/, '') + if source.sub!(/\A#{ENCODING_FLAG}/, "") encoding = magic_encoding = $1 else encoding = Encoding.default_external @@ -232,11 +263,24 @@ module ActionView end end - protected + + # 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: + [ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ] + end + + def marshal_load(array) # :nodoc: + @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array + @compile_mutex = Mutex.new + end + + private # Compile a template. This method ensures a template is compiled # just once and removes the source after it is compiled. - def compile!(view) #:nodoc: + def compile!(view) return if @compiled # Templates can be used concurrently in threaded environments @@ -248,11 +292,7 @@ module ActionView # re-compilation return if @compiled - if view.is_a?(ActionView::CompiledTemplates) - mod = ActionView::CompiledTemplates - else - mod = view.singleton_class - end + mod = view.compiled_method_container instrument("!compile_template") do compile(mod) @@ -265,6 +305,15 @@ module ActionView 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. # @@ -277,18 +326,16 @@ module ActionView # encode the source into <tt>Encoding.default_internal</tt>. # In general, this means that templates will be UTF-8 inside of Rails, # regardless of the original source encoding. - def compile(mod) #:nodoc: - encode! - method_name = self.method_name - code = @handler.call(self) + def compile(mod) + source = encode! + code = @handler.call(self, source) # Make sure that the resulting String to be eval'd is in the # encoding of the code - source = <<-end_src + original_source = source + source = +<<-end_src def #{method_name}(local_assigns, output_buffer) - _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code} - ensure - @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer + @virtual_path = #{@virtual_path.inspect};#{locals_code};#{code} end end_src @@ -303,14 +350,20 @@ 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) - 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 - def handle_render_error(view, e) #:nodoc: + def handle_render_error(view, e) if e.is_a?(Template::Error) e.sub_template_of(self) raise e @@ -324,31 +377,38 @@ module ActionView end end - def locals_code #:nodoc: - # Double assign to suppress the dreaded 'assigned but unused variable' warning - @locals.each_with_object('') { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" } + def locals_code + # Only locals with valid variable names get set directly. Others will + # still be available in local_assigns. + locals = @locals - Module::RUBY_RESERVED_KEYWORDS + locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/) + + # Assign for the same variable is to suppress unused variable warning + locals.each_with_object(+"") { |key, code| code << "#{key} = local_assigns[:#{key}]; #{key} = #{key};" } end - def method_name #:nodoc: + def method_name @method_name ||= begin - m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}" - m.tr!('-'.freeze, '_'.freeze) + m = +"_#{identifier_method_name}__#{@identifier.hash}_#{__id__}" + m.tr!("-", "_") m end end - def identifier_method_name #:nodoc: - inspect.tr('^a-z_'.freeze, '_'.freeze) + def identifier_method_name + short_identifier.tr("^a-z_", "_") end - def instrument(action, &block) - payload = { virtual_path: @virtual_path, identifier: @identifier } - case action - when "!render_template".freeze - ActiveSupport::Notifications.instrument("!render_template.action_view".freeze, payload, &block) - else - ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block) - end + def instrument(action, &block) # :doc: + ActiveSupport::Notifications.instrument("#{action}.action_view", instrument_payload, &block) + end + + def instrument_render_template(&block) + ActiveSupport::Notifications.instrument("!render_template.action_view", instrument_payload, &block) + end + + def instrument_payload + { virtual_path: @virtual_path, identifier: @identifier } end end end diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb index 3f38c3d2b9..d0ea03e228 100644 --- a/actionview/lib/action_view/template/error.rb +++ b/actionview/lib/action_view/template/error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/enumerable" module ActionView @@ -8,9 +10,6 @@ module ActionView class EncodingError < StandardError #:nodoc: end - class MissingRequestError < StandardError #:nodoc: - end - class WrongEncodingError < EncodingError #:nodoc: def initialize(string, encoding) @string, @encoding = string, encoding @@ -35,10 +34,10 @@ module ActionView prefixes = Array(prefixes) template_type = if partial "partial" - elsif path =~ /layouts/i - 'layout' + elsif /layouts/i.match?(path) + "layout" else - 'template' + "template" end if partial && path.present? @@ -62,23 +61,13 @@ module ActionView # Override to prevent #cause resetting during re-raise. attr_reader :cause - def initialize(template, original_exception = nil) - if original_exception - ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ - "Exceptions will automatically capture the original exception.", caller) - end - + def initialize(template) super($!.message) set_backtrace($!.backtrace) @cause = $! @template, @sub_templates = template, nil end - def original_exception - ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) - cause - end - def file_name @template.identifier end @@ -120,7 +109,7 @@ module ActionView end end - def annoted_source_code + def annotated_source_code source_extract(4) end @@ -130,7 +119,7 @@ module ActionView if line_number "on line ##{line_number} of " else - 'in ' + "in " end + file_name end @@ -149,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 ad4c353608..ddaac7a100 100644 --- a/actionview/lib/action_view/template/handlers.rb +++ b/actionview/lib/action_view/template/handlers.rb @@ -1,18 +1,22 @@ +# frozen_string_literal: true + +require "active_support/deprecation" + module ActionView #:nodoc: # = Action View Template Handlers - class Template + class Template #:nodoc: module Handlers #:nodoc: - autoload :Raw, 'action_view/template/handlers/raw' - autoload :ERB, 'action_view/template/handlers/erb' - autoload :Html, 'action_view/template/handlers/html' - autoload :Builder, 'action_view/template/handlers/builder' + autoload :Raw, "action_view/template/handlers/raw" + autoload :ERB, "action_view/template/handlers/erb" + autoload :Html, "action_view/template/handlers/html" + autoload :Builder, "action_view/template/handlers/builder" def self.extended(base) base.register_default_template_handler :raw, Raw.new 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 = {} @@ -22,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.class}#call(#{params.map(&:last).join(", ")}) + To: + >> #{handler.class}#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 d90b0c6378..e5413cd2a0 100644 --- a/actionview/lib/action_view/template/handlers/builder.rb +++ b/actionview/lib/action_view/template/handlers/builder.rb @@ -1,26 +1,25 @@ +# frozen_string_literal: true + module ActionView module Template::Handlers class Builder - # Default format used by Builder. - class_attribute :default_format - self.default_format = :xml + class_attribute :default_format, default: :xml - def call(template) + def call(template, source) require_engine - "xml = ::Builder::XmlMarkup.new(:indent => 2);" + + "xml = ::Builder::XmlMarkup.new(:indent => 2);" \ "self.output_buffer = xml.target!;" + - template.source + + source + ";xml.target!;" end - protected - - def require_engine - @required ||= begin - require "builder" - true + private + def require_engine # :doc: + @required ||= begin + require "builder" + true + end end - end end end end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 85a100ed4c..b6314a5bc3 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -1,96 +1,35 @@ -require 'erubis' +# frozen_string_literal: true module ActionView class Template module Handlers - class Erubis < ::Erubis::Eruby - def add_preamble(src) - @newline_pending = 0 - src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" - end - - def add_text(src, text) - return if text.empty? - - if text == "\n" - @newline_pending += 1 - else - src << "@output_buffer.safe_append='" - src << "\n" * @newline_pending if @newline_pending > 0 - src << escape_text(text) - src << "'.freeze;" - - @newline_pending = 0 - end - end - - # Erubis toggles <%= and <%== behavior when escaping is enabled. - # We override to always treat <%== as escaped. - def add_expr(src, code, indicator) - case indicator - when '==' - add_expr_escaped(src, code) - else - super - end - end - - BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ - - def add_expr_literal(src, code) - flush_newline_if_pending(src) - if code =~ BLOCK_EXPR - src << '@output_buffer.append= ' << code - else - src << '@output_buffer.append=(' << code << ');' - end - end - - def add_expr_escaped(src, code) - flush_newline_if_pending(src) - if code =~ BLOCK_EXPR - src << "@output_buffer.safe_expr_append= " << code - else - src << "@output_buffer.safe_expr_append=(" << code << ");" - end - end - - def add_stmt(src, code) - flush_newline_if_pending(src) - super - end - - def add_postamble(src) - flush_newline_if_pending(src) - src << '@output_buffer.to_s' - end - - def flush_newline_if_pending(src) - if @newline_pending > 0 - src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;" - @newline_pending = 0 - end - end - end - class ERB + autoload :Erubi, "action_view/template/handlers/erb/erubi" + # Specify trim mode for the ERB compiler. Defaults to '-'. # See ERB documentation for suitable values. - class_attribute :erb_trim_mode - self.erb_trim_mode = '-' + class_attribute :erb_trim_mode, default: "-" # Default implementation used. - class_attribute :erb_implementation - self.erb_implementation = Erubis + class_attribute :erb_implementation, default: Erubi # Do not escape templates of these mime types. - class_attribute :escape_whitelist - self.escape_whitelist = ["text/plain"] + class_attribute :escape_ignore_list, default: ["text/plain"] + + [self, singleton_class].each do |base| + base.alias_method :escape_whitelist, :escape_ignore_list + base.alias_method :escape_whitelist=, :escape_ignore_list= + + base.deprecate( + escape_whitelist: "use #escape_ignore_list instead", + :escape_whitelist= => "use #escape_ignore_list= instead" + ) + end 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? @@ -101,25 +40,25 @@ 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, '') + 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! self.class.erb_implementation.new( erb, - :escape => (self.class.escape_whitelist.include? template.type), - :trim => (self.class.erb_trim_mode == "-") + escape: (self.class.escape_ignore_list.include? template.type), + trim: (self.class.erb_trim_mode == "-") ).src end diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb new file mode 100644 index 0000000000..307b852440 --- /dev/null +++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require "erubi" + +module ActionView + class Template + module Handlers + class ERB + class Erubi < ::Erubi::Engine + # :nodoc: all + def initialize(input, properties = {}) + @newline_pending = 0 + + # Dup properties so that we don't modify argument + properties = Hash[properties] + properties[:preamble] = "" + properties[:postamble] = "@output_buffer.to_s" + properties[:bufvar] = "@output_buffer" + properties[:escapefunc] = "" + + super + end + + def evaluate(action_view_erb_handler_context) + 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) + }.empty + view._run(:_template, nil, {}, ActionView::OutputBuffer.new) + end + + private + def add_text(text) + return if text.empty? + + if text == "\n" + @newline_pending += 1 + else + src << "@output_buffer.safe_append='" + src << "\n" * @newline_pending if @newline_pending > 0 + src << text.gsub(/['\\]/, '\\\\\&') + src << "'.freeze;" + + @newline_pending = 0 + end + end + + BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ + + def add_expression(indicator, code) + flush_newline_if_pending(src) + + if (indicator == "==") || @escape + src << "@output_buffer.safe_expr_append=" + else + src << "@output_buffer.append=" + end + + if BLOCK_EXPR.match?(code) + src << " " << code + else + src << "(" << code << ");" + end + end + + def add_code(code) + flush_newline_if_pending(src) + super + end + + def add_postamble(_) + flush_newline_if_pending(src) + super + end + + def flush_newline_if_pending(src) + if @newline_pending > 0 + src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;" + @newline_pending = 0 + end + end + end + end + end + end +end diff --git a/actionview/lib/action_view/template/handlers/html.rb b/actionview/lib/action_view/template/handlers/html.rb index ccaa8d1469..65857d8587 100644 --- a/actionview/lib/action_view/template/handlers/html.rb +++ b/actionview/lib/action_view/template/handlers/html.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + 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 760f517431..57ebb169fc 100644 --- a/actionview/lib/action_view/template/handlers/raw.rb +++ b/actionview/lib/action_view/template/handlers/raw.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module ActionView module Template::Handlers class Raw - def call(template) - "#{template.source.inspect};" + 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 0321f819b5..ecd1c31e79 100644 --- a/actionview/lib/action_view/template/html.rb +++ b/actionview/lib/action_view/template/html.rb @@ -1,22 +1,28 @@ +# frozen_string_literal: true + +require "active_support/deprecation" + module ActionView #:nodoc: # = Action View HTML Template - class 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 - 'html template' + "html template" end - def inspect - 'html template' - end + alias_method :inspect, :identifier def to_str ERB::Util.h(@string) @@ -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 f33acc2103..095e6cc3a1 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "pathname" require "active_support/core_ext/class" require "active_support/core_ext/module/attribute_accessors" @@ -14,7 +16,7 @@ module ActionView alias_method :partial?, :partial def self.build(name, prefix, partial) - virtual = "" + virtual = +"" virtual << "#{prefix}/" unless prefix.empty? virtual << (partial ? "_#{name}" : name) new name, prefix, partial, virtual @@ -37,15 +39,15 @@ module ActionView class Cache #:nodoc: class SmallCache < Concurrent::Map def initialize(options = {}) - super(options.merge(:initial_capacity => 2)) + super(options.merge(initial_capacity: 2)) end end # preallocate all the default blocks for performance/memory consumption reasons - PARTIAL_BLOCK = lambda {|cache, partial| cache[partial] = SmallCache.new} - PREFIX_BLOCK = lambda {|cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK)} - NAME_BLOCK = lambda {|cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK)} - KEY_BLOCK = lambda {|cache, key| cache[key] = SmallCache.new(&NAME_BLOCK)} + PARTIAL_BLOCK = lambda { |cache, partial| cache[partial] = SmallCache.new } + PREFIX_BLOCK = lambda { |cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK) } + NAME_BLOCK = lambda { |cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK) } + KEY_BLOCK = lambda { |cache, key| cache[key] = SmallCache.new(&NAME_BLOCK) } # usually a majority of template look ups return nothing, use this canonical preallocated array to save memory NO_TEMPLATES = [].freeze @@ -55,28 +57,17 @@ module ActionView @query_cache = SmallCache.new end + def inspect + "#<#{self.class.name}:0x#{(object_id << 1).to_s(16)} keys=#{@data.size} queries=#{@query_cache.size}>" + end + # 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 @@ -84,28 +75,31 @@ module ActionView @query_cache.clear end - private + # Get the cache size. Do not call this + # method. This method is not guaranteed to be here ever. + def size # :nodoc: + size = 0 + @data.each_value do |v1| + v1.each_value do |v2| + v2.each_value do |v3| + v3.each_value do |v4| + size += v4.size + end + end + end + end - def canonical_no_templates(templates) - templates.empty? ? NO_TEMPLATES : templates + size + @query_cache.size 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 + private - # 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 + def canonical_no_templates(templates) + templates.empty? ? NO_TEMPLATES : templates + end end - cattr_accessor :caching - self.caching = true + cattr_accessor :caching, default: true class << self alias :caching? :caching @@ -120,15 +114,19 @@ module ActionView end # Normalizes the arguments and passes it on to find_templates. - def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[]) + def find_all(name, prefix = nil, 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) + find_templates(name, prefix, partial, details, false, locals) end end - def find_all_anywhere(name, prefix, partial=false, details={}, key=nil, locals=[]) + 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) + find_templates(name, prefix, partial, details, true, locals) end end @@ -143,185 +141,148 @@ module ActionView # 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) - raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) 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, outside_app_allowed = false, locals = []) + raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed = false, locals = []) method" end # Handles templates caching. If a key is given and caching is on # always check the cache before hitting the resolver. Otherwise, # it always hits the resolver but if the key is present, check if the # resolver is fresher before returning it. - def cached(key, path_info, details, locals) #:nodoc: + 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) #:nodoc: - 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 # An abstract class that implements a Resolver with path semantics. class PathResolver < Resolver #:nodoc: - EXTENSIONS = { :locale => ".", :formats => ".", :variants => "+", :handlers => "." } + EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." } DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}" - def initialize(pattern=nil) - @pattern = pattern || DEFAULT_PATTERN + def initialize(pattern = nil) + 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 super() end private - def find_templates(name, prefix, partial, details, outside_app_allowed = false) - path = Path.build(name, prefix, partial) - query(path, details, details[:formats], outside_app_allowed) - end + def find_templates(name, prefix, partial, details, outside_app_allowed = false, locals) + path = Path.build(name, prefix, partial) + query(path, details, details[:formats], outside_app_allowed, locals) + end - def query(path, details, formats, outside_app_allowed) - query = build_query(path, details) + def query(path, details, formats, outside_app_allowed, locals) + template_paths = find_template_paths_from_details(path, details) + template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed - template_paths = find_template_paths(query) - template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed + template_paths.map do |template| + build_template(template, path.virtual, locals) + end + end - template_paths.map do |template| - handler, format, variant = extract_handler_and_format_and_variant(template, formats) - contents = File.binread(template) + def build_template(template, virtual_path, locals) + handler, format, variant = extract_handler_and_format_and_variant(template) - Template.new(contents, File.expand_path(template), handler, - :virtual_path => path.virtual, - :format => format, - :variant => variant, - :updated_at => mtime(template) + FileTemplate.new(File.expand_path(template), handler, + virtual_path: virtual_path, + format: format, + variant: variant, + locals: locals ) end - end - def reject_files_external_to_app(files) - files.reject { |filename| !inside_path?(@path, filename) } - end + def reject_files_external_to_app(files) + files.reject { |filename| !inside_path?(@path, filename) } + end - def find_template_paths(query) - Dir[query].uniq.reject do |filename| - File.directory?(filename) || - # deals with case-insensitive file systems. - !File.fnmatch(query, filename, File::FNM_EXTGLOB) + def find_template_paths_from_details(path, details) + query = build_query(path, details) + find_template_paths(query) end - end - def inside_path?(path, filename) - filename = File.expand_path(filename) - path = File.join(path, '') - filename.start_with?(path) - end + def find_template_paths(query) + Dir[query].uniq.reject do |filename| + File.directory?(filename) || + # deals with case-insensitive file systems. + !File.fnmatch(query, filename, File::FNM_EXTGLOB) + end + end - # Helper for building query glob string based on resolver's pattern. - def build_query(path, details) - query = @pattern.dup + def inside_path?(path, filename) + filename = File.expand_path(filename) + path = File.join(path, "") + filename.start_with?(path) + end - prefix = path.prefix.empty? ? '' : "#{escape_entry(path.prefix)}\\1" - query.gsub!(/:prefix(\/)?/, prefix) + # Helper for building query glob string based on resolver's pattern. + def build_query(path, details) + query = @pattern.dup - partial = escape_entry(path.partial? ? "_#{path.name}" : path.name) - query.gsub!(/:action/, partial) + prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1" + query.gsub!(/:prefix(\/)?/, prefix) - details.each do |ext, candidates| - if ext == :variants && candidates == :any - query.gsub!(/:#{ext}/, "*") - else - query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}") - end - end + partial = escape_entry(path.partial? ? "_#{path.name}" : path.name) + query.gsub!(/:action/, partial) - File.expand_path(query, @path) - end + details.each do |ext, candidates| + if ext == :variants && candidates == :any + query.gsub!(/:#{ext}/, "*") + else + query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}") + end + end - def escape_entry(entry) - entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze) - end + File.expand_path(query, @path) + end - # Returns the file mtime from the filesystem. - def mtime(p) - File.mtime(p) - end + def escape_entry(entry) + entry.gsub(/[*?{}\[\]]/, '\\\\\\&') + 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. - def extract_handler_and_format_and_variant(path, default_formats) - pieces = File.basename(path).split('.'.freeze) - pieces.shift + # 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. + def extract_handler_and_format_and_variant(path) + pieces = File.basename(path).split(".") + pieces.shift - extension = pieces.pop + extension = pieces.pop - handler = Template.handler_for_extension(extension) - format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last - format &&= Template::Types[format] + handler = Template.handler_for_extension(extension) + format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last + 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 - [handler, format, variant] - 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 `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}` - # - # 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. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`, - # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, 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 - def initialize(path, pattern=nil) + attr_reader :path + + def initialize(path, pattern = nil) raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver) super(pattern) @path = File.expand_path(path) @@ -340,19 +301,60 @@ module ActionView # An Optimized resolver for Rails' most common case. class OptimizedFileSystemResolver < FileSystemResolver #:nodoc: - def build_query(path, details) - query = escape_entry(File.join(@path, path)) + def initialize(path) + super(path) + end - exts = EXTENSIONS.map do |ext, prefix| - if ext == :variants && details[ext] == :any - "{#{prefix}*,}" - else - "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}" + private + + def find_template_paths_from_details(path, details) + # Instead of checking for every possible path, as our other globs would + # do, scan the directory for files with the right prefix. + query = "#{escape_entry(File.join(@path, path))}*" + + regex = build_regex(path, details) + + Dir[query].uniq.reject do |filename| + # This regex match does double duty of finding only files which match + # details (instead of just matching the prefix) and also filtering for + # case-insensitive file systems. + !regex.match?(filename) || + File.directory?(filename) + end.sort_by do |filename| + # Because we scanned the directory, instead of checking for files + # one-by-one, they will be returned in an arbitrary order. + # We can use the matches found by the regex and sort by their index in + # details. + match = filename.match(regex) + EXTENSIONS.keys.reverse.map do |ext| + if ext == :variants && details[ext] == :any + match[ext].nil? ? 0 : 1 + elsif match[ext].nil? + # No match should be last + details[ext].length + else + found = match[ext].to_sym + details[ext].index(found) + end + end end - end.join + end - query + exts - end + def build_regex(path, details) + query = escape_entry(File.join(@path, path)) + exts = EXTENSIONS.map do |ext, prefix| + match = + if ext == :variants && details[ext] == :any + ".*?" + else + details[ext].compact.uniq.map { |e| Regexp.escape(e) }.join("|") + end + prefix = Regexp.escape(prefix) + "(#{prefix}(?<#{ext}>#{match}))?" + end.join + + %r{\A#{query}#{exts}\z} + end end # The same as FileSystemResolver but does not allow templates to store @@ -362,8 +364,8 @@ module ActionView [new(""), new("/")] end - def decorate(*) - super.each { |t| t.virtual_path = nil } + def build_template(template, virtual_path, locals) + super(template, nil, locals) end end end diff --git a/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb index 04f5b8d17a..c5fd55f1b3 100644 --- a/actionview/lib/action_view/template/text.rb +++ b/actionview/lib/action_view/template/text.rb @@ -1,22 +1,20 @@ +# frozen_string_literal: true + module ActionView #:nodoc: # = Action View Text Template - class Template + class Template #:nodoc: class Text #:nodoc: attr_accessor :type - def initialize(string, type = nil) + def initialize(string) @string = string.to_s - @type = Types[type] || type if type - @type ||= Types[:text] end def identifier - 'text template' + "text template" end - def inspect - 'text template' - end + alias_method :inspect, :identifier def to_str @string @@ -26,9 +24,12 @@ module ActionView #:nodoc: to_str end - def formats - [@type.respond_to?(:ref) ? @type.ref : @type.to_s] + def format + :text end + + def formats; Array(format); end + deprecate :formats end end end diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb index b32567cd66..67b7a62de6 100644 --- a/actionview/lib/action_view/template/types.rb +++ b/actionview/lib/action_view/template/types.rb @@ -1,7 +1,9 @@ -require 'active_support/core_ext/module/attribute_accessors' +# frozen_string_literal: true + +require "active_support/core_ext/module/attribute_accessors" module ActionView - class Template + class Template #:nodoc: class Types class Type SET = Struct.new(:symbols).new([ :html, :text, :js, :css, :xml, :json ]) diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index 120962b5aa..e14f7aaec7 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -1,9 +1,11 @@ -require 'active_support/core_ext/module/remove_method' -require 'action_controller' -require 'action_controller/test_case' -require 'action_view' +# frozen_string_literal: true -require 'rails-dom-testing' +require "active_support/core_ext/module/redefine_method" +require "action_controller" +require "action_controller/test_case" +require "action_view" + +require "rails-dom-testing" module ActionView # = Action View Test Case @@ -18,16 +20,16 @@ module ActionView end def controller_path=(path) - self.class.controller_path=(path) + self.class.controller_path = (path) end def initialize super self.class.controller_path = "" - @request = ActionController::TestRequest.create + @request = ActionController::TestRequest.create(self.class) @response = ActionDispatch::TestResponse.new - @request.env.delete('PATH_INFO') + @request.env.delete("PATH_INFO") @params = ActionController::Parameters.new end end @@ -49,7 +51,7 @@ module ActionView include ActiveSupport::Testing::ConstantLookup - delegate :lookup_context, :to => :controller + delegate :lookup_context, to: :controller attr_accessor :controller, :output_buffer, :rendered module ClassMethods @@ -71,7 +73,7 @@ module ActionView def helper_method(*methods) # Almost a duplicate from ActionController::Helpers methods.flatten.each do |method| - _helpers.module_eval <<-end_eval + _helpers.module_eval <<-end_eval, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def current_user(*args, &block) _test_case.send(%(#{method}), *args, &block) # _test_case.send(%(current_user), *args, &block) end # end @@ -96,16 +98,16 @@ module ActionView helper(helper_class) if helper_class include _helpers end - end def setup_with_controller @controller = ActionView::TestCase::TestController.new @request = @controller.request + @view_flow = ActionView::OutputFlow.new # empty string ensures buffer has UTF-8 encoding as # new without arguments returns ASCII-8BIT encoded buffer like String#new - @output_buffer = ActiveSupport::SafeBuffer.new '' - @rendered = '' + @output_buffer = ActiveSupport::SafeBuffer.new "" + @rendered = +"" make_test_case_available_to_view! say_no_to_protect_against_forgery! @@ -125,6 +127,10 @@ module ActionView @_rendered_views ||= RenderedViewsCollection.new end + def _routes + @controller._routes if @controller.respond_to?(:_routes) + end + # Need to experiment if this priority is the best one: rendered => output_buffer class RenderedViewsCollection def initialize @@ -146,13 +152,14 @@ module ActionView def view_rendered?(view, expected_locals) locals_for(view).any? do |actual_locals| - expected_locals.all? {|key, value| value == actual_locals[key] } + expected_locals.all? { |key, value| value == actual_locals[key] } end end end included do setup :setup_with_controller + ActiveSupport.run_load_hooks(:action_view_test_case, self) end private @@ -164,7 +171,7 @@ module ActionView def say_no_to_protect_against_forgery! _helpers.module_eval do - remove_possible_method :protect_against_forgery? + silence_redefinition_of_method :protect_against_forgery? def protect_against_forgery? false end @@ -206,8 +213,8 @@ module ActionView view = @controller.view_context view.singleton_class.include(_helpers) view.extend(Locals) - view.rendered_views = self.rendered_views - view.output_buffer = self.output_buffer + view.rendered_views = rendered_views + view.output_buffer = output_buffer view end end @@ -240,6 +247,7 @@ module ActionView :@test_passed, :@view, :@view_context_class, + :@view_flow, :@_subscribers, :@html_document ] @@ -258,25 +266,33 @@ module ActionView end] end - def _routes - @controller._routes if @controller.respond_to?(:_routes) - end - def method_missing(selector, *args) begin routes = @controller.respond_to?(:_routes) && @controller._routes rescue - # Dont call routes, if there is an error on _routes call + # Don't call routes, if there is an error on _routes call end if routes && - ( routes.named_routes.route_defined?(selector) || - routes.mounted_helpers.method_defined?(selector) ) + (routes.named_routes.route_defined?(selector) || + routes.mounted_helpers.method_defined?(selector)) @controller.__send__(selector, *args) else super end end + + def respond_to_missing?(name, include_private = false) + begin + routes = @controller.respond_to?(:_routes) && @controller._routes + rescue + # Don't call routes, if there is an error on _routes call + end + + routes && + (routes.named_routes.route_defined?(name) || + routes.mounted_helpers.method_defined?(name)) + end end include Behavior diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index 2664aca991..a16dc0096e 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -1,4 +1,6 @@ -require 'action_view/template/resolver' +# frozen_string_literal: true + +require "action_view/template/resolver" module ActionView #:nodoc: # Use FixtureResolver in your tests to simulate the presence of files on the @@ -6,48 +8,48 @@ 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) + def initialize(hash = {}, pattern = nil) super(pattern) @hash = hash end - def to_s - @hash.keys.join(', ') + def data + @hash end - private + def to_s + @hash.keys.join(", ") + end - def query(path, exts, formats, _) - query = "" - EXTENSIONS.each_key do |ext| - query << '(' << exts[ext].map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)' + private + + def query(path, exts, _, _, locals) + query = +"" + EXTENSIONS.each_key do |ext| + query << "(" << exts[ext].map { |e| e && Regexp.escape(".#{e}") }.join("|") << "|)" + end + query = /^(#{Regexp.escape(path)})#{query}$/ + + templates = [] + @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 + ) + end + + templates.sort_by { |t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size } end - query = /^(#{Regexp.escape(path)})#{query}$/ - - templates = [] - @hash.each do |_path, array| - source, updated_at = array - next unless _path =~ query - handler, format, variant = extract_handler_and_format_and_variant(_path, formats) - templates << Template.new(source, _path, handler, - :virtual_path => path.virtual, - :format => format, - :variant => variant, - :updated_at => updated_at - ) - end - - templates.sort_by {|t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size } - end end class NullResolver < PathResolver - def query(path, exts, formats, _) - handler, format, variant = extract_handler_and_format_and_variant(path, formats) - [ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, :virtual_path => path.virtual, :format => format, :variant => variant)] + def query(path, exts, _, _, locals) + 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 end end - diff --git a/actionview/lib/action_view/version.rb b/actionview/lib/action_view/version.rb index f55d3fdaef..be53797a14 100644 --- a/actionview/lib/action_view/version.rb +++ b/actionview/lib/action_view/version.rb @@ -1,4 +1,6 @@ -require_relative 'gem_version' +# frozen_string_literal: true + +require_relative "gem_version" module ActionView # Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt> diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index 717d6866c5..3ca5aedc14 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -1,17 +1,25 @@ +# frozen_string_literal: true + module ActionView module ViewPaths extend ActiveSupport::Concern included do - class_attribute :_view_paths - self._view_paths = ActionView::PathSet.new - self._view_paths.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 + :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? @@ -22,12 +30,28 @@ module ActionView private - # Override this method in your controller if you want to change paths prefixes for finding views. - # Prefixes defined here will still be added to parents' <tt>._prefixes</tt>. - def local_prefixes - [controller_path] - end + # Override this method in your controller if you want to change paths prefixes for finding views. + # Prefixes defined here will still be added to parents' <tt>._prefixes</tt>. + def local_prefixes + [controller_path] + 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: @@ -43,13 +67,25 @@ module ActionView end def details_for_lookup - { } + {} end + # Append a path to the list of view paths for the current <tt>LookupContext</tt>. + # + # ==== Parameters + # * <tt>path</tt> - If a String is provided, it gets converted into + # the default view path. You may also provide a custom view path + # (see ActionView::PathSet for more information) def append_view_path(path) lookup_context.view_paths.push(*path) end + # Prepend a path to the list of view paths for the current <tt>LookupContext</tt>. + # + # ==== Parameters + # * <tt>path</tt> - If a String is provided, it gets converted into + # the default view path. You may also provide a custom view path + # (see ActionView::PathSet for more information) def prepend_view_path(path) lookup_context.view_paths.unshift(*path) end |