phlex 2.0.0.beta1 → 2.0.0.rc1
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.
- 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 +3 -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} +18 -44
- 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 -16
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
|