phlex 2.0.0.beta2 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -11
- data/lib/phlex/context.rb +3 -2
- data/lib/phlex/csv.rb +4 -11
- data/lib/phlex/error.rb +1 -0
- data/lib/phlex/fifo.rb +1 -0
- data/lib/phlex/helpers.rb +1 -1
- data/lib/phlex/html/standard_elements.rb +1033 -722
- data/lib/phlex/html/void_elements.rb +94 -56
- data/lib/phlex/html.rb +1 -7
- data/lib/phlex/kit.rb +23 -11
- data/lib/phlex/{elements.rb → sgml/elements.rb} +10 -36
- data/lib/phlex/sgml.rb +221 -263
- data/lib/phlex/svg/standard_elements.rb +480 -449
- data/lib/phlex/svg.rb +0 -3
- data/lib/phlex/{black_hole.rb → vanish.rb} +1 -1
- data/lib/phlex/version.rb +1 -1
- data/lib/phlex.rb +2 -5
- metadata +5 -7
- data/lib/phlex/deferred_render.rb +0 -29
- data/lib/phlex/element_clobbering_guard.rb +0 -18
data/lib/phlex/sgml.rb
CHANGED
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
# **Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.
|
4
4
|
class Phlex::SGML
|
5
|
+
UNSAFE_ATTRIBUTES = Set.new(%w[srcdoc sandbox http-equiv]).freeze
|
6
|
+
REF_ATTRIBUTES = Set.new(%w[href src action formaction lowsrc dynsrc background ping]).freeze
|
7
|
+
|
8
|
+
autoload :Elements, "phlex/sgml/elements"
|
5
9
|
autoload :SafeObject, "phlex/sgml/safe_object"
|
6
10
|
autoload :SafeValue, "phlex/sgml/safe_value"
|
7
11
|
|
@@ -15,9 +19,9 @@ class Phlex::SGML
|
|
15
19
|
|
16
20
|
# Create a new instance of the component.
|
17
21
|
# @note The block will not be delegated {#initialize}. Instead, it will be sent to {#template} when rendering.
|
18
|
-
def new(
|
22
|
+
def new(*a, **k, &block)
|
19
23
|
if block
|
20
|
-
object = super(
|
24
|
+
object = super(*a, **k, &nil)
|
21
25
|
object.instance_exec { @_content_block = block }
|
22
26
|
object
|
23
27
|
else
|
@@ -25,12 +29,11 @@ class Phlex::SGML
|
|
25
29
|
end
|
26
30
|
end
|
27
31
|
|
28
|
-
# @api private
|
29
32
|
def __element_method__?(method_name)
|
30
33
|
if instance_methods.include?(method_name)
|
31
34
|
owner = instance_method(method_name).owner
|
32
35
|
|
33
|
-
if Phlex::Elements === owner && owner.
|
36
|
+
if Phlex::SGML::Elements === owner && owner.__registered_elements__[method_name]
|
34
37
|
true
|
35
38
|
else
|
36
39
|
false
|
@@ -41,115 +44,68 @@ class Phlex::SGML
|
|
41
44
|
end
|
42
45
|
end
|
43
46
|
|
44
|
-
# @!method initialize
|
45
|
-
# @abstract Override to define an initializer for your component.
|
46
|
-
# @note Your initializer will not receive a block passed to {.new}. Instead, this block will be sent to {#template} when rendering.
|
47
|
-
# @example
|
48
|
-
# def initialize(articles:)
|
49
|
-
# @articles = articles
|
50
|
-
# end
|
51
|
-
|
52
|
-
# @abstract Override to define a template for your component.
|
53
|
-
# @example
|
54
|
-
# def view_template
|
55
|
-
# h1 { "👋 Hello World!" }
|
56
|
-
# end
|
57
|
-
# @example Your template may yield a content block.
|
58
|
-
# def view_template
|
59
|
-
# main {
|
60
|
-
# h1 { "Hello World" }
|
61
|
-
# yield
|
62
|
-
# }
|
63
|
-
# end
|
64
|
-
# @example Alternatively, you can delegate the content block to an element.
|
65
|
-
# def view_template(&block)
|
66
|
-
# article(class: "card", &block)
|
67
|
-
# end
|
68
47
|
def view_template
|
69
48
|
if block_given?
|
70
49
|
yield
|
71
50
|
end
|
72
51
|
end
|
73
52
|
|
74
|
-
def await(task)
|
75
|
-
case task
|
76
|
-
when defined?(Concurrent::IVar) && Concurrent::IVar
|
77
|
-
flush if task.pending?
|
78
|
-
task.wait.value
|
79
|
-
when defined?(Async::Task) && Async::Task
|
80
|
-
flush if task.running?
|
81
|
-
task.wait
|
82
|
-
else
|
83
|
-
raise Phlex::ArgumentError.new("Expected an asynchronous task / promise.")
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
53
|
def to_proc
|
88
54
|
proc { |c| c.render(self) }
|
89
55
|
end
|
90
56
|
|
91
|
-
|
92
|
-
def call(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, fragments: nil, &block)
|
57
|
+
def call(buffer = +"", context: {}, view_context: nil, parent: nil, fragments: nil, &block)
|
93
58
|
@_buffer = buffer
|
94
|
-
@_context = context
|
95
|
-
@_view_context = view_context
|
59
|
+
@_context = phlex_context = parent&.__context__ || Phlex::Context.new(user_context: context, view_context:)
|
96
60
|
@_parent = parent
|
97
61
|
|
98
62
|
raise Phlex::DoubleRenderError.new("You can't render a #{self.class.name} more than once.") if @_rendered
|
99
63
|
@_rendered = true
|
100
64
|
|
101
65
|
if fragments
|
102
|
-
|
66
|
+
phlex_context.target_fragments(fragments)
|
103
67
|
end
|
104
68
|
|
105
69
|
block ||= @_content_block
|
106
70
|
|
107
71
|
return "" unless render?
|
108
72
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
73
|
+
Thread.current[:__phlex_component__] = [self, Fiber.current.object_id]
|
74
|
+
|
75
|
+
phlex_context.around_render do
|
76
|
+
before_template(&block)
|
113
77
|
|
114
|
-
@_context.around_render do
|
115
78
|
around_template do
|
116
79
|
if block
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
if args.length > 0
|
123
|
-
yield_content_with_args(*args, &block)
|
124
|
-
else
|
125
|
-
yield_content(&block)
|
126
|
-
end
|
80
|
+
view_template do |*args|
|
81
|
+
if args.length > 0
|
82
|
+
__yield_content_with_args__(*args, &block)
|
83
|
+
else
|
84
|
+
__yield_content__(&block)
|
127
85
|
end
|
128
86
|
end
|
129
87
|
else
|
130
88
|
view_template
|
131
89
|
end
|
132
90
|
end
|
91
|
+
|
92
|
+
after_template(&block)
|
133
93
|
end
|
134
94
|
|
135
95
|
unless parent
|
136
|
-
|
137
|
-
Fiber[:__phlex_component__] = original_fiber_storage
|
138
|
-
end
|
139
|
-
buffer << context.buffer
|
96
|
+
buffer << phlex_context.buffer
|
140
97
|
end
|
98
|
+
ensure
|
99
|
+
Thread.current[:__phlex_component__] = [parent, Fiber.current.object_id]
|
141
100
|
end
|
142
101
|
|
143
|
-
|
144
|
-
|
102
|
+
protected def __context__ = @_context
|
103
|
+
|
145
104
|
def context
|
146
105
|
@_context.user_context
|
147
106
|
end
|
148
107
|
|
149
|
-
# Output
|
150
|
-
# @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`
|
151
|
-
# @return [nil]
|
152
|
-
# @see #format_object
|
108
|
+
# Output plain text.
|
153
109
|
def plain(content)
|
154
110
|
unless __text__(content)
|
155
111
|
raise Phlex::ArgumentError.new("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")
|
@@ -158,9 +114,7 @@ class Phlex::SGML
|
|
158
114
|
nil
|
159
115
|
end
|
160
116
|
|
161
|
-
# Output a
|
162
|
-
# @return [nil]
|
163
|
-
# @yield If a block is given, it yields the block with no arguments.
|
117
|
+
# Output a single space character. If a block is given, a space will be output before and after the block.
|
164
118
|
def whitespace(&)
|
165
119
|
context = @_context
|
166
120
|
return if context.fragments && !context.in_target_fragment
|
@@ -170,15 +124,16 @@ class Phlex::SGML
|
|
170
124
|
buffer << " "
|
171
125
|
|
172
126
|
if block_given?
|
173
|
-
|
127
|
+
__yield_content__(&)
|
174
128
|
buffer << " "
|
175
129
|
end
|
176
130
|
|
177
131
|
nil
|
178
132
|
end
|
179
133
|
|
180
|
-
#
|
181
|
-
#
|
134
|
+
# Wrap the output in an HTML comment.
|
135
|
+
#
|
136
|
+
# [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Comments)
|
182
137
|
def comment(&)
|
183
138
|
context = @_context
|
184
139
|
return if context.fragments && !context.in_target_fragment
|
@@ -186,15 +141,13 @@ class Phlex::SGML
|
|
186
141
|
buffer = context.buffer
|
187
142
|
|
188
143
|
buffer << "<!-- "
|
189
|
-
|
144
|
+
__yield_content__(&)
|
190
145
|
buffer << " -->"
|
191
146
|
|
192
147
|
nil
|
193
148
|
end
|
194
149
|
|
195
|
-
#
|
196
|
-
# @param content [String|nil]
|
197
|
-
# @return [nil]
|
150
|
+
# Output the given safe object as-is. You may need to use `safe` to mark a string as a safe object.
|
198
151
|
def raw(content)
|
199
152
|
case content
|
200
153
|
when Phlex::SGML::SafeObject
|
@@ -210,41 +163,27 @@ class Phlex::SGML
|
|
210
163
|
nil
|
211
164
|
end
|
212
165
|
|
213
|
-
# Capture
|
214
|
-
# @note This only works if the block's receiver is the current component or the block returns a String.
|
215
|
-
# @return [String]
|
166
|
+
# Capture the output of the block and returns it as a string.
|
216
167
|
def capture(*args, &block)
|
217
168
|
return "" unless block
|
218
169
|
|
219
170
|
if args.length > 0
|
220
|
-
@_context.capturing_into(+"") {
|
171
|
+
@_context.capturing_into(+"") { __yield_content_with_args__(*args, &block) }
|
221
172
|
else
|
222
|
-
@_context.capturing_into(+"") {
|
173
|
+
@_context.capturing_into(+"") { __yield_content__(&block) }
|
223
174
|
end
|
224
175
|
end
|
225
176
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
end
|
232
|
-
|
233
|
-
if normalized_name == "script"
|
234
|
-
raise Phlex::ArgumentError.new("You can’t use the `<script>` tag from the `tag` method. Use `unsafe_tag` instead, but be careful if using user input.")
|
235
|
-
end
|
236
|
-
|
237
|
-
if registered_elements[normalized_name]
|
238
|
-
public_send(normalized_name, ...)
|
177
|
+
# Mark the given string as safe for HTML output.
|
178
|
+
def safe(value)
|
179
|
+
case value
|
180
|
+
when String
|
181
|
+
Phlex::SGML::SafeValue.new(value)
|
239
182
|
else
|
240
|
-
raise Phlex::ArgumentError.new("
|
183
|
+
raise Phlex::ArgumentError.new("Expected a String.")
|
241
184
|
end
|
242
185
|
end
|
243
186
|
|
244
|
-
def safe(value)
|
245
|
-
Phlex::SGML::SafeValue.new(value)
|
246
|
-
end
|
247
|
-
|
248
187
|
alias_method :🦺, :safe
|
249
188
|
|
250
189
|
def flush
|
@@ -258,23 +197,23 @@ class Phlex::SGML
|
|
258
197
|
def render(renderable = nil, &)
|
259
198
|
case renderable
|
260
199
|
when Phlex::SGML
|
261
|
-
renderable.call(@_buffer,
|
200
|
+
renderable.call(@_buffer, parent: self, &)
|
262
201
|
when Class
|
263
202
|
if renderable < Phlex::SGML
|
264
|
-
renderable.new.call(@_buffer,
|
203
|
+
renderable.new.call(@_buffer, parent: self, &)
|
265
204
|
end
|
266
205
|
when Enumerable
|
267
206
|
renderable.each { |r| render(r, &) }
|
268
207
|
when Proc, Method
|
269
208
|
if renderable.arity == 0
|
270
|
-
|
209
|
+
__yield_content_with_no_args__(&renderable)
|
271
210
|
else
|
272
|
-
|
211
|
+
__yield_content__(&renderable)
|
273
212
|
end
|
274
213
|
when String
|
275
214
|
plain(renderable)
|
276
215
|
when nil
|
277
|
-
|
216
|
+
__yield_content__(&) if block_given?
|
278
217
|
else
|
279
218
|
raise Phlex::ArgumentError.new("You can't render a #{renderable.inspect}.")
|
280
219
|
end
|
@@ -284,27 +223,22 @@ class Phlex::SGML
|
|
284
223
|
|
285
224
|
private
|
286
225
|
|
287
|
-
# Like {#capture} but the output is vanished into a BlackHole buffer.
|
288
|
-
# Because the BlackHole does nothing with the output, this should be faster.
|
289
|
-
# @return [nil]
|
290
226
|
def vanish(*args)
|
291
227
|
return unless block_given?
|
292
228
|
|
293
|
-
|
229
|
+
if args.length > 0
|
230
|
+
@_context.capturing_into(Phlex::Vanish) { yield(*args) }
|
231
|
+
else
|
232
|
+
@_context.capturing_into(Phlex::Vanish) { yield(self) }
|
233
|
+
end
|
294
234
|
|
295
235
|
nil
|
296
236
|
end
|
297
237
|
|
298
|
-
# Determines if the component should render. By default, it returns `true`.
|
299
|
-
# @abstract Override to define your own predicate to prevent rendering.
|
300
|
-
# @return [Boolean]
|
301
238
|
def render?
|
302
239
|
true
|
303
240
|
end
|
304
241
|
|
305
|
-
# Format the object for output
|
306
|
-
# @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.
|
307
|
-
# @return [String]
|
308
242
|
def format_object(object)
|
309
243
|
case object
|
310
244
|
when Float, Integer
|
@@ -312,74 +246,80 @@ class Phlex::SGML
|
|
312
246
|
end
|
313
247
|
end
|
314
248
|
|
315
|
-
# @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.
|
316
|
-
# @return [nil]
|
317
249
|
def around_template
|
318
|
-
before_template
|
319
250
|
yield
|
320
|
-
after_template
|
321
|
-
|
322
251
|
nil
|
323
252
|
end
|
324
253
|
|
325
|
-
# @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.
|
326
|
-
# @return [nil]
|
327
254
|
def before_template
|
328
255
|
nil
|
329
256
|
end
|
330
257
|
|
331
|
-
# @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.
|
332
|
-
# @return [nil]
|
333
258
|
def after_template
|
334
259
|
nil
|
335
260
|
end
|
336
261
|
|
337
|
-
|
338
|
-
# @yieldparam component [self]
|
339
|
-
# @return [nil]
|
340
|
-
def yield_content
|
262
|
+
def __yield_content__
|
341
263
|
return unless block_given?
|
342
264
|
|
343
265
|
buffer = @_context.buffer
|
344
266
|
|
345
267
|
original_length = buffer.bytesize
|
346
268
|
content = yield(self)
|
347
|
-
|
269
|
+
__implicit_output__(content) if original_length == buffer.bytesize
|
348
270
|
|
349
271
|
nil
|
350
272
|
end
|
351
273
|
|
352
|
-
|
353
|
-
# @yield Yields the block with no arguments.
|
354
|
-
def yield_content_with_no_args
|
274
|
+
def __yield_content_with_no_args__
|
355
275
|
return unless block_given?
|
356
276
|
|
357
277
|
buffer = @_context.buffer
|
358
278
|
|
359
279
|
original_length = buffer.bytesize
|
360
280
|
content = yield
|
361
|
-
|
281
|
+
__implicit_output__(content) if original_length == buffer.bytesize
|
362
282
|
|
363
283
|
nil
|
364
284
|
end
|
365
285
|
|
366
|
-
|
367
|
-
# @yield [*args] Yields the given arguments.
|
368
|
-
# @return [nil]
|
369
|
-
def yield_content_with_args(*)
|
286
|
+
def __yield_content_with_args__(*a)
|
370
287
|
return unless block_given?
|
371
288
|
|
372
289
|
buffer = @_context.buffer
|
373
290
|
|
374
291
|
original_length = buffer.bytesize
|
375
|
-
content = yield(*)
|
376
|
-
|
292
|
+
content = yield(*a)
|
293
|
+
__implicit_output__(content) if original_length == buffer.bytesize
|
377
294
|
|
378
295
|
nil
|
379
296
|
end
|
380
297
|
|
381
|
-
|
382
|
-
|
298
|
+
def __implicit_output__(content)
|
299
|
+
context = @_context
|
300
|
+
return true if context.fragments && !context.in_target_fragment
|
301
|
+
|
302
|
+
case content
|
303
|
+
when Phlex::SGML::SafeObject
|
304
|
+
context.buffer << content.to_s
|
305
|
+
when String
|
306
|
+
context.buffer << Phlex::Escape.html_escape(content)
|
307
|
+
when Symbol
|
308
|
+
context.buffer << Phlex::Escape.html_escape(content.name)
|
309
|
+
when nil
|
310
|
+
nil
|
311
|
+
else
|
312
|
+
if (formatted_object = format_object(content))
|
313
|
+
context.buffer << Phlex::Escape.html_escape(formatted_object)
|
314
|
+
else
|
315
|
+
return false
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
true
|
320
|
+
end
|
321
|
+
|
322
|
+
# same as __implicit_output__ but escapes even `safe` objects
|
383
323
|
def __text__(content)
|
384
324
|
context = @_context
|
385
325
|
return true if context.fragments && !context.in_target_fragment
|
@@ -402,7 +342,6 @@ class Phlex::SGML
|
|
402
342
|
true
|
403
343
|
end
|
404
344
|
|
405
|
-
# @api private
|
406
345
|
def __attributes__(attributes, buffer = +"")
|
407
346
|
attributes.each do |k, v|
|
408
347
|
next unless v
|
@@ -413,78 +352,87 @@ class Phlex::SGML
|
|
413
352
|
else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols.")
|
414
353
|
end
|
415
354
|
|
416
|
-
|
417
|
-
|
418
|
-
unless Phlex::SGML::SafeObject === v
|
419
|
-
if lower_name == "href" && v.to_s.downcase.delete("^a-z:").start_with?("javascript:")
|
420
|
-
next
|
421
|
-
end
|
422
|
-
|
423
|
-
# Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters.
|
424
|
-
if Phlex::HTML::UNSAFE_ATTRIBUTES.include?(lower_name.delete("^a-z-"))
|
425
|
-
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
426
|
-
end
|
427
|
-
end
|
428
|
-
|
429
|
-
if name.match?(/[<>&"']/)
|
430
|
-
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
431
|
-
end
|
432
|
-
|
433
|
-
if lower_name.to_sym == :id && k != :id
|
434
|
-
raise Phlex::ArgumentError.new(":id attribute should only be passed as a lowercase symbol.")
|
435
|
-
end
|
436
|
-
|
437
|
-
case v
|
355
|
+
value = case v
|
438
356
|
when true
|
439
|
-
|
357
|
+
true
|
440
358
|
when String
|
441
|
-
|
359
|
+
v.gsub('"', """)
|
442
360
|
when Symbol
|
443
|
-
|
361
|
+
v.name.tr("_", "-").gsub('"', """)
|
444
362
|
when Integer, Float
|
445
|
-
|
363
|
+
v.to_s
|
446
364
|
when Hash
|
447
365
|
case k
|
448
|
-
when :class
|
449
|
-
buffer << " " << name << '="' << __classes__(v).gsub('"', """) << '"'
|
450
366
|
when :style
|
451
|
-
|
367
|
+
__styles__(v).gsub('"', """)
|
452
368
|
else
|
453
369
|
__nested_attributes__(v, "#{name}-", buffer)
|
454
370
|
end
|
455
371
|
when Array
|
456
|
-
|
457
|
-
when :class
|
458
|
-
__classes__(v)
|
372
|
+
case k
|
459
373
|
when :style
|
460
|
-
__styles__(v)
|
374
|
+
__styles__(v).gsub('"', """)
|
461
375
|
else
|
462
376
|
__nested_tokens__(v)
|
463
377
|
end
|
464
|
-
|
465
|
-
buffer << " " << name << '="' << value.gsub('"', """) << '"'
|
466
378
|
when Set
|
467
|
-
|
379
|
+
case k
|
380
|
+
when :style
|
381
|
+
__styles__(v).gsub('"', """)
|
382
|
+
else
|
383
|
+
__nested_tokens__(v.to_a)
|
384
|
+
end
|
468
385
|
when Phlex::SGML::SafeObject
|
469
|
-
|
386
|
+
v.to_s.gsub('"', """)
|
470
387
|
else
|
471
|
-
value
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
388
|
+
raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
|
389
|
+
end
|
390
|
+
|
391
|
+
lower_name = name.downcase
|
392
|
+
|
393
|
+
unless Phlex::SGML::SafeObject === v
|
394
|
+
normalized_name = lower_name.delete("^a-z-")
|
395
|
+
|
396
|
+
if value != true && REF_ATTRIBUTES.include?(normalized_name)
|
397
|
+
case value
|
398
|
+
when String
|
399
|
+
if value.downcase.delete("^a-z:").start_with?("javascript:")
|
400
|
+
# We just ignore these because they were likely not specified by the developer.
|
401
|
+
next
|
402
|
+
end
|
403
|
+
else
|
404
|
+
raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
|
405
|
+
end
|
477
406
|
end
|
478
407
|
|
479
|
-
|
408
|
+
if normalized_name.bytesize > 2 && normalized_name.start_with?("on") && !normalized_name.include?("-")
|
409
|
+
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
410
|
+
end
|
411
|
+
|
412
|
+
if UNSAFE_ATTRIBUTES.include?(normalized_name)
|
413
|
+
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
if name.match?(/[<>&"']/)
|
418
|
+
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
419
|
+
end
|
420
|
+
|
421
|
+
if lower_name.to_sym == :id && k != :id
|
422
|
+
raise Phlex::ArgumentError.new(":id attribute should only be passed as a lowercase symbol.")
|
423
|
+
end
|
424
|
+
|
425
|
+
case value
|
426
|
+
when true
|
427
|
+
buffer << " " << name
|
428
|
+
when String
|
429
|
+
buffer << " " << name << '="' << value << '"'
|
480
430
|
end
|
481
431
|
end
|
482
432
|
|
483
433
|
buffer
|
484
434
|
end
|
485
435
|
|
486
|
-
# @api private
|
487
|
-
#
|
488
436
|
# Provides the nested-attributes case for serializing out attributes.
|
489
437
|
# This allows us to skip many of the checks the `__attributes__` method must perform.
|
490
438
|
def __nested_attributes__(attributes, base_name, buffer = +"")
|
@@ -497,6 +445,10 @@ class Phlex::SGML
|
|
497
445
|
else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols")
|
498
446
|
end
|
499
447
|
|
448
|
+
if name.match?(/[<>&"']/)
|
449
|
+
raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
|
450
|
+
end
|
451
|
+
|
500
452
|
case v
|
501
453
|
when true
|
502
454
|
buffer << " " << base_name << name
|
@@ -512,22 +464,16 @@ class Phlex::SGML
|
|
512
464
|
buffer << " " << base_name << name << '="' << __nested_tokens__(v) << '"'
|
513
465
|
when Set
|
514
466
|
buffer << " " << base_name << name << '="' << __nested_tokens__(v.to_a) << '"'
|
467
|
+
when Phlex::SGML::SafeObject
|
468
|
+
buffer << " " << base_name << name << '="' << v.to_s.gsub('"', """) << '"'
|
515
469
|
else
|
516
|
-
|
517
|
-
v.to_phlex_attribute_value
|
518
|
-
elsif v.respond_to?(:to_str)
|
519
|
-
v.to_str
|
520
|
-
else
|
521
|
-
v.to_s
|
522
|
-
end
|
523
|
-
buffer << " " << base_name << name << '="' << value.gsub('"', """) << '"'
|
470
|
+
raise Phlex::ArgumentError.new("Invalid attribute value #{v.inspect}.")
|
524
471
|
end
|
525
472
|
|
526
473
|
buffer
|
527
474
|
end
|
528
475
|
end
|
529
476
|
|
530
|
-
# @api private
|
531
477
|
def __nested_tokens__(tokens)
|
532
478
|
buffer = +""
|
533
479
|
|
@@ -549,93 +495,105 @@ class Phlex::SGML
|
|
549
495
|
else
|
550
496
|
buffer << token.name.tr("_", "-")
|
551
497
|
end
|
552
|
-
when
|
553
|
-
# Do nothing
|
554
|
-
else
|
498
|
+
when Integer, Float, Phlex::SGML::SafeObject
|
555
499
|
if i > 0
|
556
500
|
buffer << " " << token.to_s
|
557
501
|
else
|
558
502
|
buffer << token.to_s
|
559
503
|
end
|
504
|
+
when Array
|
505
|
+
if token.length > 0
|
506
|
+
if i > 0
|
507
|
+
buffer << " " << __nested_tokens__(token)
|
508
|
+
else
|
509
|
+
buffer << __nested_tokens__(token)
|
510
|
+
end
|
511
|
+
end
|
512
|
+
when nil
|
513
|
+
# Do nothing
|
514
|
+
else
|
515
|
+
raise Phlex::ArgumentError.new("Invalid token type: #{token.class}.")
|
560
516
|
end
|
561
517
|
|
562
518
|
i += 1
|
563
519
|
end
|
564
520
|
|
565
|
-
buffer.gsub
|
566
|
-
buffer
|
521
|
+
buffer.gsub('"', """)
|
567
522
|
end
|
568
523
|
|
569
|
-
#
|
570
|
-
def
|
571
|
-
case
|
572
|
-
when String
|
573
|
-
c
|
574
|
-
when Symbol
|
575
|
-
c.name.tr("_", "-")
|
524
|
+
# Result is **unsafe**, so it should be escaped!
|
525
|
+
def __styles__(styles)
|
526
|
+
case styles
|
576
527
|
when Array, Set
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
528
|
+
styles.filter_map do |s|
|
529
|
+
case s
|
530
|
+
when String
|
531
|
+
if s == "" || s.end_with?(";")
|
532
|
+
s
|
533
|
+
else
|
534
|
+
"#{s};"
|
535
|
+
end
|
536
|
+
when Phlex::SGML::SafeObject
|
537
|
+
value = s.to_s
|
538
|
+
value.end_with?(";") ? value : "#{value};"
|
539
|
+
when Hash
|
540
|
+
next __styles__(s)
|
541
|
+
when nil
|
542
|
+
next nil
|
543
|
+
else
|
544
|
+
raise Phlex::ArgumentError.new("Invalid style: #{s.inspect}.")
|
585
545
|
end
|
586
|
-
|
587
|
-
when nil, false
|
588
|
-
nil
|
589
|
-
else
|
590
|
-
if c.respond_to?(:to_phlex_attribute_value)
|
591
|
-
c.to_phlex_attribute_value
|
592
|
-
elsif c.respond_to?(:to_str)
|
593
|
-
c.to_str
|
594
|
-
else
|
595
|
-
c.to_s
|
596
|
-
end
|
597
|
-
end
|
598
|
-
end
|
599
|
-
|
600
|
-
# @api private
|
601
|
-
def __styles__(s)
|
602
|
-
style = case s
|
603
|
-
when String
|
604
|
-
s
|
605
|
-
when Symbol
|
606
|
-
s.name.tr("_", "-")
|
607
|
-
when Integer, Float
|
608
|
-
s.to_s
|
609
|
-
when Array, Set
|
610
|
-
s.filter_map { |s| __styles__(s) }.join
|
546
|
+
end.join(" ")
|
611
547
|
when Hash
|
612
548
|
buffer = +""
|
613
|
-
|
549
|
+
i = 0
|
550
|
+
styles.each do |k, v|
|
614
551
|
prop = case k
|
615
|
-
|
616
|
-
|
617
|
-
|
552
|
+
when String
|
553
|
+
k
|
554
|
+
when Symbol
|
555
|
+
k.name.tr("_", "-")
|
556
|
+
else
|
557
|
+
raise Phlex::ArgumentError.new("Style keys should be Strings or Symbols.")
|
618
558
|
end
|
619
559
|
|
620
|
-
value =
|
560
|
+
value = case v
|
561
|
+
when String
|
562
|
+
v
|
563
|
+
when Symbol
|
564
|
+
v.name.tr("_", "-")
|
565
|
+
when Integer, Float, Phlex::SGML::SafeObject
|
566
|
+
v.to_s
|
567
|
+
when nil
|
568
|
+
nil
|
569
|
+
else
|
570
|
+
raise Phlex::ArgumentError.new("Invalid style value: #{v.inspect}")
|
571
|
+
end
|
621
572
|
|
622
573
|
if value
|
623
|
-
|
574
|
+
if i == 0
|
575
|
+
buffer << prop << ": " << value << ";"
|
576
|
+
else
|
577
|
+
buffer << " " << prop << ": " << value << ";"
|
578
|
+
end
|
624
579
|
end
|
580
|
+
|
581
|
+
i += 1
|
625
582
|
end
|
583
|
+
|
626
584
|
buffer
|
627
|
-
when nil, false
|
628
|
-
return nil
|
629
|
-
else
|
630
|
-
if s.respond_to?(:to_phlex_attribute_value)
|
631
|
-
s.to_phlex_attribute_value
|
632
|
-
elsif s.respond_to?(:to_str)
|
633
|
-
s.to_str
|
634
|
-
else
|
635
|
-
s.to_s
|
636
|
-
end
|
637
585
|
end
|
586
|
+
end
|
638
587
|
|
639
|
-
|
588
|
+
private_class_method def self.method_added(method_name)
|
589
|
+
if method_name == :view_template
|
590
|
+
location = instance_method(method_name).source_location[0]
|
591
|
+
|
592
|
+
if location[0] in "/" | "."
|
593
|
+
Phlex.__expand_attribute_cache__(location)
|
594
|
+
end
|
595
|
+
else
|
596
|
+
super
|
597
|
+
end
|
640
598
|
end
|
641
599
|
end
|