diff options
Diffstat (limited to 'actionview/lib/action_view')
61 files changed, 858 insertions, 2429 deletions
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index 900f96255e..1feafc1094 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -10,8 +10,10 @@ 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 a mixture of ERB - # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> extension then Jim Weirich's Builder::XmlMarkup library is used. + # 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] + # 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 # @@ -31,7 +33,9 @@ module ActionView #:nodoc: # # If you absolutely must write from within a function use +concat+. # - # <%- and -%> suppress leading and trailing whitespace, including the trailing newline, and can be used interchangeably with <% and %>. + # 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. # # === Using sub templates # @@ -66,15 +70,6 @@ module ActionView #:nodoc: # Headline: <%= headline %> # First name: <%= person.first_name %> # - # If you need to find out whether a certain local variable has been assigned a value in a particular render call, - # you need to use the following pattern: - # - # <% if local_assigns.has_key? :headline %> - # Headline: <%= headline %> - # <% end %> - # - # Testing using <tt>defined? headline</tt> will not work. This is an implementation restriction. - # # === Template caching # # By default, Rails will compile each template to a method in order to render it. When you alter a template, @@ -131,8 +126,8 @@ module ActionView #:nodoc: # end # end # - # For more information on Builder please consult the [source - # code](https://github.com/jimweirich/builder). + # For more information on Builder please consult the {source + # code}[https://github.com/jimweirich/builder]. class Base include Helpers, ::ERB::Util, Context diff --git a/actionview/lib/action_view/buffers.rb b/actionview/lib/action_view/buffers.rb index 361a0dccbe..be5d86b1dc 100644 --- a/actionview/lib/action_view/buffers.rb +++ b/actionview/lib/action_view/buffers.rb @@ -13,10 +13,11 @@ module ActionView end alias :append= :<< - def safe_concat(value) - return self if value.nil? - super(value.to_s) + def safe_expr_append=(val) + return self if val.nil? + safe_concat val.to_s end + alias :safe_append= :safe_concat end diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index 0ccf2515c5..e34bdd4a46 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -53,6 +53,12 @@ module ActionView \s* # followed by optional spaces /x + # Part of any hash containing the :layout key + LAYOUT_HASH_KEY = / + (?:\blayout:|:layout\s*=>) # layout key in either old or new style hash syntax + \s* # followed by optional spaces + /x + # Matches: # partial: "comments/comment", collection: @all_comments => "comments/comment" # (object: @single_comment, partial: "comments/comment") => "comments/comment" @@ -65,9 +71,9 @@ module ActionView # topics => "topics/topic" # (message.topics) => "topics/topic" RENDER_ARGUMENTS = /\A - (?:\s*\(?\s*) # optional opening paren surrounded by spaces - (?:.*?#{PARTIAL_HASH_KEY})? # optional hash, up to the partial key declaration - (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest + (?:\s*\(?\s*) # optional opening paren surrounded by spaces + (?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration + (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest /xm def self.call(name, template) @@ -85,8 +91,8 @@ module ActionView attr_reader :name, :template private :name, :template - private + private def source template.source end diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 72d79735ae..1f103786cb 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -60,7 +60,7 @@ module ActionView def digest Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| - logger.try :info, " Cache digest for #{template.inspect}: #{digest}" + logger.try :debug, " Cache digest for #{template.inspect}: #{digest}" end rescue ActionView::MissingTemplate logger.try :error, " Couldn't find template for digesting: #{name}" diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb index 9266e55c47..4f45f5b8c8 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -1,12 +1,12 @@ module ActionView - # Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt> + # Returns the version of the currently loaded Action View as a <tt>Gem::Version</tt> def self.gem_version Gem::Version.new VERSION::STRING end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 7333ea999a..5c28043f8a 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -7,7 +7,7 @@ module ActionView # = Action View Asset Tag Helpers module Helpers #:nodoc: # This module provides methods for generating HTML that links views to assets such - # as images, javascripts, stylesheets, and feeds. These methods do not verify + # as images, JavaScripts, stylesheets, and feeds. These methods do not verify # the assets exist before linking to them: # # image_tag("rails.png") @@ -218,7 +218,7 @@ module ActionView tag("img", options) end - # Returns a string suitable for an html image tag alt attribute. + # Returns a string suitable for an HTML image tag alt attribute. # The +src+ argument is meant to be an image file path. # The method removes the basename of the file path and the digest, # if any. It also removes hyphens and underscores from file names and @@ -239,7 +239,7 @@ module ActionView File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize end - # Returns an html video tag for the +sources+. If +sources+ is a string, + # Returns an HTML video tag for the +sources+. If +sources+ is a string, # a single video tag will be returned. If +sources+ is an array, a video # tag with nested source tags for each source will be returned. The # +sources+ can be full paths or files that exists in your public videos @@ -318,6 +318,7 @@ module ActionView 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} diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index 006b15be91..29733442c1 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -7,10 +7,10 @@ module ActionView # urls. # # image_path("rails.png") - # # => "/images/rails.png" + # # => "/assets/rails.png" # # image_url("rails.png") - # # => "http://www.example.com/images/rails.png" + # # => "http://www.example.com/assets/rails.png" # # === Using asset hosts # @@ -121,8 +121,8 @@ module ActionView # asset_path "application", type: :stylesheet # => /assets/application.css # asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js def asset_path(source, options = {}) - return "" unless source.present? source = source.to_s + return "" unless source.present? return source if source =~ URI_REGEXP tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '') @@ -155,7 +155,7 @@ module ActionView # # All other options provided are forwarded to +asset_path+ call. # - # asset_url "application.js" # => http://example.com/application.js + # asset_url "application.js" # => http://example.com/assets/application.js # asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js # def asset_url(source, options = {}) @@ -203,7 +203,6 @@ module ActionView request = self.request if respond_to?(:request) host = options[:host] host ||= config.asset_host if defined? config.asset_host - host ||= request.base_url if request && options[:protocol] == :request if host.respond_to?(:call) arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity @@ -214,6 +213,7 @@ module ActionView host = host % (Zlib.crc32(source) % 4) end + host ||= request.base_url if request && options[:protocol] == :request return unless host if host =~ URI_REGEXP @@ -231,13 +231,13 @@ module ActionView end end - # Computes the path to a javascript asset in the public javascripts directory. + # Computes the path to a JavaScript asset in the public javascripts directory. # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) # Full paths from the document root will be passed through. # Used internally by +javascript_include_tag+ to build the script path. # - # javascript_path "xmlhr" # => /javascripts/xmlhr.js - # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js + # javascript_path "xmlhr" # => /assets/xmlhr.js + # javascript_path "dir/xmlhr.js" # => /assets/dir/xmlhr.js # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js # 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 @@ -246,7 +246,7 @@ module ActionView end alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route - # Computes the full URL to a javascript asset in the public javascripts directory. + # Computes the full URL to a JavaScript asset in the public javascripts directory. # This will use +javascript_path+ internally, so most of their behaviors will be the same. def javascript_url(source, options = {}) url_to_asset(source, {type: :javascript}.merge!(options)) @@ -258,8 +258,8 @@ module ActionView # Full paths from the document root will be passed through. # Used internally by +stylesheet_link_tag+ to build the stylesheet path. # - # stylesheet_path "style" # => /stylesheets/style.css - # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css + # stylesheet_path "style" # => /assets/style.css + # stylesheet_path "dir/style.css" # => /assets/dir/style.css # stylesheet_path "/dir/style.css" # => /dir/style.css # 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 @@ -279,9 +279,9 @@ module ActionView # Full paths from the document root will be passed through. # Used internally by +image_tag+ to build the image path: # - # image_path("edit") # => "/images/edit" - # image_path("edit.png") # => "/images/edit.png" - # image_path("icons/edit.png") # => "/images/icons/edit.png" + # image_path("edit") # => "/assets/edit" + # image_path("edit.png") # => "/assets/edit.png" + # image_path("icons/edit.png") # => "/assets/icons/edit.png" # image_path("/icons/edit.png") # => "/icons/edit.png" # image_path("http://www.example.com/img/edit.png") # => "http://www.example.com/img/edit.png" # diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb index 227ad4cdfa..d8be4e5678 100644 --- a/actionview/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -174,7 +174,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. 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 = {}) @@ -191,7 +191,8 @@ module ActionView type = options.fetch(:type, 'text/html') - @xml.link(:rel => 'alternate', :type => type, :href => options[:url] || @view.polymorphic_url(record)) + url = options.fetch(:url) { @view.polymorphic_url(record) } + @xml.link(:rel => 'alternate', :type => type, :href => url) if url yield AtomBuilder.new(@xml) end diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb index 75d1634b2e..5a3223968f 100644 --- a/actionview/lib/action_view/helpers/capture_helper.rb +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -31,7 +31,8 @@ module ActionView # <head><title><%= @greeting %></title></head> # <body> # <b><%= @greeting %></b> - # </body></html> + # </body> + # </html> # def capture(*args) value = nil diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 2efb9612ac..46ce7cf0be 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -153,8 +153,8 @@ module ActionView # # Note that you cannot pass a <tt>Numeric</tt> value to <tt>time_ago_in_words</tt>. # - def time_ago_in_words(from_time, include_seconds_or_options = {}) - distance_of_time_in_words(from_time, Time.now, include_seconds_or_options) + def time_ago_in_words(from_time, options = {}) + distance_of_time_in_words(from_time, Time.now, options) end alias_method :distance_of_time_in_words_to_now, :time_ago_in_words @@ -177,7 +177,9 @@ module ActionView # and +:name+ (string). A format string would be something like "%{name} (%<number>02d)" for example. # See <tt>Kernel.sprintf</tt> for documentation on format sequences. # * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing). - # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt>if + # * <tt>:time_separator</tt> - Specifies a string to separate the time fields. Default is "" (i.e. nothing). + # * <tt>:datetime_separator</tt>- Specifies a string to separate the date and time fields. Default is "" (i.e. nothing). + # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt> if # you are creating new record. While editing existing record, <tt>:start_year</tt> defaults to # the current selected year minus 5. # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Date.today.year + 5</tt> if @@ -330,7 +332,7 @@ module ActionView Tags::DatetimeSelect.new(object_name, method, self, options, html_options).render end - # Returns a set of html select-tags (one for year, month, day, hour, minute, and second) pre-selected with the + # Returns a set of HTML select-tags (one for year, month, day, hour, minute, and second) pre-selected with the # +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with # an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not # supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add @@ -379,7 +381,7 @@ module ActionView DateTimeSelector.new(datetime, options, html_options).select_datetime end - # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. + # Returns a set of HTML select-tags (one for year, month, and day) pre-selected with the +date+. # It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. # If the array passed to the <tt>:order</tt> option does not contain all the three symbols, all tags will be hidden. @@ -418,7 +420,7 @@ module ActionView DateTimeSelector.new(date, options, html_options).select_date end - # Returns a set of html select-tags (one for hour and minute). + # Returns a set of HTML select-tags (one for hour and minute). # You can set <tt>:time_separator</tt> key to format the output, and # the <tt>:include_seconds</tt> option to include an input for seconds. # @@ -635,7 +637,7 @@ module ActionView DateTimeSelector.new(date, options, html_options).select_year end - # Returns an html time tag for the given date or time. + # Returns an HTML time tag for the given date or time. # # time_tag Date.today # => # <time datetime="2010-11-04">November 04, 2010</time> @@ -898,7 +900,7 @@ module ActionView def translated_date_order date_order = I18n.translate(:'date.order', :locale => @options[:locale], :default => []) - date_order = date_order.map { |element| element.to_sym } + date_order = date_order.map(&:to_sym) forbidden_elements = date_order - [:year, :month, :day] if forbidden_elements.any? @@ -914,7 +916,7 @@ module ActionView build_select(type, build_options(selected, options)) end - # Build select option html from date value and options. + # Build select option HTML from date value and options. # build_options(15, start: 1, end: 31) # => "<option value="1">1</option> # <option value="2">2</option> @@ -954,7 +956,7 @@ module ActionView (select_options.join("\n") + "\n").html_safe end - # Builds select tag from date type and html select options. + # Builds select tag from date type and HTML select options. # build_select(:month, "<option value="1">January</option>...") # => "<select id="post_written_on_2i" name="post[written_on(2i)]"> # <option value="1">January</option>... @@ -1035,7 +1037,7 @@ module ActionView def build_selects_from_types(order) select = '' first_visible = order.find { |type| !@options[:"discard_#{type}"] } - order.reverse.each do |type| + order.reverse_each do |type| separator = separator(type) unless type == first_visible # don't add before first visible field select.insert(0, separator.to_s + send("select_#{type}").to_s) end diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb index 16cddec339..ba47eee9ba 100644 --- a/actionview/lib/action_view/helpers/debug_helper.rb +++ b/actionview/lib/action_view/helpers/debug_helper.rb @@ -16,15 +16,15 @@ module ActionView # # => # <pre class='debug_dump'>--- !ruby/object:User # attributes: - # updated_at: - # username: testing - # age: 42 - # password: xyz - # created_at: + # updated_at: + # username: testing + # age: 42 + # password: xyz + # created_at: # </pre> def debug(object) Marshal::dump(object) - object = ERB::Util.html_escape(object.to_yaml).gsub(" ", " ").html_safe + object = ERB::Util.html_escape(object.to_yaml) content_tag(:pre, object, :class => "debug_dump") rescue Exception # errors from Marshal or YAML # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 789a413c8d..8d78ba13d5 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -51,9 +51,7 @@ module ActionView # The HTML generated for this would be (modulus formatting): # # <form action="/people" class="new_person" id="new_person" method="post"> - # <div style="display:none"> - # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> - # </div> + # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> # <label for="person_first_name">First name</label>: # <input id="person_first_name" name="person[first_name]" type="text" /><br /> # @@ -81,10 +79,8 @@ module ActionView # the code above as is would yield instead: # # <form action="/people/256" class="edit_person" id="edit_person_256" method="post"> - # <div style="display:none"> - # <input name="_method" type="hidden" value="patch" /> - # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> - # </div> + # <input name="_method" type="hidden" value="patch" /> + # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" /> # <label for="person_first_name">First name</label>: # <input id="person_first_name" name="person[first_name]" type="text" value="John" /><br /> # @@ -142,7 +138,7 @@ module ActionView # will get expanded to # # <%= text_field :person, :first_name %> - # which results in an html <tt><input></tt> tag whose +name+ attribute is + # which results in an HTML <tt><input></tt> tag whose +name+ attribute is # <tt>person[first_name]</tt>. This means that when the form is submitted, # the value entered by the user will be available in the controller as # <tt>params[:person][:first_name]</tt>. @@ -168,6 +164,23 @@ module ActionView # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of # id attributes on form elements. The namespace attribute will be prefixed # with underscore on the generated HTML id. + # * <tt>: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 with name <tt>_method</tt> is added to + # simulate the verb over post. + # * <tt>:authenticity_token</tt> - Authenticity token to use in the form. + # Use only if you need to pass custom authenticity token string, or to + # not add authenticity_token field at all (by passing <tt>false</tt>). + # 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 you're 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>:remote</tt> - If set to true, will allow the Unobtrusive + # JavaScript drivers to control the submit behavior. By default this + # behavior is an ajax submit. + # * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name + # utf8 is not output. # * <tt>:html</tt> - Optional HTML attributes for the form tag. # # Also note that +form_for+ doesn't create an exclusive scope. It's still @@ -315,9 +328,7 @@ module ActionView # The HTML generated for this would be: # # <form action='http://www.example.com' method='post' data-remote='true'> - # <div style='display:none'> - # <input name='_method' type='hidden' value='patch' /> - # </div> + # <input name='_method' type='hidden' value='patch' /> # ... # </form> # @@ -333,9 +344,7 @@ module ActionView # The HTML generated for this would be: # # <form action='http://www.example.com' method='post' data-behavior='autosave' name='go'> - # <div style='display:none'> - # <input name='_method' type='hidden' value='patch' /> - # </div> + # <input name='_method' type='hidden' value='patch' /> # ... # </form> # @@ -428,6 +437,7 @@ module ActionView html_options[:data] = options.delete(:data) if options.has_key?(:data) html_options[:remote] = options.delete(:remote) if options.has_key?(:remote) html_options[:method] = options.delete(:method) if options.has_key?(:method) + html_options[:enforce_utf8] = options.delete(:enforce_utf8) if options.has_key?(:enforce_utf8) html_options[:authenticity_token] = options.delete(:authenticity_token) builder = instantiate_builder(object_name, object, options) @@ -482,7 +492,7 @@ module ActionView # Admin? : <%= permission_fields.check_box :admin %> # <% end %> # - # <%= f.submit %> + # <%= person_form.submit %> # <% end %> # # In this case, the checkbox field will be represented by an HTML +input+ @@ -844,6 +854,24 @@ 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 end @@ -1013,6 +1041,18 @@ module ActionView # date_field("user", "born_on", value: "1984-05-12") # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-05-12" /> # + # You can create values for the "min" and "max" attributes by passing + # instances of Date or Time to the options hash. + # + # date_field("user", "born_on", min: Date.today) + # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" /> + # + # Alternatively, you can pass a String formatted as an ISO8601 date as the + # values for "min" and "max." + # + # date_field("user", "born_on", min: "2014-05-20") + # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" /> + # def date_field(object_name, method, options = {}) Tags::DateField.new(object_name, method, self, options).render end @@ -1030,6 +1070,18 @@ module ActionView # time_field("task", "started_at") # # => <input id="task_started_at" name="task[started_at]" type="time" /> # + # You can create values for the "min" and "max" attributes by passing + # instances of Date or Time to the options hash. + # + # time_field("task", "started_at", min: Time.now) + # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" /> + # + # Alternatively, you can pass a String formatted as an ISO8601 time as the + # values for "min" and "max." + # + # time_field("task", "started_at", min: "01:00:00") + # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" /> + # def time_field(object_name, method, options = {}) Tags::TimeField.new(object_name, method, self, options).render end @@ -1047,6 +1099,18 @@ module ActionView # 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 = {}) Tags::DatetimeField.new(object_name, method, self, options).render end @@ -1064,6 +1128,18 @@ module ActionView # datetime_local_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) + # # => <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") + # # => <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 = {}) Tags::DatetimeLocalField.new(object_name, method, self, options).render end @@ -1191,8 +1267,8 @@ module ActionView # end # # The above code creates a new method +div_radio_button+ which wraps a div - # around the a new radio button. Note that when options are passed in, you - # must called +objectify_options+ in order for the model object to get + # around the new radio button. Note that when options are passed in, you + # must call +objectify_options+ in order for the model object to get # correctly passed to the method. If +objectify_options+ is not called, # then the newly created helper will not be linked back to the model. # @@ -1815,8 +1891,8 @@ module ActionView object = convert_to_model(@object) key = object ? (object.persisted? ? :update : :create) : :submit - model = if object.class.respond_to?(:model_name) - object.class.model_name.human + model = if object.respond_to?(:model_name) + object.model_name.human else @object_name.to_s.humanize end @@ -1877,6 +1953,8 @@ module ActionView end ActiveSupport.on_load(:action_view) do - cattr_accessor(:default_form_builder) { ::ActionView::Helpers::FormBuilder } + cattr_accessor(:default_form_builder, instance_writer: false, instance_reader: false) do + ::ActionView::Helpers::FormBuilder + end 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 48f42947db..bbfbf482a4 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -14,81 +14,81 @@ module ActionView # # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. # - # select("post", "category", Post::CATEGORIES, {include_blank: true}) + # select("post", "category", Post::CATEGORIES, {include_blank: true}) # - # could become: + # could become: # - # <select name="post[category]"> - # <option></option> - # <option>joke</option> - # <option>poem</option> - # </select> + # <select name="post[category]"> + # <option></option> + # <option>joke</option> + # <option>poem</option> + # </select> # - # Another common case is a select tag for a <tt>belongs_to</tt>-associated object. + # Another common case is a select tag for a <tt>belongs_to</tt>-associated object. # - # Example with @post.person_id => 2: + # Example with <tt>@post.person_id => 2</tt>: # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'}) + # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'}) # - # could become: + # could become: # - # <select name="post[person_id]"> - # <option value="">None</option> - # <option value="1">David</option> - # <option value="2" selected="selected">Sam</option> - # <option value="3">Tobias</option> - # </select> + # <select name="post[person_id]"> + # <option value="">None</option> + # <option value="1">David</option> + # <option value="2" selected="selected">Sam</option> + # <option value="3">Tobias</option> + # </select> # # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'}) + # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'}) # - # could become: + # could become: # - # <select name="post[person_id]"> - # <option value="">Select Person</option> - # <option value="1">David</option> - # <option value="2">Sam</option> - # <option value="3">Tobias</option> - # </select> + # <select name="post[person_id]"> + # <option value="">Select Person</option> + # <option value="1">David</option> + # <option value="2">Sam</option> + # <option value="3">Tobias</option> + # </select> # - # Like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this - # option to be in the +html_options+ parameter. + # * <tt>:index</tt> - like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this + # option to be in the +html_options+ parameter. # - # select("album[]", "genre", %w[rap rock country], {}, { index: nil }) + # select("album[]", "genre", %w[rap rock country], {}, { index: nil }) # - # becomes: + # becomes: # - # <select name="album[][genre]" id="album__genre"> - # <option value="rap">rap</option> - # <option value="rock">rock</option> - # <option value="country">country</option> - # </select> + # <select name="album[][genre]" id="album__genre"> + # <option value="rap">rap</option> + # <option value="rock">rock</option> + # <option value="country">country</option> + # </select> # # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output. # - # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'}) + # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'}) # - # could become: + # could become: # - # <select name="post[category]"> - # <option></option> - # <option>joke</option> - # <option>poem</option> - # <option disabled="disabled">restricted</option> - # </select> + # <select name="post[category]"> + # <option></option> + # <option>joke</option> + # <option>poem</option> + # <option disabled="disabled">restricted</option> + # </select> # - # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. + # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. # - # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }}) + # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }}) # - # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: - # <select name="post[category_id]"> - # <option value="1" disabled="disabled">2008 stuff</option> - # <option value="2" disabled="disabled">Christmas</option> - # <option value="3">Jokes</option> - # <option value="4">Poems</option> - # </select> + # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: + # <select name="post[category_id]"> + # <option value="1" disabled="disabled">2008 stuff</option> + # <option value="2" disabled="disabled">Christmas</option> + # <option value="3">Jokes</option> + # <option value="4">Poems</option> + # </select> # module FormOptionsHelper # ERB::Util can mask some helpers like textilize. Make sure to include them. @@ -152,11 +152,9 @@ module ActionView # To prevent this the helper generates an auxiliary hidden field before # every multiple select. The hidden field has the same name as multiple select and blank value. # - # This way, the client either sends only the hidden field (representing - # the deselected multiple select box), or both fields. Since the HTML specification - # says key/value pairs have to be sent in the same order they appear in the - # form, and parameters extraction gets the last occurrence of any repeated - # key in the query string, that works for ordinary forms. + # <b>Note:</b> The client either sends only the hidden field (representing + # the deselected multiple select box), or both fields. This means that the resulting array + # always contains a blank string. # # In case if you don't want the helper to generate this hidden field you can specify # <tt>include_hidden: false</tt> option. @@ -316,7 +314,7 @@ module ActionView # # => <option>MasterCard</option> # # => <option selected="selected">Discover</option> # - # You can optionally provide html attributes as the last element of the array. + # You can optionally provide HTML attributes as the last element of the array. # # options_for_select([ "Denmark", ["USA", {class: 'bold'}], "Sweden" ], ["USA", "Sweden"]) # # => <option value="Denmark">Denmark</option> @@ -353,12 +351,12 @@ module ActionView return container if String === container selected, disabled = extract_selected_and_disabled(selected).map do |r| - Array(r).map { |item| item.to_s } + Array(r).map(&:to_s) end container.map do |element| html_attributes = option_html_attributes(element) - text, value = option_text_and_value(element).map { |item| item.to_s } + text, value = option_text_and_value(element).map(&:to_s) html_attributes[:selected] ||= option_value_selected?(value, selected) html_attributes[:disabled] ||= disabled && option_value_selected?(value, disabled) @@ -463,21 +461,7 @@ module ActionView end # Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but - # wraps them with <tt><optgroup></tt> tags. - # - # Parameters: - # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the - # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a - # nested array of text-value pairs. See <tt>options_for_select</tt> for more info. - # Ex. ["North America",[["United States","US"],["Canada","CA"]]] - # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags, - # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options - # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>. - # - # Options: - # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this - # prepends an option with a generic prompt - "Please select" - or the given prompt string. - # * <tt>:divider</tt> - the divider for the options groups. + # wraps them with <tt><optgroup></tt> tags: # # grouped_options = [ # ['North America', @@ -504,22 +488,36 @@ module ActionView # <option value="France">France</option> # </optgroup> # - # grouped_options = [ - # [['United States','US'], 'Canada'], - # ['Denmark','Germany','France'] - # ] - # grouped_options_for_select(grouped_options, nil, divider: '---------') + # Parameters: + # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the + # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a + # nested array of text-value pairs. See <tt>options_for_select</tt> for more info. + # Ex. ["North America",[["United States","US"],["Canada","CA"]]] + # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags, + # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options + # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>. # - # Possible output: - # <optgroup label="---------"> - # <option value="US">United States</option> - # <option value="Canada">Canada</option> - # </optgroup> - # <optgroup label="---------"> - # <option value="Denmark">Denmark</option> - # <option value="Germany">Germany</option> - # <option value="France">France</option> - # </optgroup> + # Options: + # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this + # prepends an option with a generic prompt - "Please select" - or the given prompt string. + # * <tt>:divider</tt> - the divider for the options groups. + # + # grouped_options = [ + # [['United States','US'], 'Canada'], + # ['Denmark','Germany','France'] + # ] + # grouped_options_for_select(grouped_options, nil, divider: '---------') + # + # Possible output: + # <optgroup label="---------"> + # <option value="US">United States</option> + # <option value="Canada">Canada</option> + # </optgroup> + # <optgroup label="---------"> + # <option value="Denmark">Denmark</option> + # <option value="Germany">Germany</option> + # <option value="France">France</option> + # </optgroup> # # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to # wrap the output in an appropriate <tt><select></tt> tag. @@ -635,7 +633,7 @@ module ActionView # even use the label as wrapper, as in the example above. # # The builder methods <tt>label</tt> and <tt>radio_button</tt> also accept - # extra html options: + # extra HTML options: # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b| # b.label(class: "radio_button") { b.radio_button(class: "radio_button") } # end @@ -698,7 +696,7 @@ module ActionView # use the label as wrapper, as in the example above. # # The builder methods <tt>label</tt> and <tt>check_box</tt> also accept - # extra html options: + # extra HTML options: # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b| # b.label(class: "check_box") { b.check_box(class: "check_box") } # end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 9c0c43d096..8bb243b8b7 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -35,10 +35,10 @@ module ActionView # This is helpful when you're 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. - # * A list of parameters to feed to the URL the form will be posted to. # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the # submit behavior. By default this behavior is an ajax submit. # * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name utf8 is not output. + # * Any other key creates standard HTML attributes for the tag. # # ==== Examples # form_tag('/posts') @@ -133,12 +133,20 @@ module ActionView option_tags ||= "" html_name = (options[:multiple] == true && !name.to_s.ends_with?("[]")) ? "#{name}[]" : name - if options.delete(:include_blank) - option_tags = content_tag(:option, '', :value => '').safe_concat(option_tags) + if options.include?(:include_blank) + include_blank = options.delete(:include_blank) + + if include_blank == true + include_blank = '' + end + + if include_blank + option_tags = content_tag(:option, include_blank, value: '').safe_concat(option_tags) + end end if prompt = options.delete(:prompt) - option_tags = content_tag(:option, prompt, :value => '').safe_concat(option_tags) + option_tags = content_tag(:option, prompt, value: '').safe_concat(option_tags) end content_tag :select, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) @@ -224,7 +232,7 @@ module ActionView # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')" # # type="hidden" value="" /> def hidden_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "hidden")) + text_field_tag(name, value, options.merge(type: :hidden)) end # Creates a file upload field. If you are using file uploads then you will also need @@ -263,7 +271,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.update("type" => "file")) + text_field_tag(name, nil, options.merge(type: :file)) end # Creates a password field, a masked text field that will hide the users input behind a mask character. @@ -296,7 +304,7 @@ module ActionView # password_field_tag 'pin', '1234', maxlength: 4, size: 6, class: "pin_input" # # => <input class="pin_input" id="pin" maxlength="4" name="pin" size="6" type="password" value="1234" /> def password_field_tag(name = "password", value = nil, options = {}) - text_field_tag(name, value, options.update("type" => "password")) + text_field_tag(name, value, options.merge(type: :password)) end # Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions. @@ -508,19 +516,19 @@ module ActionView # # ==== Examples # image_submit_tag("login.png") - # # => <input alt="Login" src="/images/login.png" type="image" /> + # # => <input alt="Login" src="/assets/login.png" type="image" /> # # image_submit_tag("purchase.png", disabled: true) - # # => <input alt="Purchase" disabled="disabled" src="/images/purchase.png" type="image" /> + # # => <input alt="Purchase" 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="/images/search.png" type="image" /> + # # => <input alt="Find" 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="/images/agree.png" type="image" /> + # # => <input alt="Agree" 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="/images/save.png" data-confirm="Are you sure?" type="image" /> + # # => <input alt="Save" 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) @@ -571,7 +579,7 @@ module ActionView # color_field_tag 'color', '#DEF726', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="color" name="color" type="color" value="#DEF726" /> def color_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "color")) + text_field_tag(name, value, options.merge(type: :color)) end # Creates a text field of type "search". @@ -592,7 +600,7 @@ module ActionView # search_field_tag 'search', 'Enter your search query here', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="search" name="search" type="search" value="Enter your search query here" /> def search_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "search")) + text_field_tag(name, value, options.merge(type: :search)) end # Creates a text field of type "tel". @@ -613,7 +621,7 @@ module ActionView # telephone_field_tag 'tel', '0123456789', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="tel" name="tel" type="tel" value="0123456789" /> def telephone_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "tel")) + text_field_tag(name, value, options.merge(type: :tel)) end alias phone_field_tag telephone_field_tag @@ -635,7 +643,7 @@ module ActionView # date_field_tag 'date', '01/01/2014', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="date" name="date" type="date" value="01/01/2014" /> def date_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "date")) + text_field_tag(name, value, options.merge(type: :date)) end # Creates a text field of type "time". @@ -646,7 +654,7 @@ module ActionView # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. def time_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "time")) + text_field_tag(name, value, options.merge(type: :time)) end # Creates a text field of type "datetime". @@ -657,7 +665,7 @@ 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 = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "datetime")) + text_field_tag(name, value, options.merge(type: :datetime)) end # Creates a text field of type "datetime-local". @@ -668,7 +676,7 @@ module ActionView # * <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.stringify_keys.update("type" => "datetime-local")) + text_field_tag(name, value, options.merge(type: 'datetime-local')) end # Creates a text field of type "month". @@ -679,7 +687,7 @@ module ActionView # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. def month_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "month")) + text_field_tag(name, value, options.merge(type: :month)) end # Creates a text field of type "week". @@ -690,7 +698,7 @@ module ActionView # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. def week_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "week")) + text_field_tag(name, value, options.merge(type: :week)) end # Creates a text field of type "url". @@ -711,7 +719,7 @@ module ActionView # url_field_tag 'url', 'http://rubyonrails.org', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="url" name="url" type="url" value="http://rubyonrails.org" /> def url_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "url")) + text_field_tag(name, value, options.merge(type: :url)) end # Creates a text field of type "email". @@ -732,7 +740,7 @@ module ActionView # email_field_tag 'email', 'email@example.com', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="email" name="email" type="email" value="email@example.com" /> def email_field_tag(name, value = nil, options = {}) - text_field_tag(name, value, options.stringify_keys.update("type" => "email")) + text_field_tag(name, value, options.merge(type: :email)) end # Creates a number field. @@ -769,10 +777,10 @@ module ActionView # # => <input id="quantity" name="quantity" min="1" max="9" type="number" /> # # number_field_tag 'quantity', nil, min: 1, max: 10 - # # => <input id="quantity" name="quantity" min="1" max="9" type="number" /> + # # => <input id="quantity" name="quantity" min="1" max="10" type="number" /> # # number_field_tag 'quantity', nil, min: 1, max: 10, step: 2 - # # => <input id="quantity" name="quantity" min="1" max="9" step="2" type="number" /> + # # => <input id="quantity" name="quantity" min="1" max="10" step="2" type="number" /> # # number_field_tag 'quantity', '1', class: 'special_input', disabled: true # # => <input disabled="disabled" class="special_input" id="quantity" name="quantity" type="number" value="1" /> @@ -790,7 +798,7 @@ module ActionView # ==== Options # * Accepts the same options as number_field_tag. def range_field_tag(name, value = nil, options = {}) - number_field_tag(name, value, options.stringify_keys.update("type" => "range")) + number_field_tag(name, value, options.merge(type: :range)) end # Creates the hidden UTF8 enforcer tag. Override this method in a helper @@ -862,7 +870,7 @@ module ActionView # see http://www.w3.org/TR/html4/types.html#type-name def sanitize_to_id(name) - name.to_s.delete(']').gsub(/[^-a-zA-Z0-9:.]/, "_") + name.to_s.delete(']').tr('^-a-zA-Z0-9:.', "_") end end end diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 7220bded3c..cfd617cedc 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/string/output_safety' @@ -306,12 +305,12 @@ module ActionView # string containing an i18n scope where to find this hash. It # might have the following keys: # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>, - # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, - # *<tt>:billion</tt>, <tt>:trillion</tt>, - # *<tt>:quadrillion</tt> + # <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, + # <tt>:billion</tt>, <tt>:trillion</tt>, + # <tt>:quadrillion</tt> # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>, - # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, - # *<tt>:pico</tt>, <tt>:femto</tt> + # <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, + # <tt>:pico</tt>, <tt>:femto</tt> # * <tt>:format</tt> - Sets the format of the output string # (defaults to "%n %u"). The field types are: # * %u - The quantifier (ex.: 'thousand') diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb index b0d9c7c7f9..1c2a400245 100644 --- a/actionview/lib/action_view/helpers/output_safety_helper.rb +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -17,10 +17,10 @@ module ActionView #:nodoc: stringish.to_s.html_safe end - # This method returns a html safe string similar to what <tt>Array#join</tt> + # This method returns an HTML safe string similar to what <tt>Array#join</tt> # would return. The array is flattened, and all items, including - # the supplied separator, are html escaped unless they are html - # safe, and the returned string is marked as html safe. + # the supplied separator, are HTML escaped unless they are HTML + # safe, and the returned string is marked as HTML safe. # # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />") # # => "<p>foo</p><br /><p>bar</p>" diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index ebfc35a4c7..827932d8e2 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -13,13 +13,13 @@ module ActionView # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. # * <tt>:text</tt> - Renders the text passed in out. # * <tt>:plain</tt> - Renders the text passed in out. Setting the content - # type as <tt>text/plain</tt>. - # * <tt>:html</tt> - Renders the html safe string passed in out, otherwise - # performs html escape on the string first. Setting the content type as - # <tt>text/html</tt>. + # type as <tt>text/plain</tt>. + # * <tt>:html</tt> - Renders the HTML safe string passed in out, otherwise + # performs HTML escape on the string first. Setting the content type as + # <tt>text/html</tt>. # * <tt>:body</tt> - Renders the text passed in, and inherits the content - # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt> - # object. + # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt> + # object. # # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter # as the locals hash. @@ -32,7 +32,7 @@ module ActionView view_renderer.render(self, options) end else - view_renderer.render_partial(self, :partial => options, :locals => locals) + 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 049af275b6..463a4e9f60 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/object/try' -require 'action_view/vendor/html-scanner' +require 'rails-html-sanitizer' module ActionView # = Action View Sanitize Helpers @@ -8,54 +8,77 @@ module ActionView # These helper methods extend Action View making them callable within your template files. module SanitizeHelper extend ActiveSupport::Concern - # This +sanitize+ helper will html encode all tags and strip all attributes that - # aren't specifically allowed. + # Sanitizes HTML input, stripping all tags and attributes that aren't whitelisted. # - # It also strips href/src tags with invalid protocols, like javascript: especially. - # It does its best to counter any tricks that hackers may use, like throwing in - # unicode/ascii/hex values to get past the javascript: filters. Check out - # the extensive test suite. + # 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. # - # <%= sanitize @article.body %> + # The default sanitizer is Rails::Html::WhiteListSanitizer. See {Rails HTML + # Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information. # - # You can add or remove tags/attributes if you want to customize it a bit. - # See ActionView::Base for full docs on the available options. You can add - # tags/attributes for single uses of +sanitize+ by passing either the - # <tt>:attributes</tt> or <tt>:tags</tt> options: + # Custom sanitization rules can also be provided. # - # Normal Use + # 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>. # - # <%= sanitize @article.body %> + # ==== Options # - # Custom Use (only the mentioned tags and attributes are allowed, nothing else) + # * <tt>:tags</tt> - An array of allowed tags. + # * <tt>:attributes</tt> - An array of allowed attributes. + # * <tt>:scrubber</tt> - A {Rails::Html scrubber}[https://github.com/rails/rails-html-sanitizer] + # or {Loofah::Scrubber}[https://github.com/flavorjones/loofah] object that + # defines custom sanitization rules. A custom scrubber takes precedence over + # custom tags and attributes. # - # <%= sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) %> + # ==== Examples # - # Add table tags to the default allowed tags + # Normal use: # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td' - # end + # <%= sanitize @comment.body %> + # + # Providing custom whitelisted tags and attributes: + # + # <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %> + # + # Providing a custom Rails::Html scrubber: # - # Remove tags to the default allowed tags + # class CommentScrubber < Rails::Html::PermitScrubber + # def allowed_node?(node) + # !%w(form script comment blockquote).include?(node.name) + # end + # + # def skip_node?(node) + # node.text? + # end # - # class Application < Rails::Application - # config.after_initialize do - # ActionView::Base.sanitized_allowed_tags.delete 'div' + # def scrub_attribute?(name) + # name == 'style' # end # end # - # Change allowed default attributes + # <%= sanitize @comment.body, scrubber: CommentScrubber.new %> + # + # See {Rails HTML Sanitizer}[https://github.com/rails/rails-html-sanitizer] for + # documentation about Rails::Html scrubbers. + # + # Providing a custom Loofah::Scrubber: # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_attributes = ['id', 'class', 'style'] + # scrubber = Loofah::Scrubber.new do |node| + # node.remove if node.name == 'script' # end # - # Please note that sanitizing user-provided text does not guarantee that the - # resulting markup is valid (conforming to a document type) or even well-formed. - # The output may still contain e.g. unescaped '<', '>', '&' characters and - # confuse browsers. + # <%= sanitize @comment.body, scrubber: scrubber %> + # + # See {Loofah's documentation}[https://github.com/flavorjones/loofah] for more + # information about defining custom Loofah::Scrubber objects. + # + # To set the default allowed tags or attributes across your application: # + # # In config/application.rb + # config.action_view.sanitized_allowed_tags = ['strong', 'em', 'a'] + # config.action_view.sanitized_allowed_attributes = ['href', 'title'] def sanitize(html, options = {}) self.class.white_list_sanitizer.sanitize(html, options).try(:html_safe) end @@ -65,9 +88,7 @@ module ActionView self.class.white_list_sanitizer.sanitize_css(style) end - # Strips all HTML tags from the +html+, including comments. This uses the - # html-scanner tokenizer and so its HTML parsing ability is limited by - # that of html-scanner. + # Strips all HTML tags from +html+, including comments. # # strip_tags("Strip <i>these</i> tags!") # # => Strip these tags! @@ -81,7 +102,7 @@ module ActionView self.class.full_sanitizer.sanitize(html) end - # Strips all link tags from +text+ leaving just the link text. + # Strips all link tags from +html+ leaving just the link text. # # strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>') # # => Ruby on Rails @@ -98,47 +119,21 @@ module ActionView module ClassMethods #:nodoc: attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer - def sanitized_protocol_separator - white_list_sanitizer.protocol_separator - end - - def sanitized_uri_attributes - white_list_sanitizer.uri_attributes - end - - def sanitized_bad_tags - white_list_sanitizer.bad_tags + # Vendors the full, link and white list sanitizers. + # Provided strictly for compabitility and can be removed in Rails 5. + def sanitizer_vendor + Rails::Html::Sanitizer end def sanitized_allowed_tags - white_list_sanitizer.allowed_tags + sanitizer_vendor.white_list_sanitizer.allowed_tags end def sanitized_allowed_attributes - white_list_sanitizer.allowed_attributes + sanitizer_vendor.white_list_sanitizer.allowed_attributes end - def sanitized_allowed_css_properties - white_list_sanitizer.allowed_css_properties - end - - def sanitized_allowed_css_keywords - white_list_sanitizer.allowed_css_keywords - end - - def sanitized_shorthand_css_properties - white_list_sanitizer.shorthand_css_properties - end - - def sanitized_allowed_protocols - white_list_sanitizer.allowed_protocols - end - - def sanitized_protocol_separator=(value) - white_list_sanitizer.protocol_separator = value - end - - # Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with + # Gets the Rails::Html::FullSanitizer instance used by +strip_tags+. Replace with # any object that responds to +sanitize+. # # class Application < Rails::Application @@ -146,21 +141,21 @@ module ActionView # end # def full_sanitizer - @full_sanitizer ||= HTML::FullSanitizer.new + @full_sanitizer ||= sanitizer_vendor.full_sanitizer.new end - # Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with - # any object that responds to +sanitize+. + # Gets the Rails::Html::LinkSanitizer instance used by +strip_links+. + # Replace with any object that responds to +sanitize+. # # class Application < Rails::Application # config.action_view.link_sanitizer = MySpecialSanitizer.new # end # def link_sanitizer - @link_sanitizer ||= HTML::LinkSanitizer.new + @link_sanitizer ||= sanitizer_vendor.link_sanitizer.new end - # Gets the HTML::WhiteListSanitizer instance used by sanitize and +sanitize_css+. + # Gets the Rails::Html::WhiteListSanitizer instance used by sanitize and +sanitize_css+. # Replace with any object that responds to +sanitize+. # # class Application < Rails::Application @@ -168,87 +163,7 @@ module ActionView # end # def white_list_sanitizer - @white_list_sanitizer ||= HTML::WhiteListSanitizer.new - end - - # Adds valid HTML attributes that the +sanitize+ helper checks for URIs. - # - # class Application < Rails::Application - # config.action_view.sanitized_uri_attributes = 'lowsrc', 'target' - # end - # - def sanitized_uri_attributes=(attributes) - HTML::WhiteListSanitizer.uri_attributes.merge(attributes) - end - - # Adds to the Set of 'bad' tags for the +sanitize+ helper. - # - # class Application < Rails::Application - # config.action_view.sanitized_bad_tags = 'embed', 'object' - # end - # - def sanitized_bad_tags=(attributes) - HTML::WhiteListSanitizer.bad_tags.merge(attributes) - end - - # Adds to the Set of allowed tags for the +sanitize+ helper. - # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td' - # end - # - def sanitized_allowed_tags=(attributes) - HTML::WhiteListSanitizer.allowed_tags.merge(attributes) - end - - # Adds to the Set of allowed HTML attributes for the +sanitize+ helper. - # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc'] - # end - # - def sanitized_allowed_attributes=(attributes) - HTML::WhiteListSanitizer.allowed_attributes.merge(attributes) - end - - # Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ helpers. - # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_css_properties = 'expression' - # end - # - def sanitized_allowed_css_properties=(attributes) - HTML::WhiteListSanitizer.allowed_css_properties.merge(attributes) - end - - # Adds to the Set of allowed CSS keywords for the +sanitize+ and +sanitize_css+ helpers. - # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_css_keywords = 'expression' - # end - # - def sanitized_allowed_css_keywords=(attributes) - HTML::WhiteListSanitizer.allowed_css_keywords.merge(attributes) - end - - # Adds to the Set of allowed shorthand CSS properties for the +sanitize+ and +sanitize_css+ helpers. - # - # class Application < Rails::Application - # config.action_view.sanitized_shorthand_css_properties = 'expression' - # end - # - def sanitized_shorthand_css_properties=(attributes) - HTML::WhiteListSanitizer.shorthand_css_properties.merge(attributes) - end - - # Adds to the Set of allowed protocols for the +sanitize+ helper. - # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_protocols = 'ssh', 'feed' - # end - # - def sanitized_allowed_protocols=(attributes) - HTML::WhiteListSanitizer.allowed_protocols.merge(attributes) + @white_list_sanitizer ||= sanitizer_vendor.white_list_sanitizer.new end end end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 35444bcfb4..a87c223a71 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -9,6 +9,7 @@ module ActionView module TagHelper extend ActiveSupport::Concern include CaptureHelper + include OutputSafetyHelper BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer autoplay controls loop selected hidden scoped async @@ -17,7 +18,9 @@ module ActionView itemscope allowfullscreen default inert sortable truespeed typemustmatch).to_set - BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attribute| attribute.to_sym }) + BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym)) + + TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set PRE_CONTENT_STRINGS = { :textarea => "\n" @@ -120,7 +123,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 @@ -147,9 +150,9 @@ module ActionView return if options.blank? attrs = [] options.each_pair do |key, value| - if key.to_s == 'data' && value.is_a?(Hash) + if TAG_PREFIXES.include?(key) && value.is_a?(Hash) value.each_pair do |k, v| - attrs << data_tag_option(k, v, escape) + attrs << prefix_tag_option(key, k, v, escape) end elsif BOOLEAN_ATTRIBUTES.include?(key) attrs << boolean_tag_option(key) if value @@ -157,11 +160,11 @@ module ActionView attrs << tag_option(key, value, escape) end end - " #{attrs.sort! * ' '}" unless attrs.empty? + " #{attrs * ' '}" unless attrs.empty? end - def data_tag_option(key, value, escape) - key = "data-#{key.to_s.dasherize}" + 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 diff --git a/actionview/lib/action_view/helpers/tags.rb b/actionview/lib/action_view/helpers/tags.rb index 45c75d10c0..a4f6eb0150 100644 --- a/actionview/lib/action_view/helpers/tags.rb +++ b/actionview/lib/action_view/helpers/tags.rb @@ -5,6 +5,7 @@ module ActionView eager_autoload do autoload :Base + autoload :Translator autoload :CheckBox autoload :CollectionCheckBoxes autoload :CollectionRadioButtons diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index 8607da301c..acc6443a96 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -14,7 +14,7 @@ module ActionView @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]") @object = retrieve_object(options.delete(:object)) @options = options - @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match + @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil end # This is what child classes implement. @@ -25,19 +25,26 @@ module ActionView private def value(object) - object.send @method_name if object + object.public_send @method_name if object end def value_before_type_cast(object) unless object.nil? method_before_type_cast = @method_name + "_before_type_cast" - object.respond_to?(method_before_type_cast) ? - object.send(method_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) + 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 retrieve_object(object) if object object @@ -72,35 +79,30 @@ module ActionView end def add_default_name_and_id(options) - if options.has_key?("index") - options["name"] ||= options.fetch("name"){ tag_name_with_index(options["index"], options["multiple"]) } - options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) } - options.delete("index") - elsif defined?(@auto_index) - options["name"] ||= options.fetch("name"){ tag_name_with_index(@auto_index, options["multiple"]) } - options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) } - else - options["name"] ||= options.fetch("name"){ tag_name(options["multiple"]) } - options["id"] = options.fetch("id"){ tag_id } + 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 end - - options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence - end - - def tag_name(multiple = false) - "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}" end - def tag_name_with_index(index, multiple = false) - "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" - end - - def tag_id - "#{sanitized_object_name}_#{sanitized_method_name}" + 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}" + end end - def tag_id_with_index(index) - "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" + 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}" + end end def sanitized_object_name @@ -142,6 +144,10 @@ module ActionView end option_tags end + + def name_and_id_index(options) + options.key?("index") ? options.delete("index") || "" : @auto_index + end 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 6242a2a085..1765fa6558 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -41,14 +41,7 @@ module ActionView end def hidden_field - hidden_name = @html_options[:name] - - hidden_name ||= if @options.has_key?(:index) - "#{tag_name_with_index(@options[:index])}[]" - else - "#{tag_name}[]" - end - + hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]" @template_object.hidden_field_tag(hidden_name, "", id: nil) 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 25e7e05ec6..b2cee9d198 100644 --- a/actionview/lib/action_view/helpers/tags/datetime_field.rb +++ b/actionview/lib/action_view/helpers/tags/datetime_field.rb @@ -5,8 +5,8 @@ module ActionView def render options = @options.stringify_keys options["value"] ||= format_date(value(object)) - options["min"] = format_date(options["min"]) - options["max"] = format_date(options["max"]) + options["min"] = format_date(datetime_value(options["min"])) + options["max"] = format_date(datetime_value(options["max"])) @options = options super end @@ -16,6 +16,14 @@ module ActionView def format_date(value) value.try(:strftime, "%Y-%m-%dT%T.%L%z") end + + def datetime_value(value) + if value.is_a? String + DateTime.parse(value) rescue nil + else + value + end + end end end end diff --git a/actionview/lib/action_view/helpers/tags/file_field.rb b/actionview/lib/action_view/helpers/tags/file_field.rb index 476b820d84..e6a1d9c62d 100644 --- a/actionview/lib/action_view/helpers/tags/file_field.rb +++ b/actionview/lib/action_view/helpers/tags/file_field.rb @@ -2,6 +2,21 @@ 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/label.rb b/actionview/lib/action_view/helpers/tags/label.rb index a5bcaf8153..b31d5fda66 100644 --- a/actionview/lib/action_view/helpers/tags/label.rb +++ b/actionview/lib/action_view/helpers/tags/label.rb @@ -2,6 +2,29 @@ module ActionView module Helpers module Tags # :nodoc: class Label < Base # :nodoc: + class LabelBuilder # :nodoc: + attr_reader :object + + def initialize(template_object, object_name, method_name, object, tag_value) + @template_object = template_object + @object_name = object_name + @method_name = method_name + @object = object + @tag_value = tag_value + end + + def translation + method_and_value = @tag_value.present? ? "#{@method_name}.#{@tag_value}" : @method_name + + content ||= Translator + .new(object, @object_name, method_and_value, scope: "helpers.label") + .translate + content ||= @method_name.humanize + + content + end + end + def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil) options ||= {} @@ -32,33 +55,24 @@ module ActionView options.delete("namespace") options["for"] = name_and_id["id"] unless options.key?("for") - if block_given? - content = @template_object.capture(&block) - else - method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name - content = if @content.blank? - @object_name.gsub!(/\[(.*)_attributes\]\[\d+\]/, '.\1') - - if object.respond_to?(:to_model) - key = object.class.model_name.i18n_key - i18n_default = ["#{key}.#{method_and_value}".to_sym, ""] - end - - i18n_default ||= "" - I18n.t("#{@object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence - else - @content.to_s - end + builder = LabelBuilder.new(@template_object, @object_name, @method_name, @object, tag_value) - content ||= if object && object.class.respond_to?(:human_attribute_name) - object.class.human_attribute_name(method_and_value) - end - - content ||= @method_name.humanize + content = if block_given? + @template_object.capture(builder, &block) + elsif @content.present? + @content.to_s + else + render_component(builder) end label_tag(name_and_id["id"], content, options) end + + private + + def render_component(builder) + builder.translation + end end end end diff --git a/actionview/lib/action_view/helpers/tags/placeholderable.rb b/actionview/lib/action_view/helpers/tags/placeholderable.rb new file mode 100644 index 0000000000..cf7b117614 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/placeholderable.rb @@ -0,0 +1,22 @@ +module ActionView + module Helpers + module Tags # :nodoc: + module Placeholderable # :nodoc: + def initialize(*) + super + + if tag_value = @options[:placeholder] + placeholder = tag_value if tag_value.is_a?(String) + method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}" + + placeholder ||= Tags::Translator + .new(object, @object_name, method_and_value, scope: "helpers.placeholder") + .translate + placeholder ||= @method_name.humanize + @options[:placeholder] = placeholder + end + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/search_field.rb b/actionview/lib/action_view/helpers/tags/search_field.rb index c09e2f1be7..a848aeabfa 100644 --- a/actionview/lib/action_view/helpers/tags/search_field.rb +++ b/actionview/lib/action_view/helpers/tags/search_field.rb @@ -16,6 +16,7 @@ module ActionView options["incremental"] = true unless options.has_key?("incremental") end + @options = options super end end diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb index 00881d9978..180900cc8d 100644 --- a/actionview/lib/action_view/helpers/tags/select.rb +++ b/actionview/lib/action_view/helpers/tags/select.rb @@ -3,7 +3,7 @@ module ActionView module Tags # :nodoc: class Select < Base # :nodoc: def initialize(object_name, method_name, template_object, choices, options, html_options) - @choices = block_given? ? template_object.capture { yield } : choices + @choices = block_given? ? template_object.capture { yield || "" } : choices @choices = @choices.to_a if @choices.is_a?(Range) @html_options = html_options diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb index 9ee83ee7c2..69038c1498 100644 --- a/actionview/lib/action_view/helpers/tags/text_area.rb +++ b/actionview/lib/action_view/helpers/tags/text_area.rb @@ -1,7 +1,11 @@ +require 'action_view/helpers/tags/placeholderable' + module ActionView module Helpers module Tags # :nodoc: class TextArea < Base # :nodoc: + include Placeholderable + def render options = @options.stringify_keys add_default_name_and_id(options) diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb index e0b80d81c2..5c576a20ca 100644 --- a/actionview/lib/action_view/helpers/tags/text_field.rb +++ b/actionview/lib/action_view/helpers/tags/text_field.rb @@ -1,7 +1,11 @@ +require 'action_view/helpers/tags/placeholderable' + module ActionView module Helpers module Tags # :nodoc: class TextField < Base # :nodoc: + include Placeholderable + def render options = @options.stringify_keys options["size"] = options["maxlength"] unless options.key?("size") diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb new file mode 100644 index 0000000000..8b6655481d --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/translator.rb @@ -0,0 +1,40 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class Translator # :nodoc: + def initialize(object, object_name, method_and_value, scope:) + @object_name = object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1') + @method_and_value = method_and_value + @scope = scope + @model = object.respond_to?(:to_model) ? object.to_model : nil + end + + def translate + translated_attribute = I18n.t("#{object_name}.#{method_and_value}", default: i18n_default, scope: scope).presence + translated_attribute || human_attribute_name + end + + protected + + 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 + "" + end + end + + 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/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 7cfbca5b6f..2c40ed1832 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -103,11 +103,14 @@ module ActionView # Highlights one or more +phrases+ everywhere in +text+ by inserting it into # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt> # as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to - # '<mark>\1</mark>') + # '<mark>\1</mark>') or passing a block that receives each matched term. # # highlight('You searched for: rails', 'rails') # # => You searched for: <mark>rails</mark> # + # highlight('You searched for: rails', /for|rails/) + # # => You searched <mark>for</mark>: <mark>rails</mark> + # # highlight('You searched for: ruby, rails, dhh', 'actionpack') # # => You searched for: ruby, rails, dhh # @@ -116,15 +119,25 @@ module ActionView # # highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>') # # => You searched for: <a href="search?q=rails">rails</a> + # + # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) } + # # => You searched for: <a href="search?q=rails">rails</a> def highlight(text, phrases, options = {}) text = sanitize(text) if options.fetch(:sanitize, true) if text.blank? || phrases.blank? - text + text || "" else - highlighter = options.fetch(:highlighter, '<mark>\1</mark>') - match = Array(phrases).map { |p| Regexp.escape(p) }.join('|') - text.gsub(/(#{match})(?![^<]*?>)/i, highlighter) + match = Array(phrases).map do |p| + Regexp === p ? p.to_s : Regexp.escape(p) + end.join('|') + + if block_given? + text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found } + else + highlighter = options.fetch(:highlighter, '<mark>\1</mark>') + text.gsub(/(#{match})(?![^<]*?>)/i, highlighter) + end end.html_safe end @@ -155,9 +168,13 @@ module ActionView def excerpt(text, phrase, options = {}) return unless text && phrase - separator = options[:separator] || '' - phrase = Regexp.escape(phrase) - regex = /#{phrase}/i + separator = options.fetch(:separator, nil) || "" + case phrase + when Regexp + regex = phrase + else + regex = /#{Regexp.escape(phrase)}/i + end return unless matches = text.match(regex) phrase = matches[0] @@ -171,7 +188,7 @@ module ActionView end end - first_part, second_part = text.split(regex, 2) + first_part, second_part = text.split(phrase, 2) prefix, first_part = cut_excerpt_part(:first, first_part, separator, options) postfix, second_part = cut_excerpt_part(:second, second_part, separator, options) @@ -292,7 +309,7 @@ module ActionView # <table> # <% @items.each do |item| %> # <tr class="<%= cycle("odd", "even") -%>"> - # <td>item</td> + # <td><%= item %></td> # </tr> # <% end %> # </table> diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 17ec6a40bf..342361217c 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -1,10 +1,12 @@ 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 TranslationHelper + include TagHelper # Delegates to <tt>I18n#translate</tt> but also performs three additional functions. # # First, it will ensure that any thrown +MissingTranslation+ messages will be turned @@ -35,14 +37,17 @@ module ActionView # you know what kind of output to expect when you call translate in a template. def translate(key, options = {}) options = options.dup - options[:default] = wrap_translate_defaults(options[:default]) if options[:default] + remaining_defaults = Array(options.delete(:default)) + options[:default] = remaining_defaults.shift if remaining_defaults.first.kind_of? String - # If the user has specified rescue_format then pass it all through, otherwise use - # raise and do the work ourselves - options[:raise] ||= ActionView::Base.raise_on_missing_translations - - raise_error = options[:raise] || options.key?(:rescue_format) - unless raise_error + # If the user has explicitly decided to NOT raise errors, pass that option to I18n. + # Otherwise, tell I18n to raise an exception, which we rescue further in this method. + # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default. + if options[:raise] == false || (options.key?(:rescue_format) && options[:rescue_format].nil?) + raise_error = false + options[:raise] = false + else + raise_error = options[:raise] || options[:rescue_format] || ActionView::Base.raise_on_missing_translations options[:raise] = true end @@ -60,10 +65,14 @@ module ActionView I18n.translate(scope_key_by_partial(key), options) end rescue I18n::MissingTranslationData => e - raise e if raise_error + if remaining_defaults.present? + translate remaining_defaults.shift, options.merge(default: remaining_defaults) + else + raise e if raise_error - keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) - content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}") + keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) + content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}") + end end alias :t :translate @@ -92,21 +101,6 @@ module ActionView def html_safe_translation_key?(key) key.to_s =~ /(\b|_|\.)html$/ end - - def wrap_translate_defaults(defaults) - new_defaults = [] - defaults = Array(defaults) - while key = defaults.shift - if key.is_a?(Symbol) - new_defaults << lambda { |_, options| translate key, options.merge(:default => defaults) } - break - else - new_defaults << key - end - end - - new_defaults - end end end end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index c3be47133c..33882063f9 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -46,9 +46,9 @@ module ActionView end protected :_back_url - # Creates a link tag of the given +name+ using a URL created by the set of +options+. + # Creates an anchor element of the given +name+ using a URL created by the set of +options+. # See the valid options in the documentation for +url_for+. It's also possible to - # pass a String instead of an options hash, which generates a link tag that uses the + # pass a String instead of an options hash, which generates an anchor element that uses the # value of the String as the href for the link. Using a <tt>:back</tt> Symbol instead # of an options hash will generate a link to the referrer (a JavaScript back link # will be used in place of a referrer if none exists). If +nil+ is passed as the name @@ -229,59 +229,51 @@ module ActionView # ==== Examples # <%= button_to "New", action: "new" %> # # => "<form method="post" action="/controller/new" class="button_to"> - # # <div><input value="New" type="submit" /></div> + # # <input value="New" type="submit" /> # # </form>" # # <%= button_to "New", new_articles_path %> # # => "<form method="post" action="/articles/new" class="button_to"> - # # <div><input value="New" type="submit" /></div> + # # <input value="New" type="submit" /> # # </form>" # # <%= button_to [:make_happy, @user] do %> # Make happy <strong><%= @user.name %></strong> # <% end %> # # => "<form method="post" action="/users/1/make_happy" class="button_to"> - # # <div> - # # <button type="submit"> - # # Make happy <strong><%= @user.name %></strong> - # # </button> - # # </div> + # # <button type="submit"> + # # Make happy <strong><%= @user.name %></strong> + # # </button> # # </form>" # # <%= button_to "New", { action: "new" }, form_class: "new-thing" %> # # => "<form method="post" action="/controller/new" class="new-thing"> - # # <div><input value="New" type="submit" /></div> + # # <input value="New" type="submit" /> # # </form>" # # # <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %> # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json"> - # # <div> - # # <input value="Create" type="submit" /> - # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> - # # </div> + # # <input value="Create" type="submit" /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </form>" # # # <%= button_to "Delete Image", { action: "delete", id: @image.id }, # method: :delete, data: { confirm: "Are you sure?" } %> # # => "<form method="post" action="/images/delete/1" class="button_to"> - # # <div> - # # <input type="hidden" name="_method" value="delete" /> - # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" /> - # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> - # # </div> + # # <input type="hidden" name="_method" value="delete" /> + # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </form>" # # # <%= button_to('Destroy', 'http://www.example.com', # method: "delete", remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %> # # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'> - # # <div> - # # <input name='_method' value='delete' type='hidden' /> - # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' /> - # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> - # # </div> + # # <input name='_method' value='delete' type='hidden' /> + # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' /> + # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/> # # </form>" # # def button_to(name = nil, options = nil, html_options = nil, &block) @@ -436,6 +428,7 @@ module ActionView # * <tt>:body</tt> - Preset the body of the email. # * <tt>:cc</tt> - Carbon Copy additional recipients on the email. # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email. + # * <tt>:reply_to</tt> - Preset the Reply-To field of the email. # # ==== Obfuscation # Prior to Rails 4.0, +mail_to+ provided options for encoding the address @@ -465,9 +458,9 @@ module ActionView html_options, name = name, nil if block_given? html_options = (html_options || {}).stringify_keys - extras = %w{ cc bcc body subject }.map! { |item| + extras = %w{ cc bcc body subject reply_to }.map! { |item| option = html_options.delete(item) || next - "#{item}=#{Rack::Utils.escape_path(option)}" + "#{item.dasherize}=#{Rack::Utils.escape_path(option)}" }.compact extras = extras.empty? ? '' : '?' + extras.join('&') diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 9ee05bd816..0b5c0b9991 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -262,7 +262,7 @@ module ActionView def layout(layout, conditions = {}) include LayoutConditions unless conditions.empty? - conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} } + conditions.each {|k, v| conditions[k] = Array(v).map(&:to_s) } self._layout_conditions = conditions self._layout = layout diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index 6c8d9cb5bf..9047dbdd85 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -13,11 +13,11 @@ module ActionView end def render_template(event) - return unless logger.info? - message = " Rendered #{from_rails_root(event.payload[:identifier])}" - message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << " (#{event.duration.round(1)}ms)" - info(message) + info do + message = " Rendered #{from_rails_root(event.payload[:identifier])}" + message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] + message << " (#{event.duration.round(1)}ms)" + end end alias :render_partial :render_template alias :render_collection :render_template diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 5fff6b0771..36855ec3d0 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -66,10 +66,7 @@ module ActionView def self.get(details) if details[:formats] details = details.dup - syms = Set.new Mime::SET.symbols - details[:formats] = details[:formats].select { |v| - syms.include? v - } + details[:formats] &= Mime::SET.symbols end @details_keys[details] ||= new end @@ -194,7 +191,6 @@ module ActionView def initialize(view_paths, details = {}, prefixes = []) @details, @details_key = {}, nil - @skip_default_locale = false @cache = true @prefixes = prefixes @rendered_format = nil @@ -216,12 +212,6 @@ module ActionView super(values) end - # Do not use the default locale on template lookup. - def skip_default_locale! - @skip_default_locale = true - self.locale = nil - end - # Override locale to return a symbol instead of array. def locale @details[:locale].first @@ -236,7 +226,7 @@ module ActionView config.locale = value end - super(@skip_default_locale ? I18n.locale : default_locale) + super(default_locale) end # Uses the first format in the formats array for layout lookup. diff --git a/actionview/lib/action_view/model_naming.rb b/actionview/lib/action_view/model_naming.rb index e09ebd60df..b6ed13424e 100644 --- a/actionview/lib/action_view/model_naming.rb +++ b/actionview/lib/action_view/model_naming.rb @@ -1,12 +1,12 @@ module ActionView - module ModelNaming + module ModelNaming #:nodoc: # Converts the given object to an ActiveModel compliant one. def convert_to_model(object) object.respond_to?(:to_model) ? object.to_model : object end def model_name_from_record_or_class(record_or_class) - (record_or_class.is_a?(Class) ? record_or_class : convert_to_model(record_or_class).class).model_name + convert_to_model(record_or_class).model_name end end end diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 81f9c40b85..ee3dce24b7 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -38,7 +38,7 @@ module ActionView initializer "action_view.setup_action_pack" do |app| ActiveSupport.on_load(:action_controller) do - ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor) + ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor) end end diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb index 63f645431a..6c6e69101b 100644 --- a/actionview/lib/action_view/record_identifier.rb +++ b/actionview/lib/action_view/record_identifier.rb @@ -2,29 +2,54 @@ require 'active_support/core_ext/module' require 'action_view/model_naming' module ActionView - # The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or - # pretty much any other model type that has an id. These patterns are then used to try elevate the view actions to - # a higher logical level. + # RecordIdentifier encapsulates methods used by various ActionView helpers + # to associate records with DOM elements. # - # # routes - # resources :posts + # Consider for example the following code that displays the body of a post: # - # # view - # <%= div_for(post) do %> <div id="post_45" class="post"> - # <%= post.body %> What a wonderful world! - # <% end %> </div> + # <%= div_for(post) do %> + # <%= post.body %> + # <% end %> # - # # controller - # def update - # post = Post.find(params[:id]) - # post.update(params[:post]) + # When +post+ is a new, unsaved ActiveRecord::Base intance, the resulting HTML + # is: # - # redirect_to(post) # Calls polymorphic_url(post) which in turn calls post_url(post) - # end + # <div id="new_post" class="post"> + # </div> + # + # When +post+ is a persisted ActiveRecord::Base instance, the resulting HTML + # is: + # + # <div id="post_42" class="post"> + # What a wonderful world! + # </div> + # + # In both cases, the +id+ and +class+ of the wrapping DOM element are + # automatically generated, following naming conventions encapsulated by the + # RecordIdentifier methods #dom_id and #dom_class: + # + # dom_id(Post.new) # => "new_post" + # dom_class(Post.new) # => "post" + # dom_id(Post.find 42) # => "post_42" + # dom_class(Post.find 42) # => "post" # - # As the example above shows, you can stop caring to a large extent what the actual id of the post is. - # You just know that one is being assigned and that the subsequent calls in redirect_to expect that - # same naming convention and allows you to write less code if you follow it. + # Note that these methods do not strictly require +Post+ to be a subclass of + # ActiveRecord::Base. + # Any +Post+ class will work as long as its instances respond to +to_key+ + # and +model_name+, given that +model_name+ responds to +param_key+. + # For instance: + # + # class Post + # attr_accessor :to_key + # + # def model_name + # OpenStruct.new param_key: 'post' + # end + # + # def self.find(id) + # new.tap { |post| post.to_key = [id] } + # end + # end module RecordIdentifier extend self extend ModelNaming @@ -78,7 +103,7 @@ module ActionView # make sure yourself that your dom ids are valid, in case you overwrite this method. def record_key_for_dom_id(record) key = convert_to_model(record).to_key - key ? key.join('_') : key + key ? key.join(JOIN) : key end end end diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index 73c19a0ae2..1f122f9bc6 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -29,8 +29,9 @@ module ActionView def extract_details(options) @lookup_context.registered_details.each_with_object({}) do |key, details| - next unless value = options[key] - details[key] = Array(value) + value = options[key] + + details[key] = Array(value) if value end end @@ -41,6 +42,7 @@ module ActionView def prepend_formats(formats) formats = Array(formats) return if formats.empty? || @lookup_context.html_fallback_for_js + @lookup_context.formats = formats | @lookup_context.formats end end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 36f17f01fd..5ff15411cf 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -1,6 +1,33 @@ require 'thread_safe' module ActionView + class PartialIteration + # The number of iterations that will be done by the partial. + attr_reader :size + + # The current iteration of the partial. + attr_reader :index + + def initialize(size) + @size = size + @index = 0 + end + + # Check if this is the first iteration of the partial. + def first? + index == 0 + end + + # Check if this is the last iteration of the partial. + def last? + index == size - 1 + end + + def iterate! # :nodoc: + @index += 1 + end + end + # = Action View Partials # # There's also a convenience method for rendering sub templates within the current controller that depends on a @@ -46,7 +73,7 @@ module ActionView # # <%= render partial: "account", locals: { user: @buyer } %> # - # == Rendering a collection of partials + # == \Rendering a collection of partials # # The example of partial use describes a familiar pattern where a template needs to iterate over an array and # render a sub template for each of the elements. This pattern has been implemented as a single method that @@ -56,8 +83,12 @@ 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 - # iteration counter will automatically be made available to the template with a name of the form - # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+. + # 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, + # +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+. + # For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's + # +index+ method. # # The <tt>:as</tt> option may be used when rendering partials. # @@ -74,7 +105,7 @@ module ActionView # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also # just keep domain objects, like Active Records, in there. # - # == Rendering shared partials + # == \Rendering shared partials # # Two controllers can share a set of partials and render them like this: # @@ -82,7 +113,7 @@ module ActionView # # This will render the partial "advertisement/_ad.html.erb" 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. @@ -96,7 +127,7 @@ module ActionView # # <%= render partial: "posts/post", collection: @posts %> # <%= render partial: @posts %> # - # == Rendering the default case + # == \Rendering the default case # # If you're not going to be using any of the options like collections or layouts, you can also use the short-hand # defaults of render to render partials. Examples: @@ -116,7 +147,7 @@ module ActionView # # <%= render partial: "posts/post", collection: @posts %> # <%= render @posts %> # - # == Rendering partials with layouts + # == \Rendering partials with layouts # # Partials can have their own layouts applied to them. These layouts are different than the ones that are # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types @@ -281,6 +312,8 @@ module ActionView end end + private + def render_collection return nil if @collection.blank? @@ -322,37 +355,41 @@ module ActionView # respond to +to_partial_path+ in order to setup the path. def setup(context, options, block) @view = context - partial = options[:partial] - @options = options - @locals = options[:locals] || {} @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 - @collection = collection else + @has_object = true @object = partial + @collection = collection_from_object || collection_from_options - if @collection = collection_from_object || collection + if @collection paths = @collection_data = @collection.map { |o| partial_path(o) } - @path = paths.uniq.size == 1 ? paths.first : nil + @path = paths.uniq.one? ? paths.first : nil else @path = partial_path end end if as = options[:as] - raise_invalid_identifier(as) unless as.to_s =~ /\A[a-z_]\w*\z/ + raise_invalid_option_as(as) unless as.to_s =~ /\A[a-z_]\w*\z/ as = as.to_sym end if @path - @variable, @variable_counter = retrieve_variable(@path, as) + @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as) @template_keys = retrieve_template_keys else paths.map! { |path| retrieve_variable(path, as).unshift(path) } @@ -361,7 +398,7 @@ module ActionView self end - def collection + def collection_from_options if @options.key?(:collection) collection = @options[:collection] collection.respond_to?(:to_ary) ? collection.to_ary : [] @@ -373,9 +410,7 @@ module ActionView end def find_partial - if path = @path - find_template(path, @template_keys) - end + find_template(@path, @template_keys) if @path end def find_template(path, locals) @@ -385,19 +420,22 @@ module ActionView def collection_with_template view, locals, template = @view, @locals, @template - as, counter = @variable, @variable_counter + as, counter, iteration = @variable, @variable_counter, @variable_iteration if layout = @options[:layout] layout = find_template(layout, @template_keys) end - index = -1 + partial_iteration = PartialIteration.new(@collection.size) + locals[iteration] = partial_iteration + @collection.map do |object| - locals[as] = object - locals[counter] = (index += 1) + locals[as] = object + locals[counter] = partial_iteration.index content = template.render(view, locals) content = layout.render(view, locals) { content } if layout + partial_iteration.iterate! content end end @@ -407,16 +445,20 @@ module ActionView cache = {} keys = @locals.keys - index = -1 + partial_iteration = PartialIteration.new(@collection.size) + @collection.map do |object| - index += 1 - path, as, counter = collection_data[index] + index = partial_iteration.index + path, as, counter, iteration = collection_data[index] - locals[as] = object - locals[counter] = index + locals[as] = object + locals[counter] = index + locals[iteration] = partial_iteration template = (cache[path] ||= find_template(path, keys + [as, counter])) - template.render(view, locals) + content = template.render(view, locals) + partial_iteration.iterate! + content end end @@ -466,27 +508,40 @@ module ActionView def retrieve_template_keys keys = @locals.keys - keys << @variable if @object || @collection - keys << @variable_counter if @collection + keys << @variable if @has_object || @collection + if @collection + keys << @variable_counter + keys << @variable_iteration + end keys end def retrieve_variable(path, as) variable = as || begin base = path[-1] == "/" ? "" : File.basename(path) - raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/ + raise_invalid_identifier(path) unless base =~ /\A_?(.*)(?:\.\w+)*\z/ $1.to_sym end - variable_counter = :"#{variable}_counter" if @collection - [variable, variable_counter] + if @collection + variable_counter = :"#{variable}_counter" + variable_iteration = :"#{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 a lowercase letter or underscore, " + + "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." 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 end end diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index be17097428..cd21d7ab47 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -6,20 +6,19 @@ module ActionView @view = context @details = extract_details(options) template = determine_template(options) - context = @lookup_context prepend_formats(template.formats) - unless context.rendered_format - context.rendered_format = template.formats.first || formats.first - end + @lookup_context.rendered_format ||= (template.formats.first || formats.first) render_template(template, options[:layout], options[:locals]) end + private + # Determine the template to be rendered using the given options. - def determine_template(options) #:nodoc: - keys = options.fetch(:locals, {}).keys + def determine_template(options) + keys = options.has_key?(:locals) ? options[:locals].keys : [] if options.key?(:body) Template::Text.new(options[:body]) diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index c92d090cce..1e8e7415d1 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -35,12 +35,13 @@ module ActionView module ClassMethods def view_context_class @view_context_class ||= begin - routes = respond_to?(:_routes) && _routes + supports_path = supports_path? + routes = respond_to?(:_routes) && _routes helpers = respond_to?(:_helpers) && _helpers Class.new(ActionView::Base) do if routes - include routes.url_helpers + include routes.url_helpers(supports_path) include routes.mounted_helpers end @@ -91,12 +92,15 @@ module ActionView # Find and render a template based on the options given. # :api: private def _render_template(options) #:nodoc: - variant = options[:variant] + variant = options.delete(:variant) + assigns = options.delete(:assigns) + context = view_context + context.assign assigns if assigns lookup_context.rendered_format = nil if options[:formats] lookup_context.variants = variant if variant - view_renderer.render(view_context, options) + view_renderer.render(context, options) end # Assign the rendered format to lookup context. @@ -107,7 +111,7 @@ module ActionView end # Normalize args by converting render "foo" to render :action => "foo" and - # render "foo/bar" to render :file => "foo/bar". + # render "foo/bar" to render :template => "foo/bar". # :api: private def _normalize_args(action=nil, options={}) options = super(action, options) @@ -117,7 +121,7 @@ module ActionView options = action when String, Symbol action = action.to_s - key = action.include?(?/) ? :file : :action + key = action.include?(?/) ? :template : :action options[key] = action else options[:partial] = action diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index 881a123572..f281333a41 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -80,19 +80,38 @@ module ActionView when String options when nil - super({:only_path => true}) + super(only_path: _generate_paths_by_default) when Hash - super({ :only_path => options[:host].nil? }.merge!(options.symbolize_keys)) + options = options.symbolize_keys + unless options.key?(:only_path) + if options[:host].nil? + options[:only_path] = _generate_paths_by_default + else + options[:only_path] = false + end + end + + super(options) when :back _back_url - when Symbol - ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_string_call self, options when Array - polymorphic_path(options, options.extract_options!) - when Class - ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_class_call self, options + if _generate_paths_by_default + polymorphic_path(options, options.extract_options!) + else + polymorphic_url(options, options.extract_options!) + end else - ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call self, options + method = _generate_paths_by_default ? :path : :url + builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.send(method) + + case options + when Symbol + builder.handle_string_call(self, options) + when Class + builder.handle_class_call(self, options) + else + builder.handle_model_call(self, options) + end end end diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 4cfd11fa20..465022f714 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -242,7 +242,7 @@ module ActionView end instrument("!compile_template") do - compile(view, mod) + compile(mod) end # Just discard the source if we have a virtual path. This @@ -271,7 +271,7 @@ 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(view, mod) #:nodoc: + def compile(mod) #:nodoc: encode! method_name = self.method_name code = @handler.call(self) @@ -300,18 +300,8 @@ module ActionView raise WrongEncodingError.new(@source, Encoding.default_internal) end - begin - mod.module_eval(source, identifier, 0) - ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) - rescue => e # errors from template code - if logger = (view && view.logger) - logger.debug "ERROR: compiling #{method_name} RAISED #{e}" - logger.debug "Function body: #{source}" - logger.debug "Backtrace: #{e.backtrace.join("\n")}" - end - - raise ActionView::Template::Error.new(self, e) - end + mod.module_eval(source, identifier, 0) + ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) end def handle_render_error(view, e) #:nodoc: @@ -330,15 +320,19 @@ module ActionView def locals_code #:nodoc: # Double assign to suppress the dreaded 'assigned but unused variable' warning - @locals.map { |key| "#{key} = #{key} = local_assigns[:#{key}];" }.join + @locals.each_with_object('') { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" } end def method_name #:nodoc: - @method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_") + @method_name ||= begin + m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}" + m.tr!('-', '_') + m + end end def identifier_method_name #:nodoc: - inspect.gsub(/[^a-z_]/, '_') + inspect.tr('^a-z_', '_') end def instrument(action, &block) diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb index 743ef6de0a..390bce98a2 100644 --- a/actionview/lib/action_view/template/error.rb +++ b/actionview/lib/action_view/template/error.rb @@ -75,7 +75,7 @@ module ActionView def sub_template_message if @sub_templates "Trace of template inclusion: " + - @sub_templates.collect { |template| template.inspect }.join(", ") + @sub_templates.collect(&:inspect).join(", ") else "" end diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb index 33bfcb458c..0105e88a49 100644 --- a/actionview/lib/action_view/template/handlers.rb +++ b/actionview/lib/action_view/template/handlers.rb @@ -7,9 +7,9 @@ module ActionView #:nodoc: autoload :Raw, 'action_view/template/handlers/raw' def self.extended(base) - base.register_default_template_handler :erb, ERB.new + base.register_default_template_handler :raw, Raw.new + base.register_template_handler :erb, ERB.new base.register_template_handler :builder, Builder.new - base.register_template_handler :raw, Raw.new base.register_template_handler :ruby, :source.to_proc end @@ -22,7 +22,7 @@ module ActionView #:nodoc: # Register an object that knows how to handle template files with the given # extensions. This can be used to implement new template types. - # The handler must respond to `:call`, which will be passed the template + # The handler must respond to +:call+, which will be passed the template # and should return the rendered template as a String. def register_template_handler(*extensions, handler) raise(ArgumentError, "Extension is required") if extensions.empty? @@ -42,7 +42,7 @@ module ActionView #:nodoc: end def template_handler_extensions - @@template_handlers.keys.map {|key| key.to_s }.sort + @@template_handlers.keys.map(&:to_s).sort end def registered_template_handler(extension) diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 4523060442..85a100ed4c 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -35,7 +35,7 @@ module ActionView end end - BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ + BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ def add_expr_literal(src, code) flush_newline_if_pending(src) @@ -49,9 +49,9 @@ module ActionView def add_expr_escaped(src, code) flush_newline_if_pending(src) if code =~ BLOCK_EXPR - src << "@output_buffer.safe_append= " << code + src << "@output_buffer.safe_expr_append= " << code else - src << "@output_buffer.safe_append=(" << code << ");" + src << "@output_buffer.safe_expr_append=(" << code << ");" end end diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb index 0c0d1fffcb..397c86014a 100644 --- a/actionview/lib/action_view/template/handlers/raw.rb +++ b/actionview/lib/action_view/template/handlers/raw.rb @@ -2,7 +2,7 @@ module ActionView module Template::Handlers class Raw def call(template) - escaped = template.source.gsub(':', '\:') + escaped = template.source.gsub(/:/, '\:') '%q:' + escaped + ':;' end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index d29d020c17..bc0db330ea 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -138,7 +138,7 @@ module ActionView # resolver is fresher before returning it. def cached(key, path_info, details, locals) #:nodoc: name, prefix, partial = path_info - locals = locals.map { |x| x.to_s }.sort! + locals = locals.map(&:to_s).sort! if key @cache.cache(key, name, prefix, partial, locals) do @@ -196,24 +196,12 @@ module ActionView } end - if RUBY_VERSION >= '2.2.0' - def find_template_paths(query) - Dir[query].reject { |filename| - File.directory?(filename) || - # deals with case-insensitive file systems. - !File.fnmatch(query, filename, File::FNM_EXTGLOB) - } - end - else - def find_template_paths(query) - # deals with case-insensitive file systems. - sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] } - - Dir[query].reject { |filename| - File.directory?(filename) || - !sanitizer[File.dirname(filename)].include?(filename) - } - end + def find_template_paths(query) + Dir[query].reject { |filename| + File.directory?(filename) || + # deals with case-insensitive file systems. + !File.fnmatch(query, filename, File::FNM_EXTGLOB) + } end # Helper for building query glob string based on resolver's pattern. @@ -250,11 +238,6 @@ module ActionView pieces.shift extension = pieces.pop - unless extension - message = "The file #{path} did not specify a template handler. The default is currently ERB, " \ - "but will change to RAW in the future." - ActiveSupport::Deprecation.warn message - end handler = Template.handler_for_extension(extension) format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last @@ -272,13 +255,13 @@ module ActionView # Default pattern, loads views the same way as previous versions of rails, eg. when you're # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}` # - # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}") + # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}") # # This one allows you to keep files with different formats in separate subdirectories, # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`, # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc. # - # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{.:handlers,}") + # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}") # # If you don't specify a pattern then the default will be used. # @@ -287,7 +270,7 @@ module ActionView # # ActionController::Base.view_paths = FileSystemResolver.new( # Rails.root.join("app/views"), - # ":prefix{/:locale}/:action{.:formats,}{.:handlers,}" + # ":prefix{/:locale}/:action{.:formats,}{+:variants,}{.:handlers,}" # ) # # ==== Pattern format and variables @@ -299,6 +282,7 @@ module ActionView # * <tt>:action</tt> - name of the action # * <tt>:locale</tt> - possible locale versions # * <tt>:formats</tt> - possible request formats (for example html, json, xml...) + # * <tt>:variants</tt> - possible request variants (for example phone, tablet...) # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...) # class FileSystemResolver < PathResolver diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb index b84e0281ae..be45fcf742 100644 --- a/actionview/lib/action_view/template/types.rb +++ b/actionview/lib/action_view/template/types.rb @@ -9,7 +9,7 @@ module ActionView self.types = Set.new def self.register(*t) - types.merge(t.map { |type| type.to_s }) + types.merge(t.map(&:to_s)) end register :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 9e8e6f43d5..06810ad14d 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -3,6 +3,8 @@ require 'action_controller' require 'action_controller/test_case' require 'action_view' +require 'rails-dom-testing' + module ActionView # = Action View Test Case class TestCase < ActiveSupport::TestCase @@ -34,6 +36,7 @@ module ActionView extend ActiveSupport::Concern include ActionDispatch::Assertions, ActionDispatch::TestProcess + include Rails::Dom::Testing::Assertions include ActionController::TemplateAssertions include ActionView::Context @@ -99,7 +102,9 @@ module ActionView def setup_with_controller @controller = ActionView::TestCase::TestController.new @request = @controller.request - @output_buffer = ActiveSupport::SafeBuffer.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 = '' make_test_case_available_to_view! @@ -120,6 +125,7 @@ module ActionView @_rendered_views ||= RenderedViewsCollection.new end + # Need to experiment if this priority is the best one: rendered => output_buffer class RenderedViewsCollection def initialize @rendered_views ||= Hash.new { |hash, key| hash[key] = [] } @@ -151,11 +157,9 @@ module ActionView private - # Support the selector assertions - # # Need to experiment if this priority is the best one: rendered => output_buffer - def response_from_page - HTML::Document.new(@rendered.blank? ? @output_buffer : @rendered).root + def document_root_element + Nokogiri::HTML::Document.parse(@rendered.blank? ? @output_buffer : @rendered).root end def say_no_to_protect_against_forgery! @@ -200,7 +204,7 @@ module ActionView def view @view ||= begin view = @controller.view_context - view.singleton_class.send :include, _helpers + view.singleton_class.include(_helpers) view.extend(Locals) view.rendered_views = self.rendered_views view.output_buffer = self.output_buffer @@ -236,7 +240,8 @@ module ActionView :@test_passed, :@view, :@view_context_class, - :@_subscribers + :@_subscribers, + :@html_document ] def _user_defined_ivars @@ -259,7 +264,7 @@ module ActionView def method_missing(selector, *args) if @controller.respond_to?(:_routes) && - ( @controller._routes.named_routes.helpers.include?(selector) || + ( @controller._routes.named_routes.route_defined?(selector) || @controller._routes.mounted_helpers.method_defined?(selector) ) @controller.__send__(selector, *args) else diff --git a/actionview/lib/action_view/vendor/html-scanner.rb b/actionview/lib/action_view/vendor/html-scanner.rb deleted file mode 100644 index 775b827529..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner.rb +++ /dev/null @@ -1,20 +0,0 @@ -$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/html-scanner" - -module HTML - extend ActiveSupport::Autoload - - eager_autoload do - autoload :CDATA, 'html/node' - autoload :Document, 'html/document' - autoload :FullSanitizer, 'html/sanitizer' - autoload :LinkSanitizer, 'html/sanitizer' - autoload :Node, 'html/node' - autoload :Sanitizer, 'html/sanitizer' - autoload :Selector, 'html/selector' - autoload :Tag, 'html/node' - autoload :Text, 'html/node' - autoload :Tokenizer, 'html/tokenizer' - autoload :Version, 'html/version' - autoload :WhiteListSanitizer, 'html/sanitizer' - end -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/document.rb b/actionview/lib/action_view/vendor/html-scanner/html/document.rb deleted file mode 100644 index 386820300a..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/document.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'html/tokenizer' -require 'html/node' -require 'html/selector' -require 'html/sanitizer' - -module HTML #:nodoc: - # A top-level HTML document. You give it a body of text, and it will parse that - # text into a tree of nodes. - class Document #:nodoc: - - # The root of the parsed document. - attr_reader :root - - # Create a new Document from the given text. - def initialize(text, strict=false, xml=false) - tokenizer = Tokenizer.new(text) - @root = Node.new(nil) - node_stack = [ @root ] - while token = tokenizer.next - node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token, strict) - - node_stack.last.children << node unless node.tag? && node.closing == :close - if node.tag? - if node_stack.length > 1 && node.closing == :close - if node_stack.last.name == node.name - if node_stack.last.children.empty? - node_stack.last.children << Text.new(node_stack.last, node.line, node.position, "") - end - node_stack.pop - else - open_start = node_stack.last.position - 20 - open_start = 0 if open_start < 0 - close_start = node.position - 20 - close_start = 0 if close_start < 0 - msg = <<EOF.strip -ignoring attempt to close #{node_stack.last.name} with #{node.name} - opened at byte #{node_stack.last.position}, line #{node_stack.last.line} - closed at byte #{node.position}, line #{node.line} - attributes at open: #{node_stack.last.attributes.inspect} - text around open: #{text[open_start,40].inspect} - text around close: #{text[close_start,40].inspect} -EOF - strict ? raise(msg) : warn(msg) - end - elsif !node.childless?(xml) && node.closing != :close - node_stack.push node - end - end - end - end - - # Search the tree for (and return) the first node that matches the given - # conditions. The conditions are interpreted differently for different node - # types, see HTML::Text#find and HTML::Tag#find. - def find(conditions) - @root.find(conditions) - end - - # Search the tree for (and return) all nodes that match the given - # conditions. The conditions are interpreted differently for different node - # types, see HTML::Text#find and HTML::Tag#find. - def find_all(conditions) - @root.find_all(conditions) - end - - end - -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/node.rb b/actionview/lib/action_view/vendor/html-scanner/html/node.rb deleted file mode 100644 index 27f0f2f6f8..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/node.rb +++ /dev/null @@ -1,532 +0,0 @@ -require 'strscan' - -module HTML #:nodoc: - - class Conditions < Hash #:nodoc: - def initialize(hash) - super() - hash = { :content => hash } unless Hash === hash - hash = keys_to_symbols(hash) - hash.each do |k,v| - case k - when :tag, :content then - # keys are valid, and require no further processing - when :attributes then - hash[k] = keys_to_strings(v) - when :parent, :child, :ancestor, :descendant, :sibling, :before, - :after - hash[k] = Conditions.new(v) - when :children - hash[k] = v = keys_to_symbols(v) - v.each do |key,value| - case key - when :count, :greater_than, :less_than - # keys are valid, and require no further processing - when :only - v[key] = Conditions.new(value) - else - raise "illegal key #{key.inspect} => #{value.inspect}" - end - end - else - raise "illegal key #{k.inspect} => #{v.inspect}" - end - end - update hash - end - - private - - def keys_to_strings(hash) - Hash[hash.keys.map {|k| [k.to_s, hash[k]]}] - end - - def keys_to_symbols(hash) - Hash[hash.keys.map do |k| - raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym) - [k.to_sym, hash[k]] - end] - end - end - - # The base class of all nodes, textual and otherwise, in an HTML document. - class Node #:nodoc: - # The array of children of this node. Not all nodes have children. - attr_reader :children - - # The parent node of this node. All nodes have a parent, except for the - # root node. - attr_reader :parent - - # The line number of the input where this node was begun - attr_reader :line - - # The byte position in the input where this node was begun - attr_reader :position - - # Create a new node as a child of the given parent. - def initialize(parent, line=0, pos=0) - @parent = parent - @children = [] - @line, @position = line, pos - end - - # Returns a textual representation of the node. - def to_s - @children.join() - end - - # Returns false (subclasses must override this to provide specific matching - # behavior.) +conditions+ may be of any type. - def match(conditions) - false - end - - # Search the children of this node for the first node for which #find - # returns non +nil+. Returns the result of the #find call that succeeded. - def find(conditions) - conditions = validate_conditions(conditions) - @children.each do |child| - node = child.find(conditions) - return node if node - end - nil - end - - # Search for all nodes that match the given conditions, and return them - # as an array. - def find_all(conditions) - conditions = validate_conditions(conditions) - - matches = [] - matches << self if match(conditions) - @children.each do |child| - matches.concat child.find_all(conditions) - end - matches - end - - # Returns +false+. Subclasses may override this if they define a kind of - # tag. - def tag? - false - end - - def validate_conditions(conditions) - Conditions === conditions ? conditions : Conditions.new(conditions) - end - - def ==(node) - return false unless self.class == node.class && children.size == node.children.size - - equivalent = true - - children.size.times do |i| - equivalent &&= children[i] == node.children[i] - end - - equivalent - end - - class <<self - def parse(parent, line, pos, content, strict=true) - if content !~ /^<\S/ - Text.new(parent, line, pos, content) - else - scanner = StringScanner.new(content) - - unless scanner.skip(/</) - if strict - raise "expected <" - else - return Text.new(parent, line, pos, content) - end - end - - if scanner.skip(/!\[CDATA\[/) - unless scanner.skip_until(/\]\]>/) - if strict - raise "expected ]]> (got #{scanner.rest.inspect} for #{content})" - else - scanner.skip_until(/\Z/) - end - end - - return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, '')) - end - - closing = ( scanner.scan(/\//) ? :close : nil ) - return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/) - name.downcase! - - unless closing - scanner.skip(/\s*/) - attributes = {} - while attr = scanner.scan(/[-\w:]+/) - value = true - if scanner.scan(/\s*=\s*/) - if delim = scanner.scan(/['"]/) - value = "" - while text = scanner.scan(/[^#{delim}\\]+|./) - case text - when "\\" then - value << text - break if scanner.eos? - value << scanner.getch - when delim - break - else value << text - end - end - else - value = scanner.scan(/[^\s>\/]+/) - end - end - attributes[attr.downcase] = value - scanner.skip(/\s*/) - end - - closing = ( scanner.scan(/\//) ? :self : nil ) - end - - unless scanner.scan(/\s*>/) - if strict - raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})" - else - # throw away all text until we find what we're looking for - scanner.skip_until(/>/) or scanner.terminate - end - end - - Tag.new(parent, line, pos, name, attributes, closing) - end - end - end - end - - # A node that represents text, rather than markup. - class Text < Node #:nodoc: - - attr_reader :content - - # Creates a new text node as a child of the given parent, with the given - # content. - def initialize(parent, line, pos, content) - super(parent, line, pos) - @content = content - end - - # Returns the content of this node. - def to_s - @content - end - - # Returns +self+ if this node meets the given conditions. Text nodes support - # conditions of the following kinds: - # - # * if +conditions+ is a string, it must be a substring of the node's - # content - # * if +conditions+ is a regular expression, it must match the node's - # content - # * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that - # is either a string or a regexp, and which is interpreted as described - # above. - def find(conditions) - match(conditions) && self - end - - # Returns non-+nil+ if this node meets the given conditions, or +nil+ - # otherwise. See the discussion of #find for the valid conditions. - def match(conditions) - case conditions - when String - @content == conditions - when Regexp - @content =~ conditions - when Hash - conditions = validate_conditions(conditions) - - # Text nodes only have :content, :parent, :ancestor - unless (conditions.keys - [:content, :parent, :ancestor]).empty? - return false - end - - match(conditions[:content]) - else - nil - end - end - - def ==(node) - return false unless super - content == node.content - end - end - - # A CDATA node is simply a text node with a specialized way of displaying - # itself. - class CDATA < Text #:nodoc: - def to_s - "<![CDATA[#{super}]]>" - end - end - - # A Tag is any node that represents markup. It may be an opening tag, a - # closing tag, or a self-closing tag. It has a name, and may have a hash of - # attributes. - class Tag < Node #:nodoc: - - # Either +nil+, <tt>:close</tt>, or <tt>:self</tt> - attr_reader :closing - - # Either +nil+, or a hash of attributes for this node. - attr_reader :attributes - - # The name of this tag. - attr_reader :name - - # Create a new node as a child of the given parent, using the given content - # to describe the node. It will be parsed and the node name, attributes and - # closing status extracted. - def initialize(parent, line, pos, name, attributes, closing) - super(parent, line, pos) - @name = name - @attributes = attributes - @closing = closing - end - - # A convenience for obtaining an attribute of the node. Returns +nil+ if - # the node has no attributes. - def [](attr) - @attributes ? @attributes[attr] : nil - end - - # Returns non-+nil+ if this tag can contain child nodes. - def childless?(xml = false) - return false if xml && @closing.nil? - !@closing.nil? || - @name =~ /^(img|br|hr|link|meta|area|base|basefont| - col|frame|input|isindex|param)$/ox - end - - # Returns a textual representation of the node - def to_s - if @closing == :close - "</#{@name}>" - else - s = "<#{@name}" - @attributes.each do |k,v| - s << " #{k}" - s << "=\"#{v}\"" if String === v - end - s << " /" if @closing == :self - s << ">" - @children.each { |child| s << child.to_s } - s << "</#{@name}>" if @closing != :self && !@children.empty? - s - end - end - - # If either the node or any of its children meet the given conditions, the - # matching node is returned. Otherwise, +nil+ is returned. (See the - # description of the valid conditions in the +match+ method.) - def find(conditions) - match(conditions) && self || super - end - - # Returns +true+, indicating that this node represents an HTML tag. - def tag? - true - end - - # Returns +true+ if the node meets any of the given conditions. The - # +conditions+ parameter must be a hash of any of the following keys - # (all are optional): - # - # * <tt>:tag</tt>: the node name must match the corresponding value - # * <tt>:attributes</tt>: a hash. The node's values must match the - # corresponding values in the hash. - # * <tt>:parent</tt>: a hash. The node's parent must match the - # corresponding hash. - # * <tt>:child</tt>: a hash. At least one of the node's immediate children - # must meet the criteria described by the hash. - # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must - # meet the criteria described by the hash. - # * <tt>:descendant</tt>: a hash. At least one of the node's descendants - # must meet the criteria described by the hash. - # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must - # meet the criteria described by the hash. - # * <tt>:after</tt>: a hash. The node must be after any sibling meeting - # the criteria described by the hash, and at least one sibling must match. - # * <tt>:before</tt>: a hash. The node must be before any sibling meeting - # the criteria described by the hash, and at least one sibling must match. - # * <tt>:children</tt>: a hash, for counting children of a node. Accepts the - # keys: - # ** <tt>:count</tt>: either a number or a range which must equal (or - # include) the number of children that match. - # ** <tt>:less_than</tt>: the number of matching children must be less than - # this number. - # ** <tt>:greater_than</tt>: the number of matching children must be - # greater than this number. - # ** <tt>:only</tt>: another hash consisting of the keys to use - # to match on the children, and only matching children will be - # counted. - # - # Conditions are matched using the following algorithm: - # - # * if the condition is a string, it must be a substring of the value. - # * if the condition is a regexp, it must match the value. - # * if the condition is a number, the value must match number.to_s. - # * if the condition is +true+, the value must not be +nil+. - # * if the condition is +false+ or +nil+, the value must be +nil+. - # - # Usage: - # - # # test if the node is a "span" tag - # node.match tag: "span" - # - # # test if the node's parent is a "div" - # node.match parent: { tag: "div" } - # - # # test if any of the node's ancestors are "table" tags - # node.match ancestor: { tag: "table" } - # - # # test if any of the node's immediate children are "em" tags - # node.match child: { tag: "em" } - # - # # test if any of the node's descendants are "strong" tags - # node.match descendant: { tag: "strong" } - # - # # test if the node has between 2 and 4 span tags as immediate children - # node.match children: { count: 2..4, only: { tag: "span" } } - # - # # get funky: test to see if the node is a "div", has a "ul" ancestor - # # and an "li" parent (with "class" = "enum"), and whether or not it has - # # a "span" descendant that contains # text matching /hello world/: - # node.match tag: "div", - # ancestor: { tag: "ul" }, - # parent: { tag: "li", - # attributes: { class: "enum" } }, - # descendant: { tag: "span", - # child: /hello world/ } - def match(conditions) - conditions = validate_conditions(conditions) - # check content of child nodes - if conditions[:content] - if children.empty? - return false unless match_condition("", conditions[:content]) - else - return false unless children.find { |child| child.match(conditions[:content]) } - end - end - - # test the name - return false unless match_condition(@name, conditions[:tag]) if conditions[:tag] - - # test attributes - (conditions[:attributes] || {}).each do |key, value| - return false unless match_condition(self[key], value) - end - - # test parent - return false unless parent.match(conditions[:parent]) if conditions[:parent] - - # test children - return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child] - - # test ancestors - if conditions[:ancestor] - return false unless catch :found do - p = self - throw :found, true if p.match(conditions[:ancestor]) while p = p.parent - end - end - - # test descendants - if conditions[:descendant] - return false unless children.find do |child| - # test the child - child.match(conditions[:descendant]) || - # test the child's descendants - child.match(:descendant => conditions[:descendant]) - end - end - - # count children - if opts = conditions[:children] - matches = children.select do |c| - (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?)) - end - - matches = matches.select { |c| c.match(opts[:only]) } if opts[:only] - opts.each do |key, value| - next if key == :only - case key - when :count - if Integer === value - return false if matches.length != value - else - return false unless value.include?(matches.length) - end - when :less_than - return false unless matches.length < value - when :greater_than - return false unless matches.length > value - else raise "unknown count condition #{key}" - end - end - end - - # test siblings - if conditions[:sibling] || conditions[:before] || conditions[:after] - siblings = parent ? parent.children : [] - self_index = siblings.index(self) - - if conditions[:sibling] - return false unless siblings.detect do |s| - s != self && s.match(conditions[:sibling]) - end - end - - if conditions[:before] - return false unless siblings[self_index+1..-1].detect do |s| - s != self && s.match(conditions[:before]) - end - end - - if conditions[:after] - return false unless siblings[0,self_index].detect do |s| - s != self && s.match(conditions[:after]) - end - end - end - - true - end - - def ==(node) - return false unless super - return false unless closing == node.closing && self.name == node.name - attributes == node.attributes - end - - private - # Match the given value to the given condition. - def match_condition(value, condition) - case condition - when String - value && value == condition - when Regexp - value && value.match(condition) - when Numeric - value == condition.to_s - when true - !value.nil? - when false, nil - value.nil? - else - false - end - end - end -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb deleted file mode 100644 index ed34eecf55..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb +++ /dev/null @@ -1,188 +0,0 @@ -require 'set' -require 'cgi' -require 'active_support/core_ext/module/attribute_accessors' - -module HTML - class Sanitizer - def sanitize(text, options = {}) - validate_options(options) - return text unless sanitizeable?(text) - tokenize(text, options).join - end - - def sanitizeable?(text) - !(text.nil? || text.empty? || !text.index("<")) - end - - protected - def tokenize(text, options) - tokenizer = HTML::Tokenizer.new(text) - result = [] - while token = tokenizer.next - node = Node.parse(nil, 0, 0, token, false) - process_node node, result, options - end - result - end - - def process_node(node, result, options) - result << node.to_s - end - - def validate_options(options) - if options[:tags] && !options[:tags].is_a?(Enumerable) - raise ArgumentError, "You should pass :tags as an Enumerable" - end - - if options[:attributes] && !options[:attributes].is_a?(Enumerable) - raise ArgumentError, "You should pass :attributes as an Enumerable" - end - end - end - - class FullSanitizer < Sanitizer - def sanitize(text, options = {}) - result = super - # strip any comments, and if they have a newline at the end (ie. line with - # only a comment) strip that too - result = result.gsub(/<!--(.*?)-->[\n]?/m, "") if (result && result =~ /<!--(.*?)-->[\n]?/m) - # Recurse - handle all dirty nested tags - result == text ? result : sanitize(result, options) - end - - def process_node(node, result, options) - result << node.to_s if node.class == HTML::Text - end - end - - class LinkSanitizer < FullSanitizer - cattr_accessor :included_tags, :instance_writer => false - self.included_tags = Set.new(%w(a href)) - - def sanitizeable?(text) - !(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">"))) - end - - protected - def process_node(node, result, options) - result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name) - end - end - - class WhiteListSanitizer < Sanitizer - [:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags, - :allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr| - class_attribute attr, :instance_writer => false - end - - # A regular expression of the valid characters used to separate protocols like - # the ':' in 'http://foo.com' - self.protocol_separator = /:|(�*58)|(p)|(�*3a)|(%|%)3A/i - - # Specifies a Set of HTML attributes that can have URIs. - self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc)) - - # Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed - # to just escaping harmless tags like <font> - self.bad_tags = Set.new(%w(script)) - - # Specifies the default Set of tags that the #sanitize helper will allow unscathed. - self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub - sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr - acronym a img blockquote del ins)) - - # Specifies the default Set of html attributes that the #sanitize helper will leave - # in the allowed tag. - self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr)) - - # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept. - self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto - feed svn urn aim rsync tag ssh sftp rtsp afs)) - - # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept. - self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse - border-color border-left-color border-right-color border-top-color clear color cursor direction display - elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height - overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation - speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space - width)) - - # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept. - self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center - collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal - nowrap olive pointer purple red right solid silver teal top transparent underline white yellow)) - - # Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers. - self.shorthand_css_properties = Set.new(%w(background border margin padding)) - - # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute - def sanitize_css(style) - # disallow urls - style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ') - - # gauntlet - if style !~ /\A([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*\z/ || - style !~ /\A(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*\z/ - return '' - end - - clean = [] - style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val| - if allowed_css_properties.include?(prop.downcase) - clean << prop + ': ' + val + ';' - elsif shorthand_css_properties.include?(prop.split('-')[0].downcase) - unless val.split().any? do |keyword| - !allowed_css_keywords.include?(keyword) && - keyword !~ /\A(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)\z/ - end - clean << prop + ': ' + val + ';' - end - end - end - clean.join(' ') - end - - protected - def tokenize(text, options) - options[:parent] = [] - options[:attributes] ||= allowed_attributes - options[:tags] ||= allowed_tags - super - end - - def process_node(node, result, options) - result << case node - when HTML::Tag - if node.closing == :close - options[:parent].shift - else - options[:parent].unshift node.name - end - - process_attributes_for node, options - - options[:tags].include?(node.name) ? node : nil - else - bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "<") - end - end - - def process_attributes_for(node, options) - return unless node.attributes - node.attributes.keys.each do |attr_name| - value = node.attributes[attr_name].to_s - - if !options[:attributes].include?(attr_name) || contains_bad_protocols?(attr_name, value) - node.attributes.delete(attr_name) - else - node.attributes[attr_name] = attr_name == 'style' ? sanitize_css(value) : CGI::escapeHTML(CGI::unescapeHTML(value)) - end - end - end - - def contains_bad_protocols?(attr_name, value) - uri_attributes.include?(attr_name) && - (value =~ /(^[^\/:]*):|(�*58)|(p)|(�*3a)|(%|%)3A/i && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip)) - end - end -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb deleted file mode 100644 index dfdd724b9b..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb +++ /dev/null @@ -1,830 +0,0 @@ -#-- -# Copyright (c) 2006 Assaf Arkin (http://labnotes.org) -# Under MIT and/or CC By license. -#++ - -module HTML - - # Selects HTML elements using CSS 2 selectors. - # - # The +Selector+ class uses CSS selector expressions to match and select - # HTML elements. - # - # For example: - # selector = HTML::Selector.new "form.login[action=/login]" - # creates a new selector that matches any +form+ element with the class - # +login+ and an attribute +action+ with the value <tt>/login</tt>. - # - # === Matching Elements - # - # Use the #match method to determine if an element matches the selector. - # - # For simple selectors, the method returns an array with that element, - # or +nil+ if the element does not match. For complex selectors (see below) - # the method returns an array with all matched elements, of +nil+ if no - # match found. - # - # For example: - # if selector.match(element) - # puts "Element is a login form" - # end - # - # === Selecting Elements - # - # Use the #select method to select all matching elements starting with - # one element and going through all children in depth-first order. - # - # This method returns an array of all matching elements, an empty array - # if no match is found - # - # For example: - # selector = HTML::Selector.new "input[type=text]" - # matches = selector.select(element) - # matches.each do |match| - # puts "Found text field with name #{match.attributes['name']}" - # end - # - # === Expressions - # - # Selectors can match elements using any of the following criteria: - # * <tt>name</tt> -- Match an element based on its name (tag name). - # For example, <tt>p</tt> to match a paragraph. You can use <tt>*</tt> - # to match any element. - # * <tt>#</tt><tt>id</tt> -- Match an element based on its identifier (the - # <tt>id</tt> attribute). For example, <tt>#</tt><tt>page</tt>. - # * <tt>.class</tt> -- Match an element based on its class name, all - # class names if more than one specified. - # * <tt>[attr]</tt> -- Match an element that has the specified attribute. - # * <tt>[attr=value]</tt> -- Match an element that has the specified - # attribute and value. (More operators are supported see below) - # * <tt>:pseudo-class</tt> -- Match an element based on a pseudo class, - # such as <tt>:nth-child</tt> and <tt>:empty</tt>. - # * <tt>:not(expr)</tt> -- Match an element that does not match the - # negation expression. - # - # When using a combination of the above, the element name comes first - # followed by identifier, class names, attributes, pseudo classes and - # negation in any order. Do not separate these parts with spaces! - # Space separation is used for descendant selectors. - # - # For example: - # selector = HTML::Selector.new "form.login[action=/login]" - # The matched element must be of type +form+ and have the class +login+. - # It may have other classes, but the class +login+ is required to match. - # It must also have an attribute called +action+ with the value - # <tt>/login</tt>. - # - # This selector will match the following element: - # <form class="login form" method="post" action="/login"> - # but will not match the element: - # <form method="post" action="/logout"> - # - # === Attribute Values - # - # Several operators are supported for matching attributes: - # * <tt>name</tt> -- The element must have an attribute with that name. - # * <tt>name=value</tt> -- The element must have an attribute with that - # name and value. - # * <tt>name^=value</tt> -- The attribute value must start with the - # specified value. - # * <tt>name$=value</tt> -- The attribute value must end with the - # specified value. - # * <tt>name*=value</tt> -- The attribute value must contain the - # specified value. - # * <tt>name~=word</tt> -- The attribute value must contain the specified - # word (space separated). - # * <tt>name|=word</tt> -- The attribute value must start with specified - # word. - # - # For example, the following two selectors match the same element: - # #my_id - # [id=my_id] - # and so do the following two selectors: - # .my_class - # [class~=my_class] - # - # === Alternatives, siblings, children - # - # Complex selectors use a combination of expressions to match elements: - # * <tt>expr1 expr2</tt> -- Match any element against the second expression - # if it has some parent element that matches the first expression. - # * <tt>expr1 > expr2</tt> -- Match any element against the second expression - # if it is the child of an element that matches the first expression. - # * <tt>expr1 + expr2</tt> -- Match any element against the second expression - # if it immediately follows an element that matches the first expression. - # * <tt>expr1 ~ expr2</tt> -- Match any element against the second expression - # that comes after an element that matches the first expression. - # * <tt>expr1, expr2</tt> -- Match any element against the first expression, - # or against the second expression. - # - # Since children and sibling selectors may match more than one element given - # the first element, the #match method may return more than one match. - # - # === Pseudo classes - # - # Pseudo classes were introduced in CSS 3. They are most often used to select - # elements in a given position: - # * <tt>:root</tt> -- Match the element only if it is the root element - # (no parent element). - # * <tt>:empty</tt> -- Match the element only if it has no child elements, - # and no text content. - # * <tt>:content(string)</tt> -- Match the element only if it has <tt>string</tt> - # as its text content (ignoring leading and trailing whitespace). - # * <tt>:only-child</tt> -- Match the element if it is the only child (element) - # of its parent element. - # * <tt>:only-of-type</tt> -- Match the element if it is the only child (element) - # of its parent element and its type. - # * <tt>:first-child</tt> -- Match the element if it is the first child (element) - # of its parent element. - # * <tt>:first-of-type</tt> -- Match the element if it is the first child (element) - # of its parent element of its type. - # * <tt>:last-child</tt> -- Match the element if it is the last child (element) - # of its parent element. - # * <tt>:last-of-type</tt> -- Match the element if it is the last child (element) - # of its parent element of its type. - # * <tt>:nth-child(b)</tt> -- Match the element if it is the b-th child (element) - # of its parent element. The value <tt>b</tt> specifies its index, starting with 1. - # * <tt>:nth-child(an+b)</tt> -- Match the element if it is the b-th child (element) - # in each group of <tt>a</tt> child elements of its parent element. - # * <tt>:nth-child(-an+b)</tt> -- Match the element if it is the first child (element) - # in each group of <tt>a</tt> child elements, up to the first <tt>b</tt> child - # elements of its parent element. - # * <tt>:nth-child(odd)</tt> -- Match element in the odd position (i.e. first, third). - # Same as <tt>:nth-child(2n+1)</tt>. - # * <tt>:nth-child(even)</tt> -- Match element in the even position (i.e. second, - # fourth). Same as <tt>:nth-child(2n+2)</tt>. - # * <tt>:nth-of-type(..)</tt> -- As above, but only counts elements of its type. - # * <tt>:nth-last-child(..)</tt> -- As above, but counts from the last child. - # * <tt>:nth-last-of-type(..)</tt> -- As above, but counts from the last child and - # only elements of its type. - # * <tt>:not(selector)</tt> -- Match the element only if the element does not - # match the simple selector. - # - # As you can see, <tt>:nth-child</tt> pseudo class and its variant can get quite - # tricky and the CSS specification doesn't do a much better job explaining it. - # But after reading the examples and trying a few combinations, it's easy to - # figure out. - # - # For example: - # table tr:nth-child(odd) - # Selects every second row in the table starting with the first one. - # - # div p:nth-child(4) - # Selects the fourth paragraph in the +div+, but not if the +div+ contains - # other elements, since those are also counted. - # - # div p:nth-of-type(4) - # Selects the fourth paragraph in the +div+, counting only paragraphs, and - # ignoring all other elements. - # - # div p:nth-of-type(-n+4) - # Selects the first four paragraphs, ignoring all others. - # - # And you can always select an element that matches one set of rules but - # not another using <tt>:not</tt>. For example: - # p:not(.post) - # Matches all paragraphs that do not have the class <tt>.post</tt>. - # - # === Substitution Values - # - # You can use substitution with identifiers, class names and element values. - # A substitution takes the form of a question mark (<tt>?</tt>) and uses the - # next value in the argument list following the CSS expression. - # - # The substitution value may be a string or a regular expression. All other - # values are converted to strings. - # - # For example: - # selector = HTML::Selector.new "#?", /^\d+$/ - # matches any element whose identifier consists of one or more digits. - # - # See http://www.w3.org/TR/css3-selectors/ - class Selector - - - # An invalid selector. - class InvalidSelectorError < StandardError #:nodoc: - end - - - class << self - - # :call-seq: - # Selector.for_class(cls) => selector - # - # Creates a new selector for the given class name. - def for_class(cls) - self.new([".?", cls]) - end - - - # :call-seq: - # Selector.for_id(id) => selector - # - # Creates a new selector for the given id. - def for_id(id) - self.new(["#?", id]) - end - - end - - - # :call-seq: - # Selector.new(string, [values ...]) => selector - # - # Creates a new selector from a CSS 2 selector expression. - # - # The first argument is the selector expression. All other arguments - # are used for value substitution. - # - # Throws InvalidSelectorError is the selector expression is invalid. - def initialize(selector, *values) - raise ArgumentError, "CSS expression cannot be empty" if selector.empty? - @source = "" - values = values[0] if values.size == 1 && values[0].is_a?(Array) - - # We need a copy to determine if we failed to parse, and also - # preserve the original pass by-ref statement. - statement = selector.strip.dup - - # Create a simple selector, along with negation. - simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) } - - @alternates = [] - @depends = nil - - # Alternative selector. - if statement.sub!(/^\s*,\s*/, "") - second = Selector.new(statement, values) - @alternates << second - # If there are alternate selectors, we group them in the top selector. - if alternates = second.instance_variable_get(:@alternates) - second.instance_variable_set(:@alternates, []) - @alternates.concat alternates - end - @source << " , " << second.to_s - # Sibling selector: create a dependency into second selector that will - # match element immediately following this one. - elsif statement.sub!(/^\s*\+\s*/, "") - second = next_selector(statement, values) - @depends = lambda do |element, first| - if element = next_element(element) - second.match(element, first) - end - end - @source << " + " << second.to_s - # Adjacent selector: create a dependency into second selector that will - # match all elements following this one. - elsif statement.sub!(/^\s*~\s*/, "") - second = next_selector(statement, values) - @depends = lambda do |element, first| - matches = [] - while element = next_element(element) - if subset = second.match(element, first) - if first && !subset.empty? - matches << subset.first - break - else - matches.concat subset - end - end - end - matches.empty? ? nil : matches - end - @source << " ~ " << second.to_s - # Child selector: create a dependency into second selector that will - # match a child element of this one. - elsif statement.sub!(/^\s*>\s*/, "") - second = next_selector(statement, values) - @depends = lambda do |element, first| - matches = [] - element.children.each do |child| - if child.tag? && subset = second.match(child, first) - if first && !subset.empty? - matches << subset.first - break - else - matches.concat subset - end - end - end - matches.empty? ? nil : matches - end - @source << " > " << second.to_s - # Descendant selector: create a dependency into second selector that - # will match all descendant elements of this one. Note, - elsif statement =~ /^\s+\S+/ && statement != selector - second = next_selector(statement, values) - @depends = lambda do |element, first| - matches = [] - stack = element.children.reverse - while node = stack.pop - next unless node.tag? - if subset = second.match(node, first) - if first && !subset.empty? - matches << subset.first - break - else - matches.concat subset - end - elsif children = node.children - stack.concat children.reverse - end - end - matches.empty? ? nil : matches - end - @source << " " << second.to_s - else - # The last selector is where we check that we parsed - # all the parts. - unless statement.empty? || statement.strip.empty? - raise ArgumentError, "Invalid selector: #{statement}" - end - end - end - - - # :call-seq: - # match(element, first?) => array or nil - # - # Matches an element against the selector. - # - # For a simple selector this method returns an array with the - # element if the element matches, nil otherwise. - # - # For a complex selector (sibling and descendant) this method - # returns an array with all matching elements, nil if no match is - # found. - # - # Use +first_only=true+ if you are only interested in the first element. - # - # For example: - # if selector.match(element) - # puts "Element is a login form" - # end - def match(element, first_only = false) - # Match element if no element name or element name same as element name - if matched = (!@tag_name || @tag_name == element.name) - # No match if one of the attribute matches failed - for attr in @attributes - if element.attributes[attr[0]] !~ attr[1] - matched = false - break - end - end - end - - # Pseudo class matches (nth-child, empty, etc). - if matched - for pseudo in @pseudo - unless pseudo.call(element) - matched = false - break - end - end - end - - # Negation. Same rules as above, but we fail if a match is made. - if matched && @negation - for negation in @negation - if negation[:tag_name] == element.name - matched = false - else - for attr in negation[:attributes] - if element.attributes[attr[0]] =~ attr[1] - matched = false - break - end - end - end - if matched - for pseudo in negation[:pseudo] - if pseudo.call(element) - matched = false - break - end - end - end - break unless matched - end - end - - # If element matched but depends on another element (child, - # sibling, etc), apply the dependent matches instead. - if matched && @depends - matches = @depends.call(element, first_only) - else - matches = matched ? [element] : nil - end - - # If this selector is part of the group, try all the alternative - # selectors (unless first_only). - if !first_only || !matches - @alternates.each do |alternate| - break if matches && first_only - if subset = alternate.match(element, first_only) - if matches - matches.concat subset - else - matches = subset - end - end - end - end - - matches - end - - - # :call-seq: - # select(root) => array - # - # Selects and returns an array with all matching elements, beginning - # with one node and traversing through all children depth-first. - # Returns an empty array if no match is found. - # - # The root node may be any element in the document, or the document - # itself. - # - # For example: - # selector = HTML::Selector.new "input[type=text]" - # matches = selector.select(element) - # matches.each do |match| - # puts "Found text field with name #{match.attributes['name']}" - # end - def select(root) - matches = [] - stack = [root] - while node = stack.pop - if node.tag? && subset = match(node, false) - subset.each do |match| - matches << match unless matches.any? { |item| item.equal?(match) } - end - elsif children = node.children - stack.concat children.reverse - end - end - matches - end - - - # Similar to #select but returns the first matching element. Returns +nil+ - # if no element matches the selector. - def select_first(root) - stack = [root] - while node = stack.pop - if node.tag? && subset = match(node, true) - return subset.first if !subset.empty? - elsif children = node.children - stack.concat children.reverse - end - end - nil - end - - - def to_s #:nodoc: - @source - end - - - # Returns the next element after this one. Skips sibling text nodes. - # - # With the +name+ argument, returns the next element with that name, - # skipping other sibling elements. - def next_element(element, name = nil) - if siblings = element.parent.children - found = false - siblings.each do |node| - if node.equal?(element) - found = true - elsif found && node.tag? - return node if (name.nil? || node.name == name) - end - end - end - nil - end - - - protected - - - # Creates a simple selector given the statement and array of - # substitution values. - # - # Returns a hash with the values +tag_name+, +attributes+, - # +pseudo+ (classes) and +negation+. - # - # Called the first time with +can_negate+ true to allow - # negation. Called a second time with false since negation - # cannot be negated. - def simple_selector(statement, values, can_negate = true) - tag_name = nil - attributes = [] - pseudo = [] - negation = [] - - # Element name. (Note that in negation, this can come at - # any order, but for simplicity we allow if only first). - statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match| - match.strip! - tag_name = match.downcase unless match == "*" - @source << match - "" # Remove - end - - # Get identifier, class, attribute name, pseudo or negation. - while true - # Element identifier. - next if statement.sub!(/^#(\?|[\w\-]+)/) do - id = $1 - if id == "?" - id = values.shift - end - @source << "##{id}" - id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp) - attributes << ["id", id] - "" # Remove - end - - # Class name. - next if statement.sub!(/^\.([\w\-]+)/) do - class_name = $1 - @source << ".#{class_name}" - class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp) - attributes << ["class", class_name] - "" # Remove - end - - # Attribute value. - next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do - name, equality, value = $1, $2, $3 - if value == "?" - value = values.shift - else - # Handle single and double quotes. - value.strip! - if (value[0] == ?" || value[0] == ?') && value[0] == value[-1] - value = value[1..-2] - end - end - @source << "[#{name}#{equality}'#{value}']" - attributes << [name.downcase.strip, attribute_match(equality, value)] - "" # Remove - end - - # Root element only. - next if statement.sub!(/^:root/) do - pseudo << lambda do |element| - element.parent.nil? || !element.parent.tag? - end - @source << ":root" - "" # Remove - end - - # Nth-child including last and of-type. - next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match| - reverse = $1 == "last-" - of_type = $2 == "of-type" - @source << ":nth-#{$1}#{$2}(" - case $3 - when "odd" - pseudo << nth_child(2, 1, of_type, reverse) - @source << "odd)" - when "even" - pseudo << nth_child(2, 2, of_type, reverse) - @source << "even)" - when /^(\d+|\?)$/ # b only - b = ($1 == "?" ? values.shift : $1).to_i - pseudo << nth_child(0, b, of_type, reverse) - @source << "#{b})" - when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/ - a = ($1 == "?" ? values.shift : - $1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i - b = ($2 == "?" ? values.shift : $2).to_i - pseudo << nth_child(a, b, of_type, reverse) - @source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})") - else - raise ArgumentError, "Invalid nth-child #{match}" - end - "" # Remove - end - # First/last child (of type). - next if statement.sub!(/^:(first|last)-(child|of-type)/) do - reverse = $1 == "last" - of_type = $2 == "of-type" - pseudo << nth_child(0, 1, of_type, reverse) - @source << ":#{$1}-#{$2}" - "" # Remove - end - # Only child (of type). - next if statement.sub!(/^:only-(child|of-type)/) do - of_type = $1 == "of-type" - pseudo << only_child(of_type) - @source << ":only-#{$1}" - "" # Remove - end - - # Empty: no child elements or meaningful content (whitespaces - # are ignored). - next if statement.sub!(/^:empty/) do - pseudo << lambda do |element| - empty = true - for child in element.children - if child.tag? || !child.content.strip.empty? - empty = false - break - end - end - empty - end - @source << ":empty" - "" # Remove - end - # Content: match the text content of the element, stripping - # leading and trailing spaces. - next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do - content = $1 - if content == "?" - content = values.shift - elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1] - content = content[1..-2] - end - @source << ":content('#{content}')" - content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp) - pseudo << lambda do |element| - text = "" - for child in element.children - unless child.tag? - text << child.content - end - end - text.strip =~ content - end - "" # Remove - end - - # Negation. Create another simple selector to handle it. - if statement.sub!(/^:not\(\s*/, "") - raise ArgumentError, "Double negatives are not missing feature" unless can_negate - @source << ":not(" - negation << simple_selector(statement, values, false) - raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "") - @source << ")" - next - end - - # No match: moving on. - break - end - - # Return hash. The keys are mapped to instance variables. - {:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation} - end - - - # Create a regular expression to match an attribute value based - # on the equality operator (=, ^=, |=, etc). - def attribute_match(equality, value) - regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s) - case equality - when "=" then - # Match the attribute value in full - Regexp.new("^#{regexp}$") - when "~=" then - # Match a space-separated word within the attribute value - Regexp.new("(^|\s)#{regexp}($|\s)") - when "^=" - # Match the beginning of the attribute value - Regexp.new("^#{regexp}") - when "$=" - # Match the end of the attribute value - Regexp.new("#{regexp}$") - when "*=" - # Match substring of the attribute value - regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp) - when "|=" then - # Match the first space-separated item of the attribute value - Regexp.new("^#{regexp}($|\s)") - else - raise InvalidSelectorError, "Invalid operation/value" unless value.empty? - # Match all attributes values (existence check) - // - end - end - - - # Returns a lambda that can match an element against the nth-child - # pseudo class, given the following arguments: - # * +a+ -- Value of a part. - # * +b+ -- Value of b part. - # * +of_type+ -- True to test only elements of this type (of-type). - # * +reverse+ -- True to count in reverse order (last-). - def nth_child(a, b, of_type, reverse) - # a = 0 means select at index b, if b = 0 nothing selected - return lambda { |element| false } if a == 0 && b == 0 - # a < 0 and b < 0 will never match against an index - return lambda { |element| false } if a < 0 && b < 0 - b = a + b + 1 if b < 0 # b < 0 just picks last element from each group - b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based - lambda do |element| - # Element must be inside parent element. - return false unless element.parent && element.parent.tag? - index = 0 - # Get siblings, reverse if counting from last. - siblings = element.parent.children - siblings = siblings.reverse if reverse - # Match element name if of-type, otherwise ignore name. - name = of_type ? element.name : nil - found = false - for child in siblings - # Skip text nodes/comments. - if child.tag? && (name == nil || child.name == name) - if a == 0 - # Shortcut when a == 0 no need to go past count - if index == b - found = child.equal?(element) - break - end - elsif a < 0 - # Only look for first b elements - break if index > b - if child.equal?(element) - found = (index % a) == 0 - break - end - else - # Otherwise, break if child found and count == an+b - if child.equal?(element) - found = (index % a) == b - break - end - end - index += 1 - end - end - found - end - end - - - # Creates a only child lambda. Pass +of-type+ to only look at - # elements of its type. - def only_child(of_type) - lambda do |element| - # Element must be inside parent element. - return false unless element.parent && element.parent.tag? - name = of_type ? element.name : nil - other = false - for child in element.parent.children - # Skip text nodes/comments. - if child.tag? && (name == nil || child.name == name) - unless child.equal?(element) - other = true - break - end - end - end - !other - end - end - - - # Called to create a dependent selector (sibling, descendant, etc). - # Passes the remainder of the statement that will be reduced to zero - # eventually, and array of substitution values. - # - # This method is called from four places, so it helps to put it here - # for reuse. The only logic deals with the need to detect comma - # separators (alternate) and apply them to the selector group of the - # top selector. - def next_selector(statement, values) - second = Selector.new(statement, values) - # If there are alternate selectors, we group them in the top selector. - if alternates = second.instance_variable_get(:@alternates) - second.instance_variable_set(:@alternates, []) - @alternates.concat alternates - end - second - end - - end - - - # See HTML::Selector.new - def self.selector(statement, *values) - Selector.new(statement, *values) - end - - - class Tag - - def select(selector, *values) - selector = HTML::Selector.new(selector, values) - selector.select(self) - end - - end - -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb deleted file mode 100644 index adf4e45930..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb +++ /dev/null @@ -1,107 +0,0 @@ -require 'strscan' - -module HTML #:nodoc: - - # A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each - # token is a string. Each string represents either "text", or an HTML element. - # - # This currently assumes valid XHTML, which means no free < or > characters. - # - # Usage: - # - # tokenizer = HTML::Tokenizer.new(text) - # while token = tokenizer.next - # p token - # end - class Tokenizer #:nodoc: - - # The current (byte) position in the text - attr_reader :position - - # The current line number - attr_reader :line - - # Create a new Tokenizer for the given text. - def initialize(text) - text.encode! - @scanner = StringScanner.new(text) - @position = 0 - @line = 0 - @current_line = 1 - end - - # Returns the next token in the sequence, or +nil+ if there are no more tokens in - # the stream. - def next - return nil if @scanner.eos? - @position = @scanner.pos - @line = @current_line - if @scanner.check(/<\S/) - update_current_line(scan_tag) - else - update_current_line(scan_text) - end - end - - private - - # Treat the text at the current position as a tag, and scan it. Supports - # comments, doctype tags, and regular tags, and ignores less-than and - # greater-than characters within quoted strings. - def scan_tag - tag = @scanner.getch - if @scanner.scan(/!--/) # comment - tag << @scanner.matched - tag << (@scanner.scan_until(/--\s*>/) || @scanner.scan_until(/\Z/)) - elsif @scanner.scan(/!\[CDATA\[/) - tag << @scanner.matched - tag << (@scanner.scan_until(/\]\]>/) || @scanner.scan_until(/\Z/)) - elsif @scanner.scan(/!/) # doctype - tag << @scanner.matched - tag << consume_quoted_regions - else - tag << consume_quoted_regions - end - tag - end - - # Scan all text up to the next < character and return it. - def scan_text - "#{@scanner.getch}#{@scanner.scan(/[^<]*/)}" - end - - # Counts the number of newlines in the text and updates the current line - # accordingly. - def update_current_line(text) - text.scan(/\r?\n/) { @current_line += 1 } - end - - # Skips over quoted strings, so that less-than and greater-than characters - # within the strings are ignored. - def consume_quoted_regions - text = "" - loop do - match = @scanner.scan_until(/['"<>]/) or break - - delim = @scanner.matched - if delim == "<" - match = match.chop - @scanner.pos -= 1 - end - - text << match - break if delim == "<" || delim == ">" - - # consume the quoted region - while match = @scanner.scan_until(/[\\#{delim}]/) - text << match - break if @scanner.matched == delim - break if @scanner.eos? - text << @scanner.getch # skip the escaped character - end - end - text - end - end - -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/version.rb b/actionview/lib/action_view/vendor/html-scanner/html/version.rb deleted file mode 100644 index 6d645c3e14..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/version.rb +++ /dev/null @@ -1,11 +0,0 @@ -module HTML #:nodoc: - module Version #:nodoc: - - MAJOR = 0 - MINOR = 5 - TINY = 3 - - STRING = [ MAJOR, MINOR, TINY ].join(".") - - end -end diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index 80a41f2418..492f67f45d 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -16,14 +16,9 @@ module ActionView module ClassMethods def _prefixes # :nodoc: @_prefixes ||= begin - deprecated_prefixes = handle_deprecated_parent_prefixes - if deprecated_prefixes - deprecated_prefixes - else - return local_prefixes if superclass.abstract? - - local_prefixes + superclass._prefixes - end + return local_prefixes if superclass.abstract? + + local_prefixes + superclass._prefixes end end @@ -34,13 +29,6 @@ module ActionView def local_prefixes [controller_path] end - - def handle_deprecated_parent_prefixes # TODO: remove in 4.3/5.0. - return unless respond_to?(:parent_prefixes) - - ActiveSupport::Deprecation.warn "Overriding ActionController::Base::parent_prefixes is deprecated, override .local_prefixes instead." - local_prefixes + parent_prefixes - end end # The prefixes used in render "foo" shortcuts. |