hanami-helpers 0.0.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/LICENSE.md +22 -0
- data/README.md +362 -9
- data/hanami-helpers.gemspec +16 -12
- data/lib/hanami-helpers.rb +1 -0
- data/lib/hanami/helpers.rb +28 -2
- data/lib/hanami/helpers/escape_helper.rb +271 -0
- data/lib/hanami/helpers/form_helper.rb +424 -0
- data/lib/hanami/helpers/form_helper/form_builder.rb +911 -0
- data/lib/hanami/helpers/form_helper/html_node.rb +79 -0
- data/lib/hanami/helpers/form_helper/values.rb +38 -0
- data/lib/hanami/helpers/html_helper.rb +207 -0
- data/lib/hanami/helpers/html_helper/empty_html_node.rb +92 -0
- data/lib/hanami/helpers/html_helper/html_builder.rb +376 -0
- data/lib/hanami/helpers/html_helper/html_fragment.rb +45 -0
- data/lib/hanami/helpers/html_helper/html_node.rb +70 -0
- data/lib/hanami/helpers/html_helper/text_node.rb +33 -0
- data/lib/hanami/helpers/link_to_helper.rb +135 -0
- data/lib/hanami/helpers/number_formatting_helper.rb +220 -0
- data/lib/hanami/helpers/routing_helper.rb +52 -0
- data/lib/hanami/helpers/version.rb +4 -1
- metadata +60 -14
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -0,0 +1,45 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Helpers
|
3
|
+
module HtmlHelper
|
4
|
+
# HTML Fragment
|
5
|
+
#
|
6
|
+
# @since 0.2.6
|
7
|
+
# @api private
|
8
|
+
#
|
9
|
+
# @see Hanami::Helpers::HtmlHelper::HtmlFragment
|
10
|
+
class HtmlFragment
|
11
|
+
# Initialize a HTML Fragment
|
12
|
+
#
|
13
|
+
# @param blk [Proc,Hanami::Helpers::HtmlHelper::HtmlBuilder,NilClass] the content block
|
14
|
+
#
|
15
|
+
# @return [Hanami::Helpers::HtmlHelper::HtmlFragment]
|
16
|
+
def initialize(&blk)
|
17
|
+
@builder = HtmlBuilder.new
|
18
|
+
@blk = blk
|
19
|
+
end
|
20
|
+
|
21
|
+
# Resolve and return the output
|
22
|
+
#
|
23
|
+
# @return [String] the output
|
24
|
+
#
|
25
|
+
# @since 0.2.6
|
26
|
+
# @api private
|
27
|
+
#
|
28
|
+
# @see Hanami::Helpers::HtmlHelper::EmptyHtmlNode#to_s
|
29
|
+
def to_s
|
30
|
+
content.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
def content
|
34
|
+
result = @builder.resolve(&@blk)
|
35
|
+
|
36
|
+
if @builder.nested?
|
37
|
+
@builder.to_s
|
38
|
+
else
|
39
|
+
"#{ Utils::Escape.html(result) }"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'hanami/helpers/html_helper/empty_html_node'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Helpers
|
5
|
+
module HtmlHelper
|
6
|
+
# HTML node
|
7
|
+
#
|
8
|
+
# @since 0.1.0
|
9
|
+
# @api private
|
10
|
+
#
|
11
|
+
# @see Hanami::Helpers::HtmlHelper::EmptyHtmlNode
|
12
|
+
class HtmlNode < EmptyHtmlNode
|
13
|
+
# Initialize a new HTML node
|
14
|
+
#
|
15
|
+
# @param name [Symbol,String] the name of the tag
|
16
|
+
# @param content [String,Proc,Hanami::Helpers::HtmlHelper::HtmlBuilder,NilClass] the optional content
|
17
|
+
# @param attributes [Hash,NilClass] the optional tag attributes
|
18
|
+
# @param options [Hash] a optional set of data
|
19
|
+
#
|
20
|
+
# @return [Hanami::Helpers::HtmlHelper::HtmlNode]
|
21
|
+
def initialize(name, content, attributes, options = {})
|
22
|
+
@builder = HtmlBuilder.new
|
23
|
+
@name = name
|
24
|
+
@content = case content
|
25
|
+
when Hash
|
26
|
+
@attributes = content
|
27
|
+
nil
|
28
|
+
else
|
29
|
+
@attributes = attributes.to_h if attributes.respond_to?(:to_h)
|
30
|
+
content
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Resolve and return the output
|
35
|
+
#
|
36
|
+
# @return [String] the output
|
37
|
+
#
|
38
|
+
# @since 0.1.0
|
39
|
+
# @api private
|
40
|
+
#
|
41
|
+
# @see Hanami::Helpers::HtmlHelper::EmptyHtmlNode#to_s
|
42
|
+
def to_s
|
43
|
+
%(#{ super }#{ content }</#{ @name }>)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
# Resolve the (nested) content
|
48
|
+
#
|
49
|
+
# @return [String] the content
|
50
|
+
#
|
51
|
+
# @since 0.1.0
|
52
|
+
# @api private
|
53
|
+
def content
|
54
|
+
case @content
|
55
|
+
when Proc
|
56
|
+
result = @builder.resolve(&@content)
|
57
|
+
|
58
|
+
if @builder.nested?
|
59
|
+
"\n#{ @builder }\n"
|
60
|
+
else
|
61
|
+
"\n#{ Utils::Escape.html(result) }\n"
|
62
|
+
end
|
63
|
+
else
|
64
|
+
Utils::Escape.html(@content)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Helpers
|
3
|
+
module HtmlHelper
|
4
|
+
# Text node. Allows for text to be inserted between HTML tags.
|
5
|
+
#
|
6
|
+
# @since 0.2.5
|
7
|
+
# @api private
|
8
|
+
class TextNode
|
9
|
+
# Initialize a new text node
|
10
|
+
#
|
11
|
+
# @param content [String,#to_s] The content to be added.
|
12
|
+
#
|
13
|
+
# @return [Hanami::Helpers::HtmlHelper::TextNode]
|
14
|
+
#
|
15
|
+
# @since 0.2.5
|
16
|
+
# @api private
|
17
|
+
def initialize(content)
|
18
|
+
@content = content.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
# Resolve and return the output
|
22
|
+
#
|
23
|
+
# @return [String] the output
|
24
|
+
#
|
25
|
+
# @since 0.2.5
|
26
|
+
# @api private
|
27
|
+
def to_s
|
28
|
+
Utils::Escape.html(@content)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'hanami/helpers/html_helper'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Helpers
|
5
|
+
# LinkTo Helper
|
6
|
+
#
|
7
|
+
# Including <tt>Hanami::Helpers::LinkTo</tt> will include the
|
8
|
+
# <tt>link_to</tt> public method.
|
9
|
+
#
|
10
|
+
# This helper can be used both in views and templates.
|
11
|
+
#
|
12
|
+
# @since 0.2.0
|
13
|
+
module LinkToHelper
|
14
|
+
include Hanami::Helpers::HtmlHelper
|
15
|
+
|
16
|
+
# Generates an anchor tag for the given arguments.
|
17
|
+
#
|
18
|
+
# Contents are automatically escaped.
|
19
|
+
#
|
20
|
+
# @overload link_to(content, url, options)
|
21
|
+
# Use string as content
|
22
|
+
# @param content [String] content used in the a tag
|
23
|
+
# @param url [String] url used in href attribute
|
24
|
+
# @param options [Hash] HTML attributes to pass to the a tag
|
25
|
+
#
|
26
|
+
# @overload link_to(url, options, &blk)
|
27
|
+
# Use block as content
|
28
|
+
# @param url [String] url used in href attribute
|
29
|
+
# @param options [Hash] HTML attributes to pass to the a tag
|
30
|
+
# @param blk [Proc] A block that describes the contents of the a tag
|
31
|
+
#
|
32
|
+
# @return [String] HTML markup for the link
|
33
|
+
#
|
34
|
+
# @raise [ArgumentError] if the signature isn't respected
|
35
|
+
#
|
36
|
+
# @since 0.2.0
|
37
|
+
#
|
38
|
+
# @see Hanami::Helpers::HtmlHelper#html
|
39
|
+
#
|
40
|
+
# @example Both content and URL are strings
|
41
|
+
# <%= link_to('Home', '/') %>
|
42
|
+
# # => <a href="/">Home</a>
|
43
|
+
#
|
44
|
+
# @example Content string with route helper
|
45
|
+
# <%= link_to('Home', routes.path(:home)) %>
|
46
|
+
# # => <a href="/">Home</a>
|
47
|
+
#
|
48
|
+
# @example HTML attributes
|
49
|
+
# <%= link_to('Home', routes.path(:home), class: 'button') %>
|
50
|
+
# # => <a href="/" class="button">Home</a>
|
51
|
+
#
|
52
|
+
# @example Automatic content escape (XSS protection)
|
53
|
+
# <%= link_to(%(<script>alert('xss')</script>), '/') %>
|
54
|
+
# # => <a href="/"><script>alert('xss')</script></a>
|
55
|
+
#
|
56
|
+
# @example Automatic content block escape (XSS protection)
|
57
|
+
# <%=
|
58
|
+
# link_to('/') do
|
59
|
+
# %(<script>alert('xss')</script>)
|
60
|
+
# end
|
61
|
+
# %>
|
62
|
+
# # => <a href="/">\n<script>alert('xss')</script>\n</a>
|
63
|
+
#
|
64
|
+
# @example Content as block with string URL
|
65
|
+
# <%=
|
66
|
+
# link_to('/') do
|
67
|
+
# 'Home'
|
68
|
+
# end
|
69
|
+
# %>
|
70
|
+
# # => <a href="/">Home</a>
|
71
|
+
#
|
72
|
+
# @example Content as block
|
73
|
+
# <%=
|
74
|
+
# link_to(routes.path(:home)) do
|
75
|
+
# 'Home'
|
76
|
+
# end
|
77
|
+
# %>
|
78
|
+
# # => <a href="/">Home</a>
|
79
|
+
#
|
80
|
+
# @example Content as block with HTML attributes
|
81
|
+
# <%=
|
82
|
+
# link_to(routes.path(:home), id: 'home-link') do
|
83
|
+
# 'Home'
|
84
|
+
# end
|
85
|
+
# %>
|
86
|
+
# # => <a href="/" id: 'home-link'>Home</a>
|
87
|
+
#
|
88
|
+
# @example Content as HTML builder block
|
89
|
+
# <%=
|
90
|
+
# link_to(routes.path(:home)) do
|
91
|
+
# strong 'Home'
|
92
|
+
# end
|
93
|
+
# %>
|
94
|
+
# # => <a href="/"><strong>Home</strong></a>
|
95
|
+
#
|
96
|
+
# @example Provides both content as first argument and block
|
97
|
+
# <%=
|
98
|
+
# link_to('Home', routes.path(:home)) do
|
99
|
+
# strong 'Home'
|
100
|
+
# end
|
101
|
+
# %>
|
102
|
+
# # => ArgumentError
|
103
|
+
#
|
104
|
+
# @example Without any argument
|
105
|
+
# <%= link_to %>
|
106
|
+
# # => ArgumentError
|
107
|
+
#
|
108
|
+
# @example Without any argument and empty block
|
109
|
+
# <%=
|
110
|
+
# link_to do
|
111
|
+
# end
|
112
|
+
# %>
|
113
|
+
# # => ArgumentError
|
114
|
+
#
|
115
|
+
# @example With only content
|
116
|
+
# <%= link_to 'Home' %>
|
117
|
+
# # => ArgumentError
|
118
|
+
def link_to(content, url = nil, options = {}, &blk)
|
119
|
+
if block_given?
|
120
|
+
options = url || {}
|
121
|
+
url = content
|
122
|
+
content = nil
|
123
|
+
end
|
124
|
+
|
125
|
+
begin
|
126
|
+
options[:href] = url or raise ArgumentError
|
127
|
+
rescue TypeError
|
128
|
+
raise ArgumentError
|
129
|
+
end
|
130
|
+
|
131
|
+
html.a(blk || content, options).to_s
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Helpers
|
3
|
+
# Number formatter
|
4
|
+
#
|
5
|
+
# You can include this module inside your view and
|
6
|
+
# the view will have access all methods.
|
7
|
+
#
|
8
|
+
# By including <tt>Hanami::Helpers::NumberFormattingHelper</tt> it will
|
9
|
+
# inject private method: <tt>format_number</tt>.
|
10
|
+
#
|
11
|
+
# @since 0.2.0
|
12
|
+
module NumberFormattingHelper
|
13
|
+
private
|
14
|
+
# Format the given number, according to the options
|
15
|
+
#
|
16
|
+
# It accepts a number (<tt>Numeric</tt>) or a string representation.
|
17
|
+
#
|
18
|
+
# If an integer is given, no precision is applied.
|
19
|
+
# For the rest of the numbers, it will format as a float representation.
|
20
|
+
# This is the case of: <tt>Float</tt>, <tt>BigDecimal</tt>,
|
21
|
+
# <tt>Complex</tt>, <tt>Rational</tt>.
|
22
|
+
#
|
23
|
+
# If the argument cannot be coerced into a number, it will raise a
|
24
|
+
# <tt>TypeError</tt>.
|
25
|
+
#
|
26
|
+
# @param number [Numeric,String] the number to be formatted
|
27
|
+
#
|
28
|
+
# @return [String] formatted number
|
29
|
+
#
|
30
|
+
# @raise [TypeError] if number can't be formatted
|
31
|
+
#
|
32
|
+
# @since 0.2.0
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# require 'hanami/helpers/number_formatting_helper'
|
36
|
+
#
|
37
|
+
# class Checkout
|
38
|
+
# include Hanami::Helpers::NumberFormattingHelper
|
39
|
+
#
|
40
|
+
# def total
|
41
|
+
# format_number 1999.99
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# def euros
|
45
|
+
# format_number 1256.95, delimiter: '.', separator: ','
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# def visitors_count
|
49
|
+
# format_number '1000'
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# view = Checkout.new
|
54
|
+
#
|
55
|
+
# view.total
|
56
|
+
# # => "1,999.99"
|
57
|
+
#
|
58
|
+
# view.euros
|
59
|
+
# # => "1.256,95"
|
60
|
+
#
|
61
|
+
# view.visitors_count
|
62
|
+
# # => "1,000"
|
63
|
+
def format_number(number, options = {})
|
64
|
+
Formatter.new(number, options).format
|
65
|
+
end
|
66
|
+
|
67
|
+
# Formatter
|
68
|
+
#
|
69
|
+
# @since 0.2.0
|
70
|
+
# @api private
|
71
|
+
class Formatter
|
72
|
+
# Regex to delimitate integer part of a number
|
73
|
+
#
|
74
|
+
# @return [Regexp] the delimitation regex
|
75
|
+
#
|
76
|
+
# @since 0.2.0
|
77
|
+
# @api private
|
78
|
+
#
|
79
|
+
# @see Hanami::Helpers::NumberFormatter::Formatter#delimitate
|
80
|
+
DELIMITATION_REGEX = /(\d)(?=(\d{3})+$)/
|
81
|
+
|
82
|
+
# Regex to guess if the number is a integer
|
83
|
+
#
|
84
|
+
# @return [Regexp] the guessing regex
|
85
|
+
#
|
86
|
+
# @since 0.2.0
|
87
|
+
# @api private
|
88
|
+
#
|
89
|
+
# @see Hanami::Helpers::NumberFormatter::Formatter#to_number
|
90
|
+
INTEGER_REGEXP = /\A[\d]+\z/
|
91
|
+
|
92
|
+
# Default separator
|
93
|
+
#
|
94
|
+
# @return [String] default separator
|
95
|
+
#
|
96
|
+
# @since 0.2.0
|
97
|
+
# @api private
|
98
|
+
DEFAULT_SEPARATOR = '.'.freeze
|
99
|
+
|
100
|
+
# Default delimiter
|
101
|
+
#
|
102
|
+
# @return [String] default delimiter
|
103
|
+
#
|
104
|
+
# @since 0.2.0
|
105
|
+
# @api private
|
106
|
+
DEFAULT_DELIMITER = ','.freeze
|
107
|
+
|
108
|
+
# Default precision
|
109
|
+
#
|
110
|
+
# @return [String] default precision
|
111
|
+
#
|
112
|
+
# @since 0.2.0
|
113
|
+
# @api private
|
114
|
+
DEFAULT_PRECISION = 2
|
115
|
+
|
116
|
+
# Initialize a new formatter
|
117
|
+
#
|
118
|
+
# @param number [Numeric,String] the number to format
|
119
|
+
# @param options [Hash] options for number formatting
|
120
|
+
# @option options [String] :delimiter hundred delimiter
|
121
|
+
# @option options [String] :separator fractional part delimiter
|
122
|
+
# @option options [Integer] :precision rounding precision
|
123
|
+
#
|
124
|
+
# @since 0.2.0
|
125
|
+
# @api private
|
126
|
+
#
|
127
|
+
# @see Hanami::Helpers::NumberFormatter::Formatter::DEFAULT_DELIMITER
|
128
|
+
# @see Hanami::Helpers::NumberFormatter::Formatter::DEFAULT_SEPARATOR
|
129
|
+
# @see Hanami::Helpers::NumberFormatter::Formatter::DEFAULT_PRECISION
|
130
|
+
def initialize(number, options)
|
131
|
+
@number = number
|
132
|
+
@delimiter = options.fetch(:delimiter, DEFAULT_DELIMITER)
|
133
|
+
@separator = options.fetch(:separator, DEFAULT_SEPARATOR)
|
134
|
+
@precision = options.fetch(:precision, DEFAULT_PRECISION)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Format number according to the specified options
|
138
|
+
#
|
139
|
+
# @return [String] formatted number
|
140
|
+
#
|
141
|
+
# @raise [TypeError] if number can't be formatted
|
142
|
+
#
|
143
|
+
# @since 0.2.0
|
144
|
+
# @api private
|
145
|
+
def format
|
146
|
+
parts.join(@separator)
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
# Return integer and fractional parts
|
152
|
+
#
|
153
|
+
# @return [Array] parts
|
154
|
+
#
|
155
|
+
# @since 0.2.0
|
156
|
+
# @api private
|
157
|
+
def parts
|
158
|
+
integer_part, fractional_part = to_str.split(DEFAULT_SEPARATOR)
|
159
|
+
[delimitate(integer_part), fractional_part].compact
|
160
|
+
end
|
161
|
+
|
162
|
+
# Delimitate the given part
|
163
|
+
#
|
164
|
+
# @return [String] delimitated string
|
165
|
+
#
|
166
|
+
# @since 0.2.0
|
167
|
+
# @api private
|
168
|
+
def delimitate(part)
|
169
|
+
part.gsub(DELIMITATION_REGEX) { |digit| "#{digit}#{@delimiter}" }
|
170
|
+
end
|
171
|
+
|
172
|
+
# String coercion
|
173
|
+
#
|
174
|
+
# @return [String] coerced number
|
175
|
+
#
|
176
|
+
# @raise [TypeError] if number can't be formatted
|
177
|
+
#
|
178
|
+
# @since 0.2.0
|
179
|
+
# @api private
|
180
|
+
def to_str
|
181
|
+
to_number.to_s
|
182
|
+
end
|
183
|
+
|
184
|
+
# Numeric coercion
|
185
|
+
#
|
186
|
+
# @return [Numeric] coerced number
|
187
|
+
#
|
188
|
+
# @raise [TypeError] if number can't be formatted
|
189
|
+
#
|
190
|
+
# @since 0.2.0
|
191
|
+
# @api private
|
192
|
+
def to_number
|
193
|
+
case @number
|
194
|
+
when NilClass
|
195
|
+
raise TypeError
|
196
|
+
when ->(n) { n.to_s.match(INTEGER_REGEXP) }
|
197
|
+
Utils::Kernel.Integer(@number)
|
198
|
+
else
|
199
|
+
Utils::Kernel.Float(rounded_number)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Round number in case we need to return a <tt>Float</tt> representation.
|
204
|
+
# If <tt>@number</tt> doesn't respond to <tt>#round</tt> return the number as it is.
|
205
|
+
#
|
206
|
+
# @return [Float,Complex,Rational,BigDecimal] rounded number, if applicable
|
207
|
+
#
|
208
|
+
# @since 0.2.0
|
209
|
+
# @api private
|
210
|
+
def rounded_number
|
211
|
+
if @number.respond_to?(:round)
|
212
|
+
@number.round(@precision)
|
213
|
+
else
|
214
|
+
@number
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|