phlex 1.9.3 → 1.10.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.

@@ -8,71 +8,71 @@ module Phlex::HTML::VoidElements
8
8
  # Outputs an `<area>` tag.
9
9
  # @return [nil]
10
10
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/area
11
- register_void_element :area, tag: "area"
11
+ register_void_element :area
12
12
 
13
13
  # @!method br(**attributes, &content)
14
14
  # Outputs a `<br>` tag.
15
15
  # @return [nil]
16
16
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/br
17
- register_void_element :br, tag: "br"
17
+ register_void_element :br
18
+
19
+ # @!method col(**attributes, &content)
20
+ # Outputs a `<col>` tag.
21
+ # @return [nil]
22
+ # @see https://developer.mozilla.org/docs/Web/HTML/Element/col
23
+ register_void_element :col
18
24
 
19
25
  # @!method embed(**attributes, &content)
20
26
  # Outputs an `<embed>` tag.
21
27
  # @return [nil]
22
28
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/embed
23
- register_void_element :embed, tag: "embed"
29
+ register_void_element :embed
24
30
 
25
31
  # @!method hr(**attributes, &content)
26
32
  # Outputs an `<hr>` tag.
27
33
  # @return [nil]
28
34
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/hr
29
- register_void_element :hr, tag: "hr"
35
+ register_void_element :hr
30
36
 
31
37
  # @!method img(**attributes, &content)
32
38
  # Outputs an `<img>` tag.
33
39
  # @return [nil]
34
40
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/img
35
- register_void_element :img, tag: "img"
41
+ register_void_element :img
36
42
 
37
43
  # @!method input(**attributes, &content)
38
44
  # Outputs an `<input>` tag.
39
45
  # @return [nil]
40
46
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/input
41
- register_void_element :input, tag: "input"
47
+ register_void_element :input
42
48
 
43
49
  # @!method link(**attributes, &content)
44
50
  # Outputs a `<link>` tag.
45
51
  # @return [nil]
46
52
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/link
47
- register_void_element :link, tag: "link"
53
+ register_void_element :link
48
54
 
49
55
  # @!method meta(**attributes, &content)
50
56
  # Outputs a `<meta>` tag.
51
57
  # @return [nil]
52
58
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/meta
53
- register_void_element :meta, tag: "meta"
59
+ register_void_element :meta
54
60
 
55
61
  # @!method param(**attributes, &content)
56
62
  # Outputs a `<param>` tag.
57
63
  # @return [nil]
58
64
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/param
59
- register_void_element :param, tag: "param"
65
+ register_void_element :param, deprecated: "⚠️ [DEPRECATION] The <param> tag is deprecated. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/param"
60
66
 
61
67
  # @!method source(**attributes, &content)
62
68
  # Outputs a `<source>` tag.
63
69
  # @return [nil]
64
70
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/source
65
- register_void_element :source, tag: "source"
71
+ register_void_element :source
66
72
 
67
73
  # @!method track(**attributes, &content)
68
74
  # Outputs a `<track>` tag.
69
75
  # @return [nil]
70
76
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/track
71
- register_void_element :track, tag: "track"
72
-
73
- # @!method col(**attributes, &content)
74
- # Outputs a `<col>` tag.
75
- # @return [nil]
76
- # @see https://developer.mozilla.org/docs/Web/HTML/Element/col
77
- register_void_element :col, tag: "col"
77
+ register_void_element :track
78
78
  end
data/lib/phlex/html.rb CHANGED
@@ -3,6 +3,9 @@
3
3
  module Phlex
4
4
  # @abstract Subclass and define {#template} to create an HTML component class.
5
5
  class HTML < SGML
6
+ autoload :StandardElements, "phlex/html/standard_elements"
7
+ autoload :VoidElements, "phlex/html/void_elements"
8
+
6
9
  # A list of HTML attributes that have the potential to execute unsafe JavaScript.
7
10
  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
8
11
 
@@ -22,11 +25,14 @@ module Phlex
22
25
  end
23
26
 
24
27
  extend Elements
25
- include Helpers, VoidElements, StandardElements
28
+ include VoidElements, StandardElements
26
29
 
27
30
  # Output an HTML doctype.
28
31
  def doctype
29
- @_context.target << "<!DOCTYPE html>"
32
+ context = @_context
33
+ return if context.fragments && !context.in_target_fragment
34
+
35
+ context.buffer << "<!DOCTYPE html>"
30
36
  nil
31
37
  end
32
38
 
@@ -46,6 +52,14 @@ module Phlex
46
52
  self.class.__unbuffered_class__.new(self)
47
53
  end
48
54
 
55
+ def filename
56
+ nil
57
+ end
58
+
59
+ def content_type
60
+ "text/html"
61
+ end
62
+
49
63
  # This should be extended after all method definitions
50
64
  extend ElementClobberingGuard
51
65
  end
data/lib/phlex/kit.rb ADDED
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex::Kit
4
+ def self.extended(mod)
5
+ warn "⚠️ [WARNING] Phlex::Kit is experimental and may be removed from future versions of Phlex."
6
+ super
7
+ end
8
+
9
+ # When a kit is included in a module, we need to load all of its components.
10
+ def included(mod)
11
+ constants.each { |c| const_get(c) if autoload?(c) }
12
+ super
13
+ end
14
+
15
+ def method_missing(name, *args, **kwargs, &block)
16
+ if name[0] == name[0].upcase && constants.include?(name) && const_get(name) && methods.include?(name)
17
+ public_send(name, *args, **kwargs, &block)
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def respond_to_missing?(name, include_private = false)
24
+ if name[0] == name[0].upcase && constants.include?(name) && const_get(name) && methods.include?(name)
25
+ true
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def const_added(name)
32
+ return if autoload?(name)
33
+
34
+ constant = const_get(name)
35
+
36
+ if Class === constant && constant < Phlex::SGML
37
+ if instance_methods.include?(name)
38
+ raise NameError, "The instance method `#{name}' is already defined on `#{inspect}`."
39
+ elsif methods.include?(name)
40
+ raise NameError, "The method `#{name}' is already defined on `#{inspect}`."
41
+ end
42
+
43
+ constant.include(self)
44
+
45
+ define_method(name) do |*args, **kwargs, &block|
46
+ render(constant.new(*args, **kwargs), &block)
47
+ end
48
+
49
+ define_singleton_method(name) do |*args, **kwargs, &block|
50
+ if (component = Fiber[:__phlex_component__])
51
+ component.instance_exec do
52
+ render(constant.new(*args, **kwargs), &block)
53
+ end
54
+ else
55
+ raise "You can't call `#{name}' outside of a Phlex rendering context."
56
+ end
57
+ end
58
+ end
59
+
60
+ super
61
+ end
62
+ end
data/lib/phlex/sgml.rb CHANGED
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
4
- using Phlex::Overrides::Symbol::Name
5
- end
6
-
7
3
  module Phlex
8
4
  # **Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.
9
5
  class SGML
6
+ include Helpers
7
+
10
8
  class << self
11
9
  # Render the view to a String. Arguments are delegated to {.new}.
12
10
  def call(...)
@@ -53,33 +51,41 @@ module Phlex
53
51
 
54
52
  # @abstract Override to define a template for your component.
55
53
  # @example
56
- # def template
54
+ # def view_template
57
55
  # h1 { "👋 Hello World!" }
58
56
  # end
59
57
  # @example Your template may yield a content block.
60
- # def template
58
+ # def view_template
61
59
  # main {
62
60
  # h1 { "Hello World" }
63
61
  # yield
64
62
  # }
65
63
  # end
66
64
  # @example Alternatively, you can delegate the content block to an element.
67
- # def template(&block)
65
+ # def view_template(&block)
68
66
  # article(class: "card", &block)
69
67
  # end
70
68
  def template
71
69
  yield
72
70
  end
73
71
 
74
- # @api private
72
+ def self.method_added(method_name)
73
+ if method_name == :template
74
+ Kernel.warn "⚠️ [DEPRECATION] Defining the `template` method on a Phlex component will not be supported in Phlex 2.0. Please rename the method to `view_template` instead."
75
+ end
76
+ end
77
+
78
+ def view_template(&block)
79
+ template(&block)
80
+ end
81
+
75
82
  def await(task)
76
- if task.is_a?(Concurrent::IVar)
83
+ case task
84
+ when defined?(Concurrent::IVar) && Concurrent::IVar
77
85
  flush if task.pending?
78
-
79
86
  task.wait.value
80
- elsif defined?(Async::Task) && task.is_a?(Async::Task)
87
+ when defined?(Async::Task) && Async::Task
81
88
  flush if task.running?
82
-
83
89
  task.wait
84
90
  else
85
91
  raise ArgumentError, "Expected an asynchronous task / promise."
@@ -87,43 +93,65 @@ module Phlex
87
93
  end
88
94
 
89
95
  # Renders the view and returns the buffer. The default buffer is a mutable String.
90
- def call(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, &block)
91
- __final_call__(buffer, context: context, view_context: view_context, parent: parent, &block).tap do
96
+ def call(...)
97
+ __final_call__(...).tap do
92
98
  self.class.rendered_at_least_once!
93
99
  end
94
100
  end
95
101
 
96
102
  # @api private
97
- def __final_call__(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, &block)
103
+ def __final_call__(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, fragments: nil, &block)
98
104
  @_buffer = buffer
99
105
  @_context = context
100
106
  @_view_context = view_context
101
107
  @_parent = parent
108
+ if fragments
109
+ warn "⚠️ [WARNING] Selective Rendering is experimental, incomplete, and may change in future versions."
110
+ @_context.target_fragments(fragments)
111
+ end
102
112
 
103
113
  block ||= @_content_block
104
114
 
105
115
  return "" unless render?
106
116
 
107
- around_template do
108
- if block
109
- if is_a?(DeferredRender)
110
- __vanish__(self, &block)
111
- template
112
- else
113
- template do |*args|
114
- if args.length > 0
115
- yield_content_with_args(*args, &block)
116
- else
117
- yield_content(&block)
117
+ if !parent && Phlex::SUPPORTS_FIBER_STORAGE
118
+ original_fiber_storage = Fiber[:__phlex_component__]
119
+ Fiber[:__phlex_component__] = self
120
+ end
121
+
122
+ @_context.around_render do
123
+ around_template do
124
+ if block
125
+ if is_a?(DeferredRender)
126
+ __vanish__(self, &block)
127
+ view_template
128
+ else
129
+ view_template do |*args|
130
+ if args.length > 0
131
+ yield_content_with_args(*args, &block)
132
+ else
133
+ yield_content(&block)
134
+ end
118
135
  end
119
136
  end
137
+ else
138
+ view_template
120
139
  end
121
- else
122
- template
123
140
  end
124
141
  end
125
142
 
126
- buffer << context.target unless parent
143
+ unless parent
144
+ if Phlex::SUPPORTS_FIBER_STORAGE
145
+ Fiber[:__phlex_component__] = original_fiber_storage
146
+ end
147
+ buffer << context.buffer
148
+ end
149
+ end
150
+
151
+ # Access the current render context data
152
+ # @return the supplied context object, by default a Hash
153
+ def context
154
+ @_context.user_context
127
155
  end
128
156
 
129
157
  # Output text content. The text will be HTML-escaped.
@@ -141,14 +169,17 @@ module Phlex
141
169
  # 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.
142
170
  # @return [nil]
143
171
  # @yield If a block is given, it yields the block with no arguments.
144
- def whitespace
145
- target = @_context.target
172
+ def whitespace(&block)
173
+ context = @_context
174
+ return if context.fragments && !context.in_target_fragment
146
175
 
147
- target << " "
176
+ buffer = context.buffer
177
+
178
+ buffer << " "
148
179
 
149
180
  if block_given?
150
- yield
151
- target << " "
181
+ yield_content(&block)
182
+ buffer << " "
152
183
  end
153
184
 
154
185
  nil
@@ -157,11 +188,14 @@ module Phlex
157
188
  # Output an HTML comment.
158
189
  # @return [nil]
159
190
  def comment(&block)
160
- target = @_context.target
191
+ context = @_context
192
+ return if context.fragments && !context.in_target_fragment
193
+
194
+ buffer = context.buffer
161
195
 
162
- target << "<!-- "
196
+ buffer << "<!-- "
163
197
  yield_content(&block)
164
- target << " -->"
198
+ buffer << " -->"
165
199
 
166
200
  nil
167
201
  end
@@ -172,7 +206,10 @@ module Phlex
172
206
  def unsafe_raw(content = nil)
173
207
  return nil unless content
174
208
 
175
- @_context.target << content
209
+ context = @_context
210
+ return if context.fragments && !context.in_target_fragment
211
+
212
+ context.buffer << content
176
213
  nil
177
214
  end
178
215
 
@@ -191,9 +228,9 @@ module Phlex
191
228
  def flush
192
229
  return if @_context.capturing
193
230
 
194
- target = @_context.target
195
- @_buffer << target.dup
196
- target.clear
231
+ buffer = @_context.buffer
232
+ @_buffer << buffer.dup
233
+ buffer.clear
197
234
  end
198
235
 
199
236
  # Render another component, block or enumerable
@@ -294,11 +331,11 @@ module Phlex
294
331
  def yield_content
295
332
  return unless block_given?
296
333
 
297
- target = @_context.target
334
+ buffer = @_context.buffer
298
335
 
299
- original_length = target.length
336
+ original_length = buffer.bytesize
300
337
  content = yield(self)
301
- __text__(content) if original_length == target.length
338
+ __text__(content) if original_length == buffer.bytesize
302
339
 
303
340
  nil
304
341
  end
@@ -308,11 +345,11 @@ module Phlex
308
345
  def yield_content_with_no_args
309
346
  return unless block_given?
310
347
 
311
- target = @_context.target
348
+ buffer = @_context.buffer
312
349
 
313
- original_length = target.length
350
+ original_length = buffer.bytesize
314
351
  content = yield
315
- __text__(content) if original_length == target.length
352
+ __text__(content) if original_length == buffer.bytesize
316
353
 
317
354
  nil
318
355
  end
@@ -323,11 +360,11 @@ module Phlex
323
360
  def yield_content_with_args(*args)
324
361
  return unless block_given?
325
362
 
326
- target = @_context.target
363
+ buffer = @_context.buffer
327
364
 
328
- original_length = target.length
365
+ original_length = buffer.bytesize
329
366
  content = yield(*args)
330
- __text__(content) if original_length == target.length
367
+ __text__(content) if original_length == buffer.bytesize
331
368
 
332
369
  nil
333
370
  end
@@ -335,16 +372,19 @@ module Phlex
335
372
  # Performs the same task as the public method #plain, but does not raise an error if an unformattable object is passed
336
373
  # @api private
337
374
  def __text__(content)
375
+ context = @_context
376
+ return true if context.fragments && !context.in_target_fragment
377
+
338
378
  case content
339
379
  when String
340
- @_context.target << ERB::Escape.html_escape(content)
380
+ @_context.buffer << Phlex::Escape.html_escape(content)
341
381
  when Symbol
342
- @_context.target << ERB::Escape.html_escape(content.name)
382
+ @_context.buffer << Phlex::Escape.html_escape(content.name)
343
383
  when nil
344
384
  nil
345
385
  else
346
386
  if (formatted_object = format_object(content))
347
- @_context.target << ERB::Escape.html_escape(formatted_object)
387
+ @_context.buffer << Phlex::Escape.html_escape(formatted_object)
348
388
  else
349
389
  return false
350
390
  end
@@ -384,10 +424,10 @@ module Phlex
384
424
  end
385
425
 
386
426
  lower_name = name.downcase
387
- next if lower_name == "href" && v.to_s.downcase.tr("^a-z:", "").start_with?("javascript:")
427
+ next if lower_name == "href" && v.start_with?(/\s*javascript:/i)
388
428
 
389
429
  # Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters.
390
- if HTML::EVENT_ATTRIBUTES.include?(lower_name.tr("^a-z-", "")) || name.match?(/[<>&"']/)
430
+ if HTML::EVENT_ATTRIBUTES[lower_name] || name.match?(/[<>&"']/)
391
431
  raise ArgumentError, "Unsafe attribute name detected: #{k}."
392
432
  end
393
433
 
@@ -395,26 +435,34 @@ module Phlex
395
435
  when true
396
436
  buffer << " " << name
397
437
  when String
398
- buffer << " " << name << '="' << ERB::Escape.html_escape(v) << '"'
438
+ buffer << " " << name << '="' << Phlex::Escape.html_escape(v) << '"'
399
439
  when Symbol
400
- buffer << " " << name << '="' << ERB::Escape.html_escape(v.name) << '"'
440
+ buffer << " " << name << '="' << Phlex::Escape.html_escape(v.name) << '"'
401
441
  when Integer, Float
402
442
  buffer << " " << name << '="' << v.to_s << '"'
403
443
  when Hash
404
444
  __build_attributes__(
405
445
  v.transform_keys { |subkey|
406
446
  case subkey
407
- when Symbol then"#{k}-#{subkey.name.tr('_', '-')}"
408
- else "#{k}-#{subkey}"
447
+ when Symbol then"#{name}-#{subkey.name.tr('_', '-')}"
448
+ else "#{name}-#{subkey}"
409
449
  end
410
450
  }, buffer: buffer
411
451
  )
412
452
  when Array
413
- buffer << " " << name << '="' << ERB::Escape.html_escape(v.compact.join(" ")) << '"'
453
+ buffer << " " << name << '="' << Phlex::Escape.html_escape(v.compact.join(" ")) << '"'
414
454
  when Set
415
- buffer << " " << name << '="' << ERB::Escape.html_escape(v.to_a.compact.join(" ")) << '"'
455
+ buffer << " " << name << '="' << Phlex::Escape.html_escape(v.to_a.compact.join(" ")) << '"'
416
456
  else
417
- buffer << " " << name << '="' << ERB::Escape.html_escape(v.to_str) << '"'
457
+ value = if v.respond_to?(:to_phlex_attribute_value)
458
+ v.to_phlex_attribute_value
459
+ elsif v.respond_to?(:to_str)
460
+ v.to_str
461
+ else
462
+ v.to_s
463
+ end
464
+
465
+ buffer << " " << name << '="' << Phlex::Escape.html_escape(value) << '"'
418
466
  end
419
467
  end
420
468