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.
@@ -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>&lt;script&gt;alert(&apos;xss&apos;)&lt;&#x2F;script&gt;</div>"
112
+ #
113
+ # html.div { "<script>alert('xss')</script>" }
114
+ # # => "<div>&lt;script&gt;alert(&apos;xss&apos;)&lt;&#x2F;script&gt;</div>"
115
+ #
116
+ # html.div(html.p("<script>alert('xss')</script>"))
117
+ # # => "<div><p>&lt;script&gt;alert(&apos;xss&apos;)&lt;&#x2F;script&gt;</p></div>"
118
+ #
119
+ # html.div do
120
+ # p "<script>alert('xss')</script>"
121
+ # end
122
+ # # => "<div><p>&lt;script&gt;alert(&apos;xss&apos;)&lt;&#x2F;script&gt;</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