dommy 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/dommy/bridge.rb +16 -3
- data/lib/dommy/browser.rb +43 -47
- data/lib/dommy/callable_invoker.rb +16 -0
- data/lib/dommy/compression_streams.rb +12 -62
- data/lib/dommy/document.rb +5 -5
- data/lib/dommy/element.rb +2 -3
- data/lib/dommy/encodings/tables.rb +751 -0
- data/lib/dommy/encodings.rb +299 -0
- data/lib/dommy/event.rb +4 -5
- data/lib/dommy/fetch.rb +60 -49
- data/lib/dommy/form_data.rb +10 -0
- data/lib/dommy/html_elements.rb +136 -62
- data/lib/dommy/internal/exception_report.rb +123 -0
- data/lib/dommy/internal/idna.rb +4 -4
- data/lib/dommy/internal/idna_data.rb +513 -100
- data/lib/dommy/internal/url_parser.rb +70 -18
- data/lib/dommy/js/dom_interfaces.rb +1 -0
- data/lib/dommy/js/error_log.rb +137 -0
- data/lib/dommy/js/host_bridge.rb +13 -0
- data/lib/dommy/js/host_runtime.js +107 -17
- data/lib/dommy/js/marshaller.rb +21 -2
- data/lib/dommy/js/runtime.rb +8 -1
- data/lib/dommy/js/script_boot.rb +58 -14
- data/lib/dommy/js/wire_tags.rb +8 -0
- data/lib/dommy/location.rb +14 -4
- data/lib/dommy/navigator.rb +3 -1
- data/lib/dommy/promise.rb +4 -4
- data/lib/dommy/scheduler.rb +35 -6
- data/lib/dommy/streams.rb +1200 -187
- data/lib/dommy/text_codec.rb +110 -213
- data/lib/dommy/url.rb +34 -32
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +162 -34
- data/lib/dommy/xml_http_request.rb +18 -4
- data/lib/dommy.rb +2 -0
- metadata +5 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "encodings/tables"
|
|
4
|
+
|
|
5
|
+
module Dommy
|
|
6
|
+
# The Encoding Standard's encodings: every label it knows, and a decoder for
|
|
7
|
+
# each encoding that turns bytes into code points, streaming, with the
|
|
8
|
+
# replacement-or-throw error mode TextDecoder exposes as `fatal`.
|
|
9
|
+
#
|
|
10
|
+
# UTF-8, UTF-16 and the legacy single-byte encodings are decoded here, from
|
|
11
|
+
# the spec's own algorithms and index tables. The legacy multi-byte
|
|
12
|
+
# encodings (Big5, EUC-JP, ISO-2022-JP, Shift_JIS, EUC-KR, GBK, gb18030) go
|
|
13
|
+
# through Ruby's converters for the encoding closest to the spec's index,
|
|
14
|
+
# which differ from it in a few code points and in how many U+FFFD a broken
|
|
15
|
+
# sequence yields.
|
|
16
|
+
#
|
|
17
|
+
# Spec: https://encoding.spec.whatwg.org/
|
|
18
|
+
module Encodings
|
|
19
|
+
# The Ruby encoding whose mapping is closest to the spec's index for each
|
|
20
|
+
# legacy multi-byte encoding.
|
|
21
|
+
RUBY_ENCODINGS = {
|
|
22
|
+
"Big5" => "Big5-HKSCS",
|
|
23
|
+
"EUC-JP" => "EUC-JP",
|
|
24
|
+
"ISO-2022-JP" => "ISO-2022-JP",
|
|
25
|
+
"Shift_JIS" => "Windows-31J",
|
|
26
|
+
"EUC-KR" => "CP949",
|
|
27
|
+
"GBK" => "GBK",
|
|
28
|
+
"gb18030" => "GB18030"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
REPLACEMENT_CHARACTER = 0xFFFD
|
|
32
|
+
|
|
33
|
+
# The name of the encoding `label` stands for, or nil. Only ASCII
|
|
34
|
+
# whitespace around the label is ignored.
|
|
35
|
+
#
|
|
36
|
+
# Spec: https://encoding.spec.whatwg.org/#concept-encoding-get
|
|
37
|
+
def self.get(label)
|
|
38
|
+
LABELS[label.gsub(/\A[\t\n\f\r ]+|[\t\n\f\r ]+\z/, "").downcase]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The charset parameter of a MIME type string, or nil.
|
|
42
|
+
def self.charset_of(mime_type)
|
|
43
|
+
match = mime_type.to_s.match(/;\s*charset\s*=\s*(?:"([^"]*)"|([^\s;]*))/i)
|
|
44
|
+
match && (match[1] || match[2])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Bytes to a String in `fallback` (an encoding name), unless they start
|
|
48
|
+
# with a byte-order mark, which names the encoding itself and is dropped.
|
|
49
|
+
# Errors are U+FFFD.
|
|
50
|
+
#
|
|
51
|
+
# Spec: https://encoding.spec.whatwg.org/#decode
|
|
52
|
+
def self.decode(bytes, fallback = "UTF-8")
|
|
53
|
+
bytes = bytes.b
|
|
54
|
+
name = fallback
|
|
55
|
+
if bytes.start_with?("\xEF\xBB\xBF".b)
|
|
56
|
+
name = "UTF-8"
|
|
57
|
+
bytes = bytes.byteslice(3..)
|
|
58
|
+
elsif bytes.start_with?("\xFE\xFF".b)
|
|
59
|
+
name = "UTF-16BE"
|
|
60
|
+
bytes = bytes.byteslice(2..)
|
|
61
|
+
elsif bytes.start_with?("\xFF\xFE".b)
|
|
62
|
+
name = "UTF-16LE"
|
|
63
|
+
bytes = bytes.byteslice(2..)
|
|
64
|
+
end
|
|
65
|
+
decoder_for(name).decode(bytes, flush: true).pack("U*")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# A fresh decoder for the encoding named `name` (as `get` returns it).
|
|
69
|
+
def self.decoder_for(name, fatal: false)
|
|
70
|
+
case name
|
|
71
|
+
when "UTF-8" then Utf8Decoder.new(fatal)
|
|
72
|
+
when "UTF-16LE" then Utf16Decoder.new(fatal, big_endian: false)
|
|
73
|
+
when "UTF-16BE" then Utf16Decoder.new(fatal, big_endian: true)
|
|
74
|
+
when "x-user-defined" then XUserDefinedDecoder.new(fatal)
|
|
75
|
+
when "replacement" then ReplacementDecoder.new(fatal)
|
|
76
|
+
else
|
|
77
|
+
index = SINGLE_BYTE_INDEXES[name]
|
|
78
|
+
return SingleByteDecoder.new(fatal, index) if index
|
|
79
|
+
|
|
80
|
+
ConverterDecoder.new(fatal, RUBY_ENCODINGS.fetch(name))
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# What every decoder shares: `decode(bytes, flush:)` takes a binary String
|
|
85
|
+
# and returns the code points it yields, keeping whatever partial sequence
|
|
86
|
+
# is left over for the next call unless `flush` is true. An error is
|
|
87
|
+
# U+FFFD, or a TypeError in fatal mode.
|
|
88
|
+
class Decoder
|
|
89
|
+
def initialize(fatal)
|
|
90
|
+
@fatal = fatal
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def error
|
|
96
|
+
raise Bridge::TypeError, "The encoded data was not valid" if @fatal
|
|
97
|
+
|
|
98
|
+
REPLACEMENT_CHARACTER
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Spec: https://encoding.spec.whatwg.org/#utf-8-decoder
|
|
103
|
+
class Utf8Decoder < Decoder
|
|
104
|
+
def initialize(fatal)
|
|
105
|
+
super
|
|
106
|
+
reset
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def decode(bytes, flush:)
|
|
110
|
+
out = []
|
|
111
|
+
queue = bytes.bytes
|
|
112
|
+
until queue.empty?
|
|
113
|
+
byte = queue.shift
|
|
114
|
+
if @needed.zero?
|
|
115
|
+
if byte <= 0x7F
|
|
116
|
+
out << byte
|
|
117
|
+
elsif byte.between?(0xC2, 0xDF)
|
|
118
|
+
@needed = 1
|
|
119
|
+
@code_point = byte & 0x1F
|
|
120
|
+
elsif byte.between?(0xE0, 0xEF)
|
|
121
|
+
@lower = 0xA0 if byte == 0xE0
|
|
122
|
+
@upper = 0x9F if byte == 0xED
|
|
123
|
+
@needed = 2
|
|
124
|
+
@code_point = byte & 0x0F
|
|
125
|
+
elsif byte.between?(0xF0, 0xF4)
|
|
126
|
+
@lower = 0x90 if byte == 0xF0
|
|
127
|
+
@upper = 0x8F if byte == 0xF4
|
|
128
|
+
@needed = 3
|
|
129
|
+
@code_point = byte & 0x07
|
|
130
|
+
else
|
|
131
|
+
out << error
|
|
132
|
+
end
|
|
133
|
+
next
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
unless byte.between?(@lower, @upper)
|
|
137
|
+
# Not a continuation byte: an error, and the byte is read again.
|
|
138
|
+
reset
|
|
139
|
+
out << error
|
|
140
|
+
queue.unshift(byte)
|
|
141
|
+
next
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
@lower = 0x80
|
|
145
|
+
@upper = 0xBF
|
|
146
|
+
@code_point = (@code_point << 6) | (byte & 0x3F)
|
|
147
|
+
@seen += 1
|
|
148
|
+
next unless @seen == @needed
|
|
149
|
+
|
|
150
|
+
out << @code_point
|
|
151
|
+
reset
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if flush && @needed != 0
|
|
155
|
+
reset
|
|
156
|
+
out << error
|
|
157
|
+
end
|
|
158
|
+
out
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def reset
|
|
164
|
+
@needed = 0
|
|
165
|
+
@seen = 0
|
|
166
|
+
@code_point = 0
|
|
167
|
+
@lower = 0x80
|
|
168
|
+
@upper = 0xBF
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Spec: https://encoding.spec.whatwg.org/#shared-utf-16-decoder
|
|
173
|
+
class Utf16Decoder < Decoder
|
|
174
|
+
def initialize(fatal, big_endian:)
|
|
175
|
+
super(fatal)
|
|
176
|
+
@big_endian = big_endian
|
|
177
|
+
@lead_byte = nil
|
|
178
|
+
@lead_surrogate = nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def decode(bytes, flush:)
|
|
182
|
+
out = []
|
|
183
|
+
bytes.each_byte do |byte|
|
|
184
|
+
if @lead_byte.nil?
|
|
185
|
+
@lead_byte = byte
|
|
186
|
+
next
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
code_unit = @big_endian ? ((@lead_byte << 8) + byte) : ((byte << 8) + @lead_byte)
|
|
190
|
+
@lead_byte = nil
|
|
191
|
+
|
|
192
|
+
if @lead_surrogate
|
|
193
|
+
lead_surrogate = @lead_surrogate
|
|
194
|
+
@lead_surrogate = nil
|
|
195
|
+
if code_unit.between?(0xDC00, 0xDFFF)
|
|
196
|
+
out << (0x10000 + ((lead_surrogate - 0xD800) << 10) + (code_unit - 0xDC00))
|
|
197
|
+
next
|
|
198
|
+
end
|
|
199
|
+
# The lead surrogate had no trail: an error, and the code unit's
|
|
200
|
+
# bytes are read again.
|
|
201
|
+
out << error
|
|
202
|
+
out.concat(decode_code_unit(code_unit))
|
|
203
|
+
next
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
out.concat(decode_code_unit(code_unit))
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
if flush && (@lead_byte || @lead_surrogate)
|
|
210
|
+
@lead_byte = nil
|
|
211
|
+
@lead_surrogate = nil
|
|
212
|
+
out << error
|
|
213
|
+
end
|
|
214
|
+
out
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
def decode_code_unit(code_unit)
|
|
220
|
+
if code_unit.between?(0xD800, 0xDBFF)
|
|
221
|
+
@lead_surrogate = code_unit
|
|
222
|
+
[]
|
|
223
|
+
elsif code_unit.between?(0xDC00, 0xDFFF)
|
|
224
|
+
[error]
|
|
225
|
+
else
|
|
226
|
+
[code_unit]
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Spec: https://encoding.spec.whatwg.org/#single-byte-decoder
|
|
232
|
+
class SingleByteDecoder < Decoder
|
|
233
|
+
def initialize(fatal, index)
|
|
234
|
+
super(fatal)
|
|
235
|
+
@index = index
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def decode(bytes, flush:)
|
|
239
|
+
bytes.each_byte.map do |byte|
|
|
240
|
+
if byte <= 0x7F
|
|
241
|
+
byte
|
|
242
|
+
else
|
|
243
|
+
@index[byte - 0x80] || error
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Spec: https://encoding.spec.whatwg.org/#x-user-defined-decoder
|
|
250
|
+
class XUserDefinedDecoder < Decoder
|
|
251
|
+
def decode(bytes, flush:)
|
|
252
|
+
bytes.each_byte.map { |byte| byte <= 0x7F ? byte : 0xF780 + byte - 0x80 }
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# The whole stream is one error, reported once.
|
|
257
|
+
#
|
|
258
|
+
# Spec: https://encoding.spec.whatwg.org/#replacement-decoder
|
|
259
|
+
class ReplacementDecoder < Decoder
|
|
260
|
+
def initialize(fatal)
|
|
261
|
+
super
|
|
262
|
+
@reported = false
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def decode(bytes, flush:)
|
|
266
|
+
return [] if bytes.empty? || @reported
|
|
267
|
+
|
|
268
|
+
@reported = true
|
|
269
|
+
[error]
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# A legacy multi-byte encoding, through Ruby's converter. Partial input is
|
|
274
|
+
# held between calls; at a flush what is left over is an error.
|
|
275
|
+
class ConverterDecoder < Decoder
|
|
276
|
+
def initialize(fatal, ruby_encoding)
|
|
277
|
+
super(fatal)
|
|
278
|
+
options = fatal ? {} : {invalid: :replace, undef: :replace, replace: "\uFFFD"}
|
|
279
|
+
@converter = ::Encoding::Converter.new(ruby_encoding, "UTF-8", **options)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def decode(bytes, flush:)
|
|
283
|
+
output = +""
|
|
284
|
+
result = @converter.primitive_convert(bytes.dup, output, nil, nil, partial_input: !flush)
|
|
285
|
+
code_points = output.codepoints
|
|
286
|
+
case result
|
|
287
|
+
when :finished, :source_buffer_empty
|
|
288
|
+
code_points
|
|
289
|
+
when :incomplete_input, :invalid_byte_sequence, :undefined_conversion
|
|
290
|
+
# Only reached in fatal mode, or by an incomplete sequence at a
|
|
291
|
+
# flush; either way the stream is over.
|
|
292
|
+
code_points << error
|
|
293
|
+
else
|
|
294
|
+
code_points
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
data/lib/dommy/event.rb
CHANGED
|
@@ -473,15 +473,14 @@ module Dommy
|
|
|
473
473
|
# exception reported as an `error` event at the relevant global (the Window),
|
|
474
474
|
# so `window.onerror` / an "error" listener sees it. The error value is the
|
|
475
475
|
# thrown value itself — a JS value keeps its identity (via ThrowValue), so
|
|
476
|
-
# `event.error === thrown
|
|
477
|
-
#
|
|
476
|
+
# `event.error === thrown`; the Ruby exception rides along as `host_error` so
|
|
477
|
+
# an unhandled report reaches the host with its backtrace. No window (a node
|
|
478
|
+
# in a windowless document, or a target that has none) → nothing to report to.
|
|
478
479
|
def __internal_report_listener_exception__(error)
|
|
479
480
|
win = window_of(self)
|
|
480
481
|
return unless win.respond_to?(:__internal_report_exception__)
|
|
481
482
|
|
|
482
|
-
|
|
483
|
-
message = value.respond_to?(:message) ? value.message.to_s : value.to_s
|
|
484
|
-
win.__internal_report_exception__(value, message)
|
|
483
|
+
Internal::ExceptionReport.report_at(win, error)
|
|
485
484
|
end
|
|
486
485
|
|
|
487
486
|
# Diagnostic only (DOMMY_EVENT_DEBUG=<file>): when a listener throws, append
|
data/lib/dommy/fetch.rb
CHANGED
|
@@ -557,6 +557,60 @@ module Dommy
|
|
|
557
557
|
end
|
|
558
558
|
end
|
|
559
559
|
|
|
560
|
+
# The `formData()` half of the Body mixin, shared by Request and Response:
|
|
561
|
+
# the body parsed as a FormData by its Content-Type. The including class
|
|
562
|
+
# provides `@headers`, `@window`, `immediate` and `rejected`.
|
|
563
|
+
module FormDataBody
|
|
564
|
+
private
|
|
565
|
+
|
|
566
|
+
# WHATWG: parse the body as a FormData based on Content-Type —
|
|
567
|
+
# application/x-www-form-urlencoded or multipart/form-data. Any other type
|
|
568
|
+
# rejects with a TypeError.
|
|
569
|
+
def consume_form_data(body)
|
|
570
|
+
content_type = (@headers.__js_call__("get", ["content-type"]) || "").to_s
|
|
571
|
+
if content_type.start_with?("application/x-www-form-urlencoded")
|
|
572
|
+
immediate(parse_urlencoded_form(body))
|
|
573
|
+
elsif (match = content_type.match(/\bmultipart\/form-data\b.*?boundary=("?)([^";]+)\1/i))
|
|
574
|
+
immediate(parse_multipart_form(body, match[2]))
|
|
575
|
+
else
|
|
576
|
+
rejected(ErrorValue.new("Failed to read body as FormData: unsupported Content-Type", name: "TypeError"))
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def parse_urlencoded_form(body)
|
|
581
|
+
form = FormData.new
|
|
582
|
+
URLSearchParams.new(body).__js_call__("entries", []).each { |name, value| form.append(name, value) }
|
|
583
|
+
form
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# Parse a multipart/form-data body (the inverse of Response.multipart_body):
|
|
587
|
+
# a part with a `filename` becomes a File entry, otherwise a string entry.
|
|
588
|
+
def parse_multipart_form(body, boundary)
|
|
589
|
+
form = FormData.new
|
|
590
|
+
body.split("--#{boundary}").each do |section|
|
|
591
|
+
next if section.empty? || section.start_with?("--") # preamble / closing
|
|
592
|
+
|
|
593
|
+
section = section.sub(/\A\r\n/, "")
|
|
594
|
+
head, content = section.split("\r\n\r\n", 2)
|
|
595
|
+
next unless content
|
|
596
|
+
|
|
597
|
+
content = content.sub(/\r\n\z/, "")
|
|
598
|
+
disposition = head[/Content-Disposition:\s*form-data;([^\r\n]*)/i, 1].to_s
|
|
599
|
+
name = disposition[/name="([^"]*)"/i, 1]
|
|
600
|
+
next unless name
|
|
601
|
+
|
|
602
|
+
filename = disposition[/filename="([^"]*)"/i, 1]
|
|
603
|
+
if filename
|
|
604
|
+
type = head[/Content-Type:\s*([^\r\n]+)/i, 1].to_s.strip
|
|
605
|
+
form.append(name, File.new([content], filename, {"type" => type}, @window))
|
|
606
|
+
else
|
|
607
|
+
form.append(name, content)
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
form
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
560
614
|
# `Request` polyfill — minimal Fetch API Request object so callers
|
|
561
615
|
# constructing `new Request(url, init)` get a value with `.url`,
|
|
562
616
|
# `.method`, `.headers`, `.body`. The stub-based `fetch` doesn't
|
|
@@ -626,7 +680,8 @@ module Dommy
|
|
|
626
680
|
end
|
|
627
681
|
|
|
628
682
|
include Bridge::Methods
|
|
629
|
-
|
|
683
|
+
include FormDataBody
|
|
684
|
+
js_methods %w[clone text json arrayBuffer blob bytes formData]
|
|
630
685
|
def __js_call__(method, _args)
|
|
631
686
|
case method
|
|
632
687
|
when "clone"
|
|
@@ -659,6 +714,8 @@ module Dommy
|
|
|
659
714
|
when "blob"
|
|
660
715
|
ct = @headers.__js_call__("get", ["content-type"]) || ""
|
|
661
716
|
consume_body { immediate(Blob.new([@body_bytes], {"type" => ct}, @window)) }
|
|
717
|
+
when "formData"
|
|
718
|
+
consume_body { consume_form_data(@body_bytes) }
|
|
662
719
|
end
|
|
663
720
|
end
|
|
664
721
|
|
|
@@ -946,6 +1003,7 @@ module Dommy
|
|
|
946
1003
|
end
|
|
947
1004
|
|
|
948
1005
|
include Bridge::Methods
|
|
1006
|
+
include FormDataBody
|
|
949
1007
|
js_methods %w[text json arrayBuffer blob formData clone]
|
|
950
1008
|
def __js_call__(method, _args)
|
|
951
1009
|
case method
|
|
@@ -966,7 +1024,7 @@ module Dommy
|
|
|
966
1024
|
immediate(Blob.new([@body], {"type" => @headers.__js_call__("get", ["content-type"]) || ""}, @window))
|
|
967
1025
|
end
|
|
968
1026
|
when "formData"
|
|
969
|
-
consume_body { consume_form_data }
|
|
1027
|
+
consume_body { consume_form_data(@body) }
|
|
970
1028
|
when "clone"
|
|
971
1029
|
clone_response
|
|
972
1030
|
end
|
|
@@ -1004,53 +1062,6 @@ module Dommy
|
|
|
1004
1062
|
end
|
|
1005
1063
|
end
|
|
1006
1064
|
|
|
1007
|
-
# WHATWG: parse the body as a FormData based on Content-Type —
|
|
1008
|
-
# application/x-www-form-urlencoded or multipart/form-data. Any other type
|
|
1009
|
-
# rejects with a TypeError.
|
|
1010
|
-
def consume_form_data
|
|
1011
|
-
content_type = (@headers.__js_call__("get", ["content-type"]) || "").to_s
|
|
1012
|
-
if content_type.start_with?("application/x-www-form-urlencoded")
|
|
1013
|
-
immediate(parse_urlencoded_form(@body))
|
|
1014
|
-
elsif (match = content_type.match(/\bmultipart\/form-data\b.*?boundary=("?)([^";]+)\1/i))
|
|
1015
|
-
immediate(parse_multipart_form(@body, match[2]))
|
|
1016
|
-
else
|
|
1017
|
-
rejected(ErrorValue.new("Failed to read body as FormData: unsupported Content-Type", name: "TypeError"))
|
|
1018
|
-
end
|
|
1019
|
-
end
|
|
1020
|
-
|
|
1021
|
-
def parse_urlencoded_form(body)
|
|
1022
|
-
form = FormData.new
|
|
1023
|
-
URLSearchParams.new(body).__js_call__("entries", []).each { |name, value| form.append(name, value) }
|
|
1024
|
-
form
|
|
1025
|
-
end
|
|
1026
|
-
|
|
1027
|
-
# Parse a multipart/form-data body (the inverse of Response.multipart_body):
|
|
1028
|
-
# a part with a `filename` becomes a File entry, otherwise a string entry.
|
|
1029
|
-
def parse_multipart_form(body, boundary)
|
|
1030
|
-
form = FormData.new
|
|
1031
|
-
body.split("--#{boundary}").each do |section|
|
|
1032
|
-
next if section.empty? || section.start_with?("--") # preamble / closing
|
|
1033
|
-
|
|
1034
|
-
section = section.sub(/\A\r\n/, "")
|
|
1035
|
-
head, content = section.split("\r\n\r\n", 2)
|
|
1036
|
-
next unless content
|
|
1037
|
-
|
|
1038
|
-
content = content.sub(/\r\n\z/, "")
|
|
1039
|
-
disposition = head[/Content-Disposition:\s*form-data;([^\r\n]*)/i, 1].to_s
|
|
1040
|
-
name = disposition[/name="([^"]*)"/i, 1]
|
|
1041
|
-
next unless name
|
|
1042
|
-
|
|
1043
|
-
filename = disposition[/filename="([^"]*)"/i, 1]
|
|
1044
|
-
if filename
|
|
1045
|
-
type = head[/Content-Type:\s*([^\r\n]+)/i, 1].to_s.strip
|
|
1046
|
-
form.append(name, File.new([content], filename, {"type" => type}, @window))
|
|
1047
|
-
else
|
|
1048
|
-
form.append(name, content)
|
|
1049
|
-
end
|
|
1050
|
-
end
|
|
1051
|
-
form
|
|
1052
|
-
end
|
|
1053
|
-
|
|
1054
1065
|
# WHATWG: clone throws if the body is already disturbed/locked; otherwise the
|
|
1055
1066
|
# clone is an independent Response over the same bytes.
|
|
1056
1067
|
def clone_response
|
data/lib/dommy/form_data.rb
CHANGED
|
@@ -15,6 +15,16 @@ module Dommy
|
|
|
15
15
|
class FormData
|
|
16
16
|
include Enumerable
|
|
17
17
|
|
|
18
|
+
# `new FormData(form)` from JavaScript: an absent or undefined form means
|
|
19
|
+
# an empty FormData; null or anything but a form element is a TypeError,
|
|
20
|
+
# since the argument is a non-nullable HTMLFormElement.
|
|
21
|
+
def self.from_js(args)
|
|
22
|
+
return new if args.empty? || args[0].equal?(Bridge::UNDEFINED)
|
|
23
|
+
raise Bridge::TypeError, "FormData constructor: argument 1 is not an HTMLFormElement" unless args[0].is_a?(HTMLFormElement)
|
|
24
|
+
|
|
25
|
+
new(args[0])
|
|
26
|
+
end
|
|
27
|
+
|
|
18
28
|
def initialize(form = nil)
|
|
19
29
|
@pairs = []
|
|
20
30
|
collect_from(form) if form
|