phlex 1.3.1 → 1.4.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -24
- data/.ruby-version +1 -1
- data/Gemfile +1 -0
- data/README.md +1 -0
- data/fixtures/view_helper.rb +6 -0
- data/lib/phlex/black_hole.rb +7 -9
- data/lib/phlex/callable.rb +3 -5
- data/lib/phlex/deferred_render.rb +2 -4
- data/lib/phlex/elements.rb +56 -50
- data/lib/phlex/helpers.rb +5 -2
- data/lib/phlex/html/standard_elements.rb +590 -0
- data/lib/phlex/html/void_elements.rb +79 -0
- data/lib/phlex/html.rb +14 -410
- data/lib/{overrides → phlex/overrides}/symbol/name.rb +1 -1
- data/lib/phlex/sgml.rb +315 -0
- data/lib/phlex/svg/standard_elements.rb +391 -0
- data/lib/phlex/svg.rb +11 -0
- data/lib/phlex/testing/view_helper.rb +10 -8
- data/lib/phlex/unbuffered.rb +34 -36
- data/lib/phlex/version.rb +1 -1
- data/lib/phlex.rb +5 -1
- metadata +9 -5
- data/lib/phlex/buffered.rb +0 -19
data/lib/phlex/html.rb
CHANGED
@@ -1,157 +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 = {
|
12
|
-
a: "a",
|
13
|
-
abbr: "abbr",
|
14
|
-
address: "address",
|
15
|
-
article: "article",
|
16
|
-
aside: "aside",
|
17
|
-
b: "b",
|
18
|
-
bdi: "bdi",
|
19
|
-
bdo: "bdo",
|
20
|
-
blockquote: "blockquote",
|
21
|
-
body: "body",
|
22
|
-
button: "button",
|
23
|
-
caption: "caption",
|
24
|
-
cite: "cite",
|
25
|
-
code: "code",
|
26
|
-
colgroup: "colgroup",
|
27
|
-
data: "data",
|
28
|
-
datalist: "datalist",
|
29
|
-
dd: "dd",
|
30
|
-
del: "del",
|
31
|
-
details: "details",
|
32
|
-
dfn: "dfn",
|
33
|
-
dialog: "dialog",
|
34
|
-
div: "div",
|
35
|
-
dl: "dl",
|
36
|
-
dt: "dt",
|
37
|
-
em: "em",
|
38
|
-
fieldset: "fieldset",
|
39
|
-
figcaption: "figcaption",
|
40
|
-
figure: "figure",
|
41
|
-
footer: "footer",
|
42
|
-
form: "form",
|
43
|
-
g: "g",
|
44
|
-
h1: "h1",
|
45
|
-
h2: "h2",
|
46
|
-
h3: "h3",
|
47
|
-
h4: "h4",
|
48
|
-
h5: "h5",
|
49
|
-
h6: "h6",
|
50
|
-
head: "head",
|
51
|
-
header: "header",
|
52
|
-
html: "html",
|
53
|
-
i: "i",
|
54
|
-
iframe: "iframe",
|
55
|
-
ins: "ins",
|
56
|
-
kbd: "kbd",
|
57
|
-
label: "label",
|
58
|
-
legend: "legend",
|
59
|
-
li: "li",
|
60
|
-
main: "main",
|
61
|
-
map: "map",
|
62
|
-
mark: "mark",
|
63
|
-
menuitem: "menuitem",
|
64
|
-
meter: "meter",
|
65
|
-
nav: "nav",
|
66
|
-
noscript: "noscript",
|
67
|
-
object: "object",
|
68
|
-
ol: "ol",
|
69
|
-
optgroup: "optgroup",
|
70
|
-
option: "option",
|
71
|
-
output: "output",
|
72
|
-
p: "p",
|
73
|
-
path: "path",
|
74
|
-
picture: "picture",
|
75
|
-
pre: "pre",
|
76
|
-
progress: "progress",
|
77
|
-
q: "q",
|
78
|
-
rp: "rp",
|
79
|
-
rt: "rt",
|
80
|
-
ruby: "ruby",
|
81
|
-
s: "s",
|
82
|
-
samp: "samp",
|
83
|
-
script: "script",
|
84
|
-
section: "section",
|
85
|
-
select: "select",
|
86
|
-
slot: "slot",
|
87
|
-
small: "small",
|
88
|
-
span: "span",
|
89
|
-
strong: "strong",
|
90
|
-
style: "style",
|
91
|
-
sub: "sub",
|
92
|
-
summary: "summary",
|
93
|
-
sup: "sup",
|
94
|
-
svg: "svg",
|
95
|
-
table: "table",
|
96
|
-
tbody: "tbody",
|
97
|
-
td: "td",
|
98
|
-
template_tag: "template",
|
99
|
-
textarea: "textarea",
|
100
|
-
tfoot: "tfoot",
|
101
|
-
th: "th",
|
102
|
-
thead: "thead",
|
103
|
-
time: "time",
|
104
|
-
title: "title",
|
105
|
-
tr: "tr",
|
106
|
-
u: "u",
|
107
|
-
ul: "ul",
|
108
|
-
video: "video",
|
109
|
-
wbr: "wbr",
|
110
|
-
}.freeze
|
111
|
-
|
112
|
-
VOID_ELEMENTS = {
|
113
|
-
area: "area",
|
114
|
-
br: "br",
|
115
|
-
embed: "embed",
|
116
|
-
hr: "hr",
|
117
|
-
img: "img",
|
118
|
-
input: "input",
|
119
|
-
link: "link",
|
120
|
-
meta: "meta",
|
121
|
-
param: "param",
|
122
|
-
source: "source",
|
123
|
-
track: "track",
|
124
|
-
col: "col",
|
125
|
-
}.freeze
|
126
|
-
|
127
|
-
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 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
|
4
|
+
class HTML < SGML
|
5
|
+
# A list of HTML attributes that have the potential to execute unsafe JavaScript.
|
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
|
128
7
|
|
129
8
|
UNBUFFERED_MUTEX = Mutex.new
|
130
9
|
|
131
|
-
extend Elements
|
132
|
-
include Helpers
|
133
|
-
|
134
10
|
class << self
|
135
|
-
|
136
|
-
new(...).call
|
137
|
-
end
|
138
|
-
alias_method :render, :call
|
139
|
-
|
140
|
-
def new(*args, **kwargs, &block)
|
141
|
-
if block
|
142
|
-
object = super(*args, **kwargs, &nil)
|
143
|
-
object.instance_variable_set(:@_content_block, block)
|
144
|
-
object
|
145
|
-
else
|
146
|
-
super
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
def rendered_at_least_once!
|
151
|
-
alias_method :__attributes__, :__final_attributes__
|
152
|
-
alias_method :call, :__final_call__
|
153
|
-
end
|
154
|
-
|
11
|
+
# @api private
|
155
12
|
def __unbuffered_class__
|
156
13
|
UNBUFFERED_MUTEX.synchronize do
|
157
14
|
if defined? @unbuffered_class
|
@@ -163,281 +20,28 @@ module Phlex
|
|
163
20
|
end
|
164
21
|
end
|
165
22
|
|
166
|
-
|
167
|
-
|
168
|
-
self.class.rendered_at_least_once!
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
def __final_call__(buffer = +"", view_context: nil, parent: nil, &block)
|
173
|
-
@_target = buffer
|
174
|
-
@_view_context = view_context
|
175
|
-
@_parent = parent
|
176
|
-
|
177
|
-
block ||= @_content_block
|
178
|
-
|
179
|
-
return buffer unless render?
|
180
|
-
|
181
|
-
around_template do
|
182
|
-
if block
|
183
|
-
if DeferredRender === self
|
184
|
-
__vanish__(self, &block)
|
185
|
-
template
|
186
|
-
else
|
187
|
-
template do |*args|
|
188
|
-
if args.length > 0
|
189
|
-
yield_content_with_args(*args, &block)
|
190
|
-
else
|
191
|
-
yield_content(&block)
|
192
|
-
end
|
193
|
-
end
|
194
|
-
end
|
195
|
-
else
|
196
|
-
template
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
buffer
|
201
|
-
end
|
202
|
-
|
203
|
-
def render(renderable, &block)
|
204
|
-
case renderable
|
205
|
-
when Phlex::HTML
|
206
|
-
renderable.call(@_target, view_context: @_view_context, parent: self, &block)
|
207
|
-
when Class
|
208
|
-
if renderable < Phlex::HTML
|
209
|
-
renderable.new.call(@_target, view_context: @_view_context, parent: self, &block)
|
210
|
-
end
|
211
|
-
else
|
212
|
-
raise ArgumentError, "You can't render a #{renderable}."
|
213
|
-
end
|
23
|
+
extend Elements
|
24
|
+
include Helpers, VoidElements, StandardElements
|
214
25
|
|
26
|
+
# Output an HTML doctype.
|
27
|
+
def doctype
|
28
|
+
@_target << "<!DOCTYPE html>"
|
215
29
|
nil
|
216
30
|
end
|
217
31
|
|
218
|
-
def
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
STANDARD_ELEMENTS.each do |method_name, tag|
|
223
|
-
register_element(method_name, tag: tag)
|
224
|
-
end
|
225
|
-
|
226
|
-
VOID_ELEMENTS.each do |method_name, tag|
|
227
|
-
register_void_element(method_name, tag: tag)
|
228
|
-
end
|
229
|
-
|
230
|
-
def text(content)
|
231
|
-
@_target << ERB::Util.html_escape(
|
232
|
-
case content
|
233
|
-
when String then content
|
234
|
-
when Symbol then content.name
|
235
|
-
when Integer then content.to_s
|
236
|
-
else format_object(content) || content.to_s
|
32
|
+
def svg(...)
|
33
|
+
super do
|
34
|
+
render Phlex::SVG.new do |svg|
|
35
|
+
yield(svg)
|
237
36
|
end
|
238
|
-
)
|
239
|
-
|
240
|
-
nil
|
241
|
-
end
|
242
|
-
|
243
|
-
def whitespace
|
244
|
-
@_target << " "
|
245
|
-
|
246
|
-
if block_given?
|
247
|
-
yield
|
248
|
-
@_target << " "
|
249
37
|
end
|
250
|
-
|
251
|
-
nil
|
252
|
-
end
|
253
|
-
|
254
|
-
def comment(&block)
|
255
|
-
@_target << "<!-- "
|
256
|
-
yield_content(&block)
|
257
|
-
@_target << " -->"
|
258
|
-
|
259
|
-
nil
|
260
|
-
end
|
261
|
-
|
262
|
-
def doctype
|
263
|
-
@_target << DOCTYPE
|
264
|
-
nil
|
265
|
-
end
|
266
|
-
|
267
|
-
def unsafe_raw(content = nil)
|
268
|
-
return nil unless content
|
269
|
-
|
270
|
-
@_target << content
|
271
|
-
end
|
272
|
-
|
273
|
-
def capture
|
274
|
-
return unless block_given?
|
275
|
-
|
276
|
-
original_buffer = @_target
|
277
|
-
new_buffer = +""
|
278
|
-
@_target = new_buffer
|
279
|
-
|
280
|
-
yield
|
281
|
-
|
282
|
-
new_buffer
|
283
|
-
ensure
|
284
|
-
@_target = original_buffer
|
285
38
|
end
|
286
39
|
|
40
|
+
# @api private
|
287
41
|
def unbuffered
|
288
42
|
self.class.__unbuffered_class__.new(self)
|
289
43
|
end
|
290
44
|
|
291
|
-
# Like `capture` but the output is vanished into a BlackHole buffer.
|
292
|
-
# Becuase the BlackHole does nothing with the output, this should be faster.
|
293
|
-
private def __vanish__(*args)
|
294
|
-
return unless block_given?
|
295
|
-
|
296
|
-
original_buffer = @_target
|
297
|
-
@_target = BlackHole
|
298
|
-
|
299
|
-
yield(*args)
|
300
|
-
nil
|
301
|
-
ensure
|
302
|
-
@_target = original_buffer
|
303
|
-
end
|
304
|
-
|
305
|
-
# Default render predicate can be overridden to prevent rendering
|
306
|
-
private def render?
|
307
|
-
true
|
308
|
-
end
|
309
|
-
|
310
|
-
private def format_object(object)
|
311
|
-
case object
|
312
|
-
when Float
|
313
|
-
object.to_s
|
314
|
-
end
|
315
|
-
end
|
316
|
-
|
317
|
-
private def around_template
|
318
|
-
before_template
|
319
|
-
yield
|
320
|
-
after_template
|
321
|
-
end
|
322
|
-
|
323
|
-
private def before_template
|
324
|
-
nil
|
325
|
-
end
|
326
|
-
|
327
|
-
private def after_template
|
328
|
-
nil
|
329
|
-
end
|
330
|
-
|
331
|
-
private def yield_content
|
332
|
-
return unless block_given?
|
333
|
-
|
334
|
-
original_length = @_target.length
|
335
|
-
content = yield(self)
|
336
|
-
unchanged = (original_length == @_target.length)
|
337
|
-
|
338
|
-
if unchanged
|
339
|
-
case content
|
340
|
-
when String
|
341
|
-
@_target << ERB::Util.html_escape(content)
|
342
|
-
when Symbol
|
343
|
-
@_target << ERB::Util.html_escape(content.name)
|
344
|
-
when Integer
|
345
|
-
@_target << ERB::Util.html_escape(content.to_s)
|
346
|
-
else
|
347
|
-
if (formatted_object = format_object(content))
|
348
|
-
@_target << ERB::Util.html_escape(formatted_object)
|
349
|
-
end
|
350
|
-
end
|
351
|
-
end
|
352
|
-
|
353
|
-
nil
|
354
|
-
end
|
355
|
-
|
356
|
-
private def yield_content_with_args(*args)
|
357
|
-
return unless block_given?
|
358
|
-
|
359
|
-
original_length = @_target.length
|
360
|
-
content = yield(*args)
|
361
|
-
unchanged = (original_length == @_target.length)
|
362
|
-
|
363
|
-
if unchanged
|
364
|
-
case content
|
365
|
-
when String
|
366
|
-
@_target << ERB::Util.html_escape(content)
|
367
|
-
when Symbol
|
368
|
-
@_target << ERB::Util.html_escape(content.name)
|
369
|
-
when Integer, Float
|
370
|
-
@_target << ERB::Util.html_escape(content.to_s)
|
371
|
-
else
|
372
|
-
if (formatted_object = format_object(content))
|
373
|
-
@_target << ERB::Util.html_escape(formatted_object)
|
374
|
-
end
|
375
|
-
end
|
376
|
-
end
|
377
|
-
|
378
|
-
nil
|
379
|
-
end
|
380
|
-
|
381
|
-
private def __attributes__(**attributes)
|
382
|
-
__final_attributes__(**attributes).tap do |buffer|
|
383
|
-
Phlex::ATTRIBUTE_CACHE[attributes.hash] = buffer.freeze
|
384
|
-
end
|
385
|
-
end
|
386
|
-
|
387
|
-
private def __final_attributes__(**attributes)
|
388
|
-
if attributes[:href]&.start_with?(/\s*javascript:/)
|
389
|
-
attributes.delete(:href)
|
390
|
-
end
|
391
|
-
|
392
|
-
if attributes["href"]&.start_with?(/\s*javascript:/)
|
393
|
-
attributes.delete("href")
|
394
|
-
end
|
395
|
-
|
396
|
-
buffer = +""
|
397
|
-
__build_attributes__(attributes, buffer: buffer)
|
398
|
-
|
399
|
-
buffer
|
400
|
-
end
|
401
|
-
|
402
|
-
private def __build_attributes__(attributes, buffer:)
|
403
|
-
attributes.each do |k, v|
|
404
|
-
next unless v
|
405
|
-
|
406
|
-
name = case k
|
407
|
-
when String then k
|
408
|
-
when Symbol then k.name.tr("_", "-")
|
409
|
-
else k.to_s
|
410
|
-
end
|
411
|
-
|
412
|
-
# Detect unsafe attribute names. Attribute names are considered unsafe if they match an event attribute or include unsafe characters.
|
413
|
-
if HTML::EVENT_ATTRIBUTES[name] || name.match?(/[<>&"']/)
|
414
|
-
raise ArgumentError, "Unsafe attribute name detected: #{k}."
|
415
|
-
end
|
416
|
-
|
417
|
-
case v
|
418
|
-
when true
|
419
|
-
buffer << " " << name
|
420
|
-
when String
|
421
|
-
buffer << " " << name << '="' << ERB::Util.html_escape(v) << '"'
|
422
|
-
when Symbol
|
423
|
-
buffer << " " << name << '="' << ERB::Util.html_escape(v.name) << '"'
|
424
|
-
when Hash
|
425
|
-
__build_attributes__(
|
426
|
-
v.transform_keys { |subkey|
|
427
|
-
case subkey
|
428
|
-
when Symbol then"#{k}-#{subkey.name.tr('_', '-')}"
|
429
|
-
else "#{k}-#{subkey}"
|
430
|
-
end
|
431
|
-
}, buffer: buffer
|
432
|
-
)
|
433
|
-
else
|
434
|
-
buffer << " " << name << '="' << ERB::Util.html_escape(v.to_s) << '"'
|
435
|
-
end
|
436
|
-
end
|
437
|
-
|
438
|
-
buffer
|
439
|
-
end
|
440
|
-
|
441
45
|
# This should be the last method defined
|
442
46
|
def self.method_added(method_name)
|
443
47
|
if method_name[0] == "_" && Phlex::HTML.instance_methods.include?(method_name) && instance_method(method_name).owner != Phlex::HTML
|