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,79 @@
|
|
1
|
+
require 'hanami/helpers/html_helper/html_node'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Helpers
|
5
|
+
module FormHelper
|
6
|
+
# HTML form node
|
7
|
+
#
|
8
|
+
# @since 0.2.0
|
9
|
+
# @api private
|
10
|
+
#
|
11
|
+
# @see Hanami::Helpers::HtmlHelper::HtmlNode
|
12
|
+
class HtmlNode < ::Hanami::Helpers::HtmlHelper::HtmlNode
|
13
|
+
# Initialize a new HTML form node
|
14
|
+
#
|
15
|
+
# @param name [Symbol,String] the name of the tag
|
16
|
+
# @param content [String,Proc,Hanami::Helpers::HtmlHelper::FormBuilder,NilClass] the optional content
|
17
|
+
# @param attributes [Hash,NilClass] the optional tag attributes
|
18
|
+
# @param options [Hash] a set of data
|
19
|
+
#
|
20
|
+
# @return [Hanami::Helpers::FormHelper::HtmlNode]
|
21
|
+
#
|
22
|
+
# @since 0.2.0
|
23
|
+
# @api private
|
24
|
+
def initialize(name, content, attributes, options)
|
25
|
+
super
|
26
|
+
|
27
|
+
@verb = options.fetch(:verb, nil)
|
28
|
+
@csrf_token = options.fetch(:csrf_token, nil)
|
29
|
+
|
30
|
+
@builder = FormBuilder.new(
|
31
|
+
options.fetch(:name),
|
32
|
+
options.fetch(:values)
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
# Resolve the (nested) content
|
38
|
+
#
|
39
|
+
# @return [String] the content
|
40
|
+
#
|
41
|
+
# @since 0.2.0
|
42
|
+
# @api private
|
43
|
+
#
|
44
|
+
# @see Hanami::Helpers::HtmlHelper::HtmlNode#content
|
45
|
+
def content
|
46
|
+
_method_override!
|
47
|
+
_csrf_protection!
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
# Inject a hidden field to make Method Override possible
|
52
|
+
#
|
53
|
+
# @since 0.2.0
|
54
|
+
# @api private
|
55
|
+
def _method_override!
|
56
|
+
return if @verb.nil?
|
57
|
+
|
58
|
+
verb = @verb
|
59
|
+
@builder.resolve do
|
60
|
+
input(type: :hidden, name: :_method, value: verb)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Inject a hidden field for CSRF Protection token
|
65
|
+
#
|
66
|
+
# @since 0.2.0
|
67
|
+
# @api private
|
68
|
+
def _csrf_protection!
|
69
|
+
return if @csrf_token.nil?
|
70
|
+
|
71
|
+
_csrf_token = @csrf_token
|
72
|
+
@builder.resolve do
|
73
|
+
input(type: :hidden, name: FormHelper::CSRF_TOKEN, value: _csrf_token)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'hanami/utils/hash'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Helpers
|
5
|
+
module FormHelper
|
6
|
+
class Values
|
7
|
+
GET_SEPARATOR = '.'.freeze
|
8
|
+
|
9
|
+
def initialize(values, params)
|
10
|
+
@values = Utils::Hash.new(values).stringify!
|
11
|
+
@params = params
|
12
|
+
end
|
13
|
+
|
14
|
+
def get(key)
|
15
|
+
@params.get(key) || _get_from_values(key)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def _get_from_values(key)
|
20
|
+
initial_key, *keys = key.to_s.split(GET_SEPARATOR)
|
21
|
+
result = @values[initial_key]
|
22
|
+
|
23
|
+
Array(keys).each do |k|
|
24
|
+
break if result.nil?
|
25
|
+
|
26
|
+
result = if result.respond_to?(k)
|
27
|
+
result.public_send(k)
|
28
|
+
else
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
result
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'hanami/helpers/html_helper/html_builder'
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
module Helpers
|
5
|
+
# HTML builder
|
6
|
+
#
|
7
|
+
# By including <tt>Hanami::Helpers::HtmlHelper</tt> it will inject one private method: <tt>html</tt>.
|
8
|
+
# This is a HTML5 markup builder.
|
9
|
+
#
|
10
|
+
# Features:
|
11
|
+
# * Support for complex markup without the need of concatenation
|
12
|
+
# * Auto closing HTML5 tags
|
13
|
+
# * Custom tags
|
14
|
+
# * Content tag auto escape (XSS protection)
|
15
|
+
# * Support for view local variables
|
16
|
+
#
|
17
|
+
# Usage:
|
18
|
+
#
|
19
|
+
# * It knows how to close tags according to HTML5 spec (1)
|
20
|
+
# * It accepts content as first argument (2)
|
21
|
+
# * It accepts another builder as first argument (3)
|
22
|
+
# * It accepts content as block which returns a string (4)
|
23
|
+
# * It accepts content as a block with nested markup builders (5)
|
24
|
+
# * It builds attributes from given hash (6)
|
25
|
+
# * It combines attributes and block (7)
|
26
|
+
#
|
27
|
+
# @since 0.1.0
|
28
|
+
#
|
29
|
+
# @see Hanami::Helpers::HtmlHelper#html
|
30
|
+
#
|
31
|
+
# @example Usage
|
32
|
+
# # 1
|
33
|
+
# html.div # => <div></div>
|
34
|
+
# html.img # => <img>
|
35
|
+
#
|
36
|
+
# # 2
|
37
|
+
# html.div('hello') # => <div>hello</div>
|
38
|
+
#
|
39
|
+
# # 3
|
40
|
+
# html.div(html.p('hello')) # => <div><p>hello</p></div>
|
41
|
+
#
|
42
|
+
# # 4
|
43
|
+
# html.div { 'hello' }
|
44
|
+
# # =>
|
45
|
+
# #<div>
|
46
|
+
# # hello
|
47
|
+
# #</div>
|
48
|
+
#
|
49
|
+
# # 5
|
50
|
+
# html.div do
|
51
|
+
# p 'hello'
|
52
|
+
# end
|
53
|
+
# # =>
|
54
|
+
# #<div>
|
55
|
+
# # <p>hello</p>
|
56
|
+
# #</div>
|
57
|
+
#
|
58
|
+
# # 6
|
59
|
+
# html.div('hello', id: 'el', 'data-x': 'y') # => <div id="el" data-x="y">hello</div>
|
60
|
+
#
|
61
|
+
# # 7
|
62
|
+
# html.div(id: 'yay') { 'hello' }
|
63
|
+
# # =>
|
64
|
+
# #<div id="yay">
|
65
|
+
# # hello
|
66
|
+
# #</div>
|
67
|
+
#
|
68
|
+
# # 8
|
69
|
+
# html do
|
70
|
+
# li 'Hello'
|
71
|
+
# li 'Hanami'
|
72
|
+
# end
|
73
|
+
# # =>
|
74
|
+
# #<li>Hello</li>
|
75
|
+
# #<li>Hanami</li>
|
76
|
+
#
|
77
|
+
#
|
78
|
+
#
|
79
|
+
# @example Complex markup
|
80
|
+
# #
|
81
|
+
# # NOTICE THE LACK OF CONCATENATION BETWEEN div AND input BLOCKS <3
|
82
|
+
# #
|
83
|
+
#
|
84
|
+
# html.form(action: '/users', method: 'POST') do
|
85
|
+
# div do
|
86
|
+
# label 'First name', for: 'user-first-name'
|
87
|
+
# input type: 'text', id: 'user-first-name', name: 'user[first_name]', value: 'L'
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# input type: 'submit', value: 'Save changes'
|
91
|
+
# end
|
92
|
+
# # =>
|
93
|
+
# #<form action="/users" method="POST" accept-charset="utf-8">
|
94
|
+
# # <div>
|
95
|
+
# # <label for="user-first-name">First name</label>
|
96
|
+
# # <input type="text" id="user-first-name" name="user[first_name]" value="L">
|
97
|
+
# # </div>
|
98
|
+
# # <input type="submit" value="Save changes">
|
99
|
+
# #</form>
|
100
|
+
#
|
101
|
+
#
|
102
|
+
#
|
103
|
+
# @example Custom tags
|
104
|
+
# html.tag(:custom, 'Foo', id: 'next') # => <custom id="next">Foo</custom>
|
105
|
+
# html.empty_tag(:xr, id: 'next') # => <xr id="next">
|
106
|
+
#
|
107
|
+
#
|
108
|
+
#
|
109
|
+
# @example Auto escape
|
110
|
+
# html.div('hello') # => <div>hello</hello>
|
111
|
+
# html.div { 'hello' } # => <div>hello</hello>
|
112
|
+
# html.div(html.p('hello')) # => <div><p>hello</p></hello>
|
113
|
+
# html.div do
|
114
|
+
# p 'hello'
|
115
|
+
# end # => <div><p>hello</p></hello>
|
116
|
+
#
|
117
|
+
#
|
118
|
+
#
|
119
|
+
# html.div("<script>alert('xss')</script>")
|
120
|
+
# # => "<div><script>alert('xss')</script></div>"
|
121
|
+
#
|
122
|
+
# html.div { "<script>alert('xss')</script>" }
|
123
|
+
# # => "<div><script>alert('xss')</script></div>"
|
124
|
+
#
|
125
|
+
# html.div(html.p("<script>alert('xss')</script>"))
|
126
|
+
# # => "<div><p><script>alert('xss')</script></p></div>"
|
127
|
+
#
|
128
|
+
# html.div do
|
129
|
+
# p "<script>alert('xss')</script>"
|
130
|
+
# end
|
131
|
+
# # => "<div><p><script>alert('xss')</script></p></div>"
|
132
|
+
#
|
133
|
+
#
|
134
|
+
# @example Basic usage
|
135
|
+
# #
|
136
|
+
# # THE VIEW CAN BE A SIMPLE RUBY OBJECT
|
137
|
+
# #
|
138
|
+
#
|
139
|
+
# require 'hanami/helpers'
|
140
|
+
#
|
141
|
+
# class MyView
|
142
|
+
# include Hanami::Helpers::HtmlHelper
|
143
|
+
#
|
144
|
+
# # Generates
|
145
|
+
# # <aside id="sidebar">
|
146
|
+
# # <div>hello</hello>
|
147
|
+
# # </aside>
|
148
|
+
# def sidebar
|
149
|
+
# html.aside(id: 'sidebar') do
|
150
|
+
# div 'hello'
|
151
|
+
# end
|
152
|
+
# end
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
#
|
156
|
+
# @example View context
|
157
|
+
# #
|
158
|
+
# # LOCAL VARIABLES FROM VIEWS ARE AVAILABLE INSIDE THE NESTED BLOCKS OF HTML BUILDER
|
159
|
+
# #
|
160
|
+
#
|
161
|
+
# require 'hanami/view'
|
162
|
+
# require 'hanami/helpers'
|
163
|
+
#
|
164
|
+
# Book = Struct.new(:title)
|
165
|
+
#
|
166
|
+
# module Books
|
167
|
+
# class Show
|
168
|
+
# include Hanami::View
|
169
|
+
# include Hanami::Helpers::HtmlHelper
|
170
|
+
#
|
171
|
+
# def title_widget
|
172
|
+
# html.div do
|
173
|
+
# h1 book.title
|
174
|
+
# end
|
175
|
+
# end
|
176
|
+
# end
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# book = Book.new('The Work of Art in the Age of Mechanical Reproduction')
|
180
|
+
# rendered = Books::Show.render(format: :html, book: book)
|
181
|
+
#
|
182
|
+
# rendered
|
183
|
+
# # => <div>
|
184
|
+
# # <h1>The Work of Art in the Age of Mechanical Reproduction</h1>
|
185
|
+
# # </div>
|
186
|
+
module HtmlHelper
|
187
|
+
private
|
188
|
+
# Instantiate an HTML builder
|
189
|
+
#
|
190
|
+
# @param blk [Proc,Hanami::Helpers::HtmlHelper::HtmlBuilder,NilClass] the optional content block
|
191
|
+
#
|
192
|
+
# @return [Hanami::Helpers::HtmlHelper::HtmlBuilder] the HTML builder
|
193
|
+
#
|
194
|
+
# @since 0.1.0
|
195
|
+
#
|
196
|
+
# @see Hanami::Helpers::HtmlHelper
|
197
|
+
# @see Hanami::Helpers::HtmlHelper::HtmlBuilder
|
198
|
+
def html(&blk)
|
199
|
+
if block_given?
|
200
|
+
HtmlBuilder.new.fragment(&blk)
|
201
|
+
else
|
202
|
+
HtmlBuilder.new
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Hanami
|
2
|
+
module Helpers
|
3
|
+
module HtmlHelper
|
4
|
+
# Empty HTML node
|
5
|
+
#
|
6
|
+
# @since 0.1.0
|
7
|
+
# @api private
|
8
|
+
class EmptyHtmlNode
|
9
|
+
# List of attributes that get special treatment when rendering.
|
10
|
+
#
|
11
|
+
# @since 0.2.5
|
12
|
+
# @api private
|
13
|
+
#
|
14
|
+
# @see http://www.w3.org/html/wg/drafts/html/master/infrastructure.html#boolean-attribute
|
15
|
+
BOOLEAN_ATTRIBUTES = %w{allowfullscreen async autobuffer autofocus
|
16
|
+
autoplay checked compact controls declare default defaultchecked
|
17
|
+
defaultmuted defaultselected defer disabled draggable enabled
|
18
|
+
formnovalidate hidden indeterminate inert ismap itemscope loop
|
19
|
+
multiple muted nohref noresize noshade novalidate nowrap open
|
20
|
+
pauseonexit pubdate readonly required reversed scoped seamless
|
21
|
+
selected sortable spellcheck translate truespeed typemustmatch
|
22
|
+
visible
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
# Attributes separator
|
26
|
+
#
|
27
|
+
# @since 0.1.0
|
28
|
+
# @api private
|
29
|
+
ATTRIBUTES_SEPARATOR = ' '.freeze
|
30
|
+
|
31
|
+
# Initialize a new empty HTML node
|
32
|
+
#
|
33
|
+
# @param name [Symbol,String] the name of the tag
|
34
|
+
# @param attributes [Hash,NilClass] the optional tag attributes
|
35
|
+
#
|
36
|
+
# @return [Hanami::Helpers::HtmlHelper::EmptyHtmlNode]
|
37
|
+
#
|
38
|
+
# @since 0.1.0
|
39
|
+
# @api private
|
40
|
+
def initialize(name, attributes)
|
41
|
+
@name = name
|
42
|
+
@attributes = attributes
|
43
|
+
end
|
44
|
+
|
45
|
+
# Resolve and return the output
|
46
|
+
#
|
47
|
+
# @return [String] the output
|
48
|
+
#
|
49
|
+
# @since 0.1.0
|
50
|
+
# @api private
|
51
|
+
def to_s
|
52
|
+
%(<#{ @name }#{attributes}>)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
# Resolve the attributes
|
57
|
+
#
|
58
|
+
# @return [String,NilClass] the tag attributes
|
59
|
+
#
|
60
|
+
# @since 0.1.0
|
61
|
+
# @api private
|
62
|
+
def attributes
|
63
|
+
return if @attributes.nil?
|
64
|
+
result = ''
|
65
|
+
|
66
|
+
@attributes.each do |attribute_name, value|
|
67
|
+
if boolean_attribute?(attribute_name)
|
68
|
+
result << boolean_attribute(attribute_name, value) if value
|
69
|
+
else
|
70
|
+
result << attribute(attribute_name, value)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
def boolean_attribute?(attribute_name)
|
78
|
+
BOOLEAN_ATTRIBUTES.include?(attribute_name.to_s)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Do not render boolean attributes when their value is _false_.
|
82
|
+
def boolean_attribute(attribute_name, value)
|
83
|
+
%(#{ATTRIBUTES_SEPARATOR}#{ attribute_name }="#{ attribute_name }")
|
84
|
+
end
|
85
|
+
|
86
|
+
def attribute(attribute_name, value)
|
87
|
+
%(#{ATTRIBUTES_SEPARATOR}#{ attribute_name }="#{ value }")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,376 @@
|
|
1
|
+
require 'hanami/utils' # RUBY_VERSION >= '2.2'
|
2
|
+
require 'hanami/utils/class_attribute'
|
3
|
+
require 'hanami/utils/escape'
|
4
|
+
require 'hanami/helpers/html_helper/empty_html_node'
|
5
|
+
require 'hanami/helpers/html_helper/html_node'
|
6
|
+
require 'hanami/helpers/html_helper/html_fragment'
|
7
|
+
require 'hanami/helpers/html_helper/text_node'
|
8
|
+
|
9
|
+
module Hanami
|
10
|
+
module Helpers
|
11
|
+
module HtmlHelper
|
12
|
+
# HTML Builder
|
13
|
+
#
|
14
|
+
# @since 0.1.0
|
15
|
+
class HtmlBuilder
|
16
|
+
# HTML5 content tags
|
17
|
+
#
|
18
|
+
# @since 0.1.0
|
19
|
+
# @api private
|
20
|
+
#
|
21
|
+
# @see Hanami::Helpers::HtmlHelper::HtmlNode
|
22
|
+
# @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
|
23
|
+
CONTENT_TAGS = [
|
24
|
+
'a',
|
25
|
+
'abbr',
|
26
|
+
'address',
|
27
|
+
'article',
|
28
|
+
'aside',
|
29
|
+
'audio',
|
30
|
+
'b',
|
31
|
+
'bdi',
|
32
|
+
'bdo',
|
33
|
+
'blockquote',
|
34
|
+
'body',
|
35
|
+
'button',
|
36
|
+
'canvas',
|
37
|
+
'caption',
|
38
|
+
'cite',
|
39
|
+
'code',
|
40
|
+
'colgroup',
|
41
|
+
'data',
|
42
|
+
'datalist',
|
43
|
+
'del',
|
44
|
+
'details',
|
45
|
+
'dfn',
|
46
|
+
'div',
|
47
|
+
'dl',
|
48
|
+
'dt',
|
49
|
+
'dd',
|
50
|
+
'em',
|
51
|
+
'fieldset',
|
52
|
+
'figcaption',
|
53
|
+
'figure',
|
54
|
+
'footer',
|
55
|
+
'form',
|
56
|
+
'h1',
|
57
|
+
'h2',
|
58
|
+
'h3',
|
59
|
+
'h4',
|
60
|
+
'h5',
|
61
|
+
'h6',
|
62
|
+
'head',
|
63
|
+
'header',
|
64
|
+
'i',
|
65
|
+
'iframe',
|
66
|
+
'ins',
|
67
|
+
'kbd',
|
68
|
+
'label',
|
69
|
+
'legend',
|
70
|
+
'li',
|
71
|
+
'link',
|
72
|
+
'main',
|
73
|
+
'map',
|
74
|
+
'mark',
|
75
|
+
'math',
|
76
|
+
'menu',
|
77
|
+
'meter',
|
78
|
+
'nav',
|
79
|
+
'noscript',
|
80
|
+
'object',
|
81
|
+
'ol',
|
82
|
+
'optgroup',
|
83
|
+
'option',
|
84
|
+
'output',
|
85
|
+
'p',
|
86
|
+
'pre',
|
87
|
+
'progress',
|
88
|
+
'q',
|
89
|
+
'rp',
|
90
|
+
'rt',
|
91
|
+
'ruby',
|
92
|
+
's',
|
93
|
+
'samp',
|
94
|
+
'script',
|
95
|
+
'section',
|
96
|
+
'select',
|
97
|
+
'small',
|
98
|
+
'span',
|
99
|
+
'strong',
|
100
|
+
'style',
|
101
|
+
'sub',
|
102
|
+
'summary',
|
103
|
+
'sup',
|
104
|
+
'svg',
|
105
|
+
'table',
|
106
|
+
'tbody',
|
107
|
+
'td',
|
108
|
+
'template',
|
109
|
+
'textarea',
|
110
|
+
'tfoot',
|
111
|
+
'th',
|
112
|
+
'thead',
|
113
|
+
'time',
|
114
|
+
'title',
|
115
|
+
'tr',
|
116
|
+
'u',
|
117
|
+
'ul',
|
118
|
+
'video',
|
119
|
+
].freeze
|
120
|
+
|
121
|
+
# HTML5 empty tags
|
122
|
+
#
|
123
|
+
# @since 0.1.0
|
124
|
+
# @api private
|
125
|
+
#
|
126
|
+
# @see Hanami::Helpers::HtmlHelper::EmptyHtmlNode
|
127
|
+
# @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
|
128
|
+
EMPTY_TAGS = [
|
129
|
+
'area',
|
130
|
+
'base',
|
131
|
+
'br',
|
132
|
+
'col',
|
133
|
+
'embed',
|
134
|
+
'hr',
|
135
|
+
'img',
|
136
|
+
'input',
|
137
|
+
'keygen',
|
138
|
+
'link',
|
139
|
+
'menuitem',
|
140
|
+
'meta',
|
141
|
+
'param',
|
142
|
+
'source',
|
143
|
+
'track',
|
144
|
+
'wbr',
|
145
|
+
].freeze
|
146
|
+
|
147
|
+
# New line separator
|
148
|
+
#
|
149
|
+
# @since 0.1.0
|
150
|
+
# @api private
|
151
|
+
NEWLINE = "\n".freeze
|
152
|
+
|
153
|
+
CONTENT_TAGS.each do |tag|
|
154
|
+
class_eval %{
|
155
|
+
def #{ tag }(content = nil, attributes = nil, &blk)
|
156
|
+
@nodes << self.class.html_node.new(:#{ tag }, blk || content, attributes || content, options)
|
157
|
+
self
|
158
|
+
end
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
EMPTY_TAGS.each do |tag|
|
163
|
+
class_eval %{
|
164
|
+
def #{ tag }(attributes = nil)
|
165
|
+
@nodes << EmptyHtmlNode.new(:#{ tag }, attributes)
|
166
|
+
self
|
167
|
+
end
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
include Utils::ClassAttribute
|
172
|
+
|
173
|
+
class_attribute :html_node
|
174
|
+
self.html_node = ::Hanami::Helpers::HtmlHelper::HtmlNode
|
175
|
+
|
176
|
+
# Initialize a new builder
|
177
|
+
#
|
178
|
+
# @return [Hanami::Helpers::HtmlHelper::HtmlBuilder] the builder
|
179
|
+
#
|
180
|
+
# @since 0.1.0
|
181
|
+
# @api private
|
182
|
+
def initialize
|
183
|
+
@nodes = []
|
184
|
+
end
|
185
|
+
|
186
|
+
def options
|
187
|
+
end
|
188
|
+
|
189
|
+
# Define a custom tag
|
190
|
+
#
|
191
|
+
# @param name [Symbol,String] the name of the tag
|
192
|
+
# @param content [String,Hanami::Helpers::HtmlHelper::HtmlBuilder,NilClass] the optional content
|
193
|
+
# @param attributes [Hash,NilClass] the optional tag attributes
|
194
|
+
# @param blk [Proc] the optional nested content espressed as a block
|
195
|
+
#
|
196
|
+
# @return [self]
|
197
|
+
#
|
198
|
+
# @since 0.1.0
|
199
|
+
# @api public
|
200
|
+
#
|
201
|
+
# @see Hanami::Helpers::HtmlHelper
|
202
|
+
#
|
203
|
+
# @example
|
204
|
+
# html.tag(:custom) # => <custom></custom>
|
205
|
+
#
|
206
|
+
# html.tag(:custom, 'foo') # => <custom>foo</custom>
|
207
|
+
#
|
208
|
+
# html.tag(:custom, html.p('hello')) # => <custom><p>hello</p></custom>
|
209
|
+
#
|
210
|
+
# html.tag(:custom) { 'foo' }
|
211
|
+
# # =>
|
212
|
+
# #<custom>
|
213
|
+
# # foo
|
214
|
+
# #</custom>
|
215
|
+
#
|
216
|
+
# html.tag(:custom) do
|
217
|
+
# p 'hello'
|
218
|
+
# end
|
219
|
+
# # =>
|
220
|
+
# #<custom>
|
221
|
+
# # <p>hello</p>
|
222
|
+
# #</custom>
|
223
|
+
#
|
224
|
+
# html.tag(:custom, 'hello', id: 'foo', 'data-xyz': 'bar') # => <custom id="foo" data-xyz="bar">hello</custom>
|
225
|
+
#
|
226
|
+
# html.tag(:custom, id: 'foo') { 'hello' }
|
227
|
+
# # =>
|
228
|
+
# #<custom id="foo">
|
229
|
+
# # hello
|
230
|
+
# #</custom>
|
231
|
+
def tag(name, content = nil, attributes = nil, &blk)
|
232
|
+
@nodes << HtmlNode.new(name, blk || content, attributes || content, options)
|
233
|
+
self
|
234
|
+
end
|
235
|
+
|
236
|
+
# Define a HTML fragment
|
237
|
+
#
|
238
|
+
# @param blk [Proc] the optional nested content espressed as a block
|
239
|
+
#
|
240
|
+
# @return [self]
|
241
|
+
#
|
242
|
+
# @since 0.2.6
|
243
|
+
# @api public
|
244
|
+
#
|
245
|
+
# @see Hanami::Helpers::HtmlHelper
|
246
|
+
#
|
247
|
+
# @example
|
248
|
+
# html.fragment('Hanami') # => Hanami
|
249
|
+
#
|
250
|
+
# html do
|
251
|
+
# p 'hello'
|
252
|
+
# p 'hanami'
|
253
|
+
# end
|
254
|
+
# # =>
|
255
|
+
# <p>hello</p>
|
256
|
+
# <p>hanami</p>
|
257
|
+
def fragment(&blk)
|
258
|
+
@nodes << HtmlFragment.new(&blk)
|
259
|
+
self
|
260
|
+
end
|
261
|
+
|
262
|
+
# Defines a custom empty tag
|
263
|
+
#
|
264
|
+
# @param name [Symbol,String] the name of the tag
|
265
|
+
# @param attributes [Hash,NilClass] the optional tag attributes
|
266
|
+
#
|
267
|
+
# @return [self]
|
268
|
+
#
|
269
|
+
# @since 0.1.0
|
270
|
+
# @api public
|
271
|
+
#
|
272
|
+
# @see Hanami::Helpers::HtmlHelper
|
273
|
+
#
|
274
|
+
# @example
|
275
|
+
# html.empty_tag(:xr) # => <xr>
|
276
|
+
#
|
277
|
+
# html.empty_tag(:xr, id: 'foo') # => <xr id="foo">
|
278
|
+
#
|
279
|
+
# html.empty_tag(:xr, id: 'foo', 'data-xyz': 'bar') # => <xr id="foo" data-xyz="bar">
|
280
|
+
def empty_tag(name, attributes = nil)
|
281
|
+
@nodes << EmptyHtmlNode.new(name, attributes)
|
282
|
+
self
|
283
|
+
end
|
284
|
+
|
285
|
+
# Defines a plain string of text. This particularly useful when you
|
286
|
+
# want to build more complex HTML.
|
287
|
+
#
|
288
|
+
# @param content [String] the text to be rendered.
|
289
|
+
#
|
290
|
+
# @return [self]
|
291
|
+
#
|
292
|
+
# @see Hanami::Helpers::HtmlHelper
|
293
|
+
# @see Hanami::Helpers::HtmlHelper::TextNode
|
294
|
+
#
|
295
|
+
# @example
|
296
|
+
#
|
297
|
+
# html.label do
|
298
|
+
# text "Option 1"
|
299
|
+
# radio_button :option, 1
|
300
|
+
# end
|
301
|
+
#
|
302
|
+
# # <label>
|
303
|
+
# # Option 1
|
304
|
+
# # <input type="radio" name="option" value="1" />
|
305
|
+
# # </label>
|
306
|
+
def text(content)
|
307
|
+
@nodes << TextNode.new(content)
|
308
|
+
self
|
309
|
+
end
|
310
|
+
|
311
|
+
# @since 0.2.5
|
312
|
+
# @api private
|
313
|
+
alias_method :+, :text
|
314
|
+
|
315
|
+
# Resolves all the nodes and generates the markup
|
316
|
+
#
|
317
|
+
# @return [Hanami::Utils::Escape::SafeString] the output
|
318
|
+
#
|
319
|
+
# @since 0.1.0
|
320
|
+
# @api private
|
321
|
+
#
|
322
|
+
# @see http://www.rubydoc.info/gems/hanami-utils/Hanami/Utils/Escape/SafeString
|
323
|
+
def to_s
|
324
|
+
Utils::Escape::SafeString.new(@nodes.map(&:to_s).join(NEWLINE))
|
325
|
+
end
|
326
|
+
|
327
|
+
# Encode the content with the given character encoding
|
328
|
+
#
|
329
|
+
# @param encoding [Encoding,String] the encoding or its string representation
|
330
|
+
#
|
331
|
+
# @return [String] the encoded string
|
332
|
+
#
|
333
|
+
# @since 0.2.5
|
334
|
+
# @api private
|
335
|
+
def encode(encoding)
|
336
|
+
to_s.encode(encoding)
|
337
|
+
end
|
338
|
+
|
339
|
+
# Check if there are nested nodes
|
340
|
+
#
|
341
|
+
# @return [TrueClass,FalseClass] the result of the check
|
342
|
+
#
|
343
|
+
# @since 0.1.0
|
344
|
+
# @api private
|
345
|
+
def nested?
|
346
|
+
@nodes.any?
|
347
|
+
end
|
348
|
+
|
349
|
+
# Resolve the context for nested contents
|
350
|
+
#
|
351
|
+
# @since 0.1.0
|
352
|
+
# @api private
|
353
|
+
if RUBY_VERSION >= '2.2' && !Utils.jruby?
|
354
|
+
def resolve(&blk)
|
355
|
+
@context = blk.binding.receiver
|
356
|
+
instance_exec(&blk)
|
357
|
+
end
|
358
|
+
else
|
359
|
+
def resolve(&blk)
|
360
|
+
@context = eval 'self', blk.binding
|
361
|
+
instance_exec(&blk)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
# Forward missing methods to the current context.
|
366
|
+
# This allows to access views local variables from nested content blocks.
|
367
|
+
#
|
368
|
+
# @since 0.1.0
|
369
|
+
# @api private
|
370
|
+
def method_missing(m, *args, &blk)
|
371
|
+
@context.__send__(m, *args, &blk)
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|