phlex 1.3.2 → 1.5.1

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.

data/lib/phlex/html.rb CHANGED
@@ -1,47 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
4
- using Overrides::Symbol::Name
5
- end
6
-
7
3
  module Phlex
8
- class HTML
9
- DOCTYPE = "<!DOCTYPE html>"
10
-
11
- STANDARD_ELEMENTS = Concurrent::Map.new
12
- VOID_ELEMENTS = Concurrent::Map.new
13
-
4
+ class HTML < SGML
5
+ # A list of HTML attributes that have the potential to execute unsafe JavaScript.
14
6
  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
15
7
 
16
8
  UNBUFFERED_MUTEX = Mutex.new
17
9
 
18
- extend Elements
19
-
20
- include Helpers
21
- include VoidElements
22
- include StandardElements
23
-
24
10
  class << self
25
- def call(...)
26
- new(...).call
27
- end
28
- alias_method :render, :call
29
-
30
- def new(*args, **kwargs, &block)
31
- if block
32
- object = super(*args, **kwargs, &nil)
33
- object.instance_variable_set(:@_content_block, block)
34
- object
35
- else
36
- super
37
- end
38
- end
39
-
40
- def rendered_at_least_once!
41
- alias_method :__attributes__, :__final_attributes__
42
- alias_method :call, :__final_call__
43
- end
44
-
11
+ # @api private
45
12
  def __unbuffered_class__
46
13
  UNBUFFERED_MUTEX.synchronize do
47
14
  if defined? @unbuffered_class
@@ -53,280 +20,35 @@ module Phlex
53
20
  end
54
21
  end
55
22
 
56
- def call(...)
57
- __final_call__(...).tap do
58
- self.class.rendered_at_least_once!
59
- end
60
- end
61
-
62
- def __final_call__(buffer = +"", view_context: nil, parent: nil, &block)
63
- @_target = buffer
64
- @_view_context = view_context
65
- @_parent = parent
66
-
67
- block ||= @_content_block
68
-
69
- return buffer unless render?
70
-
71
- around_template do
72
- if block
73
- if DeferredRender === self
74
- __vanish__(self, &block)
75
- template
76
- else
77
- template do |*args|
78
- if args.length > 0
79
- yield_content_with_args(*args, &block)
80
- else
81
- yield_content(&block)
82
- end
83
- end
84
- end
85
- else
86
- template
87
- end
88
- end
89
-
90
- buffer
91
- end
92
-
93
- def render(renderable, &block)
94
- case renderable
95
- when Phlex::HTML
96
- renderable.call(@_target, view_context: @_view_context, parent: self, &block)
97
- when Class
98
- if renderable < Phlex::HTML
99
- renderable.new.call(@_target, view_context: @_view_context, parent: self, &block)
100
- end
101
- else
102
- raise ArgumentError, "You can't render a #{renderable}."
103
- end
23
+ extend Elements
24
+ include Helpers, VoidElements, StandardElements
104
25
 
26
+ # Output an HTML doctype.
27
+ def doctype
28
+ @_target << "<!DOCTYPE html>"
105
29
  nil
106
30
  end
107
31
 
108
- def format
109
- :html
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(...)
110
36
  end
111
37
 
112
- def text(content)
113
- @_target << ERB::Util.html_escape(
114
- case content
115
- when String then content
116
- when Symbol then content.name
117
- when Integer then content.to_s
118
- else format_object(content) || content.to_s
38
+ def svg(...)
39
+ super do
40
+ render Phlex::SVG.new do |svg|
41
+ yield(svg)
119
42
  end
120
- )
121
-
122
- nil
123
- end
124
-
125
- def whitespace
126
- @_target << " "
127
-
128
- if block_given?
129
- yield
130
- @_target << " "
131
43
  end
132
-
133
- nil
134
- end
135
-
136
- def comment(&block)
137
- @_target << "<!-- "
138
- yield_content(&block)
139
- @_target << " -->"
140
-
141
- nil
142
- end
143
-
144
- def doctype
145
- @_target << DOCTYPE
146
- nil
147
- end
148
-
149
- def unsafe_raw(content = nil)
150
- return nil unless content
151
-
152
- @_target << content
153
- end
154
-
155
- def capture(&block)
156
- return unless block_given?
157
-
158
- original_buffer = @_target
159
- new_buffer = +""
160
- @_target = new_buffer
161
-
162
- yield_content(&block)
163
-
164
- new_buffer
165
- ensure
166
- @_target = original_buffer
167
44
  end
168
45
 
46
+ # @api private
169
47
  def unbuffered
170
48
  self.class.__unbuffered_class__.new(self)
171
49
  end
172
50
 
173
- # Like `capture` but the output is vanished into a BlackHole buffer.
174
- # Becuase the BlackHole does nothing with the output, this should be faster.
175
- private def __vanish__(*args)
176
- return unless block_given?
177
-
178
- original_buffer = @_target
179
- @_target = BlackHole
180
-
181
- yield(*args)
182
- nil
183
- ensure
184
- @_target = original_buffer
185
- end
186
-
187
- # Default render predicate can be overridden to prevent rendering
188
- private def render?
189
- true
190
- end
191
-
192
- private def format_object(object)
193
- case object
194
- when Float
195
- object.to_s
196
- end
197
- end
198
-
199
- private def around_template
200
- before_template
201
- yield
202
- after_template
203
- end
204
-
205
- private def before_template
206
- nil
207
- end
208
-
209
- private def after_template
210
- nil
211
- end
212
-
213
- private def yield_content
214
- return unless block_given?
215
-
216
- original_length = @_target.length
217
- content = yield(self)
218
- unchanged = (original_length == @_target.length)
219
-
220
- if unchanged
221
- case content
222
- when String
223
- @_target << ERB::Util.html_escape(content)
224
- when Symbol
225
- @_target << ERB::Util.html_escape(content.name)
226
- when Integer
227
- @_target << ERB::Util.html_escape(content.to_s)
228
- else
229
- if (formatted_object = format_object(content))
230
- @_target << ERB::Util.html_escape(formatted_object)
231
- end
232
- end
233
- end
234
-
235
- nil
236
- end
237
-
238
- private def yield_content_with_args(*args)
239
- return unless block_given?
240
-
241
- original_length = @_target.length
242
- content = yield(*args)
243
- unchanged = (original_length == @_target.length)
244
-
245
- if unchanged
246
- case content
247
- when String
248
- @_target << ERB::Util.html_escape(content)
249
- when Symbol
250
- @_target << ERB::Util.html_escape(content.name)
251
- when Integer, Float
252
- @_target << ERB::Util.html_escape(content.to_s)
253
- else
254
- if (formatted_object = format_object(content))
255
- @_target << ERB::Util.html_escape(formatted_object)
256
- end
257
- end
258
- end
259
-
260
- nil
261
- end
262
-
263
- private def __attributes__(**attributes)
264
- __final_attributes__(**attributes).tap do |buffer|
265
- Phlex::ATTRIBUTE_CACHE[attributes.hash] = buffer.freeze
266
- end
267
- end
268
-
269
- private def __final_attributes__(**attributes)
270
- if attributes[:href]&.start_with?(/\s*javascript:/)
271
- attributes.delete(:href)
272
- end
273
-
274
- if attributes["href"]&.start_with?(/\s*javascript:/)
275
- attributes.delete("href")
276
- end
277
-
278
- buffer = +""
279
- __build_attributes__(attributes, buffer: buffer)
280
-
281
- buffer
282
- end
283
-
284
- private def __build_attributes__(attributes, buffer:)
285
- attributes.each do |k, v|
286
- next unless v
287
-
288
- name = case k
289
- when String then k
290
- when Symbol then k.name.tr("_", "-")
291
- else k.to_s
292
- end
293
-
294
- # Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters.
295
- if HTML::EVENT_ATTRIBUTES[name] || name.match?(/[<>&"']/)
296
- raise ArgumentError, "Unsafe attribute name detected: #{k}."
297
- end
298
-
299
- case v
300
- when true
301
- buffer << " " << name
302
- when String
303
- buffer << " " << name << '="' << ERB::Util.html_escape(v) << '"'
304
- when Symbol
305
- buffer << " " << name << '="' << ERB::Util.html_escape(v.name) << '"'
306
- when Hash
307
- __build_attributes__(
308
- v.transform_keys { |subkey|
309
- case subkey
310
- when Symbol then"#{k}-#{subkey.name.tr('_', '-')}"
311
- else "#{k}-#{subkey}"
312
- end
313
- }, buffer: buffer
314
- )
315
- else
316
- buffer << " " << name << '="' << ERB::Util.html_escape(v.to_s) << '"'
317
- end
318
- end
319
-
320
- buffer
321
- end
322
-
323
- # This should be the last method defined
324
- def self.method_added(method_name)
325
- if method_name[0] == "_" && Phlex::HTML.instance_methods.include?(method_name) && instance_method(method_name).owner != Phlex::HTML
326
- raise NameError, "👋 Redefining the method `#{name}##{method_name}` is not a good idea."
327
- end
328
-
329
- super
330
- end
51
+ # This should be extended after all method definitions
52
+ extend ElementClobberingGuard
331
53
  end
332
54
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Overrides::Symbol::Name
3
+ module Phlex::Overrides::Symbol::Name
4
4
  refine(Symbol) { alias_method :name, :to_s }
5
5
  end
data/lib/phlex/sgml.rb ADDED
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.0")
4
+ using Phlex::Overrides::Symbol::Name
5
+ end
6
+
7
+ module Phlex
8
+ class SGML
9
+ class << self
10
+ # Render the view to a String. Arguments are delegated to <code>new</code>.
11
+ def call(...)
12
+ new(...).call
13
+ end
14
+
15
+ alias_method :render, :call
16
+
17
+ # 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.
19
+ def new(*args, **kwargs, &block)
20
+ if block
21
+ object = super(*args, **kwargs, &nil)
22
+ object.instance_variable_set(:@_content_block, block)
23
+ object
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ # @api private
30
+ def rendered_at_least_once!
31
+ alias_method :__attributes__, :__final_attributes__
32
+ alias_method :call, :__final_call__
33
+ end
34
+ end
35
+
36
+ # Renders the view and returns the buffer. The default buffer is a mutable String.
37
+ def call(buffer = nil, target: +"", view_context: nil, parent: nil, &block)
38
+ __final_call__(buffer, target: target, view_context: view_context, parent: parent, &block).tap do
39
+ self.class.rendered_at_least_once!
40
+ end
41
+ end
42
+
43
+ # @api private
44
+ def __final_call__(buffer = nil, target: +"", view_context: nil, parent: nil, &block)
45
+ @_target = target
46
+ @_view_context = view_context
47
+ @_parent = parent
48
+
49
+ block ||= @_content_block
50
+
51
+ return buffer || target unless render?
52
+
53
+ around_template do
54
+ if block
55
+ if DeferredRender === self
56
+ __vanish__(self, &block)
57
+ template
58
+ else
59
+ template do |*args|
60
+ if args.length > 0
61
+ yield_content_with_args(*args, &block)
62
+ else
63
+ yield_content(&block)
64
+ end
65
+ end
66
+ end
67
+ else
68
+ template
69
+ end
70
+ end
71
+
72
+ buffer ? (buffer << target) : 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(target: @_target, view_context: @_view_context, parent: self, &block)
82
+ when Class
83
+ if renderable < Phlex::SGML
84
+ renderable.new.call(target: @_target, view_context: @_view_context, parent: self, &block)
85
+ end
86
+ else
87
+ raise ArgumentError, "You can't render a #{renderable}."
88
+ end
89
+
90
+ nil
91
+ end
92
+
93
+ # Output text content. The text will be HTML-escaped.
94
+ # @return [nil]
95
+ def plain(content)
96
+ case content
97
+ when String
98
+ @_target << ERB::Escape.html_escape(content)
99
+ when Symbol
100
+ @_target << ERB::Escape.html_escape(content.name)
101
+ when Integer
102
+ @_target << ERB::Escape.html_escape(content.to_s)
103
+ when nil
104
+ nil
105
+ else
106
+ if (formatted_object = format_object(content))
107
+ @_target << ERB::Escape.html_escape(formatted_object)
108
+ end
109
+ end
110
+
111
+ nil
112
+ end
113
+
114
+ # 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.
115
+ # @return [nil]
116
+ def whitespace
117
+ @_target << " "
118
+
119
+ if block_given?
120
+ yield
121
+ @_target << " "
122
+ end
123
+
124
+ nil
125
+ end
126
+
127
+ # Output an HTML comment.
128
+ # @return [nil]
129
+ def comment(&block)
130
+ @_target << "<!-- "
131
+ yield_content(&block)
132
+ @_target << " -->"
133
+
134
+ nil
135
+ end
136
+
137
+ # 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.
138
+ # @param content [String|nil]
139
+ # @return [nil]
140
+ def unsafe_raw(content = nil)
141
+ return nil unless content
142
+
143
+ @_target << content
144
+ nil
145
+ end
146
+
147
+ # Capture a block of output as a String.
148
+ # @return [String]
149
+ def capture(&block)
150
+ return "" unless block_given?
151
+
152
+ original_buffer_content = @_target.dup
153
+ @_target.clear
154
+
155
+ begin
156
+ yield_content(&block)
157
+ new_buffer_content = @_target.dup
158
+ ensure
159
+ @_target.clear
160
+ @_target << original_buffer_content
161
+ end
162
+
163
+ new_buffer_content.is_a?(String) ? new_buffer_content : ""
164
+ end
165
+
166
+ # Like `capture` but the output is vanished into a BlackHole buffer.
167
+ # Because the BlackHole does nothing with the output, this should be faster.
168
+ # @return [nil]
169
+ private def __vanish__(*args)
170
+ return unless block_given?
171
+
172
+ original_buffer = @_target
173
+
174
+ begin
175
+ @_target = BlackHole
176
+ yield(*args)
177
+ ensure
178
+ @_target = original_buffer
179
+ end
180
+
181
+ nil
182
+ end
183
+
184
+ # Default render predicate can be overridden to prevent rendering
185
+ # @return [bool]
186
+ private def render?
187
+ true
188
+ end
189
+
190
+ # Format the object for output
191
+ # @return [String]
192
+ private def format_object(object)
193
+ case object
194
+ when Float
195
+ object.to_s
196
+ end
197
+ end
198
+
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.
200
+ # @return [nil]
201
+ private def around_template
202
+ before_template
203
+ yield
204
+ after_template
205
+
206
+ nil
207
+ end
208
+
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.
210
+ # @return [nil]
211
+ private def before_template
212
+ nil
213
+ end
214
+
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.
216
+ # @return [nil]
217
+ private def after_template
218
+ nil
219
+ end
220
+
221
+ # Yields the block and checks if it buffered anything. If nothing was buffered, the return value is treated as text.
222
+ # @return [nil]
223
+ private def yield_content
224
+ return unless block_given?
225
+
226
+ original_length = @_target.length
227
+ content = yield(self)
228
+
229
+ plain(content) if original_length == @_target.length
230
+
231
+ nil
232
+ end
233
+
234
+ # 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.
235
+ # @return [nil]
236
+ private def yield_content_with_args(*args)
237
+ return unless block_given?
238
+
239
+ original_length = @_target.length
240
+ content = yield(*args)
241
+ plain(content) if original_length == @_target.length
242
+
243
+ nil
244
+ end
245
+
246
+ # @api private
247
+ private def __attributes__(**attributes)
248
+ __final_attributes__(**attributes).tap do |buffer|
249
+ Phlex::ATTRIBUTE_CACHE[respond_to?(:process_attributes) ? (attributes.hash + self.class.hash) : attributes.hash] = buffer.freeze
250
+ end
251
+ end
252
+
253
+ # @api private
254
+ private def __final_attributes__(**attributes)
255
+ if respond_to?(:process_attributes)
256
+ attributes = process_attributes(**attributes)
257
+ end
258
+
259
+ if attributes[:href]&.start_with?(/\s*javascript:/)
260
+ attributes.delete(:href)
261
+ end
262
+
263
+ if attributes["href"]&.start_with?(/\s*javascript:/)
264
+ attributes.delete("href")
265
+ end
266
+
267
+ buffer = +""
268
+ __build_attributes__(attributes, buffer: buffer)
269
+
270
+ buffer
271
+ end
272
+
273
+ # @api private
274
+ private def __build_attributes__(attributes, buffer:)
275
+ attributes.each do |k, v|
276
+ next unless v
277
+
278
+ name = case k
279
+ when String then k
280
+ when Symbol then k.name.tr("_", "-")
281
+ else k.to_s
282
+ end
283
+
284
+ # Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters.
285
+ if HTML::EVENT_ATTRIBUTES[name] || name.match?(/[<>&"']/)
286
+ raise ArgumentError, "Unsafe attribute name detected: #{k}."
287
+ end
288
+
289
+ case v
290
+ when true
291
+ buffer << " " << name
292
+ when String
293
+ buffer << " " << name << '="' << ERB::Escape.html_escape(v) << '"'
294
+ when Symbol
295
+ buffer << " " << name << '="' << ERB::Escape.html_escape(v.name) << '"'
296
+ when Hash
297
+ __build_attributes__(
298
+ v.transform_keys { |subkey|
299
+ case subkey
300
+ when Symbol then"#{k}-#{subkey.name.tr('_', '-')}"
301
+ else "#{k}-#{subkey}"
302
+ end
303
+ }, buffer: buffer
304
+ )
305
+ else
306
+ buffer << " " << name << '="' << ERB::Escape.html_escape(v.to_s) << '"'
307
+ end
308
+ end
309
+
310
+ buffer
311
+ end
312
+ end
313
+ end