tennpipes-helper 3.6.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +239 -0
- data/Rakefile +1 -0
- data/lib/tennpipes-helper.rb +62 -0
- data/lib/tennpipes-helper/asset_tag_helpers.rb +394 -0
- data/lib/tennpipes-helper/form_builder/abstract_form_builder.rb +279 -0
- data/lib/tennpipes-helper/form_builder/standard_form_builder.rb +40 -0
- data/lib/tennpipes-helper/form_helpers.rb +639 -0
- data/lib/tennpipes-helper/form_helpers/errors.rb +138 -0
- data/lib/tennpipes-helper/form_helpers/options.rb +98 -0
- data/lib/tennpipes-helper/form_helpers/security.rb +70 -0
- data/lib/tennpipes-helper/format_helpers.rb +372 -0
- data/lib/tennpipes-helper/locale/cs.yml +103 -0
- data/lib/tennpipes-helper/locale/da.yml +91 -0
- data/lib/tennpipes-helper/locale/de.yml +81 -0
- data/lib/tennpipes-helper/locale/en.yml +103 -0
- data/lib/tennpipes-helper/locale/es.yml +103 -0
- data/lib/tennpipes-helper/locale/fr.yml +79 -0
- data/lib/tennpipes-helper/locale/hu.yml +103 -0
- data/lib/tennpipes-helper/locale/it.yml +89 -0
- data/lib/tennpipes-helper/locale/ja.yml +103 -0
- data/lib/tennpipes-helper/locale/lv.yml +103 -0
- data/lib/tennpipes-helper/locale/nl.yml +82 -0
- data/lib/tennpipes-helper/locale/no.yml +91 -0
- data/lib/tennpipes-helper/locale/pl.yml +95 -0
- data/lib/tennpipes-helper/locale/pt_br.yml +103 -0
- data/lib/tennpipes-helper/locale/ro.yml +103 -0
- data/lib/tennpipes-helper/locale/ru.yml +103 -0
- data/lib/tennpipes-helper/locale/sv.yml +103 -0
- data/lib/tennpipes-helper/locale/tr.yml +103 -0
- data/lib/tennpipes-helper/locale/uk.yml +103 -0
- data/lib/tennpipes-helper/locale/zh_cn.yml +103 -0
- data/lib/tennpipes-helper/locale/zh_tw.yml +103 -0
- data/lib/tennpipes-helper/number_helpers.rb +283 -0
- data/lib/tennpipes-helper/output_helpers.rb +226 -0
- data/lib/tennpipes-helper/output_helpers/abstract_handler.rb +61 -0
- data/lib/tennpipes-helper/output_helpers/erb_handler.rb +27 -0
- data/lib/tennpipes-helper/output_helpers/haml_handler.rb +25 -0
- data/lib/tennpipes-helper/output_helpers/slim_handler.rb +18 -0
- data/lib/tennpipes-helper/render_helpers.rb +63 -0
- data/lib/tennpipes-helper/tag_helpers.rb +294 -0
- data/lib/tennpipes-helper/translation_helpers.rb +36 -0
- data/lib/tennpipes/rendering.rb +369 -0
- data/lib/tennpipes/rendering/erb_template.rb +40 -0
- data/lib/tennpipes/rendering/erubis_template.rb +66 -0
- data/lib/tennpipes/rendering/haml_template.rb +26 -0
- data/lib/tennpipes/rendering/slim_template.rb +20 -0
- data/test/fixtures/apps/render.rb +25 -0
- data/test/fixtures/apps/views/article/comment/show.slim +1 -0
- data/test/fixtures/apps/views/blog/post.erb +1 -0
- data/test/fixtures/apps/views/layouts/specific.erb +1 -0
- data/test/fixtures/apps/views/test/post.erb +1 -0
- data/test/fixtures/layouts/layout.erb +1 -0
- data/test/fixtures/markup_app/app.rb +87 -0
- data/test/fixtures/markup_app/views/button_to.erb +8 -0
- data/test/fixtures/markup_app/views/button_to.haml +5 -0
- data/test/fixtures/markup_app/views/button_to.slim +6 -0
- data/test/fixtures/markup_app/views/capture_concat.erb +14 -0
- data/test/fixtures/markup_app/views/capture_concat.haml +12 -0
- data/test/fixtures/markup_app/views/capture_concat.slim +12 -0
- data/test/fixtures/markup_app/views/content_for.erb +23 -0
- data/test/fixtures/markup_app/views/content_for.haml +19 -0
- data/test/fixtures/markup_app/views/content_for.slim +19 -0
- data/test/fixtures/markup_app/views/content_tag.erb +13 -0
- data/test/fixtures/markup_app/views/content_tag.haml +11 -0
- data/test/fixtures/markup_app/views/content_tag.slim +11 -0
- data/test/fixtures/markup_app/views/current_engine.erb +5 -0
- data/test/fixtures/markup_app/views/current_engine.haml +5 -0
- data/test/fixtures/markup_app/views/current_engine.slim +5 -0
- data/test/fixtures/markup_app/views/fields_for.erb +20 -0
- data/test/fixtures/markup_app/views/fields_for.haml +15 -0
- data/test/fixtures/markup_app/views/fields_for.slim +15 -0
- data/test/fixtures/markup_app/views/form_for.erb +72 -0
- data/test/fixtures/markup_app/views/form_for.haml +59 -0
- data/test/fixtures/markup_app/views/form_for.slim +59 -0
- data/test/fixtures/markup_app/views/form_tag.erb +95 -0
- data/test/fixtures/markup_app/views/form_tag.haml +78 -0
- data/test/fixtures/markup_app/views/form_tag.slim +79 -0
- data/test/fixtures/markup_app/views/link_to.erb +5 -0
- data/test/fixtures/markup_app/views/link_to.haml +4 -0
- data/test/fixtures/markup_app/views/link_to.slim +4 -0
- data/test/fixtures/markup_app/views/mail_to.erb +3 -0
- data/test/fixtures/markup_app/views/mail_to.haml +3 -0
- data/test/fixtures/markup_app/views/mail_to.slim +3 -0
- data/test/fixtures/markup_app/views/meta_tag.erb +3 -0
- data/test/fixtures/markup_app/views/meta_tag.haml +3 -0
- data/test/fixtures/markup_app/views/meta_tag.slim +3 -0
- data/test/fixtures/markup_app/views/partials/_erb.erb +1 -0
- data/test/fixtures/markup_app/views/partials/_haml.haml +1 -0
- data/test/fixtures/markup_app/views/partials/_slim.slim +1 -0
- data/test/fixtures/markup_app/views/simple_partial.erb +1 -0
- data/test/fixtures/markup_app/views/simple_partial.haml +1 -0
- data/test/fixtures/markup_app/views/simple_partial.slim +1 -0
- data/test/fixtures/render_app/app.rb +110 -0
- data/test/fixtures/render_app/views/_deep.erb +3 -0
- data/test/fixtures/render_app/views/_deep.haml +2 -0
- data/test/fixtures/render_app/views/_deep.slim +2 -0
- data/test/fixtures/render_app/views/_partial_block_erb.erb +10 -0
- data/test/fixtures/render_app/views/_partial_block_haml.haml +7 -0
- data/test/fixtures/render_app/views/_partial_block_slim.slim +7 -0
- data/test/fixtures/render_app/views/_unsafe.html.builder +2 -0
- data/test/fixtures/render_app/views/_unsafe_object.html.builder +2 -0
- data/test/fixtures/render_app/views/current_engine.haml +5 -0
- data/test/fixtures/render_app/views/current_engines/_erb.erb +1 -0
- data/test/fixtures/render_app/views/current_engines/_haml.haml +1 -0
- data/test/fixtures/render_app/views/current_engines/_slim.slim +1 -0
- data/test/fixtures/render_app/views/dive_inner_erb.erb +3 -0
- data/test/fixtures/render_app/views/dive_inner_haml.haml +2 -0
- data/test/fixtures/render_app/views/dive_inner_slim.slim +2 -0
- data/test/fixtures/render_app/views/dive_outer_erb.erb +3 -0
- data/test/fixtures/render_app/views/dive_outer_haml.haml +2 -0
- data/test/fixtures/render_app/views/dive_outer_slim.slim +2 -0
- data/test/fixtures/render_app/views/double_capture_erb.erb +3 -0
- data/test/fixtures/render_app/views/double_capture_haml.haml +2 -0
- data/test/fixtures/render_app/views/double_capture_slim.slim +2 -0
- data/test/fixtures/render_app/views/erb/test.erb +1 -0
- data/test/fixtures/render_app/views/explicit_engine.haml +5 -0
- data/test/fixtures/render_app/views/haml/test.haml +1 -0
- data/test/fixtures/render_app/views/render_block_erb.erb +5 -0
- data/test/fixtures/render_app/views/render_block_haml.haml +4 -0
- data/test/fixtures/render_app/views/render_block_slim.slim +4 -0
- data/test/fixtures/render_app/views/ruby_block_capture_erb.erb +1 -0
- data/test/fixtures/render_app/views/ruby_block_capture_haml.haml +1 -0
- data/test/fixtures/render_app/views/ruby_block_capture_slim.slim +1 -0
- data/test/fixtures/render_app/views/template/_user.haml +7 -0
- data/test/fixtures/render_app/views/template/haml_template.haml +1 -0
- data/test/fixtures/render_app/views/template/some_template.haml +2 -0
- data/test/fixtures/render_app/views/wrong_capture_erb.erb +3 -0
- data/test/fixtures/render_app/views/wrong_capture_haml.haml +2 -0
- data/test/fixtures/render_app/views/wrong_capture_slim.slim +2 -0
- data/test/helper.rb +88 -0
- data/test/test_asset_tag_helpers.rb +401 -0
- data/test/test_form_builder.rb +1216 -0
- data/test/test_form_helpers.rb +1056 -0
- data/test/test_format_helpers.rb +251 -0
- data/test/test_helpers.rb +10 -0
- data/test/test_locale.rb +20 -0
- data/test/test_number_helpers.rb +142 -0
- data/test/test_output_helpers.rb +157 -0
- data/test/test_render_helpers.rb +225 -0
- data/test/test_rendering.rb +706 -0
- data/test/test_rendering_extensions.rb +14 -0
- data/test/test_tag_helpers.rb +131 -0
- 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>") => "<b>Hey<b;gt;"
|
18
|
+
# h("Me & Bob") => "Me & 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 & Bob"
|
38
|
+
# h!("", "Whoops") => "Whoops"
|
39
|
+
#
|
40
|
+
def h!(text, blank_text = ' ')
|
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
|