dommy 0.7.0 → 0.8.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/animation.rb +9 -1
- data/lib/dommy/attr.rb +192 -39
- data/lib/dommy/backend/nokogiri_adapter.rb +76 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +37 -0
- data/lib/dommy/backend.rb +46 -0
- data/lib/dommy/blob.rb +28 -9
- data/lib/dommy/bridge/constructor_registry.rb +28 -0
- data/lib/dommy/bridge/methods.rb +57 -0
- data/lib/dommy/bridge.rb +97 -0
- data/lib/dommy/callable_invoker.rb +36 -0
- data/lib/dommy/cookie_store.rb +3 -1
- data/lib/dommy/crypto.rb +7 -1
- data/lib/dommy/css.rb +46 -0
- data/lib/dommy/custom_elements.rb +27 -3
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +615 -48
- data/lib/dommy/dom_parser.rb +28 -15
- data/lib/dommy/element.rb +999 -471
- data/lib/dommy/event.rb +260 -96
- data/lib/dommy/event_source.rb +6 -2
- data/lib/dommy/fetch.rb +505 -43
- data/lib/dommy/file_reader.rb +11 -3
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +43 -8
- data/lib/dommy/html_collection.rb +55 -2
- data/lib/dommy/html_elements.rb +102 -1519
- data/lib/dommy/internal/css_pseudo_handlers.rb +109 -0
- data/lib/dommy/internal/global_functions.rb +26 -0
- data/lib/dommy/internal/idna.rb +16 -7
- data/lib/dommy/internal/ipv4_parser.rb +22 -7
- data/lib/dommy/internal/mutation_coordinator.rb +11 -2
- data/lib/dommy/internal/namespaces.rb +70 -0
- data/lib/dommy/internal/node_equality.rb +86 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +62 -27
- data/lib/dommy/internal/observable_callback.rb +1 -5
- data/lib/dommy/internal/parent_node.rb +126 -0
- data/lib/dommy/internal/reflected_attributes.rb +103 -13
- data/lib/dommy/internal/selector_parser.rb +664 -0
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +7 -1
- data/lib/dommy/message_channel.rb +32 -2
- data/lib/dommy/mutation_observer.rb +55 -12
- data/lib/dommy/navigator.rb +26 -12
- data/lib/dommy/node.rb +158 -28
- data/lib/dommy/notification.rb +3 -1
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +2 -0
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +2 -0
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +23 -54
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +18 -27
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +174 -21
- data/lib/dommy/tree_walker.rb +255 -66
- data/lib/dommy/url.rb +287 -449
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +37 -7
- data/lib/dommy/window.rb +202 -213
- data/lib/dommy/worker.rb +7 -7
- data/lib/dommy/xml_http_request.rb +15 -5
- data/lib/dommy.rb +7 -0
- metadata +12 -3
data/lib/dommy/url.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "uri"
|
|
4
4
|
require "cgi"
|
|
5
|
+
require_relative "internal/url_parser"
|
|
5
6
|
|
|
6
7
|
module Dommy
|
|
7
8
|
# `URL` — WHATWG-style URL parsing. Public API mirrors the JS class:
|
|
@@ -18,10 +19,10 @@ module Dommy
|
|
|
18
19
|
# Dommy::URL.new("/a", "https://x.test").href
|
|
19
20
|
# # => "https://x.test/a"
|
|
20
21
|
#
|
|
21
|
-
# Internally backed by
|
|
22
|
-
#
|
|
23
|
-
# `
|
|
24
|
-
#
|
|
22
|
+
# Internally backed by a WHATWG basic URL parser
|
|
23
|
+
# (`Internal::UrlParser`). A parse failure in the constructor or the
|
|
24
|
+
# `href` setter raises `Bridge::TypeError` — matching the URL
|
|
25
|
+
# Standard, which throws a JS `TypeError` (not a DOMException) there.
|
|
25
26
|
class URL
|
|
26
27
|
# Registry of Blob URLs created via `URL.createObjectURL(blob)`.
|
|
27
28
|
# Process-wide because the spec scopes them to the document/window
|
|
@@ -65,10 +66,10 @@ module Dommy
|
|
|
65
66
|
# WHATWG URL Standard — `URL.parse(input, base)` is the
|
|
66
67
|
# non-throwing static factory. Returns a URL on success, `nil`
|
|
67
68
|
# on parse failure. The constructor (`new URL(...)`) raises
|
|
68
|
-
# `
|
|
69
|
+
# `TypeError` for the same failure case.
|
|
69
70
|
def parse(input, base = nil)
|
|
70
71
|
new(input, base)
|
|
71
|
-
rescue
|
|
72
|
+
rescue Bridge::TypeError
|
|
72
73
|
nil
|
|
73
74
|
end
|
|
74
75
|
|
|
@@ -80,503 +81,290 @@ module Dommy
|
|
|
80
81
|
end
|
|
81
82
|
end
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
attr_reader :search_params
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
raw = parse_with_base(value.to_s, nil)
|
|
97
|
-
@uri = raw
|
|
98
|
-
@search_params.__internal_replace__(raw.query.to_s)
|
|
99
|
-
build_href
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def protocol
|
|
103
|
-
@uri.scheme ? "#{@uri.scheme}:" : ""
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def protocol=(value)
|
|
107
|
-
s = value.to_s.sub(/:$/, "")
|
|
108
|
-
@uri.scheme = s
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def host
|
|
112
|
-
port = @uri.port
|
|
113
|
-
default = default_port_for(@uri.scheme.to_s.downcase)
|
|
114
|
-
hostpart = @uri.host.to_s
|
|
115
|
-
return hostpart if port.nil? || port == default
|
|
116
|
-
|
|
117
|
-
"#{hostpart}:#{port}"
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def host=(value)
|
|
121
|
-
h, p = value.to_s.split(":", 2)
|
|
122
|
-
@uri.host = h
|
|
123
|
-
@uri.port = p.to_i if p
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def hostname
|
|
127
|
-
@uri.host.to_s
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def hostname=(value)
|
|
131
|
-
# WHATWG: a non-ASCII hostname assigned through the setter is
|
|
132
|
-
# Punycode-encoded before storage (matches `new URL("...")`).
|
|
133
|
-
@uri.host = Internal::IDNA.to_ascii(value.to_s)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def port
|
|
137
|
-
default = default_port_for(@uri.scheme.to_s.downcase)
|
|
138
|
-
return "" if @uri.port.nil? || @uri.port == default
|
|
139
|
-
|
|
140
|
-
@uri.port.to_s
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def port=(value)
|
|
144
|
-
@uri.port = value.to_s.empty? ? nil : value.to_i
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# WHATWG: for opaque-body schemes (javascript:, mailto:, data:,
|
|
148
|
-
# tel:, blob:) the body sits in `URI`'s `opaque` slot, not `path`.
|
|
149
|
-
# For special schemes (http/https/ws/wss/ftp), an empty path is
|
|
150
|
-
# canonicalized to `"/"`.
|
|
151
|
-
def pathname
|
|
152
|
-
opaque = @uri.respond_to?(:opaque) ? @uri.opaque : nil
|
|
153
|
-
return opaque.to_s if opaque
|
|
154
|
-
|
|
155
|
-
path = @uri.path.to_s
|
|
156
|
-
return "/" if path.empty? && special_scheme?
|
|
157
|
-
|
|
158
|
-
path
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def pathname=(value)
|
|
162
|
-
v = value.to_s
|
|
163
|
-
v = "/#{v}" if !v.start_with?("/") && !v.empty?
|
|
164
|
-
@uri.path = v
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# WHATWG: `url.search` is the raw query string (with `?` prefix),
|
|
168
|
-
# preserving percent-encoding and stray `?` characters as parsed.
|
|
169
|
-
# `url.searchParams.toString()` re-serializes via the form-encoded
|
|
170
|
-
# contract (`+` for space, etc.) — distinct from `url.search`.
|
|
171
|
-
def search
|
|
172
|
-
q = @uri.query
|
|
173
|
-
q.nil? || q.empty? ? "" : "?#{q}"
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def search=(value)
|
|
177
|
-
q = value.to_s.sub(/^\?/, "")
|
|
178
|
-
@uri.query = q.empty? ? nil : q
|
|
179
|
-
@search_params.__internal_replace__(q)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def hash
|
|
183
|
-
f = @uri.fragment.to_s
|
|
184
|
-
f.empty? ? "" : "##{f}"
|
|
185
|
-
end
|
|
86
|
+
def initialize(input, base = nil)
|
|
87
|
+
# An explicit JS `undefined` base means "no base" (WebIDL optional arg),
|
|
88
|
+
# distinct from a string base. (JS null already arrives as nil.)
|
|
89
|
+
base = nil if base.equal?(Bridge::UNDEFINED)
|
|
90
|
+
base_str = base.is_a?(URL) ? base.href : base
|
|
91
|
+
@record = Internal::UrlParser.parse(input.to_s, base_str)
|
|
92
|
+
@search_params = URLSearchParams.new(@record.query.to_s, owner: self)
|
|
93
|
+
rescue Internal::UrlParser::Failure => e
|
|
94
|
+
# WHATWG: the URL constructor throws TypeError on a parse failure.
|
|
95
|
+
raise Bridge::TypeError, "Invalid URL: #{e.message}"
|
|
96
|
+
end
|
|
186
97
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
end
|
|
98
|
+
def href
|
|
99
|
+
Internal::UrlParser.serialize(@record)
|
|
100
|
+
end
|
|
191
101
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
102
|
+
def href=(value)
|
|
103
|
+
@record = Internal::UrlParser.parse(value.to_s, nil)
|
|
104
|
+
@search_params.__internal_replace__(@record.query.to_s)
|
|
105
|
+
href
|
|
106
|
+
rescue Internal::UrlParser::Failure => e
|
|
107
|
+
# WHATWG: the href setter throws TypeError on a parse failure.
|
|
108
|
+
raise Bridge::TypeError, "Invalid URL: #{e.message}"
|
|
109
|
+
end
|
|
200
110
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
end
|
|
111
|
+
def protocol
|
|
112
|
+
"#{@record.scheme}:"
|
|
113
|
+
end
|
|
205
114
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
opaque = @uri.respond_to?(:opaque) ? @uri.opaque : nil
|
|
211
|
-
return "null" if opaque.nil? || opaque.empty?
|
|
115
|
+
def protocol=(value)
|
|
116
|
+
s = value.to_s.sub(/:\z/, "").downcase
|
|
117
|
+
@record.scheme = s if s.match?(/\A[a-z][a-z0-9+\-.]*\z/)
|
|
118
|
+
end
|
|
212
119
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
"null"
|
|
216
|
-
end
|
|
120
|
+
def host
|
|
121
|
+
return "" if @record.host.nil?
|
|
217
122
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
end
|
|
123
|
+
@record.port ? "#{@record.host}:#{@record.port}" : @record.host
|
|
124
|
+
end
|
|
221
125
|
|
|
222
|
-
|
|
223
|
-
|
|
126
|
+
def host=(value)
|
|
127
|
+
h, sep, p = value.to_s.partition(":")
|
|
128
|
+
begin
|
|
129
|
+
@record.host = Internal::UrlParser.parse_host(h, @record.special?)
|
|
130
|
+
rescue Internal::UrlParser::Failure
|
|
131
|
+
return
|
|
224
132
|
end
|
|
133
|
+
self.port = p unless sep.empty?
|
|
134
|
+
end
|
|
225
135
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
136
|
+
def hostname
|
|
137
|
+
@record.host.to_s
|
|
138
|
+
end
|
|
229
139
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
140
|
+
def hostname=(value)
|
|
141
|
+
@record.host = Internal::UrlParser.parse_host(value.to_s, @record.special?)
|
|
142
|
+
rescue Internal::UrlParser::Failure
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
233
145
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
146
|
+
def port
|
|
147
|
+
@record.port.nil? ? "" : @record.port.to_s
|
|
148
|
+
end
|
|
237
149
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
150
|
+
def port=(value)
|
|
151
|
+
v = value.to_s
|
|
152
|
+
if v.empty?
|
|
153
|
+
@record.port = nil
|
|
154
|
+
elsif v.match?(/\A[0-9]+\z/)
|
|
155
|
+
n = v.to_i
|
|
156
|
+
@record.port = (n == @record.default_port ? nil : n) if n <= 65_535
|
|
241
157
|
end
|
|
158
|
+
end
|
|
242
159
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
href
|
|
247
|
-
when "protocol"
|
|
248
|
-
protocol
|
|
249
|
-
when "host"
|
|
250
|
-
host
|
|
251
|
-
when "hostname"
|
|
252
|
-
hostname
|
|
253
|
-
when "port"
|
|
254
|
-
port
|
|
255
|
-
when "pathname"
|
|
256
|
-
pathname
|
|
257
|
-
when "search"
|
|
258
|
-
search
|
|
259
|
-
when "hash"
|
|
260
|
-
hash
|
|
261
|
-
when "origin"
|
|
262
|
-
origin
|
|
263
|
-
when "username"
|
|
264
|
-
username
|
|
265
|
-
when "password"
|
|
266
|
-
password
|
|
267
|
-
when "searchParams"
|
|
268
|
-
@search_params
|
|
269
|
-
end
|
|
270
|
-
end
|
|
160
|
+
def pathname
|
|
161
|
+
Internal::UrlParser.serialize_path(@record)
|
|
162
|
+
end
|
|
271
163
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
when "href"
|
|
275
|
-
self.href = value
|
|
276
|
-
when "protocol"
|
|
277
|
-
self.protocol = value
|
|
278
|
-
when "host"
|
|
279
|
-
self.host = value
|
|
280
|
-
when "hostname"
|
|
281
|
-
self.hostname = value
|
|
282
|
-
when "port"
|
|
283
|
-
self.port = value
|
|
284
|
-
when "pathname"
|
|
285
|
-
self.pathname = value
|
|
286
|
-
when "search"
|
|
287
|
-
self.search = value
|
|
288
|
-
when "hash"
|
|
289
|
-
self.hash = value
|
|
290
|
-
when "username"
|
|
291
|
-
self.username = value
|
|
292
|
-
when "password"
|
|
293
|
-
self.password = value
|
|
294
|
-
end
|
|
164
|
+
def pathname=(value)
|
|
165
|
+
return if @record.opaque_path?
|
|
295
166
|
|
|
296
|
-
|
|
297
|
-
|
|
167
|
+
v = value.to_s
|
|
168
|
+
v = v.tr("\\", "/") if @record.special?
|
|
169
|
+
segs = v.split("/", -1)
|
|
170
|
+
segs.shift if segs.first == ""
|
|
171
|
+
set = Internal::UrlParser.method(:path_set?)
|
|
172
|
+
@record.path = segs.map { |s| s.each_char.map { |ch| Internal::UrlParser.pe(ch, set) }.join }
|
|
173
|
+
@record.path = [""] if @record.path.empty? && @record.special?
|
|
174
|
+
end
|
|
298
175
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
end
|
|
304
|
-
end
|
|
176
|
+
def search
|
|
177
|
+
q = @record.query
|
|
178
|
+
q.nil? || q.empty? ? "" : "?#{q}"
|
|
179
|
+
end
|
|
305
180
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
181
|
+
def search=(value)
|
|
182
|
+
v = value.to_s.sub(/\A\?/, "")
|
|
183
|
+
if v.empty?
|
|
184
|
+
@record.query = nil
|
|
185
|
+
else
|
|
186
|
+
set = @record.special? ? Internal::UrlParser.method(:special_query_set?) : Internal::UrlParser.method(:query_set?)
|
|
187
|
+
@record.query = v.each_char.map { |ch| Internal::UrlParser.pe(ch, set) }.join
|
|
311
188
|
end
|
|
189
|
+
@search_params.__internal_replace__(@record.query.to_s)
|
|
190
|
+
end
|
|
312
191
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
# handled specially (inner-URL origin).
|
|
318
|
-
TUPLE_ORIGIN_SCHEMES = %w[http https ws wss ftp].freeze
|
|
319
|
-
|
|
320
|
-
# Default ports per scheme (Ruby URI knows http/https/ftp; we add
|
|
321
|
-
# ws/wss).
|
|
322
|
-
DEFAULT_PORTS = {
|
|
323
|
-
"http" => 80,
|
|
324
|
-
"https" => 443,
|
|
325
|
-
"ws" => 80,
|
|
326
|
-
"wss" => 443,
|
|
327
|
-
"ftp" => 21
|
|
328
|
-
}.freeze
|
|
329
|
-
|
|
330
|
-
# Chars that Ruby URI rejects in the path/query/fragment portion
|
|
331
|
-
# but WHATWG silently percent-encodes.
|
|
332
|
-
UNSAFE_PATH_CHARS = /[ "<>`{}|\\\^\[\]]/
|
|
333
|
-
|
|
334
|
-
private
|
|
335
|
-
|
|
336
|
-
def special_scheme?
|
|
337
|
-
SPECIAL_SCHEMES.include?(@uri.scheme.to_s.downcase)
|
|
338
|
-
end
|
|
192
|
+
def hash
|
|
193
|
+
f = @record.fragment
|
|
194
|
+
f.nil? || f.empty? ? "" : "##{f}"
|
|
195
|
+
end
|
|
339
196
|
|
|
340
|
-
|
|
341
|
-
|
|
197
|
+
def hash=(value)
|
|
198
|
+
v = value.to_s.sub(/\A#/, "")
|
|
199
|
+
if v.empty?
|
|
200
|
+
@record.fragment = nil
|
|
201
|
+
else
|
|
202
|
+
set = Internal::UrlParser.method(:fragment_set?)
|
|
203
|
+
@record.fragment = v.each_char.map { |ch| Internal::UrlParser.pe(ch, set) }.join
|
|
342
204
|
end
|
|
205
|
+
end
|
|
343
206
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
else
|
|
352
|
-
uri = URI.parse(str)
|
|
353
|
-
raise DOMException::SyntaxError, "Invalid URL: #{str}" unless uri.scheme
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
normalize_path_segments(uri) if special_scheme_for?(uri)
|
|
357
|
-
uri
|
|
358
|
-
rescue URI::InvalidURIError => e
|
|
359
|
-
raise DOMException::SyntaxError, "Invalid URL: #{e.message}"
|
|
360
|
-
rescue Internal::Punycode::Error, Internal::IDNA::Error => e
|
|
361
|
-
raise DOMException::SyntaxError, "Invalid URL host: #{e.message}"
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
# WHATWG URL preprocessing — turn a raw input string into a form
|
|
365
|
-
# Ruby URI accepts. Order matters: each step can depend on
|
|
366
|
-
# earlier normalizations.
|
|
367
|
-
def preprocess(str)
|
|
368
|
-
str = strip_c0_and_space(str)
|
|
369
|
-
str = strip_tab_and_newline(str)
|
|
370
|
-
str = replace_backslashes_for_special_scheme(str)
|
|
371
|
-
str = normalize_idn_host(str)
|
|
372
|
-
str = normalize_ipv4_host(str)
|
|
373
|
-
percent_encode_unsafe(str)
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# WHATWG §basic-url-parser step 1: strip leading and trailing
|
|
377
|
-
# C0 controls and ASCII space.
|
|
378
|
-
def strip_c0_and_space(str)
|
|
379
|
-
str.sub(/\A[\x00-\x20]+/, "").sub(/[\x00-\x20]+\z/, "")
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
# WHATWG: remove ASCII tab and newline anywhere in the URL.
|
|
383
|
-
def strip_tab_and_newline(str)
|
|
384
|
-
str.delete("\t\n\r")
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
# WHATWG: for special-scheme URLs, treat `\` as `/` in the
|
|
388
|
-
# authority and path portions.
|
|
389
|
-
def replace_backslashes_for_special_scheme(str)
|
|
390
|
-
m = str.match(/\A([a-zA-Z][a-zA-Z0-9+.\-]*):/)
|
|
391
|
-
return str unless m
|
|
392
|
-
return str unless SPECIAL_SCHEMES.include?(m[1].downcase)
|
|
393
|
-
|
|
394
|
-
scheme_end = m.end(0)
|
|
395
|
-
str[0...scheme_end] + str[scheme_end..].tr("\\", "/")
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
# Percent-encode chars after the authority section that Ruby URI
|
|
399
|
-
# would reject (space, `<`, `>`, `{`, `}`, `|`, etc.) and any
|
|
400
|
-
# non-ASCII byte. Preserves already-encoded `%XX` sequences.
|
|
401
|
-
def percent_encode_unsafe(str)
|
|
402
|
-
m = str.match(%r{\A([a-zA-Z][a-zA-Z0-9+.\-]*:(?://[^/?#]*)?)(.*)\z}m)
|
|
403
|
-
return str unless m
|
|
404
|
-
|
|
405
|
-
prefix = m[1]
|
|
406
|
-
tail = m[2]
|
|
407
|
-
out = +""
|
|
408
|
-
i = 0
|
|
409
|
-
while i < tail.length
|
|
410
|
-
c = tail[i]
|
|
411
|
-
if c == "%" && tail[i + 1, 2].to_s.match?(/\A[0-9A-Fa-f]{2}\z/)
|
|
412
|
-
out << tail[i, 3]
|
|
413
|
-
i += 3
|
|
414
|
-
next
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
needs_encoding = c.bytesize > 1 ||
|
|
418
|
-
c.ord < 0x20 ||
|
|
419
|
-
c.ord == 0x7F ||
|
|
420
|
-
UNSAFE_PATH_CHARS.match?(c)
|
|
421
|
-
|
|
422
|
-
if needs_encoding
|
|
423
|
-
c.bytes.each { |b| out << format("%%%02X", b) }
|
|
424
|
-
else
|
|
425
|
-
out << c
|
|
426
|
-
end
|
|
207
|
+
# WHATWG URL §origin. Tuple origins for http(s) / ws(s) / ftp; `"null"`
|
|
208
|
+
# for file/data/javascript/etc. Blob URLs unwrap their inner URL.
|
|
209
|
+
def origin
|
|
210
|
+
scheme = @record.scheme
|
|
211
|
+
return blob_inner_origin if scheme == "blob"
|
|
212
|
+
return "null" unless TUPLE_ORIGIN_SCHEMES.include?(scheme)
|
|
213
|
+
return "null" if @record.host.nil?
|
|
427
214
|
|
|
428
|
-
|
|
429
|
-
|
|
215
|
+
port_part = @record.port ? ":#{@record.port}" : ""
|
|
216
|
+
"#{scheme}://#{@record.host}#{port_part}"
|
|
217
|
+
end
|
|
430
218
|
|
|
431
|
-
|
|
432
|
-
|
|
219
|
+
def username
|
|
220
|
+
@record.username
|
|
221
|
+
end
|
|
433
222
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
# only; non-special schemes are skipped.
|
|
437
|
-
def normalize_ipv4_host(str)
|
|
438
|
-
m = str.match(%r{\A([a-zA-Z][a-zA-Z0-9+.\-]*://(?:[^@/?#]*@)?)([^/:?#]+)(.*)\z}m)
|
|
439
|
-
return str unless m
|
|
223
|
+
def username=(value)
|
|
224
|
+
return if cannot_have_credentials?
|
|
440
225
|
|
|
441
|
-
|
|
442
|
-
|
|
226
|
+
set = Internal::UrlParser.method(:userinfo_set?)
|
|
227
|
+
@record.username = value.to_s.each_char.map { |ch| Internal::UrlParser.pe(ch, set) }.join
|
|
228
|
+
end
|
|
443
229
|
|
|
444
|
-
|
|
445
|
-
|
|
230
|
+
def password
|
|
231
|
+
@record.password
|
|
232
|
+
end
|
|
446
233
|
|
|
447
|
-
|
|
448
|
-
|
|
234
|
+
def password=(value)
|
|
235
|
+
return if cannot_have_credentials?
|
|
449
236
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
237
|
+
set = Internal::UrlParser.method(:userinfo_set?)
|
|
238
|
+
@record.password = value.to_s.each_char.map { |ch| Internal::UrlParser.pe(ch, set) }.join
|
|
239
|
+
end
|
|
453
240
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
path = uri.path
|
|
458
|
-
return if path.nil? || path.empty?
|
|
241
|
+
def to_s
|
|
242
|
+
href
|
|
243
|
+
end
|
|
459
244
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
case seg
|
|
464
|
-
when ".."
|
|
465
|
-
# Pop unless we'd remove the leading-empty marker.
|
|
466
|
-
result.pop if result.length > 1
|
|
467
|
-
when "."
|
|
468
|
-
# Skip.
|
|
469
|
-
else
|
|
470
|
-
result << seg
|
|
471
|
-
end
|
|
472
|
-
end
|
|
245
|
+
def to_json(*_args)
|
|
246
|
+
href.inspect
|
|
247
|
+
end
|
|
473
248
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
249
|
+
def __js_get__(key)
|
|
250
|
+
case key
|
|
251
|
+
when "href" then href
|
|
252
|
+
when "protocol" then protocol
|
|
253
|
+
when "host" then host
|
|
254
|
+
when "hostname" then hostname
|
|
255
|
+
when "port" then port
|
|
256
|
+
when "pathname" then pathname
|
|
257
|
+
when "search" then search
|
|
258
|
+
when "hash" then hash
|
|
259
|
+
when "origin" then origin
|
|
260
|
+
when "username" then username
|
|
261
|
+
when "password" then password
|
|
262
|
+
when "searchParams" then @search_params
|
|
477
263
|
end
|
|
264
|
+
end
|
|
478
265
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
266
|
+
def __js_set__(key, value)
|
|
267
|
+
case key
|
|
268
|
+
when "href" then self.href = value
|
|
269
|
+
when "protocol" then self.protocol = value
|
|
270
|
+
when "host" then self.host = value
|
|
271
|
+
when "hostname" then self.hostname = value
|
|
272
|
+
when "port" then self.port = value
|
|
273
|
+
when "pathname" then self.pathname = value
|
|
274
|
+
when "search" then self.search = value
|
|
275
|
+
when "hash" then self.hash = value
|
|
276
|
+
when "username" then self.username = value
|
|
277
|
+
when "password" then self.password = value
|
|
278
|
+
else
|
|
279
|
+
return Bridge::UNHANDLED
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
nil
|
|
283
|
+
end
|
|
487
284
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
285
|
+
include Bridge::Methods
|
|
286
|
+
js_methods %w[toString toJSON]
|
|
287
|
+
def __js_call__(method, _args)
|
|
288
|
+
case method
|
|
289
|
+
when "toString", "toJSON"
|
|
290
|
+
href
|
|
493
291
|
end
|
|
292
|
+
end
|
|
494
293
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
294
|
+
# Called by URLSearchParams when it mutates; keep the record's query in sync.
|
|
295
|
+
def __internal_notify_params_changed__
|
|
296
|
+
q = @search_params.to_s
|
|
297
|
+
@record.query = q.empty? ? nil : q
|
|
298
|
+
end
|
|
500
299
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
out << ascii_host
|
|
505
|
-
out << ":#{port}" if port
|
|
506
|
-
out
|
|
507
|
-
end
|
|
300
|
+
# WHATWG: only http(s) / ws(s) / ftp produce a tuple origin. file / data /
|
|
301
|
+
# javascript / etc. resolve to `"null"`. `blob:` is handled specially.
|
|
302
|
+
TUPLE_ORIGIN_SCHEMES = %w[http https ws wss ftp].freeze
|
|
508
303
|
|
|
509
|
-
|
|
510
|
-
out = +""
|
|
511
|
-
out << "#{@uri.scheme}:" if @uri.scheme
|
|
304
|
+
private
|
|
512
305
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
# — emit the body verbatim, no authority section.
|
|
517
|
-
out << opaque
|
|
518
|
-
else
|
|
519
|
-
if @uri.host
|
|
520
|
-
out << "//"
|
|
521
|
-
if @uri.user
|
|
522
|
-
out << @uri.user
|
|
523
|
-
out << ":#{@uri.password}" if @uri.password
|
|
524
|
-
out << "@"
|
|
525
|
-
end
|
|
306
|
+
def cannot_have_credentials?
|
|
307
|
+
@record.host.nil? || @record.host == "" || @record.scheme == "file"
|
|
308
|
+
end
|
|
526
309
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
310
|
+
# HTML Standard "origin" for a blob: URL — parse the opaque path as a URL and
|
|
311
|
+
# return its origin only when that inner URL's scheme is http/https/file;
|
|
312
|
+
# any other scheme (ftp, ws, a nested blob, …) yields an opaque origin.
|
|
313
|
+
def blob_inner_origin
|
|
314
|
+
return "null" unless @record.opaque_path?
|
|
531
315
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
# when empty (matches `pathname` accessor).
|
|
535
|
-
path = "/" if path.empty? && special_scheme?
|
|
536
|
-
out << path
|
|
537
|
-
end
|
|
316
|
+
body = @record.path
|
|
317
|
+
return "null" if body.nil? || body.empty?
|
|
538
318
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
out
|
|
542
|
-
end
|
|
319
|
+
inner = URL.new(body)
|
|
320
|
+
return "null" unless %w[http https file].include?(inner.protocol.delete_suffix(":"))
|
|
543
321
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
322
|
+
inner.origin
|
|
323
|
+
rescue Bridge::TypeError
|
|
324
|
+
"null"
|
|
325
|
+
end
|
|
548
326
|
end
|
|
549
327
|
|
|
550
|
-
# `URLSearchParams` — query-string manipulation. Constructed from a
|
|
551
|
-
# raw string (`"a=1&b=2"`), an array of `[k, v]` pairs, or a Hash.
|
|
552
|
-
# Order is preserved. Values are stringified per spec.
|
|
553
328
|
class URLSearchParams
|
|
554
329
|
include Enumerable
|
|
555
330
|
|
|
331
|
+
# Sentinel distinguishing "no second argument" from an explicit null/value
|
|
332
|
+
# in the two-argument has()/delete() forms.
|
|
333
|
+
UNSET = Object.new
|
|
334
|
+
private_constant :UNSET
|
|
335
|
+
|
|
556
336
|
def initialize(input = "", owner: nil)
|
|
557
337
|
@owner = owner
|
|
558
338
|
@pairs = parse(input)
|
|
559
339
|
end
|
|
560
340
|
|
|
561
341
|
def get(name)
|
|
562
|
-
|
|
342
|
+
key = stringify(name)
|
|
343
|
+
pair = @pairs.find { |k, _| k == key }
|
|
563
344
|
pair && pair[1]
|
|
564
345
|
end
|
|
565
346
|
|
|
566
347
|
def get_all(name)
|
|
567
|
-
|
|
348
|
+
key = stringify(name)
|
|
349
|
+
@pairs.select { |k, _| k == key }.map { |_, v| v }
|
|
568
350
|
end
|
|
569
351
|
|
|
570
352
|
alias getAll get_all
|
|
571
353
|
|
|
572
|
-
|
|
573
|
-
|
|
354
|
+
# WHATWG has(name) / has(name, value): with a value, only matches a pair
|
|
355
|
+
# whose value also equals it.
|
|
356
|
+
def has(name, value = UNSET)
|
|
357
|
+
key = stringify(name)
|
|
358
|
+
return @pairs.any? { |k, _| k == key } if UNSET.equal?(value)
|
|
359
|
+
|
|
360
|
+
val = stringify(value)
|
|
361
|
+
@pairs.any? { |k, v| k == key && v == val }
|
|
574
362
|
end
|
|
575
363
|
|
|
576
364
|
alias has? has
|
|
577
365
|
|
|
578
366
|
def set(name, value)
|
|
579
|
-
key = name
|
|
367
|
+
key = stringify(name)
|
|
580
368
|
first_done = false
|
|
581
369
|
@pairs = @pairs.reject do |k, _|
|
|
582
370
|
next false unless k == key
|
|
@@ -589,33 +377,44 @@ module Dommy
|
|
|
589
377
|
end
|
|
590
378
|
end
|
|
591
379
|
|
|
592
|
-
|
|
593
|
-
@pairs
|
|
380
|
+
val = stringify(value)
|
|
381
|
+
@pairs.map! { |pair| pair[0] == key ? [key, val] : pair }
|
|
382
|
+
@pairs << [key, val] unless first_done
|
|
594
383
|
notify
|
|
595
384
|
nil
|
|
596
385
|
end
|
|
597
386
|
|
|
598
387
|
def append(name, value)
|
|
599
|
-
@pairs << [name
|
|
388
|
+
@pairs << [stringify(name), stringify(value)]
|
|
600
389
|
notify
|
|
601
390
|
nil
|
|
602
391
|
end
|
|
603
392
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
393
|
+
# WHATWG delete(name) / delete(name, value): with a value, only removes
|
|
394
|
+
# pairs whose value also matches.
|
|
395
|
+
def delete(name, value = UNSET)
|
|
396
|
+
key = stringify(name)
|
|
397
|
+
if UNSET.equal?(value)
|
|
607
398
|
@pairs.reject! { |k, _| k == key }
|
|
608
399
|
else
|
|
609
|
-
|
|
610
|
-
@pairs.reject! { |k, vv| k == key && vv ==
|
|
400
|
+
val = stringify(value)
|
|
401
|
+
@pairs.reject! { |k, vv| k == key && vv == val }
|
|
611
402
|
end
|
|
612
403
|
|
|
613
404
|
notify
|
|
614
405
|
nil
|
|
615
406
|
end
|
|
616
407
|
|
|
408
|
+
# WHATWG: sort by comparison of the names' UTF-16 *code units*
|
|
409
|
+
# (not code points — so a surrogate-pair character sorts by its
|
|
410
|
+
# leading 0xD800–0xDBFF unit), preserving the relative order of
|
|
411
|
+
# pairs with equal names (Ruby's sort_by is not stable, hence the
|
|
412
|
+
# index tiebreak).
|
|
617
413
|
def sort
|
|
618
|
-
@pairs
|
|
414
|
+
@pairs = @pairs
|
|
415
|
+
.each_with_index
|
|
416
|
+
.sort_by { |(name, _value), idx| [name.encode(Encoding::UTF_16BE).unpack("n*"), idx] }
|
|
417
|
+
.map(&:first)
|
|
619
418
|
notify
|
|
620
419
|
nil
|
|
621
420
|
end
|
|
@@ -665,6 +464,8 @@ module Dommy
|
|
|
665
464
|
end
|
|
666
465
|
end
|
|
667
466
|
|
|
467
|
+
include Bridge::Methods
|
|
468
|
+
js_methods %w[get getAll has set append delete sort toString forEach keys values entries]
|
|
668
469
|
def __js_call__(method, args)
|
|
669
470
|
case method
|
|
670
471
|
when "get"
|
|
@@ -672,19 +473,30 @@ module Dommy
|
|
|
672
473
|
when "getAll"
|
|
673
474
|
get_all(args[0])
|
|
674
475
|
when "has"
|
|
675
|
-
has(args[0])
|
|
476
|
+
value_given?(args) ? has(args[0], args[1]) : has(args[0])
|
|
676
477
|
when "set"
|
|
677
478
|
set(args[0], args[1])
|
|
678
479
|
when "append"
|
|
679
480
|
append(args[0], args[1])
|
|
680
481
|
when "delete"
|
|
681
|
-
delete(args[0], args[1])
|
|
482
|
+
value_given?(args) ? delete(args[0], args[1]) : delete(args[0])
|
|
682
483
|
when "sort"
|
|
683
484
|
sort
|
|
684
485
|
when "toString"
|
|
685
486
|
to_s
|
|
686
487
|
when "forEach"
|
|
687
|
-
|
|
488
|
+
# The callback is a live JS function (HostCallback), not a Ruby Proc, so
|
|
489
|
+
# invoke it through the bridge ABI rather than `&block` (which would try
|
|
490
|
+
# to to_proc it). callback(value, key, this) per WHATWG.
|
|
491
|
+
cb = args[0]
|
|
492
|
+
@pairs.each do |k, v|
|
|
493
|
+
if cb.respond_to?(:__js_call__)
|
|
494
|
+
cb.__js_call__("call", [v, k, self])
|
|
495
|
+
elsif cb.respond_to?(:call)
|
|
496
|
+
cb.call(v, k, self)
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
nil
|
|
688
500
|
when "keys"
|
|
689
501
|
keys
|
|
690
502
|
when "values"
|
|
@@ -696,6 +508,12 @@ module Dommy
|
|
|
696
508
|
|
|
697
509
|
private
|
|
698
510
|
|
|
511
|
+
# True when a real second argument (value) was passed to has()/delete().
|
|
512
|
+
# An explicit JS `undefined` counts as "not provided" (one-arg form).
|
|
513
|
+
def value_given?(args)
|
|
514
|
+
args.length >= 2 && !args[1].equal?(Bridge::UNDEFINED)
|
|
515
|
+
end
|
|
516
|
+
|
|
699
517
|
def parse(input)
|
|
700
518
|
case input
|
|
701
519
|
when Array
|
|
@@ -703,18 +521,38 @@ module Dommy
|
|
|
703
521
|
when Hash
|
|
704
522
|
input.map { |k, v| [k.to_s, v.to_s] }
|
|
705
523
|
else
|
|
706
|
-
|
|
524
|
+
# The public `new URLSearchParams(str)` strips a single leading "?".
|
|
525
|
+
# But when we're owner-backed (initialized from a URL's already-extracted
|
|
526
|
+
# query), a leading "?" is literal query data — e.g. `??a=b` stores query
|
|
527
|
+
# "?a=b", whose first name is "?a" — so it must be kept.
|
|
528
|
+
s = input.to_s
|
|
529
|
+
s = s.sub(/^\?/, "") if @owner.nil?
|
|
707
530
|
return [] if s.empty?
|
|
708
531
|
|
|
709
|
-
|
|
532
|
+
# WHATWG urlencoded parser: split on "&" and skip empty sequences (so
|
|
533
|
+
# "a=b&&c" / trailing "&" don't yield phantom empty-name pairs).
|
|
534
|
+
s.split("&").reject(&:empty?).map do |pair|
|
|
710
535
|
k, v = pair.split("=", 2)
|
|
711
|
-
[
|
|
536
|
+
[decode(k.to_s), decode(v.to_s)]
|
|
712
537
|
end
|
|
713
538
|
end
|
|
714
539
|
end
|
|
715
540
|
|
|
541
|
+
def decode(str)
|
|
542
|
+
CGI.unescape(str)
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# WHATWG application/x-www-form-urlencoded serializer: byte-encode, keeping
|
|
546
|
+
# alphanumerics and *-._ literal, space as "+", everything else as %XX.
|
|
547
|
+
# (CGI.escape differs — notably it percent-encodes "*".)
|
|
716
548
|
def encode(str)
|
|
717
|
-
|
|
549
|
+
str.to_s.b.gsub(/[^*\-._A-Za-z0-9]/n) { |c| c == " " ? "+" : format("%%%02X", c.ord) }
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# USVString coercion for name/value arguments. JS null arrives as Ruby nil
|
|
553
|
+
# and must stringify to "null" (ToString(null)), not "".
|
|
554
|
+
def stringify(value)
|
|
555
|
+
value.nil? ? "null" : value.to_s
|
|
718
556
|
end
|
|
719
557
|
|
|
720
558
|
def notify
|