phlex 1.6.3 → 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,15 +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
16
  # Create a new instance of the component.
16
- # @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.
17
18
  def new(*args, **kwargs, &block)
18
19
  if block
19
20
  object = super(*args, **kwargs, &nil)
@@ -42,22 +43,66 @@ module Phlex
42
43
  end
43
44
  end
44
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
87
+ end
88
+
45
89
  # Renders the view and returns the buffer. The default buffer is a mutable String.
46
- 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)
47
91
  __final_call__(buffer, context: context, view_context: view_context, parent: parent, &block).tap do
48
92
  self.class.rendered_at_least_once!
49
93
  end
50
94
  end
51
95
 
52
96
  # @api private
53
- 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
54
99
  @_context = context
55
100
  @_view_context = view_context
56
101
  @_parent = parent
57
102
 
58
103
  block ||= @_content_block
59
104
 
60
- return buffer || context.target unless render?
105
+ return unless render?
61
106
 
62
107
  around_template do
63
108
  if block
@@ -78,47 +123,16 @@ module Phlex
78
123
  end
79
124
  end
80
125
 
81
- buffer ? (buffer << context.target) : context.target
82
- end
83
-
84
- # Render another view
85
- # @param renderable [Phlex::SGML]
86
- # @return [nil]
87
- def render(renderable, &block)
88
- case renderable
89
- when Phlex::SGML
90
- renderable.call(context: @_context, view_context: @_view_context, parent: self, &block)
91
- when Class
92
- if renderable < Phlex::SGML
93
- renderable.new.call(context: @_context, view_context: @_view_context, parent: self, &block)
94
- end
95
- when Enumerable
96
- renderable.each { |r| render(r, &block) }
97
- when Proc
98
- yield_content(&renderable)
99
- else
100
- raise ArgumentError, "You can't render a #{renderable}."
101
- end
102
-
103
- nil
126
+ buffer << context.target unless parent
104
127
  end
105
128
 
106
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`
107
131
  # @return [nil]
132
+ # @see #format_object
108
133
  def plain(content)
109
- case content
110
- when String
111
- @_context.target << ERB::Escape.html_escape(content)
112
- when Symbol
113
- @_context.target << ERB::Escape.html_escape(content.name)
114
- when Integer
115
- @_context.target << ERB::Escape.html_escape(content.to_s)
116
- when nil
117
- nil
118
- else
119
- if (formatted_object = format_object(content))
120
- @_context.target << ERB::Escape.html_escape(formatted_object)
121
- 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"
122
136
  end
123
137
 
124
138
  nil
@@ -126,6 +140,7 @@ module Phlex
126
140
 
127
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.
128
142
  # @return [nil]
143
+ # @yield If a block is given, it yields the block with no arguments.
129
144
  def whitespace
130
145
  target = @_context.target
131
146
 
@@ -170,10 +185,59 @@ module Phlex
170
185
  @_context.with_target(+"") { yield_content(&block) }
171
186
  end
172
187
 
173
- # 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.
174
237
  # Because the BlackHole does nothing with the output, this should be faster.
175
238
  # @return [nil]
176
- private def __vanish__(*args)
239
+ # @api private
240
+ def __vanish__(*args)
177
241
  return unless block_given?
178
242
 
179
243
  @_context.with_target(BlackHole) { yield(*args) }
@@ -181,24 +245,26 @@ module Phlex
181
245
  nil
182
246
  end
183
247
 
184
- # Default render predicate can be overridden to prevent rendering
185
- # @return [bool]
186
- 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?
187
252
  true
188
253
  end
189
254
 
190
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.
191
257
  # @return [String]
192
- private def format_object(object)
258
+ def format_object(object)
193
259
  case object
194
260
  when Float
195
261
  object.to_s
196
262
  end
197
263
  end
198
264
 
199
- # 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.
200
266
  # @return [nil]
201
- private def around_template
267
+ def around_template
202
268
  before_template
203
269
  yield
204
270
  after_template
@@ -206,59 +272,106 @@ module Phlex
206
272
  nil
207
273
  end
208
274
 
209
- # 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.
210
276
  # @return [nil]
211
- private def before_template
277
+ def before_template
212
278
  nil
213
279
  end
214
280
 
215
- # 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.
216
282
  # @return [nil]
217
- private def after_template
283
+ def after_template
218
284
  nil
219
285
  end
220
286
 
221
- # 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]
222
289
  # @return [nil]
223
- private def yield_content
290
+ def yield_content
224
291
  return unless block_given?
225
292
 
226
293
  target = @_context.target
227
294
 
228
295
  original_length = target.length
229
296
  content = yield(self)
230
- plain(content) if original_length == target.length
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
308
+
309
+ original_length = target.length
310
+ content = yield
311
+ __text__(content) if original_length == target.length
231
312
 
232
313
  nil
233
314
  end
234
315
 
235
- # 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.
236
318
  # @return [nil]
237
- private def yield_content_with_args(*args)
319
+ def yield_content_with_args(*args)
238
320
  return unless block_given?
239
321
 
240
322
  target = @_context.target
241
323
 
242
324
  original_length = target.length
243
325
  content = yield(*args)
244
- plain(content) if original_length == target.length
326
+ __text__(content) if original_length == target.length
245
327
 
246
328
  nil
247
329
  end
248
330
 
331
+ # Performs the same task as the public method #plain, but does not raise an error if an unformattable object is passed
249
332
  # @api private
250
- private def __attributes__(**attributes)
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
+
354
+ # @api private
355
+ def __attributes__(**attributes)
251
356
  __final_attributes__(**attributes).tap do |buffer|
252
357
  Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] = buffer.freeze
253
358
  end
254
359
  end
255
360
 
256
361
  # @api private
257
- private def __final_attributes__(**attributes)
362
+ def __final_attributes__(**attributes)
258
363
  if respond_to?(:process_attributes)
259
364
  attributes = process_attributes(**attributes)
260
365
  end
261
366
 
367
+ if attributes[:href]&.start_with?(/\s*javascript:/)
368
+ attributes.delete(:href)
369
+ end
370
+
371
+ if attributes["href"]&.start_with?(/\s*javascript:/)
372
+ attributes.delete("href")
373
+ end
374
+
262
375
  buffer = +""
263
376
  __build_attributes__(attributes, buffer: buffer)
264
377
 
@@ -266,21 +379,18 @@ module Phlex
266
379
  end
267
380
 
268
381
  # @api private
269
- private def __build_attributes__(attributes, buffer:)
382
+ def __build_attributes__(attributes, buffer:)
270
383
  attributes.each do |k, v|
271
384
  next unless v
272
385
 
273
386
  name = case k
274
387
  when String then k
275
388
  when Symbol then k.name.tr("_", "-")
276
- else k.to_s
389
+ else raise ArgumentError, "Attribute keys should be Strings or Symbols."
277
390
  end
278
391
 
279
- lower_name = name.downcase
280
- next if lower_name == "href" && v.to_s.downcase.tr("\t \n", "").start_with?("javascript:")
281
-
282
392
  # Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters.
283
- if HTML::EVENT_ATTRIBUTES[lower_name] || name.match?(/[<>&"']/)
393
+ if HTML::EVENT_ATTRIBUTES[name] || name.match?(/[<>&"']/)
284
394
  raise ArgumentError, "Unsafe attribute name detected: #{k}."
285
395
  end
286
396
 
@@ -300,8 +410,12 @@ module Phlex
300
410
  end
301
411
  }, buffer: buffer
302
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(" ")) << '"'
303
417
  else
304
- 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"
305
419
  end
306
420
  end
307
421