actionview 6.1.7.2 → 7.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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