omg-actionview 8.0.0.alpha1

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 (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +40 -0
  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 +316 -0
  8. data/lib/action_view/buffers.rb +165 -0
  9. data/lib/action_view/cache_expiry.rb +69 -0
  10. data/lib/action_view/context.rb +32 -0
  11. data/lib/action_view/dependency_tracker/erb_tracker.rb +159 -0
  12. data/lib/action_view/dependency_tracker/ruby_tracker.rb +43 -0
  13. data/lib/action_view/dependency_tracker/wildcard_resolver.rb +32 -0
  14. data/lib/action_view/dependency_tracker.rb +41 -0
  15. data/lib/action_view/deprecator.rb +7 -0
  16. data/lib/action_view/digestor.rb +130 -0
  17. data/lib/action_view/flows.rb +75 -0
  18. data/lib/action_view/gem_version.rb +17 -0
  19. data/lib/action_view/helpers/active_model_helper.rb +54 -0
  20. data/lib/action_view/helpers/asset_tag_helper.rb +680 -0
  21. data/lib/action_view/helpers/asset_url_helper.rb +473 -0
  22. data/lib/action_view/helpers/atom_feed_helper.rb +205 -0
  23. data/lib/action_view/helpers/cache_helper.rb +315 -0
  24. data/lib/action_view/helpers/capture_helper.rb +236 -0
  25. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  26. data/lib/action_view/helpers/controller_helper.rb +42 -0
  27. data/lib/action_view/helpers/csp_helper.rb +26 -0
  28. data/lib/action_view/helpers/csrf_helper.rb +35 -0
  29. data/lib/action_view/helpers/date_helper.rb +1266 -0
  30. data/lib/action_view/helpers/debug_helper.rb +38 -0
  31. data/lib/action_view/helpers/form_helper.rb +2765 -0
  32. data/lib/action_view/helpers/form_options_helper.rb +927 -0
  33. data/lib/action_view/helpers/form_tag_helper.rb +1088 -0
  34. data/lib/action_view/helpers/javascript_helper.rb +96 -0
  35. data/lib/action_view/helpers/number_helper.rb +165 -0
  36. data/lib/action_view/helpers/output_safety_helper.rb +70 -0
  37. data/lib/action_view/helpers/rendering_helper.rb +218 -0
  38. data/lib/action_view/helpers/sanitize_helper.rb +201 -0
  39. data/lib/action_view/helpers/tag_helper.rb +621 -0
  40. data/lib/action_view/helpers/tags/base.rb +138 -0
  41. data/lib/action_view/helpers/tags/check_box.rb +65 -0
  42. data/lib/action_view/helpers/tags/checkable.rb +18 -0
  43. data/lib/action_view/helpers/tags/collection_check_boxes.rb +37 -0
  44. data/lib/action_view/helpers/tags/collection_helpers.rb +118 -0
  45. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +31 -0
  46. data/lib/action_view/helpers/tags/collection_select.rb +33 -0
  47. data/lib/action_view/helpers/tags/color_field.rb +26 -0
  48. data/lib/action_view/helpers/tags/date_field.rb +14 -0
  49. data/lib/action_view/helpers/tags/date_select.rb +75 -0
  50. data/lib/action_view/helpers/tags/datetime_field.rb +39 -0
  51. data/lib/action_view/helpers/tags/datetime_local_field.rb +29 -0
  52. data/lib/action_view/helpers/tags/datetime_select.rb +10 -0
  53. data/lib/action_view/helpers/tags/email_field.rb +10 -0
  54. data/lib/action_view/helpers/tags/file_field.rb +26 -0
  55. data/lib/action_view/helpers/tags/grouped_collection_select.rb +34 -0
  56. data/lib/action_view/helpers/tags/hidden_field.rb +14 -0
  57. data/lib/action_view/helpers/tags/label.rb +84 -0
  58. data/lib/action_view/helpers/tags/month_field.rb +14 -0
  59. data/lib/action_view/helpers/tags/number_field.rb +20 -0
  60. data/lib/action_view/helpers/tags/password_field.rb +14 -0
  61. data/lib/action_view/helpers/tags/placeholderable.rb +24 -0
  62. data/lib/action_view/helpers/tags/radio_button.rb +32 -0
  63. data/lib/action_view/helpers/tags/range_field.rb +10 -0
  64. data/lib/action_view/helpers/tags/search_field.rb +27 -0
  65. data/lib/action_view/helpers/tags/select.rb +45 -0
  66. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  67. data/lib/action_view/helpers/tags/tel_field.rb +10 -0
  68. data/lib/action_view/helpers/tags/text_area.rb +24 -0
  69. data/lib/action_view/helpers/tags/text_field.rb +33 -0
  70. data/lib/action_view/helpers/tags/time_field.rb +23 -0
  71. data/lib/action_view/helpers/tags/time_select.rb +10 -0
  72. data/lib/action_view/helpers/tags/time_zone_select.rb +25 -0
  73. data/lib/action_view/helpers/tags/translator.rb +39 -0
  74. data/lib/action_view/helpers/tags/url_field.rb +10 -0
  75. data/lib/action_view/helpers/tags/week_field.rb +14 -0
  76. data/lib/action_view/helpers/tags/weekday_select.rb +31 -0
  77. data/lib/action_view/helpers/tags.rb +47 -0
  78. data/lib/action_view/helpers/text_helper.rb +568 -0
  79. data/lib/action_view/helpers/translation_helper.rb +161 -0
  80. data/lib/action_view/helpers/url_helper.rb +812 -0
  81. data/lib/action_view/helpers.rb +68 -0
  82. data/lib/action_view/layouts.rb +434 -0
  83. data/lib/action_view/locale/en.yml +56 -0
  84. data/lib/action_view/log_subscriber.rb +132 -0
  85. data/lib/action_view/lookup_context.rb +299 -0
  86. data/lib/action_view/model_naming.rb +14 -0
  87. data/lib/action_view/path_registry.rb +57 -0
  88. data/lib/action_view/path_set.rb +84 -0
  89. data/lib/action_view/railtie.rb +132 -0
  90. data/lib/action_view/record_identifier.rb +118 -0
  91. data/lib/action_view/render_parser/prism_render_parser.rb +139 -0
  92. data/lib/action_view/render_parser/ripper_render_parser.rb +350 -0
  93. data/lib/action_view/render_parser.rb +40 -0
  94. data/lib/action_view/renderer/abstract_renderer.rb +186 -0
  95. data/lib/action_view/renderer/collection_renderer.rb +204 -0
  96. data/lib/action_view/renderer/object_renderer.rb +34 -0
  97. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +120 -0
  98. data/lib/action_view/renderer/partial_renderer.rb +267 -0
  99. data/lib/action_view/renderer/renderer.rb +107 -0
  100. data/lib/action_view/renderer/streaming_template_renderer.rb +107 -0
  101. data/lib/action_view/renderer/template_renderer.rb +115 -0
  102. data/lib/action_view/rendering.rb +190 -0
  103. data/lib/action_view/routing_url_for.rb +149 -0
  104. data/lib/action_view/tasks/cache_digests.rake +25 -0
  105. data/lib/action_view/template/error.rb +264 -0
  106. data/lib/action_view/template/handlers/builder.rb +25 -0
  107. data/lib/action_view/template/handlers/erb/erubi.rb +85 -0
  108. data/lib/action_view/template/handlers/erb.rb +157 -0
  109. data/lib/action_view/template/handlers/html.rb +11 -0
  110. data/lib/action_view/template/handlers/raw.rb +11 -0
  111. data/lib/action_view/template/handlers.rb +66 -0
  112. data/lib/action_view/template/html.rb +33 -0
  113. data/lib/action_view/template/inline.rb +22 -0
  114. data/lib/action_view/template/raw_file.rb +25 -0
  115. data/lib/action_view/template/renderable.rb +30 -0
  116. data/lib/action_view/template/resolver.rb +212 -0
  117. data/lib/action_view/template/sources/file.rb +17 -0
  118. data/lib/action_view/template/sources.rb +13 -0
  119. data/lib/action_view/template/text.rb +32 -0
  120. data/lib/action_view/template/types.rb +50 -0
  121. data/lib/action_view/template.rb +580 -0
  122. data/lib/action_view/template_details.rb +66 -0
  123. data/lib/action_view/template_path.rb +66 -0
  124. data/lib/action_view/test_case.rb +449 -0
  125. data/lib/action_view/testing/resolvers.rb +44 -0
  126. data/lib/action_view/unbound_template.rb +67 -0
  127. data/lib/action_view/version.rb +10 -0
  128. data/lib/action_view/view_paths.rb +117 -0
  129. data/lib/action_view.rb +104 -0
  130. metadata +275 -0
@@ -0,0 +1,568 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/filters"
4
+ require "active_support/core_ext/string/access"
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"
9
+
10
+ module ActionView
11
+ module Helpers # :nodoc:
12
+ # = Action View Text \Helpers
13
+ #
14
+ # The TextHelper module provides a set of methods for filtering, formatting
15
+ # and transforming strings, which can reduce the amount of inline Ruby code in
16
+ # your views. These helper methods extend Action View making them callable
17
+ # within your template files.
18
+ #
19
+ # ==== Sanitization
20
+ #
21
+ # Most text helpers that generate HTML output sanitize the given input by default,
22
+ # but do not escape it. This means HTML tags will appear in the page but all malicious
23
+ # code will be removed. Let's look at some examples using the +simple_format+ method:
24
+ #
25
+ # simple_format('<a href="http://example.com/">Example</a>')
26
+ # # => "<p><a href=\"http://example.com/\">Example</a></p>"
27
+ #
28
+ # simple_format('<a href="javascript:alert(\'no!\')">Example</a>')
29
+ # # => "<p><a>Example</a></p>"
30
+ #
31
+ # If you want to escape all content, you should invoke the +h+ method before
32
+ # calling the text helper.
33
+ #
34
+ # simple_format h('<a href="http://example.com/">Example</a>')
35
+ # # => "<p>&lt;a href=\"http://example.com/\"&gt;Example&lt;/a&gt;</p>"
36
+ module TextHelper
37
+ extend ActiveSupport::Concern
38
+
39
+ include SanitizeHelper
40
+ include TagHelper
41
+ include OutputSafetyHelper
42
+
43
+ # The preferred method of outputting text in your views is to use the
44
+ # <tt><%= "text" %></tt> eRuby syntax. The regular +puts+ and +print+ methods
45
+ # do not operate as expected in an eRuby code block. If you absolutely must
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" %>
50
+ #
51
+ # <%
52
+ # unless signed_in?
53
+ # concat link_to("Sign In", action: :sign_in)
54
+ # end
55
+ # %>
56
+ #
57
+ # is equivalent to
58
+ #
59
+ # <% unless signed_in? %>
60
+ # <%= link_to "Sign In", action: :sign_in %>
61
+ # <% end %>
62
+ #
63
+ def concat(string)
64
+ output_buffer << string
65
+ end
66
+
67
+ def safe_concat(string)
68
+ output_buffer.respond_to?(:safe_concat) ? output_buffer.safe_concat(string) : concat(string)
69
+ end
70
+
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.
89
+ #
90
+ # [+:omission+]
91
+ # The string to append after truncating. Defaults to <tt>"..."</tt>.
92
+ #
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+.
96
+ #
97
+ # [+:escape+]
98
+ # Whether to escape the result. Defaults to true.
99
+ #
100
+ # ==== Examples
101
+ #
102
+ # truncate("Once upon a time in a world far far away")
103
+ # # => "Once upon a time in a world..."
104
+ #
105
+ # truncate("Once upon a time in a world far far away", length: 17)
106
+ # # => "Once upon a ti..."
107
+ #
108
+ # truncate("Once upon a time in a world far far away", length: 17, separator: ' ')
109
+ # # => "Once upon a..."
110
+ #
111
+ # truncate("And they found that many people were sleeping better.", length: 25, omission: '... (continued)')
112
+ # # => "And they f... (continued)"
113
+ #
114
+ # truncate("<p>Once upon a time in a world far far away</p>")
115
+ # # => "&lt;p&gt;Once upon a time in a wo..."
116
+ #
117
+ # truncate("<p>Once upon a time in a world far far away</p>", escape: false)
118
+ # # => "<p>Once upon a time in a wo..."
119
+ #
120
+ # truncate("Once upon a time in a world far far away") { link_to "Continue", "#" }
121
+ # # => "Once upon a time in a world...<a href=\"#\">Continue</a>"
122
+ def truncate(text, options = {}, &block)
123
+ if text
124
+ length = options.fetch(:length, 30)
125
+
126
+ content = text.truncate(length, options)
127
+ content = options[:escape] == false ? content.html_safe : ERB::Util.html_escape(content)
128
+ content << capture(&block) if block_given? && text.length > length
129
+ content
130
+ end
131
+ end
132
+
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
153
+ #
154
+ # highlight('You searched for: rails', 'rails')
155
+ # # => "You searched for: <mark>rails</mark>"
156
+ #
157
+ # highlight('You searched for: rails', /for|rails/)
158
+ # # => "You searched <mark>for</mark>: <mark>rails</mark>"
159
+ #
160
+ # highlight('You searched for: ruby, rails, dhh', 'actionpack')
161
+ # # => "You searched for: ruby, rails, dhh"
162
+ #
163
+ # highlight('You searched for: rails', ['for', 'rails'], highlighter: '<em>\1</em>')
164
+ # # => "You searched <em>for</em>: <em>rails</em>"
165
+ #
166
+ # highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>')
167
+ # # => "You searched for: <a href=\"search?q=rails\">rails</a>"
168
+ #
169
+ # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) }
170
+ # # => "You searched for: <a href=\"search?q=rails\">rails</a>"
171
+ #
172
+ # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false)
173
+ # # => "<a href=\"javascript:alert('no!')\">ruby</a> on <mark>rails</mark>"
174
+ def highlight(text, phrases, options = {}, &block)
175
+ text = sanitize(text) if options.fetch(:sanitize, true)
176
+
177
+ if text.blank? || phrases.blank?
178
+ text || ""
179
+ else
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
193
+ end.html_safe
194
+ end
195
+
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
217
+ #
218
+ # excerpt('This is an example', 'an', radius: 5)
219
+ # # => "...s is an exam..."
220
+ #
221
+ # excerpt('This is an example', 'is', radius: 5)
222
+ # # => "This is a..."
223
+ #
224
+ # excerpt('This is an example', 'is')
225
+ # # => "This is an example"
226
+ #
227
+ # excerpt('This next thing is an example', 'ex', radius: 2)
228
+ # # => "...next..."
229
+ #
230
+ # excerpt('This is also an example', 'an', radius: 8, omission: '<chop> ')
231
+ # # => "<chop> is also an example"
232
+ #
233
+ # excerpt('This is a very beautiful morning', 'very', separator: ' ', radius: 1)
234
+ # # => "...a very beautiful..."
235
+ def excerpt(text, phrase, options = {})
236
+ return unless text && phrase
237
+
238
+ separator = options.fetch(:separator, nil) || ""
239
+ case phrase
240
+ when Regexp
241
+ regex = phrase
242
+ else
243
+ regex = /#{Regexp.escape(phrase)}/i
244
+ end
245
+
246
+ return unless matches = text.match(regex)
247
+ phrase = matches[0]
248
+
249
+ unless separator.empty?
250
+ text.split(separator).each do |value|
251
+ if value.match?(regex)
252
+ phrase = value
253
+ break
254
+ end
255
+ end
256
+ end
257
+
258
+ first_part, second_part = text.split(phrase, 2)
259
+
260
+ prefix, first_part = cut_excerpt_part(:first, first_part, separator, options)
261
+ postfix, second_part = cut_excerpt_part(:second, second_part, separator, options)
262
+
263
+ affix = [first_part, separator, phrase, separator, second_part].join.strip
264
+ [prefix, affix, postfix].join
265
+ end
266
+
267
+ # Attempts to pluralize the +singular+ word unless +count+ is 1. If
268
+ # +plural+ is supplied, it will use that when count is > 1, otherwise
269
+ # it will use the Inflector to determine the plural form for the given locale,
270
+ # which defaults to +I18n.locale+.
271
+ #
272
+ # The word will be pluralized using rules defined for the locale
273
+ # (you must define your own inflection rules for languages other than English).
274
+ # See ActiveSupport::Inflector.pluralize
275
+ #
276
+ # pluralize(1, 'person')
277
+ # # => "1 person"
278
+ #
279
+ # pluralize(2, 'person')
280
+ # # => "2 people"
281
+ #
282
+ # pluralize(3, 'person', plural: 'users')
283
+ # # => "3 users"
284
+ #
285
+ # pluralize(0, 'person')
286
+ # # => "0 people"
287
+ #
288
+ # pluralize(2, 'Person', locale: :de)
289
+ # # => "2 Personen"
290
+ def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
291
+ word = if count == 1 || count.to_s.match?(/^1(\.0+)?$/)
292
+ singular
293
+ else
294
+ plural || singular.pluralize(locale)
295
+ end
296
+
297
+ "#{count || 0} #{word}"
298
+ end
299
+
300
+ # Wraps the +text+ into lines no longer than +line_width+ width. This method
301
+ # breaks on the first whitespace character that does not exceed +line_width+
302
+ # (which is 80 by default).
303
+ #
304
+ # word_wrap('Once upon a time')
305
+ # # => "Once upon a time"
306
+ #
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...')
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..."
309
+ #
310
+ # word_wrap('Once upon a time', line_width: 8)
311
+ # # => "Once\nupon a\ntime"
312
+ #
313
+ # word_wrap('Once upon a time', line_width: 1)
314
+ # # => "Once\nupon\na\ntime"
315
+ #
316
+ # You can also specify a custom +break_sequence+ ("\n" by default):
317
+ #
318
+ # word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n")
319
+ # # => "Once\r\nupon\r\na\r\ntime"
320
+ def word_wrap(text, line_width: 80, break_sequence: "\n")
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)
334
+ end
335
+
336
+ # Returns +text+ transformed into HTML using simple formatting rules.
337
+ # Two or more consecutive newlines (<tt>\n\n</tt> or <tt>\r\n\r\n</tt>) are
338
+ # considered a paragraph and wrapped in <tt><p></tt> tags. One newline
339
+ # (<tt>\n</tt> or <tt>\r\n</tt>) is considered a linebreak and a
340
+ # <tt><br /></tt> tag is appended. This method does not remove the
341
+ # newlines from the +text+.
342
+ #
343
+ # You can pass any HTML attributes into <tt>html_options</tt>. These
344
+ # will be added to all created paragraphs.
345
+ #
346
+ # ==== Options
347
+ # * <tt>:sanitize</tt> - If +false+, does not sanitize +text+.
348
+ # * <tt>:sanitize_options</tt> - Any extra options you want appended to the sanitize.
349
+ # * <tt>:wrapper_tag</tt> - String representing the wrapper tag, defaults to <tt>"p"</tt>
350
+ #
351
+ # ==== Examples
352
+ # my_text = "Here is some basic text...\n...with a line break."
353
+ #
354
+ # simple_format(my_text)
355
+ # # => "<p>Here is some basic text...\n<br />...with a line break.</p>"
356
+ #
357
+ # simple_format(my_text, {}, wrapper_tag: "div")
358
+ # # => "<div>Here is some basic text...\n<br />...with a line break.</div>"
359
+ #
360
+ # more_text = "We want to put a paragraph...\n\n...right there."
361
+ #
362
+ # simple_format(more_text)
363
+ # # => "<p>We want to put a paragraph...</p>\n\n<p>...right there.</p>"
364
+ #
365
+ # simple_format("Look ma! A class!", class: 'description')
366
+ # # => "<p class='description'>Look ma! A class!</p>"
367
+ #
368
+ # simple_format("<blink>Unblinkable.</blink>")
369
+ # # => "<p>Unblinkable.</p>"
370
+ #
371
+ # simple_format("<blink>Blinkable!</blink> It's true.", {}, sanitize: false)
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>"
376
+ def simple_format(text, html_options = {}, options = {})
377
+ wrapper_tag = options[:wrapper_tag] || "p"
378
+
379
+ text = sanitize(text, options.fetch(:sanitize_options, {})) if options.fetch(:sanitize, true)
380
+ paragraphs = split_paragraphs(text)
381
+
382
+ if paragraphs.empty?
383
+ content_tag(wrapper_tag, nil, html_options)
384
+ else
385
+ paragraphs.map! { |paragraph|
386
+ content_tag(wrapper_tag, raw(paragraph), html_options)
387
+ }.join("\n\n").html_safe
388
+ end
389
+ end
390
+
391
+ # Creates a Cycle object whose +to_s+ method cycles through elements of an
392
+ # array every time it is called. This can be used for example, to alternate
393
+ # classes for table rows. You can use named cycles to allow nesting in loops.
394
+ # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a
395
+ # named cycle. The default name for a cycle without a +:name+ key is
396
+ # <tt>"default"</tt>. You can manually reset a cycle by calling reset_cycle
397
+ # and passing the name of the cycle. The current cycle string can be obtained
398
+ # anytime using the current_cycle method.
399
+ #
400
+ # <%# Alternate CSS classes for even and odd numbers... %>
401
+ # <% @items = [1,2,3,4] %>
402
+ # <table>
403
+ # <% @items.each do |item| %>
404
+ # <tr class="<%= cycle("odd", "even") -%>">
405
+ # <td><%= item %></td>
406
+ # </tr>
407
+ # <% end %>
408
+ # </table>
409
+ #
410
+ #
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
+ # ] %>
417
+ # <% @items.each do |item| %>
418
+ # <tr class="<%= cycle("odd", "even", name: "row_class") -%>">
419
+ # <td>
420
+ # <% item.values.each do |value| %>
421
+ # <%# Create a named cycle "colors" %>
422
+ # <span style="color:<%= cycle("red", "green", "blue", name: "colors") -%>">
423
+ # <%= value %>
424
+ # </span>
425
+ # <% end %>
426
+ # <% reset_cycle("colors") %>
427
+ # </td>
428
+ # </tr>
429
+ # <% end %>
430
+ def cycle(first_value, *values)
431
+ options = values.extract_options!
432
+ name = options.fetch(:name, "default")
433
+
434
+ values.unshift(*first_value)
435
+
436
+ cycle = get_cycle(name)
437
+ unless cycle && cycle.values == values
438
+ cycle = set_cycle(name, Cycle.new(*values))
439
+ end
440
+ cycle.to_s
441
+ end
442
+
443
+ # Returns the current cycle string after a cycle has been started. Useful
444
+ # for complex table highlighting or any other design need which requires
445
+ # the current cycle string in more than one place.
446
+ #
447
+ # <%# Alternate background colors %>
448
+ # <% @items = [1,2,3,4] %>
449
+ # <% @items.each do |item| %>
450
+ # <div style="background-color:<%= cycle("red","white","blue") %>">
451
+ # <span style="background-color:<%= current_cycle %>"><%= item %></span>
452
+ # </div>
453
+ # <% end %>
454
+ def current_cycle(name = "default")
455
+ cycle = get_cycle(name)
456
+ cycle.current_value if cycle
457
+ end
458
+
459
+ # Resets a cycle so that it starts from the first element the next time
460
+ # it is called. Pass in +name+ to reset a named cycle.
461
+ #
462
+ # <%# Alternate CSS classes for even and odd numbers... %>
463
+ # <% @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]] %>
464
+ # <table>
465
+ # <% @items.each do |item| %>
466
+ # <tr class="<%= cycle("even", "odd") -%>">
467
+ # <% item.each do |value| %>
468
+ # <span style="color:<%= cycle("#333", "#666", "#999", name: "colors") -%>">
469
+ # <%= value %>
470
+ # </span>
471
+ # <% end %>
472
+ #
473
+ # <% reset_cycle("colors") %>
474
+ # </tr>
475
+ # <% end %>
476
+ # </table>
477
+ def reset_cycle(name = "default")
478
+ cycle = get_cycle(name)
479
+ cycle.reset if cycle
480
+ end
481
+
482
+ class Cycle # :nodoc:
483
+ attr_reader :values
484
+
485
+ def initialize(first_value, *values)
486
+ @values = values.unshift(first_value)
487
+ reset
488
+ end
489
+
490
+ def reset
491
+ @index = 0
492
+ end
493
+
494
+ def current_value
495
+ @values[previous_index].to_s
496
+ end
497
+
498
+ def to_s
499
+ value = @values[@index].to_s
500
+ @index = next_index
501
+ value
502
+ end
503
+
504
+ private
505
+ def next_index
506
+ step_index(1)
507
+ end
508
+
509
+ def previous_index
510
+ step_index(-1)
511
+ end
512
+
513
+ def step_index(n)
514
+ (@index + n) % @values.size
515
+ end
516
+ end
517
+
518
+ private
519
+ # The cycle helpers need to store the cycles in a place that is
520
+ # guaranteed to be reset every time a page is rendered, so it
521
+ # uses an instance variable of ActionView::Base.
522
+ def get_cycle(name)
523
+ @_cycles = Hash.new unless defined?(@_cycles)
524
+ @_cycles[name]
525
+ end
526
+
527
+ def set_cycle(name, cycle_object)
528
+ @_cycles = Hash.new unless defined?(@_cycles)
529
+ @_cycles[name] = cycle_object
530
+ end
531
+
532
+ def split_paragraphs(text)
533
+ return [] if text.blank?
534
+
535
+ text.to_str.gsub(/\r\n?/, "\n").split(/\n\n+/).map! do |t|
536
+ t.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') || t
537
+ end
538
+ end
539
+
540
+ def cut_excerpt_part(part_position, part, separator, options)
541
+ return "", "" unless part
542
+
543
+ radius = options.fetch(:radius, 100)
544
+ omission = options.fetch(:omission, "...")
545
+
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
559
+
560
+ if separator != ""
561
+ part = part.join(separator)
562
+ end
563
+
564
+ return affix, part
565
+ end
566
+ end
567
+ end
568
+ end