phlex 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of phlex might be problematic. Click here for more details.

@@ -1,76 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Void HTML elements don't accept content and never have a closing tag.
3
4
  module Phlex::HTML::VoidElements
4
5
  extend Phlex::Elements
5
6
 
6
7
  # @!method area(**attributes, &content)
7
- # Outputs an <code>area</code> tag
8
+ # Outputs an `<area>` tag.
8
9
  # @return [nil]
9
10
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/area
10
11
  register_void_element :area, tag: "area"
11
12
 
12
13
  # @!method br(**attributes, &content)
13
- # Outputs a <code>br</code> tag
14
+ # Outputs a `<br>` tag.
14
15
  # @return [nil]
15
16
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/br
16
17
  register_void_element :br, tag: "br"
17
18
 
18
19
  # @!method embed(**attributes, &content)
19
- # Outputs an <code>embed</code> tag
20
+ # Outputs an `<embed>` tag.
20
21
  # @return [nil]
21
22
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/embed
22
23
  register_void_element :embed, tag: "embed"
23
24
 
24
25
  # @!method hr(**attributes, &content)
25
- # Outputs a <code>hr</code> tag
26
+ # Outputs an `<hr>` tag.
26
27
  # @return [nil]
27
28
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/hr
28
29
  register_void_element :hr, tag: "hr"
29
30
 
30
31
  # @!method img(**attributes, &content)
31
- # Outputs an <code>img</code> tag
32
+ # Outputs an `<img>` tag.
32
33
  # @return [nil]
33
34
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/img
34
35
  register_void_element :img, tag: "img"
35
36
 
36
37
  # @!method input(**attributes, &content)
37
- # Outputs an <code>input</code> tag
38
+ # Outputs an `<input>` tag.
38
39
  # @return [nil]
39
40
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/input
40
41
  register_void_element :input, tag: "input"
41
42
 
42
43
  # @!method link(**attributes, &content)
43
- # Outputs a <code>link</code> tag
44
+ # Outputs a `<link>` tag.
44
45
  # @return [nil]
45
46
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/link
46
47
  register_void_element :link, tag: "link"
47
48
 
48
49
  # @!method meta(**attributes, &content)
49
- # Outputs a <code>meta</code> tag
50
+ # Outputs a `<meta>` tag.
50
51
  # @return [nil]
51
52
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/meta
52
53
  register_void_element :meta, tag: "meta"
53
54
 
54
55
  # @!method param(**attributes, &content)
55
- # Outputs a <code>param</code> tag
56
+ # Outputs a `<param>` tag.
56
57
  # @return [nil]
57
58
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/param
58
59
  register_void_element :param, tag: "param"
59
60
 
60
61
  # @!method source(**attributes, &content)
61
- # Outputs a <code>source</code> tag
62
+ # Outputs a `<source>` tag.
62
63
  # @return [nil]
63
64
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/source
64
65
  register_void_element :source, tag: "source"
65
66
 
66
67
  # @!method track(**attributes, &content)
67
- # Outputs a <code>track</code> tag
68
+ # Outputs a `<track>` tag.
68
69
  # @return [nil]
69
70
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/track
70
71
  register_void_element :track, tag: "track"
71
72
 
72
73
  # @!method col(**attributes, &content)
73
- # Outputs a <code>col</code> tag
74
+ # Outputs a `<col>` tag.
74
75
  # @return [nil]
75
76
  # @see https://developer.mozilla.org/docs/Web/HTML/Element/col
76
77
  register_void_element :col, tag: "col"
data/lib/phlex/html.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phlex
4
+ # @abstract Subclass and define {#template} to create an HTML component class.
4
5
  class HTML < SGML
5
6
  # A list of HTML attributes that have the potential to execute unsafe JavaScript.
6
7
  EVENT_ATTRIBUTES = %w[onabort onafterprint onbeforeprint onbeforeunload onblur oncanplay oncanplaythrough onchange onclick oncontextmenu oncopy oncuechange oncut ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended onerror onfocus onhashchange oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart onmessage onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onoffline ononline onpagehide onpageshow onpaste onpause onplay onplaying onpopstate onprogress onratechange onreset onresize onscroll onsearch onseeked onseeking onselect onstalled onstorage onsubmit onsuspend ontimeupdate ontoggle onunload onvolumechange onwaiting onwheel].to_h { [_1, true] }.freeze
@@ -29,12 +30,9 @@ module Phlex
29
30
  nil
30
31
  end
31
32
 
32
- # @deprecated use {#plain} instead.
33
- def text(...)
34
- warn "DEPRECATED: The `text` method has been deprecated in favour of `plain`. Please use `plain` instead. The `text` method will be removed in a future version of Phlex. Called from: #{caller.first}"
35
- plain(...)
36
- end
37
-
33
+ # Outputs an `<svg>` tag
34
+ # @return [nil]
35
+ # @see https://developer.mozilla.org/docs/Web/SVG/Element/svg
38
36
  def svg(...)
39
37
  super do
40
38
  render Phlex::SVG.new do |svg|
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # @api private
3
4
  module Phlex::Overrides::Symbol::Name
4
5
  refine(Symbol) { alias_method :name, :to_s }
5
6
  end
data/lib/phlex/sgml.rb CHANGED
@@ -5,17 +5,16 @@ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
5
5
  end
6
6
 
7
7
  module Phlex
8
+ # **Standard Generalized Markup Language** for behaviour common to {HTML} and {SVG}.
8
9
  class SGML
9
10
  class << self
10
- # Render the view to a String. Arguments are delegated to <code>new</code>.
11
+ # Render the view to a String. Arguments are delegated to {.new}.
11
12
  def call(...)
12
13
  new(...).call
13
14
  end
14
15
 
15
- alias_method :render, :call
16
-
17
16
  # Create a new instance of the component.
18
- # @note The block will not be delegated to the initializer. Instead, it will be provided to `template` when rendering.
17
+ # @note The block will not be delegated {#initialize}. Instead, it will be sent to {#template} when rendering.
19
18
  def new(*args, **kwargs, &block)
20
19
  if block
21
20
  object = super(*args, **kwargs, &nil)
@@ -31,28 +30,83 @@ module Phlex
31
30
  alias_method :__attributes__, :__final_attributes__
32
31
  alias_method :call, :__final_call__
33
32
  end
33
+
34
+ # @api private
35
+ def element_method?(method_name)
36
+ return false unless instance_methods.include?(method_name)
37
+
38
+ owner = instance_method(method_name).owner
39
+
40
+ return true if owner.is_a?(Phlex::Elements) && owner.registered_elements[method_name]
41
+
42
+ false
43
+ end
44
+ end
45
+
46
+ # @!method initialize
47
+ # @abstract Override to define an initializer for your component.
48
+ # @note Your initializer will not receive a block passed to {.new}. Instead, this block will be sent to {#template} when rendering.
49
+ # @example
50
+ # def initialize(articles:)
51
+ # @articles = articles
52
+ # end
53
+
54
+ # @abstract Override to define a template for your component.
55
+ # @example
56
+ # def template
57
+ # h1 { "👋 Hello World!" }
58
+ # end
59
+ # @example Your template may yield a content block.
60
+ # def template
61
+ # main {
62
+ # h1 { "Hello World" }
63
+ # yield
64
+ # }
65
+ # end
66
+ # @example Alternatively, you can delegate the content block to an element.
67
+ # def template(&block)
68
+ # article(class: "card", &block)
69
+ # end
70
+ def template
71
+ yield
72
+ end
73
+
74
+ # @api private
75
+ def await(task)
76
+ if task.is_a?(Concurrent::IVar)
77
+ flush if task.pending?
78
+
79
+ task.wait.value
80
+ elsif defined?(Async::Task) && task.is_a?(Async::Task)
81
+ flush if task.running?
82
+
83
+ task.wait
84
+ else
85
+ raise ArgumentError, "Expected an asynchronous task / promise."
86
+ end
34
87
  end
35
88
 
36
89
  # Renders the view and returns the buffer. The default buffer is a mutable String.
37
- def call(buffer = nil, context: Phlex::Context.new, view_context: nil, parent: nil, &block)
90
+ def call(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, &block)
38
91
  __final_call__(buffer, context: context, view_context: view_context, parent: parent, &block).tap do
39
92
  self.class.rendered_at_least_once!
40
93
  end
41
94
  end
42
95
 
43
96
  # @api private
44
- def __final_call__(buffer = nil, context: Phlex::Context.new, view_context: nil, parent: nil, &block)
97
+ def __final_call__(buffer = +"", context: Phlex::Context.new, view_context: nil, parent: nil, &block)
98
+ @_buffer = buffer
45
99
  @_context = context
46
100
  @_view_context = view_context
47
101
  @_parent = parent
48
102
 
49
103
  block ||= @_content_block
50
104
 
51
- return buffer || context.target unless render?
105
+ return unless render?
52
106
 
53
107
  around_template do
54
108
  if block
55
- if DeferredRender === self
109
+ if is_a?(DeferredRender)
56
110
  __vanish__(self, &block)
57
111
  template
58
112
  else
@@ -69,47 +123,16 @@ module Phlex
69
123
  end
70
124
  end
71
125
 
72
- buffer ? (buffer << context.target) : context.target
73
- end
74
-
75
- # Render another view
76
- # @param renderable [Phlex::SGML]
77
- # @return [nil]
78
- def render(renderable, &block)
79
- case renderable
80
- when Phlex::SGML
81
- renderable.call(context: @_context, view_context: @_view_context, parent: self, &block)
82
- when Class
83
- if renderable < Phlex::SGML
84
- renderable.new.call(context: @_context, view_context: @_view_context, parent: self, &block)
85
- end
86
- when Enumerable
87
- renderable.each { |r| render(r, &block) }
88
- when Proc
89
- yield_content(&renderable)
90
- else
91
- raise ArgumentError, "You can't render a #{renderable}."
92
- end
93
-
94
- nil
126
+ buffer << context.target unless parent
95
127
  end
96
128
 
97
129
  # Output text content. The text will be HTML-escaped.
130
+ # @param content [String, Symbol, Integer, void] the content to be output on the buffer. Strings, Symbols, and Integers are handled by `plain` directly, but any object can be handled by overriding `format_object`
98
131
  # @return [nil]
132
+ # @see #format_object
99
133
  def plain(content)
100
- case content
101
- when String
102
- @_context.target << ERB::Escape.html_escape(content)
103
- when Symbol
104
- @_context.target << ERB::Escape.html_escape(content.name)
105
- when Integer
106
- @_context.target << ERB::Escape.html_escape(content.to_s)
107
- when nil
108
- nil
109
- else
110
- if (formatted_object = format_object(content))
111
- @_context.target << ERB::Escape.html_escape(formatted_object)
112
- end
134
+ unless __text__(content)
135
+ raise ArgumentError, "You've passed an object to plain that is not handled by format_object. See https://rubydoc.info/gems/phlex/Phlex/SGML#format_object-instance_method for more information"
113
136
  end
114
137
 
115
138
  nil
@@ -117,12 +140,15 @@ module Phlex
117
140
 
118
141
  # Output a whitespace character. This is useful for getting inline elements to wrap. If you pass a block, a whitespace will be output before and after yielding the block.
119
142
  # @return [nil]
143
+ # @yield If a block is given, it yields the block with no arguments.
120
144
  def whitespace
121
- @_context.target << " "
145
+ target = @_context.target
146
+
147
+ target << " "
122
148
 
123
149
  if block_given?
124
150
  yield
125
- @_context.target << " "
151
+ target << " "
126
152
  end
127
153
 
128
154
  nil
@@ -131,9 +157,11 @@ module Phlex
131
157
  # Output an HTML comment.
132
158
  # @return [nil]
133
159
  def comment(&block)
134
- @_context.target << "<!-- "
160
+ target = @_context.target
161
+
162
+ target << "<!-- "
135
163
  yield_content(&block)
136
- @_context.target << " -->"
164
+ target << " -->"
137
165
 
138
166
  nil
139
167
  end
@@ -157,10 +185,59 @@ module Phlex
157
185
  @_context.with_target(+"") { yield_content(&block) }
158
186
  end
159
187
 
160
- # Like `capture` but the output is vanished into a BlackHole buffer.
188
+ private
189
+
190
+ # @api private
191
+ def flush
192
+ target = @_context.target
193
+ @_buffer << target.dup
194
+ target.clear
195
+ end
196
+
197
+ # Render another component, block or enumerable
198
+ # @return [nil]
199
+ # @overload render(component, &block)
200
+ # Renders the component.
201
+ # @param component [Phlex::SGML]
202
+ # @overload render(component_class, &block)
203
+ # Renders a new instance of the component class. This is useful for component classes that take no arguments.
204
+ # @param component_class [Class<Phlex::SGML>]
205
+ # @overload render(proc)
206
+ # Renders the proc with {#yield_content}.
207
+ # @param proc [Proc]
208
+ # @overload render(enumerable)
209
+ # Renders each item of the enumerable.
210
+ # @param enumerable [Enumerable]
211
+ # @example
212
+ # render @items
213
+ def render(renderable, &block)
214
+ case renderable
215
+ when Phlex::SGML
216
+ renderable.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &block)
217
+ when Class
218
+ if renderable < Phlex::SGML
219
+ renderable.new.call(@_buffer, context: @_context, view_context: @_view_context, parent: self, &block)
220
+ end
221
+ when Enumerable
222
+ renderable.each { |r| render(r, &block) }
223
+ when Proc
224
+ if renderable.arity == 0
225
+ yield_content_with_no_args(&renderable)
226
+ else
227
+ yield_content(&renderable)
228
+ end
229
+ else
230
+ raise ArgumentError, "You can't render a #{renderable}."
231
+ end
232
+
233
+ nil
234
+ end
235
+
236
+ # Like {#capture} but the output is vanished into a BlackHole buffer.
161
237
  # Because the BlackHole does nothing with the output, this should be faster.
162
238
  # @return [nil]
163
- private def __vanish__(*args)
239
+ # @api private
240
+ def __vanish__(*args)
164
241
  return unless block_given?
165
242
 
166
243
  @_context.with_target(BlackHole) { yield(*args) }
@@ -168,24 +245,26 @@ module Phlex
168
245
  nil
169
246
  end
170
247
 
171
- # Default render predicate can be overridden to prevent rendering
172
- # @return [bool]
173
- private def render?
248
+ # Determines if the component should render. By default, it returns `true`.
249
+ # @abstract Override to define your own predicate to prevent rendering.
250
+ # @return [Boolean]
251
+ def render?
174
252
  true
175
253
  end
176
254
 
177
255
  # Format the object for output
256
+ # @abstract Override to define your own format handling for different object types. Please remember to call `super` in the case that the passed object doesn't match, so that object formatting can be added at different layers of the inheritance tree.
178
257
  # @return [String]
179
- private def format_object(object)
258
+ def format_object(object)
180
259
  case object
181
260
  when Float
182
261
  object.to_s
183
262
  end
184
263
  end
185
264
 
186
- # Override this method to hook in around a template render. You can do things before and after calling <code>super</code> to render the template. You should always call <code>super</code> so that callbacks can be added at different layers of the inheritance tree.
265
+ # @abstract Override this method to hook in around a template render. You can do things before and after calling `super` to render the template. You should always call `super` so that callbacks can be added at different layers of the inheritance tree.
187
266
  # @return [nil]
188
- private def around_template
267
+ def around_template
189
268
  before_template
190
269
  yield
191
270
  after_template
@@ -193,52 +272,94 @@ module Phlex
193
272
  nil
194
273
  end
195
274
 
196
- # Override this method to hook in right before a template is rendered. Please remember to call <code>super</code> so that callbacks can be added at different layers of the inheritance tree.
275
+ # @abstract Override this method to hook in right before a template is rendered. Please remember to call `super` so that callbacks can be added at different layers of the inheritance tree.
197
276
  # @return [nil]
198
- private def before_template
277
+ def before_template
199
278
  nil
200
279
  end
201
280
 
202
- # Override this method to hook in right after a template is rendered. Please remember to call <code>super</code> so that callbacks can be added at different layers of the inheritance tree.
281
+ # @abstract Override this method to hook in right after a template is rendered. Please remember to call `super` so that callbacks can be added at different layers of the inheritance tree.
203
282
  # @return [nil]
204
- private def after_template
283
+ def after_template
205
284
  nil
206
285
  end
207
286
 
208
- # Yields the block and checks if it buffered anything. If nothing was buffered, the return value is treated as text.
287
+ # Yields the block and checks if it buffered anything. If nothing was buffered, the return value is treated as text. The text is always HTML-escaped.
288
+ # @yieldparam component [self]
209
289
  # @return [nil]
210
- private def yield_content
290
+ def yield_content
211
291
  return unless block_given?
212
292
 
213
- original_length = @_context.target.length
293
+ target = @_context.target
294
+
295
+ original_length = target.length
214
296
  content = yield(self)
297
+ __text__(content) if original_length == target.length
298
+
299
+ nil
300
+ end
301
+
302
+ # Same as {#yield_content} but yields no arguments.
303
+ # @yield Yields the block with no arguments.
304
+ def yield_content_with_no_args
305
+ return unless block_given?
306
+
307
+ target = @_context.target
215
308
 
216
- plain(content) if original_length == @_context.target.length
309
+ original_length = target.length
310
+ content = yield
311
+ __text__(content) if original_length == target.length
217
312
 
218
313
  nil
219
314
  end
220
315
 
221
- # Same as <code>yield_content</code> but accepts a splat of arguments to yield. This is slightly slower than <code>yield_content</code>, which is why it's defined as a different method because we don't always need arguments so we can usually use <code>yield_content</code> instead.
316
+ # Same as {#yield_content} but accepts a splat of arguments to yield. This is slightly slower than {#yield_content}.
317
+ # @yield [*args] Yields the given arguments.
222
318
  # @return [nil]
223
- private def yield_content_with_args(*args)
319
+ def yield_content_with_args(*args)
224
320
  return unless block_given?
225
321
 
226
- original_length = @_context.target.length
322
+ target = @_context.target
323
+
324
+ original_length = target.length
227
325
  content = yield(*args)
228
- plain(content) if original_length == @_context.target.length
326
+ __text__(content) if original_length == target.length
229
327
 
230
328
  nil
231
329
  end
232
330
 
331
+ # Performs the same task as the public method #plain, but does not raise an error if an unformattable object is passed
332
+ # @api private
333
+ def __text__(content)
334
+ case content
335
+ when String
336
+ @_context.target << ERB::Escape.html_escape(content)
337
+ when Symbol
338
+ @_context.target << ERB::Escape.html_escape(content.name)
339
+ when Integer
340
+ @_context.target << ERB::Escape.html_escape(content.to_s)
341
+ when nil
342
+ nil
343
+ else
344
+ if (formatted_object = format_object(content))
345
+ @_context.target << ERB::Escape.html_escape(formatted_object)
346
+ else
347
+ return false
348
+ end
349
+ end
350
+
351
+ true
352
+ end
353
+
233
354
  # @api private
234
- private def __attributes__(**attributes)
355
+ def __attributes__(**attributes)
235
356
  __final_attributes__(**attributes).tap do |buffer|
236
357
  Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] = buffer.freeze
237
358
  end
238
359
  end
239
360
 
240
361
  # @api private
241
- private def __final_attributes__(**attributes)
362
+ def __final_attributes__(**attributes)
242
363
  if respond_to?(:process_attributes)
243
364
  attributes = process_attributes(**attributes)
244
365
  end
@@ -258,14 +379,14 @@ module Phlex
258
379
  end
259
380
 
260
381
  # @api private
261
- private def __build_attributes__(attributes, buffer:)
382
+ def __build_attributes__(attributes, buffer:)
262
383
  attributes.each do |k, v|
263
384
  next unless v
264
385
 
265
386
  name = case k
266
387
  when String then k
267
388
  when Symbol then k.name.tr("_", "-")
268
- else k.to_s
389
+ else raise ArgumentError, "Attribute keys should be Strings or Symbols."
269
390
  end
270
391
 
271
392
  # Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters.
@@ -289,8 +410,12 @@ module Phlex
289
410
  end
290
411
  }, buffer: buffer
291
412
  )
413
+ when Array
414
+ buffer << " " << name << '="' << ERB::Escape.html_escape(v.compact.join(" ")) << '"'
415
+ when Set
416
+ buffer << " " << name << '="' << ERB::Escape.html_escape(v.to_a.compact.join(" ")) << '"'
292
417
  else
293
- buffer << " " << name << '="' << ERB::Escape.html_escape(v.to_s) << '"'
418
+ raise ArgumentError, "Element attributes must be either a Boolean, a String, a Symbol, an Array of Strings or Symbols, or a Hash with values of one of these types"
294
419
  end
295
420
  end
296
421