phlex 2.0.0.beta1 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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(*, **, &block)
22
+ def new(*a, **k, &block)
19
23
  if block
20
- object = super(*, **, &nil)
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.registered_elements[method_name]
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
- # Renders the view and returns the buffer. The default buffer is a mutable String.
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
- @_context.target_fragments(fragments)
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
- if !parent && Phlex::SUPPORTS_FIBER_STORAGE
110
- original_fiber_storage = Fiber[:__phlex_component__]
111
- Fiber[:__phlex_component__] = self
112
- end
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
- if Phlex::DeferredRender === self
118
- vanish(self, &block)
119
- view_template
120
- else
121
- view_template do |*args|
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
- if Phlex::SUPPORTS_FIBER_STORAGE
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
- # Access the current render context data
144
- # @return the supplied context object, by default a Hash
102
+ protected def __context__ = @_context
103
+
145
104
  def context
146
105
  @_context.user_context
147
106
  end
148
107
 
149
- # Output text content. The text will be HTML-escaped.
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 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.
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
- yield_content(&)
127
+ __yield_content__(&)
174
128
  buffer << " "
175
129
  end
176
130
 
177
131
  nil
178
132
  end
179
133
 
180
- # Output an HTML comment.
181
- # @return [nil]
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
- yield_content(&)
144
+ __yield_content__(&)
190
145
  buffer << " -->"
191
146
 
192
147
  nil
193
148
  end
194
149
 
195
- # This method is very dangerous and should usually be avoided. It will output the given String without any HTML safety. You should never use this method to output unsafe user input.
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 a block of output as a String.
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(+"") { yield_content_with_args(*args, &block) }
171
+ @_context.capturing_into(+"") { __yield_content_with_args__(*args, &block) }
221
172
  else
222
- @_context.capturing_into(+"") { yield_content(&block) }
173
+ @_context.capturing_into(+"") { __yield_content__(&block) }
223
174
  end
224
175
  end
225
176
 
226
- def tag(name, ...)
227
- normalized_name = case name
228
- when Symbol then name.name.downcase
229
- when String then name.downcase
230
- else raise Phlex::ArgumentError.new("Expected the tag name as a Symbol or String.")
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("Unknown tag: #{normalized_name}")
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, context: @_context, view_context: @_view_context, parent: self, &)
200
+ renderable.call(@_buffer, parent: self, &)
262
201
  when Class
263
202
  if renderable < Phlex::SGML
264
- renderable.new.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &)
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
- yield_content_with_no_args(&renderable)
209
+ __yield_content_with_no_args__(&renderable)
271
210
  else
272
- yield_content(&renderable)
211
+ __yield_content__(&renderable)
273
212
  end
274
213
  when String
275
214
  plain(renderable)
276
215
  when nil
277
- yield_content(&) if block_given?
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
- @_context.capturing_into(Phlex::BlackHole) { yield(*args) }
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
- # 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.
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
- __text__(content) if original_length == buffer.bytesize
269
+ __implicit_output__(content) if original_length == buffer.bytesize
348
270
 
349
271
  nil
350
272
  end
351
273
 
352
- # Same as {#yield_content} but yields no arguments.
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
- __text__(content) if original_length == buffer.bytesize
281
+ __implicit_output__(content) if original_length == buffer.bytesize
362
282
 
363
283
  nil
364
284
  end
365
285
 
366
- # Same as {#yield_content} but accepts a splat of arguments to yield. This is slightly slower than {#yield_content}.
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
- __text__(content) if original_length == buffer.bytesize
292
+ content = yield(*a)
293
+ __implicit_output__(content) if original_length == buffer.bytesize
377
294
 
378
295
  nil
379
296
  end
380
297
 
381
- # Performs the same task as the public method #plain, but does not raise an error if an unformattable object is passed
382
- # @api private
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
- lower_name = name.downcase
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
- buffer << " " << name
357
+ true
440
358
  when String
441
- buffer << " " << name << '="' << v.gsub('"', "&quot;") << '"'
359
+ v.gsub('"', "&quot;")
442
360
  when Symbol
443
- buffer << " " << name << '="' << v.name.tr("_", "-").gsub('"', "&quot;") << '"'
361
+ v.name.tr("_", "-").gsub('"', "&quot;")
444
362
  when Integer, Float
445
- buffer << " " << name << '="' << v.to_s << '"'
363
+ v.to_s
446
364
  when Hash
447
365
  case k
448
- when :class
449
- buffer << " " << name << '="' << __classes__(v).gsub('"', "&quot;") << '"'
450
366
  when :style
451
- buffer << " " << name << '="' << __styles__(v).gsub('"', "&quot;") << '"'
367
+ __styles__(v).gsub('"', "&quot;")
452
368
  else
453
369
  __nested_attributes__(v, "#{name}-", buffer)
454
370
  end
455
371
  when Array
456
- value = case k
457
- when :class
458
- __classes__(v)
372
+ case k
459
373
  when :style
460
- __styles__(v)
374
+ __styles__(v).gsub('"', "&quot;")
461
375
  else
462
376
  __nested_tokens__(v)
463
377
  end
464
-
465
- buffer << " " << name << '="' << value.gsub('"', "&quot;") << '"'
466
378
  when Set
467
- buffer << " " << name << '="' << __nested_tokens__(v.to_a) << '"'
379
+ case k
380
+ when :style
381
+ __styles__(v).gsub('"', "&quot;")
382
+ else
383
+ __nested_tokens__(v.to_a)
384
+ end
468
385
  when Phlex::SGML::SafeObject
469
- buffer << " " << name << '="' << v.to_s.gsub('"', "&quot;") << '"'
386
+ v.to_s.gsub('"', "&quot;")
470
387
  else
471
- value = if v.respond_to?(:to_phlex_attribute_value)
472
- v.to_phlex_attribute_value
473
- elsif v.respond_to?(:to_str)
474
- v.to_str
475
- else
476
- v.to_s
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
- buffer << " " << name << '="' << value.gsub('"', "&quot;") << '"'
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('"', "&quot;") << '"'
515
469
  else
516
- value = if v.respond_to?(:to_phlex_attribute_value)
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('"', "&quot;") << '"'
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 nil
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!('"', "&quot;")
566
- buffer
521
+ buffer.gsub('"', "&quot;")
567
522
  end
568
523
 
569
- # @api private
570
- def __classes__(c)
571
- case c
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
- c.filter_map { |c| __classes__(c) }.join(" ")
578
- when Hash
579
- c.filter_map { |c, add|
580
- next unless add
581
- case c
582
- when String then c
583
- when Symbol then c.name.tr("_", "-").delete_suffix("?")
584
- else raise Phlex::ArgumentError.new("Class keys should be Strings or Symbols.")
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
- }.join(" ")
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
- s.each do |k, v|
549
+ i = 0
550
+ styles.each do |k, v|
614
551
  prop = case k
615
- when String then k
616
- when Symbol then k.name.tr("_", "-")
617
- else raise Phlex::ArgumentError.new("Style keys should be Strings or Symbols.")
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 = __styles__(v)
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
- buffer << prop << ":" << value
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
- style.end_with?(";") ? style : "#{style};"
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