lotus-helpers 0.0.0 → 0.1.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/{LICENSE.txt → LICENSE.md} +0 -0
- data/README.md +198 -6
- data/lib/lotus/helpers.rb +22 -2
- data/lib/lotus/helpers/escape_helper.rb +271 -0
- data/lib/lotus/helpers/html_helper.rb +192 -0
- data/lib/lotus/helpers/html_helper/empty_html_node.rb +59 -0
- data/lib/lotus/helpers/html_helper/html_builder.rb +296 -0
- data/lib/lotus/helpers/html_helper/html_node.rb +69 -0
- data/lib/lotus/helpers/routing_helper.rb +52 -0
- data/lib/lotus/helpers/version.rb +4 -1
- data/lotus-helpers.gemspec +14 -10
- metadata +46 -13
- data/.gitignore +0 -22
- data/Gemfile +0 -4
- data/Rakefile +0 -2
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'lotus/helpers/html_helper/html_builder'
|
2
|
+
|
3
|
+
module Lotus
|
4
|
+
module Helpers
|
5
|
+
# HTML builder
|
6
|
+
#
|
7
|
+
# By including <tt>Lotus::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 Lotus::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
|
+
#
|
69
|
+
#
|
70
|
+
# @example Complex markup
|
71
|
+
# #
|
72
|
+
# # NOTICE THE LACK OF CONCATENATION BETWEEN div AND input BLOCKS <3
|
73
|
+
# #
|
74
|
+
#
|
75
|
+
# html.form(action: '/users', method: 'POST') do
|
76
|
+
# div do
|
77
|
+
# label 'First name', for: 'user-first-name'
|
78
|
+
# input type: 'text', id: 'user-first-name', name: 'user[first_name]', value: 'L'
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# input type: 'submit', value: 'Save changes'
|
82
|
+
# end
|
83
|
+
# # =>
|
84
|
+
# #<form action="/users" method="POST">
|
85
|
+
# # <div>
|
86
|
+
# # <label for="user-first-name">First name</label>
|
87
|
+
# # <input type="text" id="user-first-name" name="user[first_name]" value="L">
|
88
|
+
# # </div>
|
89
|
+
# # <input type="submit" value="Save changes">
|
90
|
+
# #</form>
|
91
|
+
#
|
92
|
+
#
|
93
|
+
#
|
94
|
+
# @example Custom tags
|
95
|
+
# html.tag(:custom, 'Foo', id: 'next') # => <custom id="next">Foo</custom>
|
96
|
+
# html.empty_tag(:xr, id: 'next') # => <xr id="next">
|
97
|
+
#
|
98
|
+
#
|
99
|
+
#
|
100
|
+
# @example Auto escape
|
101
|
+
# html.div('hello') # => <div>hello</hello>
|
102
|
+
# html.div { 'hello' } # => <div>hello</hello>
|
103
|
+
# html.div(html.p('hello')) # => <div><p>hello</p></hello>
|
104
|
+
# html.div do
|
105
|
+
# p 'hello'
|
106
|
+
# end # => <div><p>hello</p></hello>
|
107
|
+
#
|
108
|
+
#
|
109
|
+
#
|
110
|
+
# html.div("<script>alert('xss')</script>")
|
111
|
+
# # => "<div><script>alert('xss')</script></div>"
|
112
|
+
#
|
113
|
+
# html.div { "<script>alert('xss')</script>" }
|
114
|
+
# # => "<div><script>alert('xss')</script></div>"
|
115
|
+
#
|
116
|
+
# html.div(html.p("<script>alert('xss')</script>"))
|
117
|
+
# # => "<div><p><script>alert('xss')</script></p></div>"
|
118
|
+
#
|
119
|
+
# html.div do
|
120
|
+
# p "<script>alert('xss')</script>"
|
121
|
+
# end
|
122
|
+
# # => "<div><p><script>alert('xss')</script></p></div>"
|
123
|
+
#
|
124
|
+
#
|
125
|
+
# @example Basic usage
|
126
|
+
# #
|
127
|
+
# # THE VIEW CAN BE A SIMPLE RUBY OBJECT
|
128
|
+
# #
|
129
|
+
#
|
130
|
+
# require 'lotus/helpers'
|
131
|
+
#
|
132
|
+
# class MyView
|
133
|
+
# include Lotus::Helpers::HtmlHelper
|
134
|
+
#
|
135
|
+
# # Generates
|
136
|
+
# # <aside id="sidebar">
|
137
|
+
# # <div>hello</hello>
|
138
|
+
# # </aside>
|
139
|
+
# def sidebar
|
140
|
+
# html.aside(id: 'sidebar') do
|
141
|
+
# div 'hello'
|
142
|
+
# end
|
143
|
+
# end
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
#
|
147
|
+
# @example View context
|
148
|
+
# #
|
149
|
+
# # LOCAL VARIABLES FROM VIEWS ARE AVAILABLE INSIDE THE NESTED BLOCKS OF HTML BUILDER
|
150
|
+
# #
|
151
|
+
#
|
152
|
+
# require 'lotus/view'
|
153
|
+
# require 'lotus/helpers'
|
154
|
+
#
|
155
|
+
# Book = Struct.new(:title)
|
156
|
+
#
|
157
|
+
# module Books
|
158
|
+
# class Show
|
159
|
+
# include Lotus::View
|
160
|
+
# include Lotus::Helpers::HtmlHelper
|
161
|
+
#
|
162
|
+
# def title_widget
|
163
|
+
# html.div do
|
164
|
+
# h1 book.title
|
165
|
+
# end
|
166
|
+
# end
|
167
|
+
# end
|
168
|
+
# end
|
169
|
+
#
|
170
|
+
# book = Book.new('The Work of Art in the Age of Mechanical Reproduction')
|
171
|
+
# rendered = Books::Show.render(format: :html, book: book)
|
172
|
+
#
|
173
|
+
# rendered
|
174
|
+
# # => <div>
|
175
|
+
# # <h1>The Work of Art in the Age of Mechanical Reproduction</h1>
|
176
|
+
# # </div>
|
177
|
+
module HtmlHelper
|
178
|
+
private
|
179
|
+
# Instantiate an HTML builder
|
180
|
+
#
|
181
|
+
# @return [Lotus::Helpers::HtmlHelper::HtmlBuilder] the HTML builder
|
182
|
+
#
|
183
|
+
# @since 0.1.0
|
184
|
+
#
|
185
|
+
# @see Lotus::Helpers::HtmlHelper
|
186
|
+
# @see Lotus::Helpers::HtmlHelper::HtmlBuilder
|
187
|
+
def html
|
188
|
+
HtmlBuilder.new
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Helpers
|
3
|
+
module HtmlHelper
|
4
|
+
# Empty HTML node
|
5
|
+
#
|
6
|
+
# @since 0.1.0
|
7
|
+
# @api private
|
8
|
+
class EmptyHtmlNode
|
9
|
+
# Attributes separator
|
10
|
+
#
|
11
|
+
# @since 0.1.0
|
12
|
+
# @api private
|
13
|
+
ATTRIBUTES_SEPARATOR = ' '.freeze
|
14
|
+
|
15
|
+
# Initialize a new empty HTML node
|
16
|
+
#
|
17
|
+
# @param name [Symbol,String] the name of the tag
|
18
|
+
# @param attributes [Hash,NilClass] the optional tag attributes
|
19
|
+
#
|
20
|
+
# @return [Lotus::Helpers::HtmlHelper::EmptyHtmlNode]
|
21
|
+
#
|
22
|
+
# @since 0.1.0
|
23
|
+
# @api private
|
24
|
+
def initialize(name, attributes)
|
25
|
+
@name = name
|
26
|
+
@attributes = attributes
|
27
|
+
end
|
28
|
+
|
29
|
+
# Resolve and return the output
|
30
|
+
#
|
31
|
+
# @return [String] the output
|
32
|
+
#
|
33
|
+
# @since 0.1.0
|
34
|
+
# @api private
|
35
|
+
def to_s
|
36
|
+
%(<#{ @name }#{attributes}>)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
# Resolve the attributes
|
41
|
+
#
|
42
|
+
# @return [String,NilClass] the tag attributes
|
43
|
+
#
|
44
|
+
# @since 0.1.0
|
45
|
+
# @api private
|
46
|
+
def attributes
|
47
|
+
return if @attributes.nil?
|
48
|
+
result = [nil]
|
49
|
+
|
50
|
+
@attributes.each do |name, value|
|
51
|
+
result << %(#{ name }="#{ value }")
|
52
|
+
end
|
53
|
+
|
54
|
+
result.join(ATTRIBUTES_SEPARATOR)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,296 @@
|
|
1
|
+
require 'lotus/utils' # RUBY_VERSION >= '2.2'
|
2
|
+
require 'lotus/utils/escape'
|
3
|
+
require 'lotus/helpers/html_helper/empty_html_node'
|
4
|
+
require 'lotus/helpers/html_helper/html_node'
|
5
|
+
|
6
|
+
module Lotus
|
7
|
+
module Helpers
|
8
|
+
module HtmlHelper
|
9
|
+
# HTML Builder
|
10
|
+
#
|
11
|
+
# @since 0.1.0
|
12
|
+
class HtmlBuilder
|
13
|
+
# HTML5 content tags
|
14
|
+
#
|
15
|
+
# @since 0.1.0
|
16
|
+
# @api private
|
17
|
+
#
|
18
|
+
# @see Lotus::Helpers::HtmlHelper::HtmlNode
|
19
|
+
# @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
|
20
|
+
CONTENT_TAGS = [
|
21
|
+
'a',
|
22
|
+
'abbr',
|
23
|
+
'address',
|
24
|
+
'article',
|
25
|
+
'aside',
|
26
|
+
'audio',
|
27
|
+
'b',
|
28
|
+
'bdi',
|
29
|
+
'bdo',
|
30
|
+
'blockquote',
|
31
|
+
'body',
|
32
|
+
'button',
|
33
|
+
'canvas',
|
34
|
+
'caption',
|
35
|
+
'cite',
|
36
|
+
'code',
|
37
|
+
'colgroup',
|
38
|
+
'data',
|
39
|
+
'datalist',
|
40
|
+
'del',
|
41
|
+
'details',
|
42
|
+
'dfn',
|
43
|
+
'div',
|
44
|
+
'dl',
|
45
|
+
'dt',
|
46
|
+
'em',
|
47
|
+
'fieldset',
|
48
|
+
'figcaption',
|
49
|
+
'figure',
|
50
|
+
'footer',
|
51
|
+
'form',
|
52
|
+
'h1',
|
53
|
+
'h2',
|
54
|
+
'h3',
|
55
|
+
'h4',
|
56
|
+
'h5',
|
57
|
+
'h6',
|
58
|
+
'head',
|
59
|
+
'header',
|
60
|
+
'i',
|
61
|
+
'iframe',
|
62
|
+
'ins',
|
63
|
+
'kbd',
|
64
|
+
'label',
|
65
|
+
'legend',
|
66
|
+
'li',
|
67
|
+
'link',
|
68
|
+
'main',
|
69
|
+
'map',
|
70
|
+
'mark',
|
71
|
+
'math',
|
72
|
+
'menu',
|
73
|
+
'meter',
|
74
|
+
'nav',
|
75
|
+
'noscript',
|
76
|
+
'object',
|
77
|
+
'ol',
|
78
|
+
'optgroup',
|
79
|
+
'option',
|
80
|
+
'output',
|
81
|
+
'p',
|
82
|
+
'pre',
|
83
|
+
'progress',
|
84
|
+
'q',
|
85
|
+
'rp',
|
86
|
+
'rt',
|
87
|
+
'ruby',
|
88
|
+
's',
|
89
|
+
'samp',
|
90
|
+
'script',
|
91
|
+
'section',
|
92
|
+
'select',
|
93
|
+
'small',
|
94
|
+
'span',
|
95
|
+
'strong',
|
96
|
+
'style',
|
97
|
+
'sub',
|
98
|
+
'summary',
|
99
|
+
'sup',
|
100
|
+
'svg',
|
101
|
+
'table',
|
102
|
+
'tbody',
|
103
|
+
'td',
|
104
|
+
'template',
|
105
|
+
'textarea',
|
106
|
+
'tfoot',
|
107
|
+
'th',
|
108
|
+
'thead',
|
109
|
+
'time',
|
110
|
+
'title',
|
111
|
+
'tr',
|
112
|
+
'u',
|
113
|
+
'ul',
|
114
|
+
'video',
|
115
|
+
].freeze
|
116
|
+
|
117
|
+
# HTML5 empty tags
|
118
|
+
#
|
119
|
+
# @since 0.1.0
|
120
|
+
# @api private
|
121
|
+
#
|
122
|
+
# @see Lotus::Helpers::HtmlHelper::EmptyHtmlNode
|
123
|
+
# @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element
|
124
|
+
EMPTY_TAGS = [
|
125
|
+
'area',
|
126
|
+
'base',
|
127
|
+
'br',
|
128
|
+
'col',
|
129
|
+
'embed',
|
130
|
+
'hr',
|
131
|
+
'img',
|
132
|
+
'input',
|
133
|
+
'keygen',
|
134
|
+
'link',
|
135
|
+
'menuitem',
|
136
|
+
'meta',
|
137
|
+
'param',
|
138
|
+
'source',
|
139
|
+
'track',
|
140
|
+
'wbr',
|
141
|
+
].freeze
|
142
|
+
|
143
|
+
# New line separator
|
144
|
+
#
|
145
|
+
# @since 0.1.0
|
146
|
+
# @api private
|
147
|
+
NEWLINE = "\n".freeze
|
148
|
+
|
149
|
+
CONTENT_TAGS.each do |tag|
|
150
|
+
class_eval %{
|
151
|
+
def #{ tag }(content = nil, attributes = nil, &blk)
|
152
|
+
@nodes << HtmlNode.new(:#{ tag }, blk || content, attributes || content)
|
153
|
+
self
|
154
|
+
end
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
EMPTY_TAGS.each do |tag|
|
159
|
+
class_eval %{
|
160
|
+
def #{ tag }(attributes = nil)
|
161
|
+
@nodes << EmptyHtmlNode.new(:#{ tag }, attributes)
|
162
|
+
self
|
163
|
+
end
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
# Initialize a new builder
|
168
|
+
#
|
169
|
+
# @return [Lotus::Helpers::HtmlHelper::HtmlBuilder] the builder
|
170
|
+
#
|
171
|
+
# @since 0.1.0
|
172
|
+
# @api private
|
173
|
+
def initialize
|
174
|
+
@nodes = []
|
175
|
+
end
|
176
|
+
|
177
|
+
# Define a custom tag
|
178
|
+
#
|
179
|
+
# @param name [Symbol,String] the name of the tag
|
180
|
+
# @param content [String,Lotus::Helpers::HtmlHelper::HtmlBuilder,NilClass] the optional content
|
181
|
+
# @param attributes [Hash,NilClass] the optional tag attributes
|
182
|
+
# @param blk [Proc] the optional nested content espressed as a block
|
183
|
+
#
|
184
|
+
# @return [self]
|
185
|
+
#
|
186
|
+
# @since 0.1.0
|
187
|
+
# @api public
|
188
|
+
#
|
189
|
+
# @see Lotus::Helpers::HtmlHelper
|
190
|
+
#
|
191
|
+
# @example
|
192
|
+
# html.tag(:custom) # => <custom></custom>
|
193
|
+
#
|
194
|
+
# html.tag(:custom, 'foo') # => <custom>foo</custom>
|
195
|
+
#
|
196
|
+
# html.tag(:custom, html.p('hello')) # => <custom><p>hello</p></custom>
|
197
|
+
#
|
198
|
+
# html.tag(:custom) { 'foo' }
|
199
|
+
# # =>
|
200
|
+
# #<custom>
|
201
|
+
# # foo
|
202
|
+
# #</custom>
|
203
|
+
#
|
204
|
+
# html.tag(:custom) do
|
205
|
+
# p 'hello'
|
206
|
+
# end
|
207
|
+
# # =>
|
208
|
+
# #<custom>
|
209
|
+
# # <p>hello</p>
|
210
|
+
# #</custom>
|
211
|
+
#
|
212
|
+
# html.tag(:custom, 'hello', id: 'foo', 'data-xyz': 'bar') # => <custom id="foo" data-xyz="bar">hello</custom>
|
213
|
+
#
|
214
|
+
# html.tag(:custom, id: 'foo') { 'hello' }
|
215
|
+
# # =>
|
216
|
+
# #<custom id="foo">
|
217
|
+
# # hello
|
218
|
+
# #</custom>
|
219
|
+
def tag(name, content = nil, attributes = nil, &blk)
|
220
|
+
@nodes << HtmlNode.new(name, blk || content, attributes || content)
|
221
|
+
self
|
222
|
+
end
|
223
|
+
|
224
|
+
# Defines a custom empty tag
|
225
|
+
#
|
226
|
+
# @param name [Symbol,String] the name of the tag
|
227
|
+
# @param attributes [Hash,NilClass] the optional tag attributes
|
228
|
+
#
|
229
|
+
# @return [self]
|
230
|
+
#
|
231
|
+
# @since 0.1.0
|
232
|
+
# @api public
|
233
|
+
#
|
234
|
+
# @see Lotus::Helpers::HtmlHelper
|
235
|
+
#
|
236
|
+
# @example
|
237
|
+
# html.empty_tag(:xr) # => <xr>
|
238
|
+
#
|
239
|
+
# html.empty_tag(:xr, id: 'foo') # => <xr id="foo">
|
240
|
+
#
|
241
|
+
# html.empty_tag(:xr, id: 'foo', 'data-xyz': 'bar') # => <xr id="foo" data-xyz="bar">
|
242
|
+
def empty_tag(name, attributes = nil)
|
243
|
+
@nodes << EmptyHtmlNode.new(name, attributes)
|
244
|
+
self
|
245
|
+
end
|
246
|
+
|
247
|
+
# Resolves all the nodes and generates the markup
|
248
|
+
#
|
249
|
+
# @return [Lotus::Utils::Escape::SafeString] the output
|
250
|
+
#
|
251
|
+
# @since 0.1.0
|
252
|
+
# @api private
|
253
|
+
#
|
254
|
+
# @see http://www.rubydoc.info/gems/lotus-utils/Lotus/Utils/Escape/SafeString
|
255
|
+
def to_s
|
256
|
+
Utils::Escape::SafeString.new(@nodes.map(&:to_s).join(NEWLINE))
|
257
|
+
end
|
258
|
+
|
259
|
+
# Check if there are nested nodes
|
260
|
+
#
|
261
|
+
# @return [TrueClass,FalseClass] the result of the check
|
262
|
+
#
|
263
|
+
# @since 0.1.0
|
264
|
+
# @api private
|
265
|
+
def nested?
|
266
|
+
@nodes.any?
|
267
|
+
end
|
268
|
+
|
269
|
+
# Resolve the context for nested contents
|
270
|
+
#
|
271
|
+
# @since 0.1.0
|
272
|
+
# @api private
|
273
|
+
if RUBY_VERSION >= '2.2' && !Utils.jruby?
|
274
|
+
def resolve(&blk)
|
275
|
+
@context = blk.binding.receiver
|
276
|
+
instance_exec(&blk)
|
277
|
+
end
|
278
|
+
else
|
279
|
+
def resolve(&blk)
|
280
|
+
@context = eval 'self', blk.binding
|
281
|
+
instance_exec(&blk)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# Forward missing methods to the current context.
|
286
|
+
# This allows to access views local variables from nested content blocks.
|
287
|
+
#
|
288
|
+
# @since 0.1.0
|
289
|
+
# @api private
|
290
|
+
def method_missing(m, *args, &blk)
|
291
|
+
@context.__send__(m, *args, &blk)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|