diff options
Diffstat (limited to 'actionview/lib')
100 files changed, 2936 insertions, 1902 deletions
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index 0a87500a52..c1eeda75f5 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-2018 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 @@ -74,7 +76,6 @@ module ActionView autoload :MissingTemplate autoload :ActionViewError autoload :EncodingError - autoload :MissingRequestError autoload :TemplateError autoload :WrongEncodingError end @@ -89,8 +90,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..d41fe2a608 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -1,23 +1,25 @@ -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 "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 +27,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 +35,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 +112,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 +142,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? @@ -187,8 +184,8 @@ module ActionView #:nodoc: attr_accessor :view_renderer attr_internal :config, :assigns - delegate :lookup_context, :to => :view_renderer - delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :to => :lookup_context + delegate :lookup_context, to: :view_renderer + delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, to: :lookup_context def assign(new_assigns) # :nodoc: @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) } @@ -207,6 +204,7 @@ module ActionView #:nodoc: @view_renderer = ActionView::Renderer.new(lookup_context) end + @cache_hit = {} assign(assigns) assign_controller(controller) _prepare_context diff --git a/actionview/lib/action_view/buffers.rb b/actionview/lib/action_view/buffers.rb index be5d86b1dc..2a378fdc3c 100644 --- a/actionview/lib/action_view/buffers.rb +++ b/actionview/lib/action_view/buffers.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 class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc: diff --git a/actionview/lib/action_view/context.rb b/actionview/lib/action_view/context.rb index ee263df484..665a9e3171 100644 --- a/actionview/lib/action_view/context.rb +++ b/actionview/lib/action_view/context.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module CompiledTemplates #:nodoc: # holds compiled template code @@ -17,7 +19,6 @@ module ActionView 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 +28,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..1cf0bd3016 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -1,15 +1,16 @@ -require 'concurrent/map' -require 'action_view/dependency_tracker' -require 'monitor' +# frozen_string_literal: true + +require "concurrent/map" +require "action_view/dependency_tracker" +require "monitor" module ActionView class Digestor - @@digest_mutex = Mutex.new + @@digest_mutex = Mutex.new - class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc: - def call(env) + module PerExecutionDigestCacheExpiry + def self.before(target) ActionView::LookupContext::DetailsKey.clear - app.call(env) end end @@ -19,10 +20,9 @@ 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('.') + cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join(".") # this is a correctly done double-checked locking idiom # (Concurrent::Map's lookups have volatile semantics) @@ -46,8 +46,11 @@ 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) } + options = {} + options[:formats] = [finder.rendered_format] if finder.rendered_format + + if template = finder.disable_cache { finder.find_all(logical_name, [], partial, [], options).first } + finder.rendered_format ||= template.formats.first if node = seen[template.identifier] # handle cycles in the tree node @@ -61,8 +64,10 @@ 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 @@ -84,7 +89,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 +113,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/flows.rb b/actionview/lib/action_view/flows.rb index 4b912f0b2b..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: @@ -68,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 7f86994fd4..ff7f2bb853 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 @@ -6,9 +8,9 @@ module ActionView module VERSION MAJOR = 5 - MINOR = 0 + MINOR = 2 TINY = 0 - PRE = "beta4" + PRE = "beta2" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb index 787e9d67b2..46f20c4277 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: 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..da630129cb 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,18 +39,40 @@ 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. + # + # ==== 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> @@ -55,13 +81,21 @@ module ActionView # # => <script src="http://www.example.com/xmlhr.js"></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) }.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 +105,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 +128,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=stylesheet" 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 +164,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 +175,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 +184,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,52 +215,132 @@ 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 an 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 (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: "100x100")) + # # => <img src="/rails/active_storage/variants/.../tiger.jpg" /> + # image_tag(user.avatar.variant(resize: "100x100"), size: '100') + # # => <img width="100" height="100" src="/rails/active_storage/variants/.../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] @@ -239,7 +365,9 @@ module ActionView # 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 + ActiveSupport::Deprecation.warn("image_alt is deprecated and will be removed from Rails 6.0. You must explicitly set alt text on images.") + + 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, @@ -257,6 +385,8 @@ module ActionView # * <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 +394,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,9 +413,12 @@ 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 @@ -300,31 +435,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 +480,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..f7690104ee 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -1,8 +1,10 @@ -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. # @@ -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" /> # @@ -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" /> # @@ -96,8 +98,8 @@ module ActionView # 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. + # 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+. # # config.action_controller.asset_host = Proc.new { |source, request| # if request && request.ssl? @@ -117,31 +119,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(/([\?#].+)$/, "".freeze) 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 +225,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 +263,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 +275,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 +315,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 +324,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 +342,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 +351,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 +372,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 +381,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 +398,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 +407,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 +424,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 +433,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 +449,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..3cbb1ed1a7 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) @@ -204,29 +218,37 @@ module ActionView private - def fragment_name_with_digest(name, virtual_path) #:nodoc: + def fragment_name_with_digest(name, virtual_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 ] + name = controller.url_for(name).split("://").last if name.is_a?(Hash) + + if digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies).presence + [ "#{virtual_path}:#{digest}", name ] + else + [ virtual_path, name ] + end 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..92f7ddb70d 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. @@ -37,12 +39,12 @@ module ActionView 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 +110,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 +127,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 +147,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 +174,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/csrf_helper.rb b/actionview/lib/action_view/helpers/csrf_helper.rb index 5af92c4ff2..69c59844a6 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? [ - 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 9042b9cffd..09040ccbc4 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 - 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 + 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 + locale.t(:almost_x_years, count: distance_in_years + 1) + end end end end @@ -219,7 +218,7 @@ 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 @@ -266,7 +265,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 @@ -302,7 +301,7 @@ 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. + # # 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. @@ -681,19 +680,31 @@ 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) + 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_tag("time".freeze, content, options.reverse_merge(:datetime => datetime), &block) + content_tag("time".freeze, content, options.reverse_merge(datetime: datetime), &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".freeze 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[ @@ -709,8 +720,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 @@ -780,7 +791,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 @@ -800,7 +811,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 @@ -810,7 +821,7 @@ 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" end @@ -820,7 +831,7 @@ module ActionView def select_year if !@datetime || @datetime == 0 - val = '1' + val = "1" middle_year = Date.today.year else val = middle_year = year @@ -860,12 +871,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 @@ -885,7 +896,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): @@ -913,11 +924,11 @@ 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 @@ -928,7 +939,7 @@ module ActionView 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] @@ -975,7 +986,7 @@ 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 @@ -992,14 +1003,14 @@ 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[: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".dup + select_html << content_tag("option".freeze, "", value: "") + "\n" if @options[:include_blank] select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt] select_html << select_options_as_html @@ -1010,31 +1021,33 @@ module ActionView # 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(' ') + 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".freeze, prompt, value: "") : "" end # Builds hidden input tag for date part and value. @@ -1042,12 +1055,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 @@ -1058,7 +1071,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 @@ -1069,8 +1082,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 @@ -1078,7 +1091,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 = "".dup 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 @@ -1092,12 +1105,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..52dff1f750 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>. @@ -25,10 +26,10 @@ module ActionView def debug(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..6185aa133f 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1,18 +1,20 @@ -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. # @@ -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,297 @@ 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>: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 unobstrusive XHRs. + # Disable remote submits with <tt>local: true</tt>. + # * <tt>:skip_enforcing_utf8</tt> - By default a hidden field named +utf8+ + # is output to enforce UTF-8 submits. Set to true to skip the field. + # * <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) + 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, &Proc.new) + 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 +817,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 +1006,64 @@ 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. @@ -860,26 +1204,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 +1309,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 +1393,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 +1419,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 +1429,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 +1519,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: false, **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 + + 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 +1555,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 +1581,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 +1620,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 +1644,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 +1658,23 @@ 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) : {} + + 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| + (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 +1743,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 +1943,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 +2100,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 +2177,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 +2189,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 +2197,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 +2210,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 +2222,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: @@ -1982,12 +2322,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 b277efd7b6..fe5e0b693e 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -1,13 +1,15 @@ -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: @@ -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 @@ -277,17 +283,17 @@ module ActionView # 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 @@ -363,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 @@ -455,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".freeze, option_tags, label: value_for_collection(group, group_label_method)) end.join.html_safe end @@ -578,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".freeze, "-------------", value: "", disabled: true) zone_options.safe_concat "\n" zones = zones - priority_zones @@ -651,12 +657,12 @@ module ActionView # The HTML specification says when nothing is select 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. @@ -800,7 +806,7 @@ module ActionView 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 diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index cfff0bef5d..e86e18dd78 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,7 +20,7 @@ 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 # 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. @@ -113,7 +115,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,18 +136,20 @@ 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".freeze, 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".freeze, 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) @@ -270,7 +274,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. @@ -390,7 +394,7 @@ module ActionView # # => <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 +444,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 +508,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 +534,22 @@ 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) + tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options) end # Creates a field set for grouping HTML form elements. @@ -683,7 +675,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 +683,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 +847,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,7 +864,7 @@ module ActionView action: html_options["action"], method: method }) - end + end if html_options.delete("enforce_utf8") { true } utf8_enforcer_tag + method_tag @@ -906,7 +886,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..dd2cd57ac3 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".dup.force_encoding(Encoding::UTF_8).encode!] = "
" + JS_ESCAPE_MAP["\342\200\251".dup.force_encoding(Encoding::UTF_8).encode!] = "
" # Escapes carriage returns and single and double quotes for JavaScript segments. # @@ -24,10 +26,10 @@ 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] } + 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 else - '' + "" end end diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index f0222582c7..4b53b8fe6e 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 @@ -94,7 +94,7 @@ 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. @@ -173,6 +173,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. # @@ -189,6 +192,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) @@ -263,8 +269,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. # @@ -395,53 +399,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 d4b55423a8..279cde5e76 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,10 +27,10 @@ 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 @@ -42,9 +44,9 @@ module ActionView #:nodoc: 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 ' + words_connector: ", ", + two_words_connector: " and ", + last_word_connector: ", and " } if defined?(I18n) i18n_connectors = I18n.translate(:'support.array', locale: options[:locale], default: {}) @@ -54,13 +56,13 @@ module ActionView #:nodoc: case array.length when 0 - ''.html_safe + "".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]]) + safe_join([safe_join(array[0...-1], options[:words_connector]), options[:last_word_connector], array[-1]], nil) 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 index f7ee573035..a6953ee905 100644 --- a/actionview/lib/action_view/helpers/record_tag_helper.rb +++ b/actionview/lib/action_view/helpers/record_tag_helper.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module ActionView - module Helpers + module Helpers #:nodoc: module RecordTagHelper - def div_for(*) + def div_for(*) # :nodoc: 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" \ @@ -9,7 +11,7 @@ module ActionView "Consult the Rails upgrade guide for details." end - def content_tag_for(*) + def content_tag_for(*) # :nodoc: 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" \ diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index c98f2d74a8..8e505ab054 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. @@ -27,12 +29,12 @@ module ActionView case options when Hash if block_given? - view_renderer.render_partial(self, options.merge(:partial => options[:layout]), &block) + view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block) else view_renderer.render(self, options) 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..275a2dffb4 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -1,9 +1,11 @@ -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 @@ -13,6 +15,7 @@ module ActionView # 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 # @@ -45,17 +47,15 @@ module ActionView # 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 diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 42e7358a1d..a6cec3f69c 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -1,39 +1,207 @@ -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 = "".dup + 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, " ".freeze) : value.join(" ".freeze) + else + value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s + end + %(#{key}="#{value.gsub('"'.freeze, '"'.freeze)}") + 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 +210,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 /> # @@ -72,8 +232,12 @@ module ActionView # # 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 +245,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 +269,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 +289,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 +305,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..fed908fcdb 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,157 @@ 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}#{"[]" if multiple}" + when index + "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" + else + "#{@object_name}[#{sanitized_method_name}]#{"[]" if 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:]]]/, "").mb_chars.downcase.to_s + 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] + option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), value: "") + "\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..c5f0bb6bbb 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 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..345484ba92 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: @@ -6,15 +8,15 @@ module ActionView @choices = block_given? ? template_object.capture { yield || "" } : choices @choices = @choices.to_a if @choices.is_a?(Range) - @html_options = html_options + @html_options = html_options.except(:skip_default_ids, :allow_method_names_outside_object) super(object_name, method_name, template_object, options) end 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..fcf96d2c9c 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,28 @@ module ActionView translated_attribute || human_attribute_name end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :object_name, :method_and_value, :scope, :model + attr_reader :object_name, :method_and_value, :scope, :model private - 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..84d38aa416 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 @@ -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... @@ -187,7 +189,7 @@ module ActionView unless separator.empty? text.split(separator).each do |value| if value.match(regex) - regex = phrase = value + phrase = value break end end @@ -225,14 +227,7 @@ 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 - + def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale) word = if (count == 1 || count =~ /^1(\.0+)?$/) singular else @@ -269,10 +264,11 @@ module ActionView 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..1860bc4732 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 @@ -96,16 +97,16 @@ 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('.')}".dup 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 @@ -133,7 +134,7 @@ module ActionView end def html_safe_translation_key?(key) - key.to_s =~ /(\b|_|\.)html$/ + /(\b|_|\.)html$/.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..889562c478 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 %> @@ -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) : "".freeze.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 + "".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,7 +487,7 @@ 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? ? "".freeze : "?" + extras.join("&") encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@") html_options["href"] = "mailto:#{encoded_email_address}#{extras}" @@ -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 # 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,42 +575,59 @@ 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".freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options) - method = html_options.delete('method'.freeze) + method = html_options.delete("method".freeze) 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".freeze } : {} end end def link_to_remote_options?(options) if options.is_a?(Hash) - options.delete('remote'.freeze) || options.delete(:remote) + options.delete("remote".freeze) || 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: {}) + def token_tag(token = nil, form_options: {}) if token != false && 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 + "".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 @@ -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 diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index a74a5e05f3..3e6d352c15 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,9 +319,9 @@ 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 + class_eval <<-RUBY, __FILE__, __LINE__ + 1 def _layout(formats) if _conditional_layout? #{layout_definition} @@ -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: @@ -400,11 +406,11 @@ 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) + # * <tt>template</tt> - The template object for the default layout (or +nil+) def _default_layout(formats, require_layout = false) begin value = _layout(formats) if action_has_layout? @@ -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..d4ac77e10f 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,15 +16,24 @@ module ActionView def render_template(event) info do - message = " Rendered #{from_rails_root(event.payload[:identifier])}" + message = " Rendered #{from_rails_root(event.payload[:identifier])}".dup + message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] + message << " (#{event.duration.round(1)}ms)" + end + end + + def render_partial(event) + info do + message = " Rendered #{from_rails_root(event.payload[:identifier])}".dup message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] message << " (#{event.duration.round(1)}ms)" + 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)}" \ @@ -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])}".dup 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..0e56eca35c 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -1,7 +1,9 @@ -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 "action_view/template/resolver" module ActionView # = Action View Lookup Context @@ -14,14 +16,12 @@ module ActionView class LookupContext #:nodoc: attr_accessor :prefixes, :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 @@ -63,7 +63,7 @@ module ActionView details = details.dup details[:formats] &= Template::Types.symbols end - @details_keys[details] ||= new + @details_keys[details] ||= Concurrent::Map.new end def self.clear @@ -71,13 +71,7 @@ module ActionView end def self.digest_caches - @details_keys.values.map(&:digest_cache) - end - - attr_reader :digest_cache - - def initialize - @digest_cache = Concurrent::Map.new + @details_keys.values end end @@ -99,9 +93,9 @@ module ActionView @cache = old_value end - protected + private - def _set_detail(key, value) + def _set_detail(key, value) # :doc: @details = @details.dup if @details_key @details_key = nil @details[key] = value @@ -155,16 +149,16 @@ module ActionView added_resolvers.times { view_paths.pop } end - protected + private - 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) @@ -177,13 +171,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 = {} @@ -206,15 +200,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("/".freeze) 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("/".freeze) prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts] return name, prefixes @@ -236,7 +230,7 @@ module ActionView end def digest_cache - details_key.digest_cache + details_key end def initialize_details(target, details) 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 c83614c89a..73dfb267bb 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + require "action_view" require "rails" module ActionView # = Action View Railtie - class Railtie < Rails::Railtie # :nodoc: + class Railtie < Rails::Engine # :nodoc: 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.eager_load_namespaces << ActionView @@ -17,13 +19,29 @@ 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.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 @@ -39,8 +57,8 @@ module ActionView 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::Digestor::PerExecutionDigestCacheExpiry end end end diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb index 4a2547b0fb..1310a1ce0a 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 = "_".freeze + NEW = "new".freeze # 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..20b2523cac 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 :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context def initialize(lookup_context) @lookup_context = lookup_context @@ -25,29 +27,29 @@ module ActionView raise NotImplementedError end - protected + private - def extract_details(options) - @lookup_context.registered_details.each_with_object({}) do |key, details| - value = options[key] + def extract_details(options) # :doc: + @lookup_context.registered_details.each_with_object({}) do |key, details| + value = options[key] - details[key] = Array(value) if value + details[key] = Array(value) if value + end end - end - def instrument(name, **options) - options[:identifier] ||= (@template && @template.identifier) || @path + def instrument(name, **options) # :doc: + options[:identifier] ||= (@template && @template.identifier) || @path - ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| - yield payload + ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| + yield payload + end end - end - def prepend_formats(formats) - formats = Array(formats) - return if formats.empty? || @lookup_context.html_fallback_for_js + 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 + @lookup_context.formats = formats | @lookup_context.formats + 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..5b40af4f2f 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 %> @@ -307,243 +309,244 @@ module ActionView if @collection render_collection else - instrument(:partial) do - render_partial - end + render_partial end end private - def render_collection - instrument(:collection, count: @collection.size) do |payload| - return nil if @collection.blank? + 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) - end + if @options.key?(:spacer_template) + spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) + end - cache_collection_render(payload) do - @template ? collection_with_template : collection_without_template - end.join(spacer).html_safe + cache_collection_render(payload) do + @template ? collection_with_template : collection_without_template + end.join(spacer).html_safe + end end - end - def render_partial - view, locals, block = @view, @locals, @block - object, as = @object, @variable + def render_partial + instrument(:partial) do |payload| + view, locals, block = @view, @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) + content = @template.render(view, locals) do |*name| + view._layout_for(*name, &block) + end + + content = layout.render(view, locals) { content } if layout + payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path] + content + end end - content = layout.render(view, locals){ content } if layout - content - 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 <tt>@path</tt> instance variable is + # set to that string. Otherwise, the +options[:partial]+ object must + # respond to +to_partial_path+ in order to setup the path. + def setup(context, options, block) + @view = context + @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 + + if @collection + paths = @collection_data = @collection.map { |o| partial_path(o) } + @path = paths.uniq.one? ? paths.first : nil + else + @path = partial_path + end + 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 + if as = options[:as] + raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s) + as = as.to_sym + end - if @collection - paths = @collection_data = @collection.map { |o| partial_path(o) } - @path = paths.uniq.one? ? paths.first : nil + if @path + @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as) + @template_keys = retrieve_template_keys else - @path = partial_path + paths.map! { |path| retrieve_variable(path, as).unshift(path) } end + + self end - if as = options[:as] - raise_invalid_option_as(as) unless as.to_s =~ /\A[a-z_]\w*\z/ - as = as.to_sym + def collection_from_options + if @options.key?(:collection) + collection = @options[:collection] + collection ? collection.to_a : [] + end 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 collection_from_object + @object.to_ary if @object.respond_to?(:to_ary) end - self - end + def find_partial + find_template(@path, @template_keys) if @path + end - def collection_from_options - if @options.key?(:collection) - collection = @options[:collection] - collection.respond_to?(:to_ary) ? collection.to_ary : [] + def find_template(path, locals) + prefixes = path.include?(?/) ? [] : @lookup_context.prefixes + @lookup_context.find_template(path, prefixes, true, locals, @details) end - end - def collection_from_object - @object.to_ary if @object.respond_to?(:to_ary) - end + def collection_with_template + view, locals, template = @view, @locals, @template + as, counter, iteration = @variable, @variable_counter, @variable_iteration - def find_partial - find_template(@path, @template_keys) if @path - end + if layout = @options[:layout] + layout = find_template(layout, @template_keys) + end - def find_template(path, locals) - prefixes = path.include?(?/) ? [] : @lookup_context.prefixes - @lookup_context.find_template(path, prefixes, true, locals, @details) - end + partial_iteration = PartialIteration.new(@collection.size) + locals[iteration] = partial_iteration - def collection_with_template - view, locals, template = @view, @locals, @template - as, counter, iteration = @variable, @variable_counter, @variable_iteration + @collection.map do |object| + locals[as] = object + locals[counter] = partial_iteration.index - if layout = @options[:layout] - layout = find_template(layout, @template_keys) + content = template.render(view, locals) + content = layout.render(view, locals) { content } if layout + partial_iteration.iterate! + content + end end - partial_iteration = PartialIteration.new(@collection.size) - locals[iteration] = partial_iteration + def collection_without_template + view, locals, collection_data = @view, @locals, @collection_data + cache = {} + keys = @locals.keys - @collection.map do |object| - locals[as] = object - locals[counter] = partial_iteration.index + partial_iteration = PartialIteration.new(@collection.size) - content = template.render(view, locals) - content = layout.render(view, locals) { content } if layout - partial_iteration.iterate! - content - end - end + @collection.map do |object| + index = partial_iteration.index + path, as, counter, iteration = collection_data[index] - def collection_without_template - view, locals, collection_data = @view, @locals, @collection_data - cache = {} - keys = @locals.keys + locals[as] = object + locals[counter] = index + locals[iteration] = partial_iteration - partial_iteration = PartialIteration.new(@collection.size) - - @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 - - 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! + content + 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 = @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.") + 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 + keys = @locals.keys + keys << @variable if @has_object || @collection + 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] == "/".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" + 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..db52919e91 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,7 +7,7 @@ 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 @@ -25,14 +27,20 @@ module ActionView end end + def callable_cache_key? + @options[:cached].respond_to?(:call) + end + def collection_by_cache_keys + seed = callable_cache_key? ? @options[:cached] : ->(i) { i } + @collection.each_with_object({}) do |item, hash| - hash[expanded_cache_key(item)] = item + hash[expanded_cache_key(seed.call(item))] = item end end def expanded_cache_key(key) - key = @view.fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path)) + key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path)) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb index 2a3b89aebf..3f3a97529d 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 @@ -46,5 +48,9 @@ module ActionView def render_partial(context, options, &block) #:nodoc: PartialRenderer.new(@lookup_context).render(context, options, block) end + + def cache_hits # :nodoc: + @cache_hits ||= {} + end end end diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index f38e2764d0..276a28ce07 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,17 +28,16 @@ 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".dup + 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 end # For streaming, instead of rendering a given a template, we return a Body @@ -56,48 +56,50 @@ module ActionView 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..ce8908924a 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -1,4 +1,6 @@ -require 'active_support/core_ext/object/try' +# frozen_string_literal: true + +require "active_support/core_ext/object/try" module ActionView class TemplateRenderer < AbstractRenderer #:nodoc: @@ -16,87 +18,85 @@ module ActionView 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) + 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] + else + 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 || {} + # Renders the given template. A string representing the layout can be + # supplied as well. + def render_template(template, layout_name = nil, locals = nil) + 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) } + 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) } + 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(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 + if layout + view = @view + view.view_flow.set(:layout, content) + layout.render(view, locals) { |*name| view._layout_for(*name) } + else + content + end 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?("/") + with_fallbacks { find_template(layout, nil, false, [], details) } + else + 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(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..4e5fdfbb2d 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 @@ -73,8 +75,7 @@ module ActionView end # Returns an object that is able to render templates. - # :api: private - def view_renderer + def view_renderer # :nodoc: @_view_renderer ||= ActionView::Renderer.new(lookup_context) end @@ -90,8 +91,7 @@ module ActionView 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 @@ -104,7 +104,7 @@ module ActionView 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 @@ -112,8 +112,7 @@ module ActionView # 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 +123,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..fd563f34a9 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 @@ -123,18 +124,15 @@ 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 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..0c4bb73acb 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -1,6 +1,8 @@ -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 "thread" module ActionView # = Action View Template @@ -65,8 +67,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 @@ -141,7 +142,7 @@ module ActionView 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,8 +153,8 @@ 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 = nil, &block) + instrument_render_template do compile!(view) view.send(method_name, locals, buffer, &block) end @@ -180,12 +181,12 @@ 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 inspect - @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", ''.freeze) : identifier + @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "".freeze) : identifier end # This method is responsible for properly setting the encoding of the @@ -204,7 +205,7 @@ module ActionView # 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 +233,11 @@ module ActionView end end - protected + 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 @@ -277,14 +278,13 @@ 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: + def compile(mod) encode! - method_name = self.method_name code = @handler.call(self) # Make sure that the resulting String to be eval'd is in the # encoding of the code - source = <<-end_src + source = <<-end_src.dup 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 @@ -310,7 +310,7 @@ module ActionView ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) 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 +324,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("".dup) { |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__}".dup + m.tr!("-".freeze, "_".freeze) m end end - def identifier_method_name #:nodoc: - inspect.tr('^a-z_'.freeze, '_'.freeze) + def identifier_method_name + inspect.tr("^a-z_".freeze, "_".freeze) 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".freeze, 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..4e3c02e05e 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 @@ -130,7 +119,7 @@ module ActionView if line_number "on line ##{line_number} of " else - 'in ' + "in " end + file_name end diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb index ad4c353608..7ec76dcc3f 100644 --- a/actionview/lib/action_view/template/handlers.rb +++ b/actionview/lib/action_view/template/handlers.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + 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 diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb index d90b0c6378..61492ce448 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) require_engine - "xml = ::Builder::XmlMarkup.new(:indent => 2);" + + "xml = ::Builder::XmlMarkup.new(:indent => 2);" \ "self.output_buffer = xml.target!;" + template.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..b7b749f9da 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -1,91 +1,20 @@ -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_whitelist, default: ["text/plain"] ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") @@ -108,7 +37,7 @@ module ActionView # expression template_source = template.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) @@ -118,8 +47,8 @@ module ActionView self.class.erb_implementation.new( erb, - :escape => (self.class.escape_whitelist.include? template.type), - :trim => (self.class.erb_trim_mode == "-") + escape: (self.class.escape_whitelist.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..db75f028ed --- /dev/null +++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb @@ -0,0 +1,83 @@ +# 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] = "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" + properties[:postamble] = "@output_buffer.to_s" + properties[:bufvar] = "@output_buffer" + properties[:escapefunc] = "" + + super + end + + def evaluate(action_view_erb_handler_context) + pr = eval("proc { #{@src} }", binding, @filename || "(erubi)") + action_view_erb_handler_context.instance_eval(&pr) + 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..27004a318c 100644 --- a/actionview/lib/action_view/template/handlers/html.rb +++ b/actionview/lib/action_view/template/handlers/html.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActionView module Template::Handlers class Html < Raw diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb index 760f517431..5cd23a0060 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};" + "#{template.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..a262c6d9ad 100644 --- a/actionview/lib/action_view/template/html.rb +++ b/actionview/lib/action_view/template/html.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActionView #:nodoc: # = Action View HTML Template - class Template + class Template #:nodoc: class HTML #:nodoc: attr_accessor :type @@ -11,12 +13,10 @@ module ActionView #:nodoc: end def identifier - 'html template' + "html template" end - def inspect - 'html template' - end + alias_method :inspect, :identifier def to_str ERB::Util.h(@string) diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index c5e69b1833..5a86f10973 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 = "".dup 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 @@ -88,28 +90,44 @@ 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? + private + + def canonical_no_templates(templates) + templates.empty? ? NO_TEMPLATES : templates end - cached_templates_max_updated_at = cached_templates.map(&:updated_at).max + def templates_have_changed?(cached_templates, fresh_templates) + # if either the old or new template list is empty, we don't need to (and can't) + # compare modification times, and instead just check whether the lists are different + if cached_templates.blank? || fresh_templates.blank? + return fresh_templates.blank? != cached_templates.blank? + end + + cached_templates_max_updated_at = cached_templates.map(&:updated_at).max - # if a template has changed, it will be now be newer than all the cached templates - fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at } - end + # if a template has changed, it will be now be newer than all the cached templates + fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at } + end end - cattr_accessor :caching - self.caching = true + cattr_accessor :caching, default: true class << self alias :caching? :caching @@ -124,13 +142,13 @@ 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 = []) cached(key, [name, prefix, partial], details, locals) do find_templates(name, prefix, partial, details) 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 = []) cached(key, [name, prefix, partial], details, locals) do find_templates(name, prefix, partial, details, true) end @@ -147,8 +165,8 @@ 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" + def find_templates(name, prefix, partial, details, outside_app_allowed = false) + raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed = false) method" end # Helpers that builds a path. Useful for building virtual paths. @@ -160,7 +178,7 @@ module ActionView # 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! @@ -174,7 +192,7 @@ module ActionView end # Ensures all the resolver information is set in the template. - def decorate(templates, path_info, details, locals) #:nodoc: + def decorate(templates, path_info, details, locals) cached = nil templates.each do |t| t.locals = locals @@ -187,103 +205,103 @@ module ActionView # 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) + def initialize(pattern = nil) @pattern = pattern || DEFAULT_PATTERN 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) + path = Path.build(name, prefix, partial) + query(path, details, details[:formats], outside_app_allowed) + end - def query(path, details, formats, outside_app_allowed) - query = build_query(path, details) + def query(path, details, formats, outside_app_allowed) + query = build_query(path, details) - template_paths = find_template_paths(query) - 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| - handler, format, variant = extract_handler_and_format_and_variant(template, formats) - contents = File.binread(template) + template_paths.map do |template| + handler, format, variant = extract_handler_and_format_and_variant(template) + contents = File.binread(template) - Template.new(contents, File.expand_path(template), handler, - :virtual_path => path.virtual, - :format => format, - :variant => variant, - :updated_at => mtime(template) - ) + Template.new(contents, File.expand_path(template), handler, + virtual_path: path.virtual, + format: format, + variant: variant, + updated_at: mtime(template) + ) + end 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(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 - end - def inside_path?(path, filename) - filename = File.expand_path(filename) - path = File.join(path, '') - filename.start_with?(path) - end + def inside_path?(path, filename) + filename = File.expand_path(filename) + path = File.join(path, "") + filename.start_with?(path) + end - # Helper for building query glob string based on resolver's pattern. - def build_query(path, details) - query = @pattern.dup + # Helper for building query glob string based on resolver's pattern. + def build_query(path, details) + query = @pattern.dup - prefix = path.prefix.empty? ? '' : "#{escape_entry(path.prefix)}\\1" - query.gsub!(/:prefix(\/)?/, prefix) + prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1" + query.gsub!(/:prefix(\/)?/, prefix) - partial = escape_entry(path.partial? ? "_#{path.name}" : path.name) - query.gsub!(/:action/, partial) + partial = escape_entry(path.partial? ? "_#{path.name}" : path.name) + query.gsub!(/:action/, partial) - details.each do |ext, candidates| - if ext == :variants && candidates == :any - query.gsub!(/:#{ext}/, "*") - else - query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}") + details.each do |ext, candidates| + if ext == :variants && candidates == :any + query.gsub!(/:#{ext}/, "*") + else + query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}") + end end - end - File.expand_path(query, @path) - end + File.expand_path(query, @path) + end - def escape_entry(entry) - entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze) - end + def escape_entry(entry) + entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze) + end - # Returns the file mtime from the filesystem. - def mtime(p) - File.mtime(p) - end + # Returns the file mtime from the filesystem. + def mtime(p) + File.mtime(p) + end - # Extract handler, formats and variant from path. If a format cannot be found neither - # from the path, or the handler, we should return the array of formats given - # to the resolver. - 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(".".freeze) + 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 &&= Template::Types[format] - [handler, format, variant] - end + [handler, format, variant] + end end # A resolver that loads files from the filesystem. It allows setting your own @@ -292,13 +310,13 @@ module ActionView # ==== 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},}` + # looking for <tt>users/new</tt> it will produce query glob: <tt>users/new{.{en},}{.{html,js},}{.{erb,haml},}</tt> # # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}") # # This one allows you to keep files with different formats in separate subdirectories, - # eg. `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. + # eg. <tt>users/new.html</tt> will be loaded from <tt>users/html/new.erb</tt> or <tt>users/new.html.erb</tt>, + # <tt>users/new.js</tt> from <tt>users/js/new.erb</tt> or <tt>users/new.js.erb</tt>, etc. # # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}") # @@ -325,7 +343,7 @@ module ActionView # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...) # class FileSystemResolver < PathResolver - def initialize(path, pattern=nil) + 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) diff --git a/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb index 04f5b8d17a..f8d6c2811f 100644 --- a/actionview/lib/action_view/template/text.rb +++ b/actionview/lib/action_view/template/text.rb @@ -1,22 +1,21 @@ +# 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] + @type = Types[:text] end def identifier - 'text template' + "text template" end - def inspect - 'text template' - end + alias_method :inspect, :identifier def to_str @string @@ -27,7 +26,7 @@ module ActionView #:nodoc: end def formats - [@type.respond_to?(:ref) ? @type.ref : @type.to_s] + [@type.ref] 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..e1cbae5845 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 = "".dup 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..68186c3bf8 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 @@ -8,46 +10,45 @@ module ActionView #:nodoc: 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(', ') + @hash.keys.join(", ") end - private - - def query(path, exts, formats, _) - 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, 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 - ) + private + + def query(path, exts, _, _) + query = "".dup + 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, array| + source, updated_at = array + next unless query.match?(_path) + handler, format, variant = extract_handler_and_format_and_variant(_path) + templates << Template.new(source, _path, handler, + virtual_path: path.virtual, + format: format, + variant: variant, + updated_at: updated_at + ) + end + + templates.sort_by { |t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size } 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, _, _) + 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)] 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..d5694d77f4 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -1,15 +1,15 @@ +# 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 + class_attribute :_view_paths, default: 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 _prefixes # :nodoc: @@ -22,11 +22,11 @@ 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 # The prefixes used in render "foo" shortcuts. @@ -43,13 +43,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 |