tennpipes-helper 3.6.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.rdoc +239 -0
  4. data/Rakefile +1 -0
  5. data/lib/tennpipes-helper.rb +62 -0
  6. data/lib/tennpipes-helper/asset_tag_helpers.rb +394 -0
  7. data/lib/tennpipes-helper/form_builder/abstract_form_builder.rb +279 -0
  8. data/lib/tennpipes-helper/form_builder/standard_form_builder.rb +40 -0
  9. data/lib/tennpipes-helper/form_helpers.rb +639 -0
  10. data/lib/tennpipes-helper/form_helpers/errors.rb +138 -0
  11. data/lib/tennpipes-helper/form_helpers/options.rb +98 -0
  12. data/lib/tennpipes-helper/form_helpers/security.rb +70 -0
  13. data/lib/tennpipes-helper/format_helpers.rb +372 -0
  14. data/lib/tennpipes-helper/locale/cs.yml +103 -0
  15. data/lib/tennpipes-helper/locale/da.yml +91 -0
  16. data/lib/tennpipes-helper/locale/de.yml +81 -0
  17. data/lib/tennpipes-helper/locale/en.yml +103 -0
  18. data/lib/tennpipes-helper/locale/es.yml +103 -0
  19. data/lib/tennpipes-helper/locale/fr.yml +79 -0
  20. data/lib/tennpipes-helper/locale/hu.yml +103 -0
  21. data/lib/tennpipes-helper/locale/it.yml +89 -0
  22. data/lib/tennpipes-helper/locale/ja.yml +103 -0
  23. data/lib/tennpipes-helper/locale/lv.yml +103 -0
  24. data/lib/tennpipes-helper/locale/nl.yml +82 -0
  25. data/lib/tennpipes-helper/locale/no.yml +91 -0
  26. data/lib/tennpipes-helper/locale/pl.yml +95 -0
  27. data/lib/tennpipes-helper/locale/pt_br.yml +103 -0
  28. data/lib/tennpipes-helper/locale/ro.yml +103 -0
  29. data/lib/tennpipes-helper/locale/ru.yml +103 -0
  30. data/lib/tennpipes-helper/locale/sv.yml +103 -0
  31. data/lib/tennpipes-helper/locale/tr.yml +103 -0
  32. data/lib/tennpipes-helper/locale/uk.yml +103 -0
  33. data/lib/tennpipes-helper/locale/zh_cn.yml +103 -0
  34. data/lib/tennpipes-helper/locale/zh_tw.yml +103 -0
  35. data/lib/tennpipes-helper/number_helpers.rb +283 -0
  36. data/lib/tennpipes-helper/output_helpers.rb +226 -0
  37. data/lib/tennpipes-helper/output_helpers/abstract_handler.rb +61 -0
  38. data/lib/tennpipes-helper/output_helpers/erb_handler.rb +27 -0
  39. data/lib/tennpipes-helper/output_helpers/haml_handler.rb +25 -0
  40. data/lib/tennpipes-helper/output_helpers/slim_handler.rb +18 -0
  41. data/lib/tennpipes-helper/render_helpers.rb +63 -0
  42. data/lib/tennpipes-helper/tag_helpers.rb +294 -0
  43. data/lib/tennpipes-helper/translation_helpers.rb +36 -0
  44. data/lib/tennpipes/rendering.rb +369 -0
  45. data/lib/tennpipes/rendering/erb_template.rb +40 -0
  46. data/lib/tennpipes/rendering/erubis_template.rb +66 -0
  47. data/lib/tennpipes/rendering/haml_template.rb +26 -0
  48. data/lib/tennpipes/rendering/slim_template.rb +20 -0
  49. data/test/fixtures/apps/render.rb +25 -0
  50. data/test/fixtures/apps/views/article/comment/show.slim +1 -0
  51. data/test/fixtures/apps/views/blog/post.erb +1 -0
  52. data/test/fixtures/apps/views/layouts/specific.erb +1 -0
  53. data/test/fixtures/apps/views/test/post.erb +1 -0
  54. data/test/fixtures/layouts/layout.erb +1 -0
  55. data/test/fixtures/markup_app/app.rb +87 -0
  56. data/test/fixtures/markup_app/views/button_to.erb +8 -0
  57. data/test/fixtures/markup_app/views/button_to.haml +5 -0
  58. data/test/fixtures/markup_app/views/button_to.slim +6 -0
  59. data/test/fixtures/markup_app/views/capture_concat.erb +14 -0
  60. data/test/fixtures/markup_app/views/capture_concat.haml +12 -0
  61. data/test/fixtures/markup_app/views/capture_concat.slim +12 -0
  62. data/test/fixtures/markup_app/views/content_for.erb +23 -0
  63. data/test/fixtures/markup_app/views/content_for.haml +19 -0
  64. data/test/fixtures/markup_app/views/content_for.slim +19 -0
  65. data/test/fixtures/markup_app/views/content_tag.erb +13 -0
  66. data/test/fixtures/markup_app/views/content_tag.haml +11 -0
  67. data/test/fixtures/markup_app/views/content_tag.slim +11 -0
  68. data/test/fixtures/markup_app/views/current_engine.erb +5 -0
  69. data/test/fixtures/markup_app/views/current_engine.haml +5 -0
  70. data/test/fixtures/markup_app/views/current_engine.slim +5 -0
  71. data/test/fixtures/markup_app/views/fields_for.erb +20 -0
  72. data/test/fixtures/markup_app/views/fields_for.haml +15 -0
  73. data/test/fixtures/markup_app/views/fields_for.slim +15 -0
  74. data/test/fixtures/markup_app/views/form_for.erb +72 -0
  75. data/test/fixtures/markup_app/views/form_for.haml +59 -0
  76. data/test/fixtures/markup_app/views/form_for.slim +59 -0
  77. data/test/fixtures/markup_app/views/form_tag.erb +95 -0
  78. data/test/fixtures/markup_app/views/form_tag.haml +78 -0
  79. data/test/fixtures/markup_app/views/form_tag.slim +79 -0
  80. data/test/fixtures/markup_app/views/link_to.erb +5 -0
  81. data/test/fixtures/markup_app/views/link_to.haml +4 -0
  82. data/test/fixtures/markup_app/views/link_to.slim +4 -0
  83. data/test/fixtures/markup_app/views/mail_to.erb +3 -0
  84. data/test/fixtures/markup_app/views/mail_to.haml +3 -0
  85. data/test/fixtures/markup_app/views/mail_to.slim +3 -0
  86. data/test/fixtures/markup_app/views/meta_tag.erb +3 -0
  87. data/test/fixtures/markup_app/views/meta_tag.haml +3 -0
  88. data/test/fixtures/markup_app/views/meta_tag.slim +3 -0
  89. data/test/fixtures/markup_app/views/partials/_erb.erb +1 -0
  90. data/test/fixtures/markup_app/views/partials/_haml.haml +1 -0
  91. data/test/fixtures/markup_app/views/partials/_slim.slim +1 -0
  92. data/test/fixtures/markup_app/views/simple_partial.erb +1 -0
  93. data/test/fixtures/markup_app/views/simple_partial.haml +1 -0
  94. data/test/fixtures/markup_app/views/simple_partial.slim +1 -0
  95. data/test/fixtures/render_app/app.rb +110 -0
  96. data/test/fixtures/render_app/views/_deep.erb +3 -0
  97. data/test/fixtures/render_app/views/_deep.haml +2 -0
  98. data/test/fixtures/render_app/views/_deep.slim +2 -0
  99. data/test/fixtures/render_app/views/_partial_block_erb.erb +10 -0
  100. data/test/fixtures/render_app/views/_partial_block_haml.haml +7 -0
  101. data/test/fixtures/render_app/views/_partial_block_slim.slim +7 -0
  102. data/test/fixtures/render_app/views/_unsafe.html.builder +2 -0
  103. data/test/fixtures/render_app/views/_unsafe_object.html.builder +2 -0
  104. data/test/fixtures/render_app/views/current_engine.haml +5 -0
  105. data/test/fixtures/render_app/views/current_engines/_erb.erb +1 -0
  106. data/test/fixtures/render_app/views/current_engines/_haml.haml +1 -0
  107. data/test/fixtures/render_app/views/current_engines/_slim.slim +1 -0
  108. data/test/fixtures/render_app/views/dive_inner_erb.erb +3 -0
  109. data/test/fixtures/render_app/views/dive_inner_haml.haml +2 -0
  110. data/test/fixtures/render_app/views/dive_inner_slim.slim +2 -0
  111. data/test/fixtures/render_app/views/dive_outer_erb.erb +3 -0
  112. data/test/fixtures/render_app/views/dive_outer_haml.haml +2 -0
  113. data/test/fixtures/render_app/views/dive_outer_slim.slim +2 -0
  114. data/test/fixtures/render_app/views/double_capture_erb.erb +3 -0
  115. data/test/fixtures/render_app/views/double_capture_haml.haml +2 -0
  116. data/test/fixtures/render_app/views/double_capture_slim.slim +2 -0
  117. data/test/fixtures/render_app/views/erb/test.erb +1 -0
  118. data/test/fixtures/render_app/views/explicit_engine.haml +5 -0
  119. data/test/fixtures/render_app/views/haml/test.haml +1 -0
  120. data/test/fixtures/render_app/views/render_block_erb.erb +5 -0
  121. data/test/fixtures/render_app/views/render_block_haml.haml +4 -0
  122. data/test/fixtures/render_app/views/render_block_slim.slim +4 -0
  123. data/test/fixtures/render_app/views/ruby_block_capture_erb.erb +1 -0
  124. data/test/fixtures/render_app/views/ruby_block_capture_haml.haml +1 -0
  125. data/test/fixtures/render_app/views/ruby_block_capture_slim.slim +1 -0
  126. data/test/fixtures/render_app/views/template/_user.haml +7 -0
  127. data/test/fixtures/render_app/views/template/haml_template.haml +1 -0
  128. data/test/fixtures/render_app/views/template/some_template.haml +2 -0
  129. data/test/fixtures/render_app/views/wrong_capture_erb.erb +3 -0
  130. data/test/fixtures/render_app/views/wrong_capture_haml.haml +2 -0
  131. data/test/fixtures/render_app/views/wrong_capture_slim.slim +2 -0
  132. data/test/helper.rb +88 -0
  133. data/test/test_asset_tag_helpers.rb +401 -0
  134. data/test/test_form_builder.rb +1216 -0
  135. data/test/test_form_helpers.rb +1056 -0
  136. data/test/test_format_helpers.rb +251 -0
  137. data/test/test_helpers.rb +10 -0
  138. data/test/test_locale.rb +20 -0
  139. data/test/test_number_helpers.rb +142 -0
  140. data/test/test_output_helpers.rb +157 -0
  141. data/test/test_render_helpers.rb +225 -0
  142. data/test/test_rendering.rb +706 -0
  143. data/test/test_rendering_extensions.rb +14 -0
  144. data/test/test_tag_helpers.rb +131 -0
  145. metadata +299 -0
@@ -0,0 +1,138 @@
1
+ module Tennpipes
2
+ module Helpers
3
+ module FormHelpers
4
+ ##
5
+ # Helpers to generate form errors.
6
+ #
7
+ module Errors
8
+ ##
9
+ # Constructs list HTML for the errors for a given symbol.
10
+ #
11
+ # @overload error_messages_for(*objects, options = {})
12
+ # @param [Array<Object>] object Splat of objects to display errors for.
13
+ # @param [Hash] options Error message display options.
14
+ # @option options [String] :header_tag ("h2")
15
+ # Used for the header of the error div.
16
+ # @option options [String] :id ("field-errors")
17
+ # The id of the error div.
18
+ # @option options [String] :class ("field-errors")
19
+ # The class of the error div.
20
+ # @option options [Array<Object>] :object
21
+ # The object (or array of objects) for which to display errors,
22
+ # if you need to escape the instance variable convention.
23
+ # @option options [String] :object_name
24
+ # The object name to use in the header, or any text that you prefer.
25
+ # If +:object_name+ is not set, the name of the first object will be used.
26
+ # @option options [String] :header_message ("X errors prohibited this object from being saved")
27
+ # The message in the header of the error div. Pass +nil+ or an empty string
28
+ # to avoid the header message altogether.
29
+ # @option options [String] :message ("There were problems with the following fields:")
30
+ # The explanation message after the header message and before
31
+ # the error list. Pass +nil+ or an empty string to avoid the explanation message
32
+ # altogether.
33
+ #
34
+ # @return [String] The html section with all errors for the specified +objects+
35
+ #
36
+ # @example
37
+ # error_messages_for :user
38
+ #
39
+ def error_messages_for(*objects)
40
+ options = objects.extract_options!.symbolize_keys
41
+ objects = objects.map{ |obj| resolve_object(obj) }.compact
42
+ count = objects.inject(0){ |sum, object| sum + object.errors.count }
43
+ return ActiveSupport::SafeBuffer.new if count.zero?
44
+
45
+ content_tag(:div, error_contents(objects, count, options), error_html_attributes(options))
46
+ end
47
+
48
+ ##
49
+ # Returns a string containing the error message attached to the
50
+ # +method+ on the +object+ if one exists.
51
+ #
52
+ # @param [Object] object
53
+ # The object to display the error for.
54
+ # @param [Symbol] field
55
+ # The field on the +object+ to display the error for.
56
+ # @param [Hash] options
57
+ # The options to control the error display.
58
+ # @option options [String] :tag ("span")
59
+ # The tag that encloses the error.
60
+ # @option options [String] :prepend ("")
61
+ # The text to prepend before the field error.
62
+ # @option options [String] :append ("")
63
+ # The text to append after the field error.
64
+ #
65
+ # @example
66
+ # # => <span class="error">can't be blank</div>
67
+ # error_message_on :post, :title
68
+ # error_message_on @post, :title
69
+ #
70
+ # # => <div class="custom" style="border:1px solid red">can't be blank</div>
71
+ # error_message_on :post, :title, :tag => :id, :class => :custom, :style => "border:1px solid red"
72
+ #
73
+ # # => <div class="error">This title can't be blank (or it won't work)</div>
74
+ # error_message_on :post, :title, :prepend => "This title", :append => "(or it won't work)"
75
+ #
76
+ # @return [String] The html display of an error for a particular +object+ and +field+.
77
+ #
78
+ # @api public
79
+ def error_message_on(object, field, options={})
80
+ error = Array(resolve_object(object).errors[field]).first
81
+ return ActiveSupport::SafeBuffer.new unless error
82
+ options = { :tag => :span, :class => :error }.update(options)
83
+ tag = options.delete(:tag)
84
+ error = [options.delete(:prepend), error, options.delete(:append)].compact.join(" ")
85
+ content_tag(tag, error, options)
86
+ end
87
+
88
+ private
89
+
90
+ def error_contents(objects, count, options)
91
+ object_name = options[:object_name] || objects.first.class.to_s.underscore.gsub(/\//, ' ')
92
+
93
+ contents = ActiveSupport::SafeBuffer.new
94
+ contents << error_header_tag(options, object_name, count)
95
+ contents << error_body_tag(options)
96
+ contents << error_list_tag(objects, object_name)
97
+ end
98
+
99
+ def error_list_tag(objects, object_name)
100
+ errors = objects.inject({}){ |all,object| all.update(object.errors) }
101
+ error_messages = errors.inject(ActiveSupport::SafeBuffer.new) do |all, (field, message)|
102
+ field_name = I18n.t(field, :default => field.to_s.humanize, :scope => [:models, object_name, :attributes])
103
+ all << content_tag(:li, "#{field_name} #{message}")
104
+ end
105
+ content_tag(:ul, error_messages)
106
+ end
107
+
108
+ def error_header_tag(options, object_name, count)
109
+ header_message = options[:header_message] || begin
110
+ model_name = I18n.t(:name, :default => object_name.humanize, :scope => [:models, object_name], :count => 1)
111
+ I18n.t :header, :count => count, :model => model_name, :locale => options[:locale], :scope => [:models, :errors, :template]
112
+ end
113
+ content_tag(options[:header_tag] || :h2, header_message) if header_message.present?
114
+ end
115
+
116
+ def error_body_tag(options)
117
+ body_message = options[:message] || I18n.t(:body, :locale => options[:locale], :scope => [:models, :errors, :template])
118
+ content_tag(:p, body_message) if body_message.present?
119
+ end
120
+
121
+ def error_html_attributes(options)
122
+ [:id, :class, :style].each_with_object({}) do |key,all|
123
+ if options.include?(key)
124
+ value = options[key]
125
+ all[key] = value unless value.blank?
126
+ else
127
+ all[key] = 'field-errors' unless key == :style
128
+ end
129
+ end
130
+ end
131
+
132
+ def resolve_object(object)
133
+ object.is_a?(Symbol) ? instance_variable_get("@#{object}") : object
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,98 @@
1
+ module Tennpipes
2
+ module Helpers
3
+ module FormHelpers
4
+ ##
5
+ # Helpers to generate options list for select tag.
6
+ #
7
+ module Options
8
+ def extract_option_tags!(options)
9
+ state = extract_option_state!(options)
10
+ option_tags = if options[:grouped_options]
11
+ grouped_options_for_select(options.delete(:grouped_options), state)
12
+ else
13
+ options_for_select(extract_option_items!(options), state)
14
+ end
15
+ if prompt = options.delete(:include_blank)
16
+ option_tags.unshift(blank_option(prompt))
17
+ end
18
+ option_tags
19
+ end
20
+
21
+ private
22
+
23
+ ##
24
+ # Returns the blank option serving as a prompt if passed.
25
+ #
26
+ def blank_option(prompt)
27
+ case prompt
28
+ when nil, false
29
+ nil
30
+ when String
31
+ content_tag(:option, prompt, :value => '')
32
+ when Array
33
+ content_tag(:option, prompt.first, :value => prompt.last)
34
+ else
35
+ content_tag(:option, '', :value => '')
36
+ end
37
+ end
38
+
39
+ ##
40
+ # Returns whether the option should be selected or not.
41
+ #
42
+ # @example
43
+ # option_is_selected?("red", "Red", ["red", "blue"]) => true
44
+ # option_is_selected?("red", "Red", ["green", "blue"]) => false
45
+ #
46
+ def option_is_selected?(value, caption, selected_values)
47
+ Array(selected_values).any? do |selected|
48
+ [value.to_s, caption.to_s].include?(selected.to_s)
49
+ end
50
+ end
51
+
52
+ ##
53
+ # Returns the options tags for a select based on the given option items.
54
+ #
55
+ def options_for_select(option_items, state = {})
56
+ return [] if option_items.blank?
57
+ option_items.map do |caption, value, attributes|
58
+ html_attributes = { :value => value ||= caption }.merge(attributes||{})
59
+ html_attributes[:selected] ||= option_is_selected?(value, caption, state[:selected])
60
+ html_attributes[:disabled] ||= option_is_selected?(value, caption, state[:disabled])
61
+ content_tag(:option, caption, html_attributes)
62
+ end
63
+ end
64
+
65
+ ##
66
+ # Returns the optgroups with options tags for a select based on the given :grouped_options items.
67
+ #
68
+ def grouped_options_for_select(collection, state = {})
69
+ collection.map do |item|
70
+ caption = item.shift
71
+ attributes = item.last.kind_of?(Hash) ? item.pop : {}
72
+ value = item.flatten(1)
73
+ attributes = value.pop if value.last.kind_of?(Hash)
74
+ html_attributes = { :label => caption }.merge(attributes||{})
75
+ content_tag(:optgroup, options_for_select(value, state), html_attributes)
76
+ end
77
+ end
78
+
79
+ def extract_option_state!(options)
80
+ {
81
+ :selected => Array(options.delete(:value))|Array(options.delete(:selected))|Array(options.delete(:selected_options)),
82
+ :disabled => Array(options.delete(:disabled_options))
83
+ }
84
+ end
85
+
86
+ def extract_option_items!(options)
87
+ if options[:collection]
88
+ fields = options.delete(:fields)
89
+ collection = options.delete(:collection)
90
+ collection.map{ |item| [ item.send(fields.first), item.send(fields.last) ] }
91
+ else
92
+ options.delete(:options) || []
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,70 @@
1
+ require 'securerandom'
2
+
3
+ module Tennpipes
4
+ module Helpers
5
+ module FormHelpers
6
+ ##
7
+ # Helpers to generate form security tags for csrf protection.
8
+ #
9
+ module Security
10
+ ##
11
+ # Constructs a hidden field containing a CSRF token.
12
+ #
13
+ # @param [String] token
14
+ # The token to use. Will be read from the session by default.
15
+ #
16
+ # @return [String] The hidden field with CSRF token as value.
17
+ #
18
+ # @example
19
+ # csrf_token_field
20
+ #
21
+ def csrf_token_field
22
+ hidden_field_tag csrf_param, :value => csrf_token
23
+ end
24
+
25
+ ##
26
+ # Constructs meta tags `csrf-param` and `csrf-token` with the name of the
27
+ # cross-site request forgery protection parameter and token, respectively.
28
+ #
29
+ # @return [String] The meta tags with the CSRF token and the param your app expects it in.
30
+ #
31
+ # @example
32
+ # csrf_meta_tags
33
+ #
34
+ def csrf_meta_tags
35
+ if is_protected_from_csrf?
36
+ meta_tag(csrf_param, :name => 'csrf-param') <<
37
+ meta_tag(csrf_token, :name => 'csrf-token')
38
+ end
39
+ end
40
+
41
+ protected
42
+
43
+ ##
44
+ # Returns whether the application is being protected from CSRF. Defaults to true.
45
+ #
46
+ def is_protected_from_csrf?
47
+ defined?(settings) ? settings.protect_from_csrf : true
48
+ end
49
+
50
+ ##
51
+ # Returns the current CSRF token (based on the session). If it doesn't exist,
52
+ # it will create one and assign it to the session's `csrf` key.
53
+ #
54
+ def csrf_token
55
+ session[:csrf] ||= SecureRandom.hex(32) if defined?(session)
56
+ end
57
+
58
+ ##
59
+ # Returns the param/field name in which your CSRF token should be expected by your
60
+ # controllers. Defaults to `authenticity_token`.
61
+ #
62
+ # Set this in your application with `set :csrf_param, :something_else`.
63
+ #
64
+ def csrf_param
65
+ defined?(settings) && settings.respond_to?(:csrf_param) ? settings.csrf_param : :authenticity_token
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,372 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Tennpipes
3
+ module Helpers
4
+ ###
5
+ # Helpers related to formatting or manipulating text within templates.
6
+ #
7
+ module FormatHelpers
8
+ ##
9
+ # Returns escaped text to protect against malicious content.
10
+ #
11
+ # @param [String] text
12
+ # Unsanitized HTML string that needs to be escaped.
13
+ #
14
+ # @return [String] HTML with escaped characters.
15
+ #
16
+ # @example
17
+ # escape_html("<b>Hey<b>") => "&lt;b&gt;Hey&lt;b;gt;"
18
+ # h("Me & Bob") => "Me &amp; Bob"
19
+ #
20
+ def escape_html(text)
21
+ Rack::Utils.escape_html(text).html_safe
22
+ end
23
+ alias h escape_html
24
+ alias sanitize_html escape_html
25
+
26
+ ##
27
+ # Returns escaped text to protect against malicious content.
28
+ #
29
+ # @param [String] text
30
+ # Unsanitized HTML string that needs to be escaped.
31
+ # @param [String] blank_text
32
+ # Text to return if escaped text is blank.
33
+ #
34
+ # @return [String] HTML with escaped characters or the value specified if blank.
35
+ #
36
+ # @example
37
+ # h!("Me & Bob") => "Me &amp; Bob"
38
+ # h!("", "Whoops") => "Whoops"
39
+ #
40
+ def h!(text, blank_text = '&nbsp;')
41
+ return blank_text.html_safe if text.nil? || text.empty?
42
+ h(text)
43
+ end
44
+
45
+ ##
46
+ # Strips all HTML tags from the html.
47
+ #
48
+ # @param [String] html
49
+ # The HTML for which to strip tags.
50
+ #
51
+ # @return [String] HTML with tags stripped.
52
+ #
53
+ # @example
54
+ # strip_tags("<b>Hey</b>") => "Hey"
55
+ #
56
+ def strip_tags(html)
57
+ html.gsub(/<\/?[^>]*>/, "") if html
58
+ end
59
+
60
+ ##
61
+ # Returns text transformed into HTML using simple formatting rules. Two or more consecutive newlines(\n\n) are considered
62
+ # as a paragraph and wrapped in <p> or your own tags. One newline (\n) is considered as a linebreak and a <br /> tag is appended.
63
+ # This method does not remove the newlines from the text.
64
+ #
65
+ # @param [String] text
66
+ # The simple text to be formatted.
67
+ # @param [Hash] options
68
+ # Formatting options for the text. Can accept html options for the wrapper tag.
69
+ # @option options [Symbol] :tag (p)
70
+ # The html tag to use for formatting newlines.
71
+ #
72
+ # @return [String] The text formatted as simple HTML.
73
+ #
74
+ # @example
75
+ # simple_format("hello\nworld") # => "<p>hello<br/>world</p>"
76
+ # simple_format("hello\nworld", :tag => :div, :class => :foo) # => "<div class="foo">hello<br/>world</div>"
77
+ #
78
+ def simple_format(text, options={})
79
+ t = options.delete(:tag) || :p
80
+ start_tag = tag(t, options, true)
81
+ text = escape_html(text.to_s.dup) unless text.html_safe?
82
+ text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n
83
+ text.gsub!(/\n\n+/, "</#{t}>\n\n#{start_tag}") # 2+ newline -> paragraph
84
+ text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
85
+ text.insert 0, start_tag
86
+ text << "</#{t}>"
87
+ text.html_safe
88
+ end
89
+
90
+ ##
91
+ # Attempts to pluralize the singular word unless count is 1. If plural is supplied, it will use that when count is > 1,
92
+ # otherwise it will use inflector to determine the plural form.
93
+ #
94
+ # @param [Fixnum] count
95
+ # The count which determines pluralization.
96
+ # @param [String] singular
97
+ # The word to be pluralized if appropriate based on +count+.
98
+ # @param [String] plural
99
+ # Explicit pluralized word to be used; if not specified uses inflector.
100
+ #
101
+ # @return [String] The properly pluralized word.
102
+ #
103
+ # @example
104
+ # pluralize(2, 'person') => '2 people'
105
+ #
106
+ def pluralize(count, singular, plural = nil)
107
+ "#{count || 0} " + ((count == 1 || count == '1') ? singular : (plural || singular.pluralize))
108
+ end
109
+
110
+ ##
111
+ # Truncates a given text after a given :length if text is longer than :length (defaults to 30).
112
+ # The last characters will be replaced with the :omission (defaults to "…") for a total length not exceeding :length.
113
+ #
114
+ # @param [String] text
115
+ # The text to be truncated.
116
+ # @param [Hash] options
117
+ # Formatting options for the truncation.
118
+ # @option options [Fixnum] :length (30)
119
+ # The number of characters before truncation occurs.
120
+ # @option options [String] :omission ("...")
121
+ # The characters that are placed after the truncated text.
122
+ #
123
+ # @return [String] The text truncated after the given number of characters.
124
+ #
125
+ # @example
126
+ # truncate("Once upon a time in a world far far away", :length => 8) => "Once upon..."
127
+ #
128
+ def truncate(text, options={})
129
+ options.reverse_merge!(:length => 30, :omission => "...")
130
+ if text
131
+ len = options[:length] - options[:omission].length
132
+ chars = text
133
+ (chars.length > options[:length] ? chars[0...len] + options[:omission] : text).to_s
134
+ end
135
+ end
136
+
137
+ ##
138
+ # Truncates words of a given text after a given :length if number of words in text is more than :length (defaults to 30).
139
+ # The last words will be replaced with the :omission (defaults to "…") for a total number of words not exceeding :length.
140
+ #
141
+ # @param [String] text
142
+ # The text to be truncated.
143
+ # @param [Hash] options
144
+ # Formatting options for the truncation.
145
+ # @option options [Fixnum] :length (30)
146
+ # The number of words before truncation occurs.
147
+ # @option options [String] :omission ("...")
148
+ # The characters that are placed after the truncated text.
149
+ #
150
+ # @return [String] The text truncated after the given number of words.
151
+ #
152
+ # @example
153
+ # truncate_words("Once upon a time in a world far far away", :length => 8) => "Once upon a time in a world far..."
154
+ #
155
+ def truncate_words(text, options={})
156
+ options.reverse_merge!(:length => 30, :omission => "...")
157
+ if text
158
+ words = text.split()
159
+ words[0..(options[:length]-1)].join(' ') + (words.length > options[:length] ? options[:omission] : '')
160
+ end
161
+ end
162
+
163
+ ##
164
+ # Wraps the text into lines no longer than line_width width.
165
+ # This method breaks on the first whitespace character that does not exceed line_width (which is 80 by default).
166
+ #
167
+ # @overload word_wrap(text, options={})
168
+ # @param [String] text
169
+ # The text to be wrapped.
170
+ # @param [Hash] options
171
+ # Formatting options for the wrapping.
172
+ # @option options [Fixnum] :line_width (80)
173
+ # The line width before a wrap should occur.
174
+ #
175
+ # @return [String] The text with line wraps for lines longer then +line_width+.
176
+ #
177
+ # @example
178
+ # word_wrap('Once upon a time', :line_width => 8) => "Once upon\na time"
179
+ #
180
+ def word_wrap(text, *args)
181
+ options = args.extract_options!
182
+ unless args.blank?
183
+ options[:line_width] = args[0] || 80
184
+ end
185
+ options.reverse_merge!(:line_width => 80)
186
+
187
+ text.split("\n").map do |line|
188
+ line.length > options[:line_width] ? line.gsub(/(.{1,#{options[:line_width]}})(\s+|$)/, "\\1\n").strip : line
189
+ end * "\n"
190
+ end
191
+
192
+ ##
193
+ # Highlights one or more words everywhere in text by inserting it into a :highlighter string.
194
+ #
195
+ # The highlighter can be customized by passing :+highlighter+ as a single-quoted string
196
+ # with \1 where the phrase is to be inserted.
197
+ #
198
+ # @overload highlight(text, words, options={})
199
+ # @param [String] text
200
+ # The text that will be searched.
201
+ # @param [String] words
202
+ # The words to be highlighted in the +text+.
203
+ # @param [Hash] options
204
+ # Formatting options for the highlight.
205
+ # @option options [String] :highlighter ('<strong class="highlight">\1</strong>')
206
+ # The html pattern for wrapping the highlighted words.
207
+ #
208
+ # @return [String] The text with the words specified wrapped with highlighted spans.
209
+ #
210
+ # @example
211
+ # highlight('Lorem ipsum dolor sit amet', 'dolor')
212
+ # # => Lorem ipsum <strong class="highlight">dolor</strong> sit amet
213
+ #
214
+ # highlight('Lorem ipsum dolor sit amet', 'dolor', :highlighter => '<span class="custom">\1</span>')
215
+ # # => Lorem ipsum <strong class="custom">dolor</strong> sit amet
216
+ #
217
+ def highlight(text, words, *args)
218
+ options = args.extract_options!
219
+ options.reverse_merge!(:highlighter => '<strong class="highlight">\1</strong>')
220
+
221
+ if text.blank? || words.blank?
222
+ text
223
+ else
224
+ match = Array(words).map { |p| Regexp.escape(p) }.join('|')
225
+ text.gsub(/(#{match})(?!(?:[^<]*?)(?:["'])[^<>]*>)/i, options[:highlighter])
226
+ end
227
+ end
228
+
229
+ ##
230
+ # Reports the approximate distance in time between two Time or Date objects or integers as seconds.
231
+ # Set +include_seconds+ to true if you want more detailed approximations when distance < 1 min, 29 secs
232
+ # Distances are reported based on the following table:
233
+ #
234
+ # 0 <-> 29 secs # => less than a minute
235
+ # 30 secs <-> 1 min, 29 secs # => 1 minute
236
+ # 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
237
+ # 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour
238
+ # 89 mins, 29 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours
239
+ # 23 hrs, 59 mins, 29 secs <-> 47 hrs, 59 mins, 29 secs # => 1 day
240
+ # 47 hrs, 59 mins, 29 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days
241
+ # 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 1 month
242
+ # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months
243
+ # 1 yr <-> 1 yr, 3 months # => about 1 year
244
+ # 1 yr, 3 months <-> 1 yr, 9 months # => over 1 year
245
+ # 1 yr, 9 months <-> 2 yr minus 1 sec # => almost 2 years
246
+ # 2 yrs <-> max time or date # => (same rules as 1 yr)
247
+ #
248
+ # With +include_seconds+ = true and the difference < 1 minute 29 seconds:
249
+ # 0-4 secs # => less than 5 seconds
250
+ # 5-9 secs # => less than 10 seconds
251
+ # 10-19 secs # => less than 20 seconds
252
+ # 20-39 secs # => half a minute
253
+ # 40-59 secs # => less than a minute
254
+ # 60-89 secs # => 1 minute
255
+ #
256
+ # @param [Time] from_time
257
+ # The time to be compared against +to_time+ in order to approximate the distance.
258
+ # @param [Time] to_time
259
+ # The time to be compared against +from_time+ in order to approximate the distance.
260
+ # @param [Boolean] include_seconds
261
+ # Set true for more detailed approximations.
262
+ # @param [Hash] options
263
+ # Flags for the approximation.
264
+ # @option options [String] :locale
265
+ # The translation locale to be used for approximating the time.
266
+ #
267
+ # @return [String] The time formatted as a relative string.
268
+ #
269
+ # @example
270
+ # from_time = Time.now
271
+ # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour
272
+ # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour
273
+ # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
274
+ # distance_of_time_in_words(from_time, from_time + 15.seconds, true) # => less than 20 seconds
275
+ # distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years
276
+ # distance_of_time_in_words(from_time, from_time + 60.hours) # => about 3 days
277
+ # distance_of_time_in_words(from_time, from_time + 45.seconds, true) # => less than a minute
278
+ # distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute
279
+ # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
280
+ # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
281
+ # distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years
282
+ # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years
283
+ # to_time = Time.now + 6.years + 19.days
284
+ # distance_of_time_in_words(from_time, to_time, true) # => about 6 years
285
+ # distance_of_time_in_words(to_time, from_time, true) # => about 6 years
286
+ # distance_of_time_in_words(Time.now, Time.now) # => less than a minute
287
+ #
288
+ def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {})
289
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
290
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
291
+ distance_in_minutes = (((to_time.to_i - from_time.to_i).abs)/60).round
292
+ distance_in_seconds = ((to_time.to_i - from_time.to_i).abs).round
293
+
294
+ I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
295
+ case distance_in_minutes
296
+ when 0..1
297
+ return distance_in_minutes == 0 ?
298
+ locale.t(:less_than_x_minutes, :count => 1) :
299
+ locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds
300
+
301
+ case distance_in_seconds
302
+ when 0..4 then locale.t :less_than_x_seconds, :count => 5
303
+ when 5..9 then locale.t :less_than_x_seconds, :count => 10
304
+ when 10..19 then locale.t :less_than_x_seconds, :count => 20
305
+ when 20..39 then locale.t :half_a_minute
306
+ when 40..59 then locale.t :less_than_x_minutes, :count => 1
307
+ else locale.t :x_minutes, :count => 1
308
+ end
309
+
310
+ when 2..44 then locale.t :x_minutes, :count => distance_in_minutes
311
+ when 45..89 then locale.t :about_x_hours, :count => 1
312
+ when 90..1439 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round
313
+ when 1440..2529 then locale.t :x_days, :count => 1
314
+ when 2530..43199 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round
315
+ when 43200..86399 then locale.t :about_x_months, :count => 1
316
+ when 86400..525599 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round
317
+ else
318
+ distance_in_years = distance_in_minutes / 525600
319
+ minute_offset_for_leap_year = (distance_in_years / 4) * 1440
320
+ remainder = ((distance_in_minutes - minute_offset_for_leap_year) % 525600)
321
+ if remainder < 131400
322
+ locale.t(:about_x_years, :count => distance_in_years)
323
+ elsif remainder < 394200
324
+ locale.t(:over_x_years, :count => distance_in_years)
325
+ else
326
+ locale.t(:almost_x_years, :count => distance_in_years + 1)
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ ##
333
+ # Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
334
+ #
335
+ # @param [Time] from_time
336
+ # The time to be compared against now in order to approximate the distance.
337
+ # @param [Boolean] include_seconds
338
+ # Set true for more detailed approximations.
339
+ #
340
+ # @return [String] The time formatted as a relative string.
341
+ #
342
+ # @example
343
+ # time_ago_in_words(3.minutes.from_now) # => 3 minutes
344
+ # time_ago_in_words(Time.now - 15.hours) # => 15 hours
345
+ # time_ago_in_words(Time.now) # => less than a minute
346
+ #
347
+ def time_ago_in_words(from_time, include_seconds = false)
348
+ distance_of_time_in_words(from_time, Time.now, include_seconds)
349
+ end
350
+
351
+ ##
352
+ # Used in xxxx.js.erb files to escape html so that it can be passed to javascript from Tennpipes.
353
+ #
354
+ # @param [String] html
355
+ # The HTML content to be escaped into javascript compatible format.
356
+ #
357
+ # @return [String] The html escaped for javascript passing.
358
+ #
359
+ # @example
360
+ # js_escape_html("<h1>Hey</h1>")
361
+ #
362
+ def js_escape_html(html_content)
363
+ return '' unless html_content
364
+ javascript_mapping = { '\\' => '\\\\', '</' => '<\/', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', '"' => '\\"', "'" => "\\'" }
365
+ escaped_content = html_content.gsub(/(\\|<\/|\r\n|[\n\r"'])/){ |m| javascript_mapping[m] }
366
+ escaped_content = escaped_content.html_safe if html_content.html_safe?
367
+ escaped_content
368
+ end
369
+ alias :escape_javascript :js_escape_html
370
+ end
371
+ end
372
+ end