actionview 6.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionview might be problematic. Click here for more details.

Files changed (113) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +271 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +40 -0
  5. data/lib/action_view.rb +98 -0
  6. data/lib/action_view/base.rb +312 -0
  7. data/lib/action_view/buffers.rb +67 -0
  8. data/lib/action_view/cache_expiry.rb +54 -0
  9. data/lib/action_view/context.rb +32 -0
  10. data/lib/action_view/dependency_tracker.rb +175 -0
  11. data/lib/action_view/digestor.rb +126 -0
  12. data/lib/action_view/flows.rb +76 -0
  13. data/lib/action_view/gem_version.rb +17 -0
  14. data/lib/action_view/helpers.rb +66 -0
  15. data/lib/action_view/helpers/active_model_helper.rb +55 -0
  16. data/lib/action_view/helpers/asset_tag_helper.rb +488 -0
  17. data/lib/action_view/helpers/asset_url_helper.rb +470 -0
  18. data/lib/action_view/helpers/atom_feed_helper.rb +205 -0
  19. data/lib/action_view/helpers/cache_helper.rb +271 -0
  20. data/lib/action_view/helpers/capture_helper.rb +216 -0
  21. data/lib/action_view/helpers/controller_helper.rb +36 -0
  22. data/lib/action_view/helpers/csp_helper.rb +26 -0
  23. data/lib/action_view/helpers/csrf_helper.rb +35 -0
  24. data/lib/action_view/helpers/date_helper.rb +1200 -0
  25. data/lib/action_view/helpers/debug_helper.rb +36 -0
  26. data/lib/action_view/helpers/form_helper.rb +2569 -0
  27. data/lib/action_view/helpers/form_options_helper.rb +896 -0
  28. data/lib/action_view/helpers/form_tag_helper.rb +920 -0
  29. data/lib/action_view/helpers/javascript_helper.rb +95 -0
  30. data/lib/action_view/helpers/number_helper.rb +456 -0
  31. data/lib/action_view/helpers/output_safety_helper.rb +70 -0
  32. data/lib/action_view/helpers/rendering_helper.rb +101 -0
  33. data/lib/action_view/helpers/sanitize_helper.rb +171 -0
  34. data/lib/action_view/helpers/tag_helper.rb +314 -0
  35. data/lib/action_view/helpers/tags.rb +44 -0
  36. data/lib/action_view/helpers/tags/base.rb +196 -0
  37. data/lib/action_view/helpers/tags/check_box.rb +66 -0
  38. data/lib/action_view/helpers/tags/checkable.rb +18 -0
  39. data/lib/action_view/helpers/tags/collection_check_boxes.rb +36 -0
  40. data/lib/action_view/helpers/tags/collection_helpers.rb +119 -0
  41. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +31 -0
  42. data/lib/action_view/helpers/tags/collection_select.rb +30 -0
  43. data/lib/action_view/helpers/tags/color_field.rb +27 -0
  44. data/lib/action_view/helpers/tags/date_field.rb +15 -0
  45. data/lib/action_view/helpers/tags/date_select.rb +74 -0
  46. data/lib/action_view/helpers/tags/datetime_field.rb +32 -0
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +21 -0
  48. data/lib/action_view/helpers/tags/datetime_select.rb +10 -0
  49. data/lib/action_view/helpers/tags/email_field.rb +10 -0
  50. data/lib/action_view/helpers/tags/file_field.rb +10 -0
  51. data/lib/action_view/helpers/tags/grouped_collection_select.rb +31 -0
  52. data/lib/action_view/helpers/tags/hidden_field.rb +10 -0
  53. data/lib/action_view/helpers/tags/label.rb +81 -0
  54. data/lib/action_view/helpers/tags/month_field.rb +15 -0
  55. data/lib/action_view/helpers/tags/number_field.rb +20 -0
  56. data/lib/action_view/helpers/tags/password_field.rb +14 -0
  57. data/lib/action_view/helpers/tags/placeholderable.rb +24 -0
  58. data/lib/action_view/helpers/tags/radio_button.rb +33 -0
  59. data/lib/action_view/helpers/tags/range_field.rb +10 -0
  60. data/lib/action_view/helpers/tags/search_field.rb +27 -0
  61. data/lib/action_view/helpers/tags/select.rb +43 -0
  62. data/lib/action_view/helpers/tags/tel_field.rb +10 -0
  63. data/lib/action_view/helpers/tags/text_area.rb +24 -0
  64. data/lib/action_view/helpers/tags/text_field.rb +34 -0
  65. data/lib/action_view/helpers/tags/time_field.rb +15 -0
  66. data/lib/action_view/helpers/tags/time_select.rb +10 -0
  67. data/lib/action_view/helpers/tags/time_zone_select.rb +22 -0
  68. data/lib/action_view/helpers/tags/translator.rb +39 -0
  69. data/lib/action_view/helpers/tags/url_field.rb +10 -0
  70. data/lib/action_view/helpers/tags/week_field.rb +15 -0
  71. data/lib/action_view/helpers/text_helper.rb +486 -0
  72. data/lib/action_view/helpers/translation_helper.rb +145 -0
  73. data/lib/action_view/helpers/url_helper.rb +676 -0
  74. data/lib/action_view/layouts.rb +433 -0
  75. data/lib/action_view/locale/en.yml +56 -0
  76. data/lib/action_view/log_subscriber.rb +96 -0
  77. data/lib/action_view/lookup_context.rb +316 -0
  78. data/lib/action_view/model_naming.rb +14 -0
  79. data/lib/action_view/path_set.rb +95 -0
  80. data/lib/action_view/railtie.rb +105 -0
  81. data/lib/action_view/record_identifier.rb +112 -0
  82. data/lib/action_view/renderer/abstract_renderer.rb +108 -0
  83. data/lib/action_view/renderer/partial_renderer.rb +563 -0
  84. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +103 -0
  85. data/lib/action_view/renderer/renderer.rb +68 -0
  86. data/lib/action_view/renderer/streaming_template_renderer.rb +105 -0
  87. data/lib/action_view/renderer/template_renderer.rb +108 -0
  88. data/lib/action_view/rendering.rb +171 -0
  89. data/lib/action_view/routing_url_for.rb +146 -0
  90. data/lib/action_view/tasks/cache_digests.rake +25 -0
  91. data/lib/action_view/template.rb +393 -0
  92. data/lib/action_view/template/error.rb +161 -0
  93. data/lib/action_view/template/handlers.rb +92 -0
  94. data/lib/action_view/template/handlers/builder.rb +25 -0
  95. data/lib/action_view/template/handlers/erb.rb +84 -0
  96. data/lib/action_view/template/handlers/erb/erubi.rb +87 -0
  97. data/lib/action_view/template/handlers/html.rb +11 -0
  98. data/lib/action_view/template/handlers/raw.rb +11 -0
  99. data/lib/action_view/template/html.rb +43 -0
  100. data/lib/action_view/template/inline.rb +22 -0
  101. data/lib/action_view/template/raw_file.rb +28 -0
  102. data/lib/action_view/template/resolver.rb +394 -0
  103. data/lib/action_view/template/sources.rb +13 -0
  104. data/lib/action_view/template/sources/file.rb +17 -0
  105. data/lib/action_view/template/text.rb +35 -0
  106. data/lib/action_view/template/types.rb +57 -0
  107. data/lib/action_view/test_case.rb +300 -0
  108. data/lib/action_view/testing/resolvers.rb +67 -0
  109. data/lib/action_view/unbound_template.rb +32 -0
  110. data/lib/action_view/version.rb +10 -0
  111. data/lib/action_view/view_paths.rb +129 -0
  112. data/lib/assets/compiled/rails-ujs.js +746 -0
  113. metadata +260 -0
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ module Placeholderable # :nodoc:
7
+ def initialize(*)
8
+ super
9
+
10
+ if tag_value = @options[:placeholder]
11
+ placeholder = tag_value if tag_value.is_a?(String)
12
+ method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}"
13
+
14
+ placeholder ||= Tags::Translator
15
+ .new(object, @object_name, method_and_value, scope: "helpers.placeholder")
16
+ .translate
17
+ placeholder ||= @method_name.humanize
18
+ @options[:placeholder] = placeholder
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/helpers/tags/checkable"
4
+
5
+ module ActionView
6
+ module Helpers
7
+ module Tags # :nodoc:
8
+ class RadioButton < Base # :nodoc:
9
+ include Checkable
10
+
11
+ def initialize(object_name, method_name, template_object, tag_value, options)
12
+ @tag_value = tag_value
13
+ super(object_name, method_name, template_object, options)
14
+ end
15
+
16
+ def render
17
+ options = @options.stringify_keys
18
+ options["type"] = "radio"
19
+ options["value"] = @tag_value
20
+ options["checked"] = "checked" if input_checked?(options)
21
+ add_default_name_and_id_for_value(@tag_value, options)
22
+ tag("input", options)
23
+ end
24
+
25
+ private
26
+
27
+ def checked?(value)
28
+ value.to_s == @tag_value.to_s
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ class RangeField < NumberField # :nodoc:
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ class SearchField < TextField # :nodoc:
7
+ def render
8
+ options = @options.stringify_keys
9
+
10
+ if options["autosave"]
11
+ if options["autosave"] == true
12
+ options["autosave"] = request.host.split(".").reverse.join(".")
13
+ end
14
+ options["results"] ||= 10
15
+ end
16
+
17
+ if options["onsearch"]
18
+ options["incremental"] = true unless options.has_key?("incremental")
19
+ end
20
+
21
+ @options = options
22
+ super
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ class Select < Base # :nodoc:
7
+ def initialize(object_name, method_name, template_object, choices, options, html_options)
8
+ @choices = block_given? ? template_object.capture { yield || "" } : choices
9
+ @choices = @choices.to_a if @choices.is_a?(Range)
10
+
11
+ @html_options = html_options
12
+
13
+ super(object_name, method_name, template_object, options)
14
+ end
15
+
16
+ def render
17
+ option_tags_options = {
18
+ selected: @options.fetch(:selected) { value },
19
+ disabled: @options[:disabled]
20
+ }
21
+
22
+ option_tags = if grouped_choices?
23
+ grouped_options_for_select(@choices, option_tags_options)
24
+ else
25
+ options_for_select(@choices, option_tags_options)
26
+ end
27
+
28
+ select_content_tag(option_tags, @options, @html_options)
29
+ end
30
+
31
+ private
32
+
33
+ # Grouped choices look like this:
34
+ #
35
+ # [nil, []]
36
+ # { nil => [] }
37
+ def grouped_choices?
38
+ !@choices.blank? && @choices.first.respond_to?(:last) && Array === @choices.first.last
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ class TelField < TextField # :nodoc:
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/helpers/tags/placeholderable"
4
+
5
+ module ActionView
6
+ module Helpers
7
+ module Tags # :nodoc:
8
+ class TextArea < Base # :nodoc:
9
+ include Placeholderable
10
+
11
+ def render
12
+ options = @options.stringify_keys
13
+ add_default_name_and_id(options)
14
+
15
+ if size = options.delete("size")
16
+ options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
17
+ end
18
+
19
+ content_tag("textarea", options.delete("value") { value_before_type_cast }, options)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/helpers/tags/placeholderable"
4
+
5
+ module ActionView
6
+ module Helpers
7
+ module Tags # :nodoc:
8
+ class TextField < Base # :nodoc:
9
+ include Placeholderable
10
+
11
+ def render
12
+ options = @options.stringify_keys
13
+ options["size"] = options["maxlength"] unless options.key?("size")
14
+ options["type"] ||= field_type
15
+ options["value"] = options.fetch("value") { value_before_type_cast } unless field_type == "file"
16
+ add_default_name_and_id(options)
17
+ tag("input", options)
18
+ end
19
+
20
+ class << self
21
+ def field_type
22
+ @field_type ||= name.split("::").last.sub("Field", "").downcase
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def field_type
29
+ self.class.field_type
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ class TimeField < DatetimeField # :nodoc:
7
+ private
8
+
9
+ def format_date(value)
10
+ value.try(:strftime, "%T.%L")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ class TimeSelect < DateSelect # :nodoc:
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ class TimeZoneSelect < Base # :nodoc:
7
+ def initialize(object_name, method_name, template_object, priority_zones, options, html_options)
8
+ @priority_zones = priority_zones
9
+ @html_options = html_options
10
+
11
+ super(object_name, method_name, template_object, options)
12
+ end
13
+
14
+ def render
15
+ select_content_tag(
16
+ time_zone_options_for_select(value || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ class Translator # :nodoc:
7
+ def initialize(object, object_name, method_and_value, scope:)
8
+ @object_name = object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1')
9
+ @method_and_value = method_and_value
10
+ @scope = scope
11
+ @model = object.respond_to?(:to_model) ? object.to_model : nil
12
+ end
13
+
14
+ def translate
15
+ translated_attribute = I18n.t("#{object_name}.#{method_and_value}", default: i18n_default, scope: scope).presence
16
+ translated_attribute || human_attribute_name
17
+ end
18
+
19
+ private
20
+ attr_reader :object_name, :method_and_value, :scope, :model
21
+
22
+ def i18n_default
23
+ if model
24
+ key = model.model_name.i18n_key
25
+ ["#{key}.#{method_and_value}".to_sym, ""]
26
+ else
27
+ ""
28
+ end
29
+ end
30
+
31
+ def human_attribute_name
32
+ if model && model.class.respond_to?(:human_attribute_name)
33
+ model.class.human_attribute_name(method_and_value)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ class UrlField < TextField # :nodoc:
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module Helpers
5
+ module Tags # :nodoc:
6
+ class WeekField < DatetimeField # :nodoc:
7
+ private
8
+
9
+ def format_date(value)
10
+ value.try(:strftime, "%Y-W%V")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,486 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/filters"
4
+ require "active_support/core_ext/array/extract_options"
5
+
6
+ module ActionView
7
+ # = Action View Text Helpers
8
+ module Helpers #:nodoc:
9
+ # The TextHelper module provides a set of methods for filtering, formatting
10
+ # and transforming strings, which can reduce the amount of inline Ruby code in
11
+ # your views. These helper methods extend Action View making them callable
12
+ # within your template files.
13
+ #
14
+ # ==== Sanitization
15
+ #
16
+ # Most text helpers that generate HTML output sanitize the given input by default,
17
+ # but do not escape it. This means HTML tags will appear in the page but all malicious
18
+ # code will be removed. Let's look at some examples using the +simple_format+ method:
19
+ #
20
+ # simple_format('<a href="http://example.com/">Example</a>')
21
+ # # => "<p><a href=\"http://example.com/\">Example</a></p>"
22
+ #
23
+ # simple_format('<a href="javascript:alert(\'no!\')">Example</a>')
24
+ # # => "<p><a>Example</a></p>"
25
+ #
26
+ # If you want to escape all content, you should invoke the +h+ method before
27
+ # calling the text helper.
28
+ #
29
+ # simple_format h('<a href="http://example.com/">Example</a>')
30
+ # # => "<p>&lt;a href=\"http://example.com/\"&gt;Example&lt;/a&gt;</p>"
31
+ module TextHelper
32
+ extend ActiveSupport::Concern
33
+
34
+ include SanitizeHelper
35
+ include TagHelper
36
+ include OutputSafetyHelper
37
+
38
+ # The preferred method of outputting text in your views is to use the
39
+ # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
40
+ # 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.
42
+ #
43
+ # <%
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
53
+ # %>
54
+ def concat(string)
55
+ output_buffer << string
56
+ end
57
+
58
+ def safe_concat(string)
59
+ output_buffer.respond_to?(:safe_concat) ? output_buffer.safe_concat(string) : concat(string)
60
+ end
61
+
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>.
65
+ #
66
+ # Pass a <tt>:separator</tt> to truncate +text+ at a natural break.
67
+ #
68
+ # Pass a block if you want to show extra content when the text is truncated.
69
+ #
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).
73
+ #
74
+ # truncate("Once upon a time in a world far far away")
75
+ # # => "Once upon a time in a world..."
76
+ #
77
+ # truncate("Once upon a time in a world far far away", length: 17)
78
+ # # => "Once upon a ti..."
79
+ #
80
+ # truncate("Once upon a time in a world far far away", length: 17, separator: ' ')
81
+ # # => "Once upon a..."
82
+ #
83
+ # truncate("And they found that many people were sleeping better.", length: 25, omission: '... (continued)')
84
+ # # => "And they f... (continued)"
85
+ #
86
+ # truncate("<p>Once upon a time in a world far far away</p>")
87
+ # # => "&lt;p&gt;Once upon a time in a wo..."
88
+ #
89
+ # truncate("<p>Once upon a time in a world far far away</p>", escape: false)
90
+ # # => "<p>Once upon a time in a wo..."
91
+ #
92
+ # 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>"
94
+ def truncate(text, options = {}, &block)
95
+ if text
96
+ length = options.fetch(:length, 30)
97
+
98
+ content = text.truncate(length, options)
99
+ content = options[:escape] == false ? content.html_safe : ERB::Util.html_escape(content)
100
+ content << capture(&block) if block_given? && text.length > length
101
+ content
102
+ end
103
+ end
104
+
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
+ # '<mark>\1</mark>') 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.
111
+ #
112
+ # highlight('You searched for: rails', 'rails')
113
+ # # => You searched for: <mark>rails</mark>
114
+ #
115
+ # highlight('You searched for: rails', /for|rails/)
116
+ # # => You searched <mark>for</mark>: <mark>rails</mark>
117
+ #
118
+ # highlight('You searched for: ruby, rails, dhh', 'actionpack')
119
+ # # => You searched for: ruby, rails, dhh
120
+ #
121
+ # highlight('You searched for: rails', ['for', 'rails'], highlighter: '<em>\1</em>')
122
+ # # => You searched <em>for</em>: <em>rails</em>
123
+ #
124
+ # 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>
126
+ #
127
+ # 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>
129
+ #
130
+ # 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 = {})
133
+ text = sanitize(text) if options.fetch(:sanitize, true)
134
+
135
+ if text.blank? || phrases.blank?
136
+ text || ""
137
+ 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
148
+ end.html_safe
149
+ end
150
+
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.
157
+ #
158
+ # excerpt('This is an example', 'an', radius: 5)
159
+ # # => ...s is an exam...
160
+ #
161
+ # excerpt('This is an example', 'is', radius: 5)
162
+ # # => This is a...
163
+ #
164
+ # excerpt('This is an example', 'is')
165
+ # # => This is an example
166
+ #
167
+ # excerpt('This next thing is an example', 'ex', radius: 2)
168
+ # # => ...next...
169
+ #
170
+ # excerpt('This is also an example', 'an', radius: 8, omission: '<chop> ')
171
+ # # => <chop> is also an example
172
+ #
173
+ # excerpt('This is a very beautiful morning', 'very', separator: ' ', radius: 1)
174
+ # # => ...a very beautiful...
175
+ def excerpt(text, phrase, options = {})
176
+ return unless text && phrase
177
+
178
+ separator = options.fetch(:separator, nil) || ""
179
+ case phrase
180
+ when Regexp
181
+ regex = phrase
182
+ else
183
+ regex = /#{Regexp.escape(phrase)}/i
184
+ end
185
+
186
+ return unless matches = text.match(regex)
187
+ phrase = matches[0]
188
+
189
+ unless separator.empty?
190
+ text.split(separator).each do |value|
191
+ if value.match?(regex)
192
+ phrase = value
193
+ break
194
+ end
195
+ end
196
+ end
197
+
198
+ first_part, second_part = text.split(phrase, 2)
199
+
200
+ prefix, first_part = cut_excerpt_part(:first, first_part, separator, options)
201
+ postfix, second_part = cut_excerpt_part(:second, second_part, separator, options)
202
+
203
+ affix = [first_part, separator, phrase, separator, second_part].join.strip
204
+ [prefix, affix, postfix].join
205
+ end
206
+
207
+ # Attempts to pluralize the +singular+ word unless +count+ is 1. If
208
+ # +plural+ is supplied, it will use that when count is > 1, otherwise
209
+ # it will use the Inflector to determine the plural form for the given locale,
210
+ # which defaults to I18n.locale
211
+ #
212
+ # The word will be pluralized using rules defined for the locale
213
+ # (you must define your own inflection rules for languages other than English).
214
+ # See ActiveSupport::Inflector.pluralize
215
+ #
216
+ # pluralize(1, 'person')
217
+ # # => 1 person
218
+ #
219
+ # pluralize(2, 'person')
220
+ # # => 2 people
221
+ #
222
+ # pluralize(3, 'person', plural: 'users')
223
+ # # => 3 users
224
+ #
225
+ # pluralize(0, 'person')
226
+ # # => 0 people
227
+ #
228
+ # pluralize(2, 'Person', locale: :de)
229
+ # # => 2 Personen
230
+ def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
231
+ word = if count == 1 || count.to_s =~ /^1(\.0+)?$/
232
+ singular
233
+ else
234
+ plural || singular.pluralize(locale)
235
+ end
236
+
237
+ "#{count || 0} #{word}"
238
+ end
239
+
240
+ # Wraps the +text+ into lines no longer than +line_width+ width. This method
241
+ # breaks on the first whitespace character that does not exceed +line_width+
242
+ # (which is 80 by default).
243
+ #
244
+ # word_wrap('Once upon a time')
245
+ # # => Once upon a time
246
+ #
247
+ # 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...
249
+ #
250
+ # word_wrap('Once upon a time', line_width: 8)
251
+ # # => Once\nupon a\ntime
252
+ #
253
+ # word_wrap('Once upon a time', line_width: 1)
254
+ # # => Once\nupon\na\ntime
255
+ #
256
+ # You can also specify a custom +break_sequence+ ("\n" by default)
257
+ #
258
+ # word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n")
259
+ # # => Once\r\nupon\r\na\r\ntime
260
+ 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
264
+ end
265
+
266
+ # 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
268
+ # considered a paragraph and wrapped in <tt><p></tt> tags. One newline
269
+ # (<tt>\n</tt> or <tt>\r\n</tt>) is considered a linebreak and a
270
+ # <tt><br /></tt> tag is appended. This method does not remove the
271
+ # newlines from the +text+.
272
+ #
273
+ # You can pass any HTML attributes into <tt>html_options</tt>. These
274
+ # will be added to all created paragraphs.
275
+ #
276
+ # ==== Options
277
+ # * <tt>:sanitize</tt> - If +false+, does not sanitize +text+.
278
+ # * <tt>:wrapper_tag</tt> - String representing the wrapper tag, defaults to <tt>"p"</tt>
279
+ #
280
+ # ==== Examples
281
+ # my_text = "Here is some basic text...\n...with a line break."
282
+ #
283
+ # simple_format(my_text)
284
+ # # => "<p>Here is some basic text...\n<br />...with a line break.</p>"
285
+ #
286
+ # simple_format(my_text, {}, wrapper_tag: "div")
287
+ # # => "<div>Here is some basic text...\n<br />...with a line break.</div>"
288
+ #
289
+ # more_text = "We want to put a paragraph...\n\n...right there."
290
+ #
291
+ # simple_format(more_text)
292
+ # # => "<p>We want to put a paragraph...</p>\n\n<p>...right there.</p>"
293
+ #
294
+ # simple_format("Look ma! A class!", class: 'description')
295
+ # # => "<p class='description'>Look ma! A class!</p>"
296
+ #
297
+ # simple_format("<blink>Unblinkable.</blink>")
298
+ # # => "<p>Unblinkable.</p>"
299
+ #
300
+ # simple_format("<blink>Blinkable!</blink> It's true.", {}, sanitize: false)
301
+ # # => "<p><blink>Blinkable!</blink> It's true.</p>"
302
+ def simple_format(text, html_options = {}, options = {})
303
+ wrapper_tag = options.fetch(:wrapper_tag, :p)
304
+
305
+ text = sanitize(text) if options.fetch(:sanitize, true)
306
+ paragraphs = split_paragraphs(text)
307
+
308
+ if paragraphs.empty?
309
+ content_tag(wrapper_tag, nil, html_options)
310
+ else
311
+ paragraphs.map! { |paragraph|
312
+ content_tag(wrapper_tag, raw(paragraph), html_options)
313
+ }.join("\n\n").html_safe
314
+ end
315
+ end
316
+
317
+ # Creates a Cycle object whose _to_s_ method cycles through elements of an
318
+ # array every time it is called. This can be used for example, to alternate
319
+ # classes for table rows. You can use named cycles to allow nesting in loops.
320
+ # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a
321
+ # named cycle. The default name for a cycle without a +:name+ key is
322
+ # <tt>"default"</tt>. You can manually reset a cycle by calling reset_cycle
323
+ # and passing the name of the cycle. The current cycle string can be obtained
324
+ # anytime using the current_cycle method.
325
+ #
326
+ # # Alternate CSS classes for even and odd numbers...
327
+ # @items = [1,2,3,4]
328
+ # <table>
329
+ # <% @items.each do |item| %>
330
+ # <tr class="<%= cycle("odd", "even") -%>">
331
+ # <td><%= item %></td>
332
+ # </tr>
333
+ # <% end %>
334
+ # </table>
335
+ #
336
+ #
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'}]
341
+ # <% @items.each do |item| %>
342
+ # <tr class="<%= cycle("odd", "even", name: "row_class") -%>">
343
+ # <td>
344
+ # <% item.values.each do |value| %>
345
+ # <%# Create a named cycle "colors" %>
346
+ # <span style="color:<%= cycle("red", "green", "blue", name: "colors") -%>">
347
+ # <%= value %>
348
+ # </span>
349
+ # <% end %>
350
+ # <% reset_cycle("colors") %>
351
+ # </td>
352
+ # </tr>
353
+ # <% end %>
354
+ def cycle(first_value, *values)
355
+ options = values.extract_options!
356
+ name = options.fetch(:name, "default")
357
+
358
+ values.unshift(*first_value)
359
+
360
+ cycle = get_cycle(name)
361
+ unless cycle && cycle.values == values
362
+ cycle = set_cycle(name, Cycle.new(*values))
363
+ end
364
+ cycle.to_s
365
+ end
366
+
367
+ # Returns the current cycle string after a cycle has been started. Useful
368
+ # for complex table highlighting or any other design need which requires
369
+ # the current cycle string in more than one place.
370
+ #
371
+ # # Alternate background colors
372
+ # @items = [1,2,3,4]
373
+ # <% @items.each do |item| %>
374
+ # <div style="background-color:<%= cycle("red","white","blue") %>">
375
+ # <span style="background-color:<%= current_cycle %>"><%= item %></span>
376
+ # </div>
377
+ # <% end %>
378
+ def current_cycle(name = "default")
379
+ cycle = get_cycle(name)
380
+ cycle.current_value if cycle
381
+ end
382
+
383
+ # Resets a cycle so that it starts from the first element the next time
384
+ # it is called. Pass in +name+ to reset a named cycle.
385
+ #
386
+ # # Alternate CSS classes for even and odd numbers...
387
+ # @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]]
388
+ # <table>
389
+ # <% @items.each do |item| %>
390
+ # <tr class="<%= cycle("even", "odd") -%>">
391
+ # <% item.each do |value| %>
392
+ # <span style="color:<%= cycle("#333", "#666", "#999", name: "colors") -%>">
393
+ # <%= value %>
394
+ # </span>
395
+ # <% end %>
396
+ #
397
+ # <% reset_cycle("colors") %>
398
+ # </tr>
399
+ # <% end %>
400
+ # </table>
401
+ def reset_cycle(name = "default")
402
+ cycle = get_cycle(name)
403
+ cycle.reset if cycle
404
+ end
405
+
406
+ class Cycle #:nodoc:
407
+ attr_reader :values
408
+
409
+ def initialize(first_value, *values)
410
+ @values = values.unshift(first_value)
411
+ reset
412
+ end
413
+
414
+ def reset
415
+ @index = 0
416
+ end
417
+
418
+ def current_value
419
+ @values[previous_index].to_s
420
+ end
421
+
422
+ def to_s
423
+ value = @values[@index].to_s
424
+ @index = next_index
425
+ value
426
+ end
427
+
428
+ private
429
+
430
+ def next_index
431
+ step_index(1)
432
+ end
433
+
434
+ def previous_index
435
+ step_index(-1)
436
+ end
437
+
438
+ def step_index(n)
439
+ (@index + n) % @values.size
440
+ end
441
+ end
442
+
443
+ private
444
+ # The cycle helpers need to store the cycles in a place that is
445
+ # guaranteed to be reset every time a page is rendered, so it
446
+ # uses an instance variable of ActionView::Base.
447
+ def get_cycle(name)
448
+ @_cycles = Hash.new unless defined?(@_cycles)
449
+ @_cycles[name]
450
+ end
451
+
452
+ def set_cycle(name, cycle_object)
453
+ @_cycles = Hash.new unless defined?(@_cycles)
454
+ @_cycles[name] = cycle_object
455
+ end
456
+
457
+ def split_paragraphs(text)
458
+ return [] if text.blank?
459
+
460
+ text.to_str.gsub(/\r\n?/, "\n").split(/\n\n+/).map! do |t|
461
+ t.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') || t
462
+ end
463
+ end
464
+
465
+ def cut_excerpt_part(part_position, part, separator, options)
466
+ return "", "" unless part
467
+
468
+ radius = options.fetch(:radius, 100)
469
+ omission = options.fetch(:omission, "...")
470
+
471
+ part = part.split(separator)
472
+ part.delete("")
473
+ affix = part.size > radius ? omission : ""
474
+
475
+ part = if part_position == :first
476
+ drop_index = [part.length - radius, 0].max
477
+ part.drop(drop_index)
478
+ else
479
+ part.first(radius)
480
+ end
481
+
482
+ return affix, part.join(separator)
483
+ end
484
+ end
485
+ end
486
+ end