phlex 1.6.3 → 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 +7 -8
- data/lib/phlex/elements.rb +38 -11
- 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 +182 -68
- 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,15 +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
16
|
# Create a new instance of the component.
|
16
|
-
# @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.
|
17
18
|
def new(*args, **kwargs, &block)
|
18
19
|
if block
|
19
20
|
object = super(*args, **kwargs, &nil)
|
@@ -42,22 +43,66 @@ module Phlex
|
|
42
43
|
end
|
43
44
|
end
|
44
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
|
87
|
+
end
|
88
|
+
|
45
89
|
# Renders the view and returns the buffer. The default buffer is a mutable String.
|
46
|
-
def call(buffer =
|
90
|
+
def call(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, &block)
|
47
91
|
__final_call__(buffer, context: context, view_context: view_context, parent: parent, &block).tap do
|
48
92
|
self.class.rendered_at_least_once!
|
49
93
|
end
|
50
94
|
end
|
51
95
|
|
52
96
|
# @api private
|
53
|
-
def __final_call__(buffer =
|
97
|
+
def __final_call__(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, &block)
|
98
|
+
@_buffer = buffer
|
54
99
|
@_context = context
|
55
100
|
@_view_context = view_context
|
56
101
|
@_parent = parent
|
57
102
|
|
58
103
|
block ||= @_content_block
|
59
104
|
|
60
|
-
return
|
105
|
+
return unless render?
|
61
106
|
|
62
107
|
around_template do
|
63
108
|
if block
|
@@ -78,47 +123,16 @@ module Phlex
|
|
78
123
|
end
|
79
124
|
end
|
80
125
|
|
81
|
-
buffer
|
82
|
-
end
|
83
|
-
|
84
|
-
# Render another view
|
85
|
-
# @param renderable [Phlex::SGML]
|
86
|
-
# @return [nil]
|
87
|
-
def render(renderable, &block)
|
88
|
-
case renderable
|
89
|
-
when Phlex::SGML
|
90
|
-
renderable.call(context: @_context, view_context: @_view_context, parent: self, &block)
|
91
|
-
when Class
|
92
|
-
if renderable < Phlex::SGML
|
93
|
-
renderable.new.call(context: @_context, view_context: @_view_context, parent: self, &block)
|
94
|
-
end
|
95
|
-
when Enumerable
|
96
|
-
renderable.each { |r| render(r, &block) }
|
97
|
-
when Proc
|
98
|
-
yield_content(&renderable)
|
99
|
-
else
|
100
|
-
raise ArgumentError, "You can't render a #{renderable}."
|
101
|
-
end
|
102
|
-
|
103
|
-
nil
|
126
|
+
buffer << context.target unless parent
|
104
127
|
end
|
105
128
|
|
106
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`
|
107
131
|
# @return [nil]
|
132
|
+
# @see #format_object
|
108
133
|
def plain(content)
|
109
|
-
|
110
|
-
|
111
|
-
@_context.target << ERB::Escape.html_escape(content)
|
112
|
-
when Symbol
|
113
|
-
@_context.target << ERB::Escape.html_escape(content.name)
|
114
|
-
when Integer
|
115
|
-
@_context.target << ERB::Escape.html_escape(content.to_s)
|
116
|
-
when nil
|
117
|
-
nil
|
118
|
-
else
|
119
|
-
if (formatted_object = format_object(content))
|
120
|
-
@_context.target << ERB::Escape.html_escape(formatted_object)
|
121
|
-
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"
|
122
136
|
end
|
123
137
|
|
124
138
|
nil
|
@@ -126,6 +140,7 @@ module Phlex
|
|
126
140
|
|
127
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.
|
128
142
|
# @return [nil]
|
143
|
+
# @yield If a block is given, it yields the block with no arguments.
|
129
144
|
def whitespace
|
130
145
|
target = @_context.target
|
131
146
|
|
@@ -170,10 +185,59 @@ module Phlex
|
|
170
185
|
@_context.with_target(+"") { yield_content(&block) }
|
171
186
|
end
|
172
187
|
|
173
|
-
|
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.
|
174
237
|
# Because the BlackHole does nothing with the output, this should be faster.
|
175
238
|
# @return [nil]
|
176
|
-
|
239
|
+
# @api private
|
240
|
+
def __vanish__(*args)
|
177
241
|
return unless block_given?
|
178
242
|
|
179
243
|
@_context.with_target(BlackHole) { yield(*args) }
|
@@ -181,24 +245,26 @@ module Phlex
|
|
181
245
|
nil
|
182
246
|
end
|
183
247
|
|
184
|
-
#
|
185
|
-
# @
|
186
|
-
|
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?
|
187
252
|
true
|
188
253
|
end
|
189
254
|
|
190
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.
|
191
257
|
# @return [String]
|
192
|
-
|
258
|
+
def format_object(object)
|
193
259
|
case object
|
194
260
|
when Float
|
195
261
|
object.to_s
|
196
262
|
end
|
197
263
|
end
|
198
264
|
|
199
|
-
# 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.
|
200
266
|
# @return [nil]
|
201
|
-
|
267
|
+
def around_template
|
202
268
|
before_template
|
203
269
|
yield
|
204
270
|
after_template
|
@@ -206,59 +272,106 @@ module Phlex
|
|
206
272
|
nil
|
207
273
|
end
|
208
274
|
|
209
|
-
# 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.
|
210
276
|
# @return [nil]
|
211
|
-
|
277
|
+
def before_template
|
212
278
|
nil
|
213
279
|
end
|
214
280
|
|
215
|
-
# 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.
|
216
282
|
# @return [nil]
|
217
|
-
|
283
|
+
def after_template
|
218
284
|
nil
|
219
285
|
end
|
220
286
|
|
221
|
-
# 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]
|
222
289
|
# @return [nil]
|
223
|
-
|
290
|
+
def yield_content
|
224
291
|
return unless block_given?
|
225
292
|
|
226
293
|
target = @_context.target
|
227
294
|
|
228
295
|
original_length = target.length
|
229
296
|
content = yield(self)
|
230
|
-
|
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
|
308
|
+
|
309
|
+
original_length = target.length
|
310
|
+
content = yield
|
311
|
+
__text__(content) if original_length == target.length
|
231
312
|
|
232
313
|
nil
|
233
314
|
end
|
234
315
|
|
235
|
-
# 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.
|
236
318
|
# @return [nil]
|
237
|
-
|
319
|
+
def yield_content_with_args(*args)
|
238
320
|
return unless block_given?
|
239
321
|
|
240
322
|
target = @_context.target
|
241
323
|
|
242
324
|
original_length = target.length
|
243
325
|
content = yield(*args)
|
244
|
-
|
326
|
+
__text__(content) if original_length == target.length
|
245
327
|
|
246
328
|
nil
|
247
329
|
end
|
248
330
|
|
331
|
+
# Performs the same task as the public method #plain, but does not raise an error if an unformattable object is passed
|
249
332
|
# @api private
|
250
|
-
|
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
|
+
|
354
|
+
# @api private
|
355
|
+
def __attributes__(**attributes)
|
251
356
|
__final_attributes__(**attributes).tap do |buffer|
|
252
357
|
Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] = buffer.freeze
|
253
358
|
end
|
254
359
|
end
|
255
360
|
|
256
361
|
# @api private
|
257
|
-
|
362
|
+
def __final_attributes__(**attributes)
|
258
363
|
if respond_to?(:process_attributes)
|
259
364
|
attributes = process_attributes(**attributes)
|
260
365
|
end
|
261
366
|
|
367
|
+
if attributes[:href]&.start_with?(/\s*javascript:/)
|
368
|
+
attributes.delete(:href)
|
369
|
+
end
|
370
|
+
|
371
|
+
if attributes["href"]&.start_with?(/\s*javascript:/)
|
372
|
+
attributes.delete("href")
|
373
|
+
end
|
374
|
+
|
262
375
|
buffer = +""
|
263
376
|
__build_attributes__(attributes, buffer: buffer)
|
264
377
|
|
@@ -266,21 +379,18 @@ module Phlex
|
|
266
379
|
end
|
267
380
|
|
268
381
|
# @api private
|
269
|
-
|
382
|
+
def __build_attributes__(attributes, buffer:)
|
270
383
|
attributes.each do |k, v|
|
271
384
|
next unless v
|
272
385
|
|
273
386
|
name = case k
|
274
387
|
when String then k
|
275
388
|
when Symbol then k.name.tr("_", "-")
|
276
|
-
else
|
389
|
+
else raise ArgumentError, "Attribute keys should be Strings or Symbols."
|
277
390
|
end
|
278
391
|
|
279
|
-
lower_name = name.downcase
|
280
|
-
next if lower_name == "href" && v.to_s.downcase.tr("\t \n", "").start_with?("javascript:")
|
281
|
-
|
282
392
|
# Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters.
|
283
|
-
if HTML::EVENT_ATTRIBUTES[
|
393
|
+
if HTML::EVENT_ATTRIBUTES[name] || name.match?(/[<>&"']/)
|
284
394
|
raise ArgumentError, "Unsafe attribute name detected: #{k}."
|
285
395
|
end
|
286
396
|
|
@@ -300,8 +410,12 @@ module Phlex
|
|
300
410
|
end
|
301
411
|
}, buffer: buffer
|
302
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(" ")) << '"'
|
303
417
|
else
|
304
|
-
|
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"
|
305
419
|
end
|
306
420
|
end
|
307
421
|
|