lotus-helpers 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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