actionview 6.1.7.2 → 7.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -277
  3. data/MIT-LICENSE +2 -1
  4. data/README.rdoc +3 -3
  5. data/app/assets/javascripts/rails-ujs.esm.js +686 -0
  6. data/app/assets/javascripts/rails-ujs.js +630 -0
  7. data/lib/action_view/base.rb +37 -19
  8. data/lib/action_view/buffers.rb +107 -9
  9. data/lib/action_view/cache_expiry.rb +48 -37
  10. data/lib/action_view/context.rb +1 -1
  11. data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
  12. data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
  13. data/lib/action_view/dependency_tracker.rb +6 -147
  14. data/lib/action_view/deprecator.rb +7 -0
  15. data/lib/action_view/digestor.rb +8 -5
  16. data/lib/action_view/flows.rb +4 -4
  17. data/lib/action_view/gem_version.rb +4 -4
  18. data/lib/action_view/helpers/active_model_helper.rb +3 -3
  19. data/lib/action_view/helpers/asset_tag_helper.rb +200 -60
  20. data/lib/action_view/helpers/asset_url_helper.rb +22 -21
  21. data/lib/action_view/helpers/atom_feed_helper.rb +8 -9
  22. data/lib/action_view/helpers/cache_helper.rb +55 -12
  23. data/lib/action_view/helpers/capture_helper.rb +34 -14
  24. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  25. data/lib/action_view/helpers/controller_helper.rb +8 -2
  26. data/lib/action_view/helpers/csp_helper.rb +3 -3
  27. data/lib/action_view/helpers/csrf_helper.rb +4 -4
  28. data/lib/action_view/helpers/date_helper.rb +123 -57
  29. data/lib/action_view/helpers/debug_helper.rb +6 -4
  30. data/lib/action_view/helpers/form_helper.rb +253 -97
  31. data/lib/action_view/helpers/form_options_helper.rb +72 -34
  32. data/lib/action_view/helpers/form_tag_helper.rb +189 -58
  33. data/lib/action_view/helpers/javascript_helper.rb +4 -5
  34. data/lib/action_view/helpers/number_helper.rb +43 -335
  35. data/lib/action_view/helpers/output_safety_helper.rb +6 -6
  36. data/lib/action_view/helpers/rendering_helper.rb +6 -7
  37. data/lib/action_view/helpers/sanitize_helper.rb +54 -24
  38. data/lib/action_view/helpers/tag_helper.rb +42 -35
  39. data/lib/action_view/helpers/tags/base.rb +16 -77
  40. data/lib/action_view/helpers/tags/check_box.rb +1 -1
  41. data/lib/action_view/helpers/tags/collection_check_boxes.rb +1 -0
  42. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +1 -0
  43. data/lib/action_view/helpers/tags/collection_select.rb +4 -1
  44. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  45. data/lib/action_view/helpers/tags/date_select.rb +2 -0
  46. data/lib/action_view/helpers/tags/datetime_field.rb +14 -6
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +11 -2
  48. data/lib/action_view/helpers/tags/file_field.rb +16 -0
  49. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -0
  50. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  51. data/lib/action_view/helpers/tags/select.rb +4 -1
  52. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  53. data/lib/action_view/helpers/tags/time_field.rb +11 -2
  54. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -0
  55. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  56. data/lib/action_view/helpers/tags/weekday_select.rb +31 -0
  57. data/lib/action_view/helpers/tags.rb +5 -2
  58. data/lib/action_view/helpers/text_helper.rb +180 -97
  59. data/lib/action_view/helpers/translation_helper.rb +14 -45
  60. data/lib/action_view/helpers/url_helper.rb +230 -132
  61. data/lib/action_view/helpers.rb +27 -25
  62. data/lib/action_view/layouts.rb +15 -10
  63. data/lib/action_view/log_subscriber.rb +49 -32
  64. data/lib/action_view/lookup_context.rb +58 -61
  65. data/lib/action_view/model_naming.rb +2 -2
  66. data/lib/action_view/path_registry.rb +57 -0
  67. data/lib/action_view/path_set.rb +28 -35
  68. data/lib/action_view/railtie.rb +44 -9
  69. data/lib/action_view/record_identifier.rb +16 -9
  70. data/lib/action_view/render_parser.rb +188 -0
  71. data/lib/action_view/renderer/abstract_renderer.rb +3 -3
  72. data/lib/action_view/renderer/collection_renderer.rb +10 -2
  73. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +21 -3
  74. data/lib/action_view/renderer/partial_renderer.rb +3 -36
  75. data/lib/action_view/renderer/renderer.rb +6 -4
  76. data/lib/action_view/renderer/streaming_template_renderer.rb +6 -5
  77. data/lib/action_view/renderer/template_renderer.rb +9 -4
  78. data/lib/action_view/rendering.rb +25 -7
  79. data/lib/action_view/ripper_ast_parser.rb +198 -0
  80. data/lib/action_view/routing_url_for.rb +8 -5
  81. data/lib/action_view/template/error.rb +122 -14
  82. data/lib/action_view/template/handlers/builder.rb +4 -4
  83. data/lib/action_view/template/handlers/erb/erubi.rb +23 -27
  84. data/lib/action_view/template/handlers/erb.rb +79 -1
  85. data/lib/action_view/template/handlers.rb +4 -4
  86. data/lib/action_view/template/html.rb +4 -4
  87. data/lib/action_view/template/inline.rb +3 -3
  88. data/lib/action_view/template/raw_file.rb +4 -4
  89. data/lib/action_view/template/renderable.rb +1 -1
  90. data/lib/action_view/template/resolver.rb +96 -313
  91. data/lib/action_view/template/text.rb +4 -4
  92. data/lib/action_view/template/types.rb +25 -32
  93. data/lib/action_view/template.rb +245 -41
  94. data/lib/action_view/template_details.rb +66 -0
  95. data/lib/action_view/template_path.rb +66 -0
  96. data/lib/action_view/test_case.rb +182 -23
  97. data/lib/action_view/testing/resolvers.rb +11 -12
  98. data/lib/action_view/unbound_template.rb +43 -7
  99. data/lib/action_view/version.rb +1 -1
  100. data/lib/action_view/view_paths.rb +19 -28
  101. data/lib/action_view.rb +6 -4
  102. data/lib/assets/compiled/rails-ujs.js +36 -5
  103. metadata +32 -25
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ module SelectRenderer # :nodoc:
7
+ private
8
+ def select_content_tag(option_tags, options, html_options)
9
+ html_options = html_options.stringify_keys
10
+ [:required, :multiple, :size].each do |prop|
11
+ html_options[prop.to_s] = options.delete(prop) if options.key?(prop) && !html_options.key?(prop.to_s)
12
+ end
13
+
14
+ add_default_name_and_id(html_options)
15
+
16
+ if placeholder_required?(html_options)
17
+ raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
18
+ options[:include_blank] ||= true unless options[:prompt]
19
+ end
20
+
21
+ value = options.fetch(:selected) { value() }
22
+ select = content_tag("select", add_options(option_tags, options, value), html_options)
23
+
24
+ if html_options["multiple"] && options.fetch(:include_hidden, true)
25
+ tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "", autocomplete: "off") + select
26
+ else
27
+ select
28
+ end
29
+ end
30
+
31
+ def placeholder_required?(html_options)
32
+ # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required
33
+ html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1
34
+ end
35
+
36
+ def add_options(option_tags, options, value = nil)
37
+ if options[:include_blank]
38
+ content = (options[:include_blank] if options[:include_blank].is_a?(String))
39
+ label = (" " unless content)
40
+ option_tags = tag_builder.content_tag_string("option", content, value: "", label: label) + "\n" + option_tags
41
+ end
42
+
43
+ if value.blank? && options[:prompt]
44
+ tag_options = { value: "" }.tap do |prompt_opts|
45
+ prompt_opts[:disabled] = true if options[:disabled] == ""
46
+ prompt_opts[:selected] = true if options[:selected] == ""
47
+ end
48
+ option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
49
+ end
50
+
51
+ option_tags
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -4,9 +4,18 @@ module ActionView
4
4
  module Helpers
5
5
  module Tags # :nodoc:
6
6
  class TimeField < DatetimeField # :nodoc:
7
+ def initialize(object_name, method_name, template_object, options = {})
8
+ @include_seconds = options.delete(:include_seconds) { true }
9
+ super
10
+ end
11
+
7
12
  private
8
- def format_date(value)
9
- value&.strftime("%T.%L")
13
+ def format_datetime(value)
14
+ if @include_seconds
15
+ value&.strftime("%T.%L")
16
+ else
17
+ value&.strftime("%H:%M")
18
+ end
10
19
  end
11
20
  end
12
21
  end
@@ -4,6 +4,9 @@ module ActionView
4
4
  module Helpers
5
5
  module Tags # :nodoc:
6
6
  class TimeZoneSelect < Base # :nodoc:
7
+ include SelectRenderer
8
+ include FormOptionsHelper
9
+
7
10
  def initialize(object_name, method_name, template_object, priority_zones, options, html_options)
8
11
  @priority_zones = priority_zones
9
12
  @html_options = html_options
@@ -5,7 +5,7 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class WeekField < DatetimeField # :nodoc:
7
7
  private
8
- def format_date(value)
8
+ def format_datetime(value)
9
9
  value&.strftime("%Y-W%V")
10
10
  end
11
11
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ class WeekdaySelect < Base # :nodoc:
7
+ include SelectRenderer
8
+ include FormOptionsHelper
9
+
10
+ def initialize(object_name, method_name, template_object, options, html_options)
11
+ @html_options = html_options
12
+
13
+ super(object_name, method_name, template_object, options)
14
+ end
15
+
16
+ def render
17
+ select_content_tag(
18
+ weekday_options_for_select(
19
+ value || @options[:selected],
20
+ index_as_value: @options.fetch(:index_as_value, false),
21
+ day_format: @options.fetch(:day_format, :day_names),
22
+ beginning_of_week: @options.fetch(:beginning_of_week, Date.beginning_of_week)
23
+ ),
24
+ @options,
25
+ @html_options
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionView
4
- module Helpers #:nodoc:
5
- module Tags #:nodoc:
4
+ module Helpers # :nodoc:
5
+ module Tags # :nodoc:
6
6
  extend ActiveSupport::Autoload
7
7
 
8
+ autoload :SelectRenderer
9
+
8
10
  eager_autoload do
9
11
  autoload :Base
10
12
  autoload :Translator
@@ -38,6 +40,7 @@ module ActionView
38
40
  autoload :TimeZoneSelect
39
41
  autoload :UrlField
40
42
  autoload :WeekField
43
+ autoload :WeekdaySelect
41
44
  end
42
45
  end
43
46
  end
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/string/filters"
4
+ require "active_support/core_ext/string/access"
4
5
  require "active_support/core_ext/array/extract_options"
6
+ require "action_view/helpers/sanitize_helper"
7
+ require "action_view/helpers/tag_helper"
8
+ require "action_view/helpers/output_safety_helper"
5
9
 
6
10
  module ActionView
7
- # = Action View Text Helpers
8
- module Helpers #:nodoc:
11
+ module Helpers # :nodoc:
12
+ # = Action View Text \Helpers
13
+ #
9
14
  # The TextHelper module provides a set of methods for filtering, formatting
10
15
  # and transforming strings, which can reduce the amount of inline Ruby code in
11
16
  # your views. These helper methods extend Action View making them callable
@@ -36,21 +41,25 @@ module ActionView
36
41
  include OutputSafetyHelper
37
42
 
38
43
  # The preferred method of outputting text in your views is to use the
39
- # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
44
+ # <tt><%= "text" %></tt> eRuby syntax. The regular +puts+ and +print+ methods
40
45
  # do not operate as expected in an eRuby code block. If you absolutely must
41
- # output text within a non-output code block (i.e., <% %>), you can use the concat method.
46
+ # output text within a non-output code block (i.e., <tt><% %></tt>), you
47
+ # can use the +concat+ method.
48
+ #
49
+ # <% concat "hello" %> is equivalent to <%= "hello" %>
42
50
  #
43
51
  # <%
44
- # concat "hello"
45
- # # is the equivalent of <%= "hello" %>
46
- #
47
- # if logged_in
48
- # concat "Logged in!"
49
- # else
50
- # concat link_to('login', action: :login)
51
- # end
52
- # # will either display "Logged in!" or a login link
52
+ # unless signed_in?
53
+ # concat link_to("Sign In", action: :sign_in)
54
+ # end
53
55
  # %>
56
+ #
57
+ # is equivalent to
58
+ #
59
+ # <% unless signed_in? %>
60
+ # <%= link_to "Sign In", action: :sign_in %>
61
+ # <% end %>
62
+ #
54
63
  def concat(string)
55
64
  output_buffer << string
56
65
  end
@@ -59,17 +68,36 @@ module ActionView
59
68
  output_buffer.respond_to?(:safe_concat) ? output_buffer.safe_concat(string) : concat(string)
60
69
  end
61
70
 
62
- # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
63
- # (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...")
64
- # for a total length not exceeding <tt>:length</tt>.
71
+ # Truncates +text+ if it is longer than a specified +:length+. If +text+
72
+ # is truncated, an omission marker will be appended to the result for a
73
+ # total length not exceeding +:length+.
74
+ #
75
+ # You can also pass a block to render and append extra content after the
76
+ # omission marker when +text+ is truncated. However, this content _can_
77
+ # cause the total length to exceed +:length+ characters.
78
+ #
79
+ # The result will be escaped unless <tt>escape: false</tt> is specified.
80
+ # In any case, the result will be marked HTML-safe. Care should be taken
81
+ # if +text+ might contain HTML tags or entities, because truncation could
82
+ # produce invalid HTML, such as unbalanced or incomplete tags.
83
+ #
84
+ # ==== Options
85
+ #
86
+ # [+:length+]
87
+ # The maximum number of characters that should be returned, excluding
88
+ # any extra content from the block. Defaults to 30.
65
89
  #
66
- # Pass a <tt>:separator</tt> to truncate +text+ at a natural break.
90
+ # [+:omission+]
91
+ # The string to append after truncating. Defaults to <tt>"..."</tt>.
67
92
  #
68
- # Pass a block if you want to show extra content when the text is truncated.
93
+ # [+:separator+]
94
+ # A string or regexp used to find a breaking point at which to truncate.
95
+ # By default, truncation can occur at any character in +text+.
69
96
  #
70
- # The result is marked as HTML-safe, but it is escaped by default, unless <tt>:escape</tt> is
71
- # +false+. Care should be taken if +text+ contains HTML tags or entities, because truncation
72
- # may produce invalid HTML (such as unbalanced or incomplete tags).
97
+ # [+:escape+]
98
+ # Whether to escape the result. Defaults to true.
99
+ #
100
+ # ==== Examples
73
101
  #
74
102
  # truncate("Once upon a time in a world far far away")
75
103
  # # => "Once upon a time in a world..."
@@ -90,7 +118,7 @@ module ActionView
90
118
  # # => "<p>Once upon a time in a wo..."
91
119
  #
92
120
  # truncate("Once upon a time in a world far far away") { link_to "Continue", "#" }
93
- # # => "Once upon a time in a wo...<a href="#">Continue</a>"
121
+ # # => "Once upon a time in a world...<a href=\"#\">Continue</a>"
94
122
  def truncate(text, options = {}, &block)
95
123
  if text
96
124
  length = options.fetch(:length, 30)
@@ -102,76 +130,108 @@ module ActionView
102
130
  end
103
131
  end
104
132
 
105
- # Highlights one or more +phrases+ everywhere in +text+ by inserting it into
106
- # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
107
- # as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to
108
- # <tt><mark>\1</mark></tt>) or passing a block that receives each matched term. By default +text+
109
- # is sanitized to prevent possible XSS attacks. If the input is trustworthy, passing false
110
- # for <tt>:sanitize</tt> will turn sanitizing off.
133
+ # Highlights occurrences of +phrases+ in +text+ by formatting them with a
134
+ # highlighter string. +phrases+ can be one or more strings or regular
135
+ # expressions. The result will be marked HTML safe. By default, +text+ is
136
+ # sanitized before highlighting to prevent possible XSS attacks.
137
+ #
138
+ # If a block is specified, it will be used instead of the highlighter
139
+ # string. Each occurrence of a phrase will be passed to the block, and its
140
+ # return value will be inserted into the final result.
141
+ #
142
+ # ==== Options
143
+ #
144
+ # [+:highlighter+]
145
+ # The highlighter string. Uses <tt>\1</tt> as the placeholder for a
146
+ # phrase, similar to +String#sub+. Defaults to <tt>"<mark>\1</mark>"</tt>.
147
+ # This option is ignored if a block is specified.
148
+ #
149
+ # [+:sanitize+]
150
+ # Whether to sanitize +text+ before highlighting. Defaults to true.
151
+ #
152
+ # ==== Examples
111
153
  #
112
154
  # highlight('You searched for: rails', 'rails')
113
- # # => You searched for: <mark>rails</mark>
155
+ # # => "You searched for: <mark>rails</mark>"
114
156
  #
115
157
  # highlight('You searched for: rails', /for|rails/)
116
- # # => You searched <mark>for</mark>: <mark>rails</mark>
158
+ # # => "You searched <mark>for</mark>: <mark>rails</mark>"
117
159
  #
118
160
  # highlight('You searched for: ruby, rails, dhh', 'actionpack')
119
- # # => You searched for: ruby, rails, dhh
161
+ # # => "You searched for: ruby, rails, dhh"
120
162
  #
121
163
  # highlight('You searched for: rails', ['for', 'rails'], highlighter: '<em>\1</em>')
122
- # # => You searched <em>for</em>: <em>rails</em>
164
+ # # => "You searched <em>for</em>: <em>rails</em>"
123
165
  #
124
166
  # highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>')
125
- # # => You searched for: <a href="search?q=rails">rails</a>
167
+ # # => "You searched for: <a href=\"search?q=rails\">rails</a>"
126
168
  #
127
169
  # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) }
128
- # # => You searched for: <a href="search?q=rails">rails</a>
170
+ # # => "You searched for: <a href=\"search?q=rails\">rails</a>"
129
171
  #
130
172
  # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false)
131
- # # => <a href="javascript:alert('no!')">ruby</a> on <mark>rails</mark>
132
- def highlight(text, phrases, options = {})
173
+ # # => "<a href=\"javascript:alert('no!')\">ruby</a> on <mark>rails</mark>"
174
+ def highlight(text, phrases, options = {}, &block)
133
175
  text = sanitize(text) if options.fetch(:sanitize, true)
134
176
 
135
177
  if text.blank? || phrases.blank?
136
178
  text || ""
137
179
  else
138
- match = Array(phrases).map do |p|
139
- Regexp === p ? p.to_s : Regexp.escape(p)
140
- end.join("|")
141
-
142
- if block_given?
143
- text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found }
144
- else
145
- highlighter = options.fetch(:highlighter, '<mark>\1</mark>')
146
- text.gsub(/(#{match})(?![^<]*?>)/i, highlighter)
147
- end
180
+ patterns = Array(phrases).map { |phrase| Regexp === phrase ? phrase : Regexp.escape(phrase) }
181
+ pattern = /(#{patterns.join("|")})/i
182
+ highlighter = options.fetch(:highlighter, '<mark>\1</mark>') unless block
183
+
184
+ text.scan(/<[^>]*|[^<]+/).each do |segment|
185
+ if !segment.start_with?("<")
186
+ if block
187
+ segment.gsub!(pattern, &block)
188
+ else
189
+ segment.gsub!(pattern, highlighter)
190
+ end
191
+ end
192
+ end.join
148
193
  end.html_safe
149
194
  end
150
195
 
151
- # Extracts an excerpt from +text+ that matches the first instance of +phrase+.
152
- # The <tt>:radius</tt> option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
153
- # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
154
- # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. Use the
155
- # <tt>:separator</tt> option to choose the delimitation. The resulting string will be stripped in any case. If the +phrase+
156
- # isn't found, +nil+ is returned.
196
+ # Extracts the first occurrence of +phrase+ plus surrounding text from
197
+ # +text+. An omission marker is prepended / appended if the start / end of
198
+ # the result does not coincide with the start / end of +text+. The result
199
+ # is always stripped in any case. Returns +nil+ if +phrase+ isn't found.
200
+ #
201
+ # ==== Options
202
+ #
203
+ # [+:radius+]
204
+ # The number of characters (or tokens — see +:separator+ option) around
205
+ # +phrase+ to include in the result. Defaults to 100.
206
+ #
207
+ # [+:omission+]
208
+ # The marker to prepend / append when the start / end of the excerpt
209
+ # does not coincide with the start / end of +text+. Defaults to
210
+ # <tt>"..."</tt>.
211
+ #
212
+ # [+:separator+]
213
+ # The separator between tokens to count for +:radius+. Defaults to
214
+ # <tt>""</tt>, which treats each character as a token.
215
+ #
216
+ # ==== Examples
157
217
  #
158
218
  # excerpt('This is an example', 'an', radius: 5)
159
- # # => ...s is an exam...
219
+ # # => "...s is an exam..."
160
220
  #
161
221
  # excerpt('This is an example', 'is', radius: 5)
162
- # # => This is a...
222
+ # # => "This is a..."
163
223
  #
164
224
  # excerpt('This is an example', 'is')
165
- # # => This is an example
225
+ # # => "This is an example"
166
226
  #
167
227
  # excerpt('This next thing is an example', 'ex', radius: 2)
168
- # # => ...next...
228
+ # # => "...next..."
169
229
  #
170
230
  # excerpt('This is also an example', 'an', radius: 8, omission: '<chop> ')
171
- # # => <chop> is also an example
231
+ # # => "<chop> is also an example"
172
232
  #
173
233
  # excerpt('This is a very beautiful morning', 'very', separator: ' ', radius: 1)
174
- # # => ...a very beautiful...
234
+ # # => "...a very beautiful..."
175
235
  def excerpt(text, phrase, options = {})
176
236
  return unless text && phrase
177
237
 
@@ -207,26 +267,26 @@ module ActionView
207
267
  # Attempts to pluralize the +singular+ word unless +count+ is 1. If
208
268
  # +plural+ is supplied, it will use that when count is > 1, otherwise
209
269
  # it will use the Inflector to determine the plural form for the given locale,
210
- # which defaults to I18n.locale
270
+ # which defaults to +I18n.locale+.
211
271
  #
212
272
  # The word will be pluralized using rules defined for the locale
213
273
  # (you must define your own inflection rules for languages other than English).
214
274
  # See ActiveSupport::Inflector.pluralize
215
275
  #
216
276
  # pluralize(1, 'person')
217
- # # => 1 person
277
+ # # => "1 person"
218
278
  #
219
279
  # pluralize(2, 'person')
220
- # # => 2 people
280
+ # # => "2 people"
221
281
  #
222
282
  # pluralize(3, 'person', plural: 'users')
223
- # # => 3 users
283
+ # # => "3 users"
224
284
  #
225
285
  # pluralize(0, 'person')
226
- # # => 0 people
286
+ # # => "0 people"
227
287
  #
228
288
  # pluralize(2, 'Person', locale: :de)
229
- # # => 2 Personen
289
+ # # => "2 Personen"
230
290
  def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
231
291
  word = if count == 1 || count.to_s.match?(/^1(\.0+)?$/)
232
292
  singular
@@ -242,29 +302,39 @@ module ActionView
242
302
  # (which is 80 by default).
243
303
  #
244
304
  # word_wrap('Once upon a time')
245
- # # => Once upon a time
305
+ # # => "Once upon a time"
246
306
  #
247
307
  # word_wrap('Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding a successor to the throne turned out to be more trouble than anyone could have imagined...')
248
- # # => Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding\na successor to the throne turned out to be more trouble than anyone could have\nimagined...
308
+ # # => "Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding\na successor to the throne turned out to be more trouble than anyone could have\nimagined..."
249
309
  #
250
310
  # word_wrap('Once upon a time', line_width: 8)
251
- # # => Once\nupon a\ntime
311
+ # # => "Once\nupon a\ntime"
252
312
  #
253
313
  # word_wrap('Once upon a time', line_width: 1)
254
- # # => Once\nupon\na\ntime
314
+ # # => "Once\nupon\na\ntime"
255
315
  #
256
- # You can also specify a custom +break_sequence+ ("\n" by default)
316
+ # You can also specify a custom +break_sequence+ ("\n" by default):
257
317
  #
258
318
  # word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n")
259
- # # => Once\r\nupon\r\na\r\ntime
319
+ # # => "Once\r\nupon\r\na\r\ntime"
260
320
  def word_wrap(text, line_width: 80, break_sequence: "\n")
261
- text.split("\n").collect! do |line|
262
- line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line
263
- end * break_sequence
321
+ return +"" if text.empty?
322
+
323
+ # Match up to `line_width` characters, followed by one of
324
+ # (1) non-newline whitespace plus an optional newline
325
+ # (2) the end of the string, ignoring any trailing newlines
326
+ # (3) a newline
327
+ #
328
+ # -OR-
329
+ #
330
+ # Match an empty line
331
+ pattern = /(.{1,#{line_width}})(?:[^\S\n]+\n?|\n*\Z|\n)|\n/
332
+
333
+ text.gsub(pattern, "\\1#{break_sequence}").chomp!(break_sequence)
264
334
  end
265
335
 
266
336
  # Returns +text+ transformed into HTML using simple formatting rules.
267
- # Two or more consecutive newlines(<tt>\n\n</tt> or <tt>\r\n\r\n</tt>) are
337
+ # Two or more consecutive newlines (<tt>\n\n</tt> or <tt>\r\n\r\n</tt>) are
268
338
  # considered a paragraph and wrapped in <tt><p></tt> tags. One newline
269
339
  # (<tt>\n</tt> or <tt>\r\n</tt>) is considered a linebreak and a
270
340
  # <tt><br /></tt> tag is appended. This method does not remove the
@@ -275,6 +345,7 @@ module ActionView
275
345
  #
276
346
  # ==== Options
277
347
  # * <tt>:sanitize</tt> - If +false+, does not sanitize +text+.
348
+ # * <tt>:sanitize_options</tt> - Any extra options you want appended to the sanitize.
278
349
  # * <tt>:wrapper_tag</tt> - String representing the wrapper tag, defaults to <tt>"p"</tt>
279
350
  #
280
351
  # ==== Examples
@@ -299,10 +370,13 @@ module ActionView
299
370
  #
300
371
  # simple_format("<blink>Blinkable!</blink> It's true.", {}, sanitize: false)
301
372
  # # => "<p><blink>Blinkable!</blink> It's true.</p>"
373
+ #
374
+ # simple_format("<a target=\"_blank\" href=\"http://example.com\">Continue</a>", {}, { sanitize_options: { attributes: %w[target href] } })
375
+ # # => "<p><a target=\"_blank\" href=\"http://example.com\">Continue</a></p>"
302
376
  def simple_format(text, html_options = {}, options = {})
303
- wrapper_tag = options.fetch(:wrapper_tag, :p)
377
+ wrapper_tag = options[:wrapper_tag] || "p"
304
378
 
305
- text = sanitize(text) if options.fetch(:sanitize, true)
379
+ text = sanitize(text, options.fetch(:sanitize_options, {})) if options.fetch(:sanitize, true)
306
380
  paragraphs = split_paragraphs(text)
307
381
 
308
382
  if paragraphs.empty?
@@ -314,7 +388,7 @@ module ActionView
314
388
  end
315
389
  end
316
390
 
317
- # Creates a Cycle object whose _to_s_ method cycles through elements of an
391
+ # Creates a Cycle object whose +to_s+ method cycles through elements of an
318
392
  # array every time it is called. This can be used for example, to alternate
319
393
  # classes for table rows. You can use named cycles to allow nesting in loops.
320
394
  # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a
@@ -323,8 +397,8 @@ module ActionView
323
397
  # and passing the name of the cycle. The current cycle string can be obtained
324
398
  # anytime using the current_cycle method.
325
399
  #
326
- # # Alternate CSS classes for even and odd numbers...
327
- # @items = [1,2,3,4]
400
+ # <%# Alternate CSS classes for even and odd numbers... %>
401
+ # <% @items = [1,2,3,4] %>
328
402
  # <table>
329
403
  # <% @items.each do |item| %>
330
404
  # <tr class="<%= cycle("odd", "even") -%>">
@@ -334,10 +408,12 @@ module ActionView
334
408
  # </table>
335
409
  #
336
410
  #
337
- # # Cycle CSS classes for rows, and text colors for values within each row
338
- # @items = x = [{first: 'Robert', middle: 'Daniel', last: 'James'},
339
- # {first: 'Emily', middle: 'Shannon', maiden: 'Pike', last: 'Hicks'},
340
- # {first: 'June', middle: 'Dae', last: 'Jones'}]
411
+ # <%# Cycle CSS classes for rows, and text colors for values within each row %>
412
+ # <% @items = [
413
+ # { first: "Robert", middle: "Daniel", last: "James" },
414
+ # { first: "Emily", middle: "Shannon", maiden: "Pike", last: "Hicks" },
415
+ # { first: "June", middle: "Dae", last: "Jones" },
416
+ # ] %>
341
417
  # <% @items.each do |item| %>
342
418
  # <tr class="<%= cycle("odd", "even", name: "row_class") -%>">
343
419
  # <td>
@@ -368,8 +444,8 @@ module ActionView
368
444
  # for complex table highlighting or any other design need which requires
369
445
  # the current cycle string in more than one place.
370
446
  #
371
- # # Alternate background colors
372
- # @items = [1,2,3,4]
447
+ # <%# Alternate background colors %>
448
+ # <% @items = [1,2,3,4] %>
373
449
  # <% @items.each do |item| %>
374
450
  # <div style="background-color:<%= cycle("red","white","blue") %>">
375
451
  # <span style="background-color:<%= current_cycle %>"><%= item %></span>
@@ -383,8 +459,8 @@ module ActionView
383
459
  # Resets a cycle so that it starts from the first element the next time
384
460
  # it is called. Pass in +name+ to reset a named cycle.
385
461
  #
386
- # # Alternate CSS classes for even and odd numbers...
387
- # @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]]
462
+ # <%# Alternate CSS classes for even and odd numbers... %>
463
+ # <% @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]] %>
388
464
  # <table>
389
465
  # <% @items.each do |item| %>
390
466
  # <tr class="<%= cycle("even", "odd") -%>">
@@ -403,7 +479,7 @@ module ActionView
403
479
  cycle.reset if cycle
404
480
  end
405
481
 
406
- class Cycle #:nodoc:
482
+ class Cycle # :nodoc:
407
483
  attr_reader :values
408
484
 
409
485
  def initialize(first_value, *values)
@@ -467,18 +543,25 @@ module ActionView
467
543
  radius = options.fetch(:radius, 100)
468
544
  omission = options.fetch(:omission, "...")
469
545
 
470
- part = part.split(separator)
471
- part.delete("")
472
- affix = part.size > radius ? omission : ""
546
+ if separator != ""
547
+ part = part.split(separator)
548
+ part.delete("")
549
+ end
550
+
551
+ affix = part.length > radius ? omission : ""
552
+
553
+ part =
554
+ if part_position == :first
555
+ part.last(radius)
556
+ else
557
+ part.first(radius)
558
+ end
473
559
 
474
- part = if part_position == :first
475
- drop_index = [part.length - radius, 0].max
476
- part.drop(drop_index)
477
- else
478
- part.first(radius)
560
+ if separator != ""
561
+ part = part.join(separator)
479
562
  end
480
563
 
481
- return affix, part.join(separator)
564
+ return affix, part
482
565
  end
483
566
  end
484
567
  end