hanami-helpers 0.0.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|