phlex 1.6.0 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of phlex might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +25 -0
- data/Gemfile +1 -0
- data/README.md +3 -43
- data/lib/phlex/black_hole.rb +1 -0
- data/lib/phlex/callable.rb +1 -0
- data/lib/phlex/context.rb +1 -0
- data/lib/phlex/deferred_render.rb +24 -0
- data/lib/phlex/element_clobbering_guard.rb +6 -7
- data/lib/phlex/elements.rb +54 -26
- data/lib/phlex/helpers.rb +21 -4
- data/lib/phlex/html/standard_elements.rb +193 -103
- data/lib/phlex/html/void_elements.rb +13 -12
- data/lib/phlex/html.rb +4 -6
- data/lib/phlex/overrides/symbol/name.rb +1 -0
- data/lib/phlex/sgml.rb +198 -73
- data/lib/phlex/svg/standard_elements.rb +128 -64
- data/lib/phlex/svg.rb +0 -4
- data/lib/phlex/unbuffered.rb +1 -0
- data/lib/phlex/version.rb +1 -1
- data/lib/phlex.rb +19 -6
- metadata +9 -7
@@ -1,76 +1,77 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Void HTML elements don't accept content and never have a closing tag.
|
3
4
|
module Phlex::HTML::VoidElements
|
4
5
|
extend Phlex::Elements
|
5
6
|
|
6
7
|
# @!method area(**attributes, &content)
|
7
|
-
# Outputs an
|
8
|
+
# Outputs an `<area>` tag.
|
8
9
|
# @return [nil]
|
9
10
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/area
|
10
11
|
register_void_element :area, tag: "area"
|
11
12
|
|
12
13
|
# @!method br(**attributes, &content)
|
13
|
-
# Outputs a
|
14
|
+
# Outputs a `<br>` tag.
|
14
15
|
# @return [nil]
|
15
16
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/br
|
16
17
|
register_void_element :br, tag: "br"
|
17
18
|
|
18
19
|
# @!method embed(**attributes, &content)
|
19
|
-
# Outputs an
|
20
|
+
# Outputs an `<embed>` tag.
|
20
21
|
# @return [nil]
|
21
22
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/embed
|
22
23
|
register_void_element :embed, tag: "embed"
|
23
24
|
|
24
25
|
# @!method hr(**attributes, &content)
|
25
|
-
# Outputs
|
26
|
+
# Outputs an `<hr>` tag.
|
26
27
|
# @return [nil]
|
27
28
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/hr
|
28
29
|
register_void_element :hr, tag: "hr"
|
29
30
|
|
30
31
|
# @!method img(**attributes, &content)
|
31
|
-
# Outputs an
|
32
|
+
# Outputs an `<img>` tag.
|
32
33
|
# @return [nil]
|
33
34
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/img
|
34
35
|
register_void_element :img, tag: "img"
|
35
36
|
|
36
37
|
# @!method input(**attributes, &content)
|
37
|
-
# Outputs an
|
38
|
+
# Outputs an `<input>` tag.
|
38
39
|
# @return [nil]
|
39
40
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/input
|
40
41
|
register_void_element :input, tag: "input"
|
41
42
|
|
42
43
|
# @!method link(**attributes, &content)
|
43
|
-
# Outputs a
|
44
|
+
# Outputs a `<link>` tag.
|
44
45
|
# @return [nil]
|
45
46
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/link
|
46
47
|
register_void_element :link, tag: "link"
|
47
48
|
|
48
49
|
# @!method meta(**attributes, &content)
|
49
|
-
# Outputs a
|
50
|
+
# Outputs a `<meta>` tag.
|
50
51
|
# @return [nil]
|
51
52
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/meta
|
52
53
|
register_void_element :meta, tag: "meta"
|
53
54
|
|
54
55
|
# @!method param(**attributes, &content)
|
55
|
-
# Outputs a
|
56
|
+
# Outputs a `<param>` tag.
|
56
57
|
# @return [nil]
|
57
58
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/param
|
58
59
|
register_void_element :param, tag: "param"
|
59
60
|
|
60
61
|
# @!method source(**attributes, &content)
|
61
|
-
# Outputs a
|
62
|
+
# Outputs a `<source>` tag.
|
62
63
|
# @return [nil]
|
63
64
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/source
|
64
65
|
register_void_element :source, tag: "source"
|
65
66
|
|
66
67
|
# @!method track(**attributes, &content)
|
67
|
-
# Outputs a
|
68
|
+
# Outputs a `<track>` tag.
|
68
69
|
# @return [nil]
|
69
70
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/track
|
70
71
|
register_void_element :track, tag: "track"
|
71
72
|
|
72
73
|
# @!method col(**attributes, &content)
|
73
|
-
# Outputs a
|
74
|
+
# Outputs a `<col>` tag.
|
74
75
|
# @return [nil]
|
75
76
|
# @see https://developer.mozilla.org/docs/Web/HTML/Element/col
|
76
77
|
register_void_element :col, tag: "col"
|
data/lib/phlex/html.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Phlex
|
4
|
+
# @abstract Subclass and define {#template} to create an HTML component class.
|
4
5
|
class HTML < SGML
|
5
6
|
# A list of HTML attributes that have the potential to execute unsafe JavaScript.
|
6
7
|
EVENT_ATTRIBUTES = %w[onabort onafterprint onbeforeprint onbeforeunload onblur oncanplay oncanplaythrough onchange onclick oncontextmenu oncopy oncuechange oncut ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended onerror onfocus onhashchange oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart onmessage onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onoffline ononline onpagehide onpageshow onpaste onpause onplay onplaying onpopstate onprogress onratechange onreset onresize onscroll onsearch onseeked onseeking onselect onstalled onstorage onsubmit onsuspend ontimeupdate ontoggle onunload onvolumechange onwaiting onwheel].to_h { [_1, true] }.freeze
|
@@ -29,12 +30,9 @@ module Phlex
|
|
29
30
|
nil
|
30
31
|
end
|
31
32
|
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
plain(...)
|
36
|
-
end
|
37
|
-
|
33
|
+
# Outputs an `<svg>` tag
|
34
|
+
# @return [nil]
|
35
|
+
# @see https://developer.mozilla.org/docs/Web/SVG/Element/svg
|
38
36
|
def svg(...)
|
39
37
|
super do
|
40
38
|
render Phlex::SVG.new do |svg|
|
data/lib/phlex/sgml.rb
CHANGED
@@ -5,17 +5,16 @@ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
|
|
5
5
|
end
|
6
6
|
|
7
7
|
module Phlex
|
8
|
+
# **Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.
|
8
9
|
class SGML
|
9
10
|
class << self
|
10
|
-
# Render the view to a String. Arguments are delegated to
|
11
|
+
# Render the view to a String. Arguments are delegated to {.new}.
|
11
12
|
def call(...)
|
12
13
|
new(...).call
|
13
14
|
end
|
14
15
|
|
15
|
-
alias_method :render, :call
|
16
|
-
|
17
16
|
# Create a new instance of the component.
|
18
|
-
# @note The block will not be delegated
|
17
|
+
# @note The block will not be delegated {#initialize}. Instead, it will be sent to {#template} when rendering.
|
19
18
|
def new(*args, **kwargs, &block)
|
20
19
|
if block
|
21
20
|
object = super(*args, **kwargs, &nil)
|
@@ -31,28 +30,83 @@ module Phlex
|
|
31
30
|
alias_method :__attributes__, :__final_attributes__
|
32
31
|
alias_method :call, :__final_call__
|
33
32
|
end
|
33
|
+
|
34
|
+
# @api private
|
35
|
+
def element_method?(method_name)
|
36
|
+
return false unless instance_methods.include?(method_name)
|
37
|
+
|
38
|
+
owner = instance_method(method_name).owner
|
39
|
+
|
40
|
+
return true if owner.is_a?(Phlex::Elements) && owner.registered_elements[method_name]
|
41
|
+
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @!method initialize
|
47
|
+
# @abstract Override to define an initializer for your component.
|
48
|
+
# @note Your initializer will not receive a block passed to {.new}. Instead, this block will be sent to {#template} when rendering.
|
49
|
+
# @example
|
50
|
+
# def initialize(articles:)
|
51
|
+
# @articles = articles
|
52
|
+
# end
|
53
|
+
|
54
|
+
# @abstract Override to define a template for your component.
|
55
|
+
# @example
|
56
|
+
# def template
|
57
|
+
# h1 { "👋 Hello World!" }
|
58
|
+
# end
|
59
|
+
# @example Your template may yield a content block.
|
60
|
+
# def template
|
61
|
+
# main {
|
62
|
+
# h1 { "Hello World" }
|
63
|
+
# yield
|
64
|
+
# }
|
65
|
+
# end
|
66
|
+
# @example Alternatively, you can delegate the content block to an element.
|
67
|
+
# def template(&block)
|
68
|
+
# article(class: "card", &block)
|
69
|
+
# end
|
70
|
+
def template
|
71
|
+
yield
|
72
|
+
end
|
73
|
+
|
74
|
+
# @api private
|
75
|
+
def await(task)
|
76
|
+
if task.is_a?(Concurrent::IVar)
|
77
|
+
flush if task.pending?
|
78
|
+
|
79
|
+
task.wait.value
|
80
|
+
elsif defined?(Async::Task) && task.is_a?(Async::Task)
|
81
|
+
flush if task.running?
|
82
|
+
|
83
|
+
task.wait
|
84
|
+
else
|
85
|
+
raise ArgumentError, "Expected an asynchronous task / promise."
|
86
|
+
end
|
34
87
|
end
|
35
88
|
|
36
89
|
# Renders the view and returns the buffer. The default buffer is a mutable String.
|
37
|
-
def call(buffer =
|
90
|
+
def call(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, &block)
|
38
91
|
__final_call__(buffer, context: context, view_context: view_context, parent: parent, &block).tap do
|
39
92
|
self.class.rendered_at_least_once!
|
40
93
|
end
|
41
94
|
end
|
42
95
|
|
43
96
|
# @api private
|
44
|
-
def __final_call__(buffer =
|
97
|
+
def __final_call__(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, &block)
|
98
|
+
@_buffer = buffer
|
45
99
|
@_context = context
|
46
100
|
@_view_context = view_context
|
47
101
|
@_parent = parent
|
48
102
|
|
49
103
|
block ||= @_content_block
|
50
104
|
|
51
|
-
return
|
105
|
+
return unless render?
|
52
106
|
|
53
107
|
around_template do
|
54
108
|
if block
|
55
|
-
if DeferredRender
|
109
|
+
if is_a?(DeferredRender)
|
56
110
|
__vanish__(self, &block)
|
57
111
|
template
|
58
112
|
else
|
@@ -69,47 +123,16 @@ module Phlex
|
|
69
123
|
end
|
70
124
|
end
|
71
125
|
|
72
|
-
buffer
|
73
|
-
end
|
74
|
-
|
75
|
-
# Render another view
|
76
|
-
# @param renderable [Phlex::SGML]
|
77
|
-
# @return [nil]
|
78
|
-
def render(renderable, &block)
|
79
|
-
case renderable
|
80
|
-
when Phlex::SGML
|
81
|
-
renderable.call(context: @_context, view_context: @_view_context, parent: self, &block)
|
82
|
-
when Class
|
83
|
-
if renderable < Phlex::SGML
|
84
|
-
renderable.new.call(context: @_context, view_context: @_view_context, parent: self, &block)
|
85
|
-
end
|
86
|
-
when Enumerable
|
87
|
-
renderable.each { |r| render(r, &block) }
|
88
|
-
when Proc
|
89
|
-
yield_content(&renderable)
|
90
|
-
else
|
91
|
-
raise ArgumentError, "You can't render a #{renderable}."
|
92
|
-
end
|
93
|
-
|
94
|
-
nil
|
126
|
+
buffer << context.target unless parent
|
95
127
|
end
|
96
128
|
|
97
129
|
# Output text content. The text will be HTML-escaped.
|
130
|
+
# @param content [String, Symbol, Integer, void] the content to be output on the buffer. Strings, Symbols, and Integers are handled by `plain` directly, but any object can be handled by overriding `format_object`
|
98
131
|
# @return [nil]
|
132
|
+
# @see #format_object
|
99
133
|
def plain(content)
|
100
|
-
|
101
|
-
|
102
|
-
@_context.target << ERB::Escape.html_escape(content)
|
103
|
-
when Symbol
|
104
|
-
@_context.target << ERB::Escape.html_escape(content.name)
|
105
|
-
when Integer
|
106
|
-
@_context.target << ERB::Escape.html_escape(content.to_s)
|
107
|
-
when nil
|
108
|
-
nil
|
109
|
-
else
|
110
|
-
if (formatted_object = format_object(content))
|
111
|
-
@_context.target << ERB::Escape.html_escape(formatted_object)
|
112
|
-
end
|
134
|
+
unless __text__(content)
|
135
|
+
raise ArgumentError, "You've passed an object to plain that is not handled by format_object. See https://rubydoc.info/gems/phlex/Phlex/SGML#format_object-instance_method for more information"
|
113
136
|
end
|
114
137
|
|
115
138
|
nil
|
@@ -117,12 +140,15 @@ module Phlex
|
|
117
140
|
|
118
141
|
# Output a whitespace character. This is useful for getting inline elements to wrap. If you pass a block, a whitespace will be output before and after yielding the block.
|
119
142
|
# @return [nil]
|
143
|
+
# @yield If a block is given, it yields the block with no arguments.
|
120
144
|
def whitespace
|
121
|
-
@_context.target
|
145
|
+
target = @_context.target
|
146
|
+
|
147
|
+
target << " "
|
122
148
|
|
123
149
|
if block_given?
|
124
150
|
yield
|
125
|
-
|
151
|
+
target << " "
|
126
152
|
end
|
127
153
|
|
128
154
|
nil
|
@@ -131,9 +157,11 @@ module Phlex
|
|
131
157
|
# Output an HTML comment.
|
132
158
|
# @return [nil]
|
133
159
|
def comment(&block)
|
134
|
-
@_context.target
|
160
|
+
target = @_context.target
|
161
|
+
|
162
|
+
target << "<!-- "
|
135
163
|
yield_content(&block)
|
136
|
-
|
164
|
+
target << " -->"
|
137
165
|
|
138
166
|
nil
|
139
167
|
end
|
@@ -157,10 +185,59 @@ module Phlex
|
|
157
185
|
@_context.with_target(+"") { yield_content(&block) }
|
158
186
|
end
|
159
187
|
|
160
|
-
|
188
|
+
private
|
189
|
+
|
190
|
+
# @api private
|
191
|
+
def flush
|
192
|
+
target = @_context.target
|
193
|
+
@_buffer << target.dup
|
194
|
+
target.clear
|
195
|
+
end
|
196
|
+
|
197
|
+
# Render another component, block or enumerable
|
198
|
+
# @return [nil]
|
199
|
+
# @overload render(component, &block)
|
200
|
+
# Renders the component.
|
201
|
+
# @param component [Phlex::SGML]
|
202
|
+
# @overload render(component_class, &block)
|
203
|
+
# Renders a new instance of the component class. This is useful for component classes that take no arguments.
|
204
|
+
# @param component_class [Class<Phlex::SGML>]
|
205
|
+
# @overload render(proc)
|
206
|
+
# Renders the proc with {#yield_content}.
|
207
|
+
# @param proc [Proc]
|
208
|
+
# @overload render(enumerable)
|
209
|
+
# Renders each item of the enumerable.
|
210
|
+
# @param enumerable [Enumerable]
|
211
|
+
# @example
|
212
|
+
# render @items
|
213
|
+
def render(renderable, &block)
|
214
|
+
case renderable
|
215
|
+
when Phlex::SGML
|
216
|
+
renderable.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &block)
|
217
|
+
when Class
|
218
|
+
if renderable < Phlex::SGML
|
219
|
+
renderable.new.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &block)
|
220
|
+
end
|
221
|
+
when Enumerable
|
222
|
+
renderable.each { |r| render(r, &block) }
|
223
|
+
when Proc
|
224
|
+
if renderable.arity == 0
|
225
|
+
yield_content_with_no_args(&renderable)
|
226
|
+
else
|
227
|
+
yield_content(&renderable)
|
228
|
+
end
|
229
|
+
else
|
230
|
+
raise ArgumentError, "You can't render a #{renderable}."
|
231
|
+
end
|
232
|
+
|
233
|
+
nil
|
234
|
+
end
|
235
|
+
|
236
|
+
# Like {#capture} but the output is vanished into a BlackHole buffer.
|
161
237
|
# Because the BlackHole does nothing with the output, this should be faster.
|
162
238
|
# @return [nil]
|
163
|
-
|
239
|
+
# @api private
|
240
|
+
def __vanish__(*args)
|
164
241
|
return unless block_given?
|
165
242
|
|
166
243
|
@_context.with_target(BlackHole) { yield(*args) }
|
@@ -168,24 +245,26 @@ module Phlex
|
|
168
245
|
nil
|
169
246
|
end
|
170
247
|
|
171
|
-
#
|
172
|
-
# @
|
173
|
-
|
248
|
+
# Determines if the component should render. By default, it returns `true`.
|
249
|
+
# @abstract Override to define your own predicate to prevent rendering.
|
250
|
+
# @return [Boolean]
|
251
|
+
def render?
|
174
252
|
true
|
175
253
|
end
|
176
254
|
|
177
255
|
# Format the object for output
|
256
|
+
# @abstract Override to define your own format handling for different object types. Please remember to call `super` in the case that the passed object doesn't match, so that object formatting can be added at different layers of the inheritance tree.
|
178
257
|
# @return [String]
|
179
|
-
|
258
|
+
def format_object(object)
|
180
259
|
case object
|
181
260
|
when Float
|
182
261
|
object.to_s
|
183
262
|
end
|
184
263
|
end
|
185
264
|
|
186
|
-
# Override this method to hook in around a template render. You can do things before and after calling
|
265
|
+
# @abstract Override this method to hook in around a template render. You can do things before and after calling `super` to render the template. You should always call `super` so that callbacks can be added at different layers of the inheritance tree.
|
187
266
|
# @return [nil]
|
188
|
-
|
267
|
+
def around_template
|
189
268
|
before_template
|
190
269
|
yield
|
191
270
|
after_template
|
@@ -193,52 +272,94 @@ module Phlex
|
|
193
272
|
nil
|
194
273
|
end
|
195
274
|
|
196
|
-
# Override this method to hook in right before a template is rendered. Please remember to call
|
275
|
+
# @abstract Override this method to hook in right before a template is rendered. Please remember to call `super` so that callbacks can be added at different layers of the inheritance tree.
|
197
276
|
# @return [nil]
|
198
|
-
|
277
|
+
def before_template
|
199
278
|
nil
|
200
279
|
end
|
201
280
|
|
202
|
-
# Override this method to hook in right after a template is rendered. Please remember to call
|
281
|
+
# @abstract Override this method to hook in right after a template is rendered. Please remember to call `super` so that callbacks can be added at different layers of the inheritance tree.
|
203
282
|
# @return [nil]
|
204
|
-
|
283
|
+
def after_template
|
205
284
|
nil
|
206
285
|
end
|
207
286
|
|
208
|
-
# Yields the block and checks if it buffered anything. If nothing was buffered, the return value is treated as text.
|
287
|
+
# Yields the block and checks if it buffered anything. If nothing was buffered, the return value is treated as text. The text is always HTML-escaped.
|
288
|
+
# @yieldparam component [self]
|
209
289
|
# @return [nil]
|
210
|
-
|
290
|
+
def yield_content
|
211
291
|
return unless block_given?
|
212
292
|
|
213
|
-
|
293
|
+
target = @_context.target
|
294
|
+
|
295
|
+
original_length = target.length
|
214
296
|
content = yield(self)
|
297
|
+
__text__(content) if original_length == target.length
|
298
|
+
|
299
|
+
nil
|
300
|
+
end
|
301
|
+
|
302
|
+
# Same as {#yield_content} but yields no arguments.
|
303
|
+
# @yield Yields the block with no arguments.
|
304
|
+
def yield_content_with_no_args
|
305
|
+
return unless block_given?
|
306
|
+
|
307
|
+
target = @_context.target
|
215
308
|
|
216
|
-
|
309
|
+
original_length = target.length
|
310
|
+
content = yield
|
311
|
+
__text__(content) if original_length == target.length
|
217
312
|
|
218
313
|
nil
|
219
314
|
end
|
220
315
|
|
221
|
-
# Same as
|
316
|
+
# Same as {#yield_content} but accepts a splat of arguments to yield. This is slightly slower than {#yield_content}.
|
317
|
+
# @yield [*args] Yields the given arguments.
|
222
318
|
# @return [nil]
|
223
|
-
|
319
|
+
def yield_content_with_args(*args)
|
224
320
|
return unless block_given?
|
225
321
|
|
226
|
-
|
322
|
+
target = @_context.target
|
323
|
+
|
324
|
+
original_length = target.length
|
227
325
|
content = yield(*args)
|
228
|
-
|
326
|
+
__text__(content) if original_length == target.length
|
229
327
|
|
230
328
|
nil
|
231
329
|
end
|
232
330
|
|
331
|
+
# Performs the same task as the public method #plain, but does not raise an error if an unformattable object is passed
|
332
|
+
# @api private
|
333
|
+
def __text__(content)
|
334
|
+
case content
|
335
|
+
when String
|
336
|
+
@_context.target << ERB::Escape.html_escape(content)
|
337
|
+
when Symbol
|
338
|
+
@_context.target << ERB::Escape.html_escape(content.name)
|
339
|
+
when Integer
|
340
|
+
@_context.target << ERB::Escape.html_escape(content.to_s)
|
341
|
+
when nil
|
342
|
+
nil
|
343
|
+
else
|
344
|
+
if (formatted_object = format_object(content))
|
345
|
+
@_context.target << ERB::Escape.html_escape(formatted_object)
|
346
|
+
else
|
347
|
+
return false
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
true
|
352
|
+
end
|
353
|
+
|
233
354
|
# @api private
|
234
|
-
|
355
|
+
def __attributes__(**attributes)
|
235
356
|
__final_attributes__(**attributes).tap do |buffer|
|
236
357
|
Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] = buffer.freeze
|
237
358
|
end
|
238
359
|
end
|
239
360
|
|
240
361
|
# @api private
|
241
|
-
|
362
|
+
def __final_attributes__(**attributes)
|
242
363
|
if respond_to?(:process_attributes)
|
243
364
|
attributes = process_attributes(**attributes)
|
244
365
|
end
|
@@ -258,14 +379,14 @@ module Phlex
|
|
258
379
|
end
|
259
380
|
|
260
381
|
# @api private
|
261
|
-
|
382
|
+
def __build_attributes__(attributes, buffer:)
|
262
383
|
attributes.each do |k, v|
|
263
384
|
next unless v
|
264
385
|
|
265
386
|
name = case k
|
266
387
|
when String then k
|
267
388
|
when Symbol then k.name.tr("_", "-")
|
268
|
-
else
|
389
|
+
else raise ArgumentError, "Attribute keys should be Strings or Symbols."
|
269
390
|
end
|
270
391
|
|
271
392
|
# Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters.
|
@@ -289,8 +410,12 @@ module Phlex
|
|
289
410
|
end
|
290
411
|
}, buffer: buffer
|
291
412
|
)
|
413
|
+
when Array
|
414
|
+
buffer << " " << name << '="' << ERB::Escape.html_escape(v.compact.join(" ")) << '"'
|
415
|
+
when Set
|
416
|
+
buffer << " " << name << '="' << ERB::Escape.html_escape(v.to_a.compact.join(" ")) << '"'
|
292
417
|
else
|
293
|
-
|
418
|
+
raise ArgumentError, "Element attributes must be either a Boolean, a String, a Symbol, an Array of Strings or Symbols, or a Hash with values of one of these types"
|
294
419
|
end
|
295
420
|
end
|
296
421
|
|