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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -24
- data/Gemfile +0 -2
- data/README.md +12 -0
- data/fixtures/view_helper.rb +6 -0
- data/lib/phlex/black_hole.rb +15 -9
- data/lib/phlex/callable.rb +3 -5
- data/lib/phlex/deferred_render.rb +2 -4
- data/lib/phlex/element_clobbering_guard.rb +13 -0
- data/lib/phlex/elements.rb +64 -54
- data/lib/phlex/helpers.rb +9 -2
- data/lib/phlex/html/standard_elements.rb +0 -14
- data/lib/phlex/html.rb +19 -297
- data/lib/{overrides → phlex/overrides}/symbol/name.rb +1 -1
- data/lib/phlex/sgml.rb +313 -0
- data/lib/phlex/svg/standard_elements.rb +389 -0
- data/lib/phlex/svg.rb +14 -0
- data/lib/phlex/testing/view_helper.rb +10 -12
- data/lib/phlex/unbuffered.rb +34 -36
- data/lib/phlex/version.rb +1 -1
- data/lib/phlex.rb +5 -1
- metadata +21 -4
- data/lib/phlex/buffered.rb +0 -19
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
|
-
|
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
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
109
|
-
|
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
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
#
|
174
|
-
|
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
|
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
|