dommy 0.6.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/README.md +30 -38
- data/lib/dommy/animation.rb +10 -2
- data/lib/dommy/attr.rb +197 -32
- data/lib/dommy/backend/nokogiri_adapter.rb +127 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +117 -0
- data/lib/dommy/backend.rb +175 -0
- data/lib/dommy/blob.rb +30 -11
- 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/compression_streams.rb +4 -4
- data/lib/dommy/cookie_store.rb +4 -2
- data/lib/dommy/crypto.rb +16 -9
- data/lib/dommy/css.rb +53 -7
- data/lib/dommy/custom_elements.rb +33 -9
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +693 -60
- data/lib/dommy/dom_parser.rb +29 -15
- data/lib/dommy/element.rb +1147 -438
- data/lib/dommy/event.rb +279 -79
- data/lib/dommy/event_source.rb +14 -10
- data/lib/dommy/fetch.rb +509 -39
- data/lib/dommy/file_reader.rb +14 -6
- data/lib/dommy/form_data.rb +3 -3
- data/lib/dommy/history.rb +46 -8
- data/lib/dommy/html_collection.rb +59 -6
- data/lib/dommy/html_elements.rb +153 -1502
- data/lib/dommy/internal/css_pseudo_handlers.rb +137 -0
- data/lib/dommy/internal/dom_matching.rb +3 -3
- 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_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +77 -31
- 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/template_content_registry.rb +6 -6
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +4 -2
- data/lib/dommy/location.rb +10 -4
- data/lib/dommy/media_query_list.rb +10 -4
- data/lib/dommy/message_channel.rb +41 -11
- data/lib/dommy/mutation_observer.rb +76 -23
- data/lib/dommy/navigator.rb +38 -24
- data/lib/dommy/node.rb +158 -16
- data/lib/dommy/notification.rb +6 -4
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +4 -2
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +4 -2
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +31 -60
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +40 -49
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +178 -25
- data/lib/dommy/tree_walker.rb +270 -81
- data/lib/dommy/url.rb +305 -450
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +49 -19
- data/lib/dommy/window.rb +205 -203
- data/lib/dommy/worker.rb +12 -12
- data/lib/dommy/xml_http_request.rb +32 -7
- data/lib/dommy.rb +19 -2
- metadata +22 -27
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
|
|
@@ -30,7 +31,7 @@ module Dommy
|
|
|
30
31
|
|
|
31
32
|
class << self
|
|
32
33
|
# Create a unique blob: URL that resolves back to `blob` via
|
|
33
|
-
# `URL.
|
|
34
|
+
# `URL.__test_resolve_blob_url__(url)`. Returns nil for non-Blob input.
|
|
34
35
|
def create_object_url(blob)
|
|
35
36
|
return nil unless blob.is_a?(Blob)
|
|
36
37
|
|
|
@@ -53,513 +54,317 @@ module Dommy
|
|
|
53
54
|
|
|
54
55
|
# Resolve a blob: URL back to its Blob, or nil if revoked / unknown.
|
|
55
56
|
# Internal — used by fetch / XHR implementations that load blob URLs.
|
|
56
|
-
def
|
|
57
|
+
def __test_resolve_blob_url__(url)
|
|
57
58
|
@blob_urls[url.to_s]
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
# Test seam: drop all registered blob URLs.
|
|
61
|
-
def
|
|
62
|
+
def __test_reset_blob_urls__
|
|
62
63
|
@blob_urls.clear
|
|
63
64
|
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
attr_reader :search_params
|
|
67
|
-
|
|
68
|
-
def initialize(input, base = nil)
|
|
69
|
-
raw = parse_with_base(input, base)
|
|
70
|
-
@uri = raw
|
|
71
|
-
@search_params = URLSearchParams.new(raw.query.to_s, owner: self)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def href
|
|
75
|
-
build_href
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def href=(value)
|
|
79
|
-
raw = parse_with_base(value.to_s, nil)
|
|
80
|
-
@uri = raw
|
|
81
|
-
@search_params.__replace__(raw.query.to_s)
|
|
82
|
-
build_href
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def protocol
|
|
86
|
-
@uri.scheme ? "#{@uri.scheme}:" : ""
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def protocol=(value)
|
|
90
|
-
s = value.to_s.sub(/:$/, "")
|
|
91
|
-
@uri.scheme = s
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def host
|
|
95
|
-
port = @uri.port
|
|
96
|
-
default = default_port_for(@uri.scheme.to_s.downcase)
|
|
97
|
-
hostpart = @uri.host.to_s
|
|
98
|
-
return hostpart if port.nil? || port == default
|
|
99
|
-
|
|
100
|
-
"#{hostpart}:#{port}"
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def host=(value)
|
|
104
|
-
h, p = value.to_s.split(":", 2)
|
|
105
|
-
@uri.host = h
|
|
106
|
-
@uri.port = p.to_i if p
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def hostname
|
|
110
|
-
@uri.host.to_s
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def hostname=(value)
|
|
114
|
-
# WHATWG: a non-ASCII hostname assigned through the setter is
|
|
115
|
-
# Punycode-encoded before storage (matches `new URL("...")`).
|
|
116
|
-
@uri.host = Internal::IDNA.to_ascii(value.to_s)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def port
|
|
120
|
-
default = default_port_for(@uri.scheme.to_s.downcase)
|
|
121
|
-
return "" if @uri.port.nil? || @uri.port == default
|
|
122
|
-
|
|
123
|
-
@uri.port.to_s
|
|
124
|
-
end
|
|
125
65
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
opaque = @uri.respond_to?(:opaque) ? @uri.opaque : nil
|
|
136
|
-
return opaque.to_s if opaque
|
|
137
|
-
|
|
138
|
-
path = @uri.path.to_s
|
|
139
|
-
return "/" if path.empty? && special_scheme?
|
|
140
|
-
|
|
141
|
-
path
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def pathname=(value)
|
|
145
|
-
v = value.to_s
|
|
146
|
-
v = "/#{v}" if !v.start_with?("/") && !v.empty?
|
|
147
|
-
@uri.path = v
|
|
148
|
-
end
|
|
66
|
+
# WHATWG URL Standard — `URL.parse(input, base)` is the
|
|
67
|
+
# non-throwing static factory. Returns a URL on success, `nil`
|
|
68
|
+
# on parse failure. The constructor (`new URL(...)`) raises
|
|
69
|
+
# `TypeError` for the same failure case.
|
|
70
|
+
def parse(input, base = nil)
|
|
71
|
+
new(input, base)
|
|
72
|
+
rescue Bridge::TypeError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
149
75
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
q.nil? || q.empty? ? "" : "?#{q}"
|
|
76
|
+
# WHATWG URL Standard — `URL.canParse(input, base)`. Boolean
|
|
77
|
+
# counterpart to `parse`: lets callers peek at validity
|
|
78
|
+
# without rescuing an exception or holding a URL reference.
|
|
79
|
+
def can_parse(input, base = nil)
|
|
80
|
+
!parse(input, base).nil?
|
|
81
|
+
end
|
|
157
82
|
end
|
|
158
83
|
|
|
159
|
-
|
|
160
|
-
q = value.to_s.sub(/^\?/, "")
|
|
161
|
-
@uri.query = q.empty? ? nil : q
|
|
162
|
-
@search_params.__replace__(q)
|
|
163
|
-
end
|
|
84
|
+
attr_reader :search_params
|
|
164
85
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
169
97
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
end
|
|
98
|
+
def href
|
|
99
|
+
Internal::UrlParser.serialize(@record)
|
|
100
|
+
end
|
|
174
101
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
183
110
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
end
|
|
111
|
+
def protocol
|
|
112
|
+
"#{@record.scheme}:"
|
|
113
|
+
end
|
|
188
114
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
opaque = @uri.respond_to?(:opaque) ? @uri.opaque : nil
|
|
194
|
-
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
|
|
195
119
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"null"
|
|
199
|
-
end
|
|
120
|
+
def host
|
|
121
|
+
return "" if @record.host.nil?
|
|
200
122
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
end
|
|
123
|
+
@record.port ? "#{@record.host}:#{@record.port}" : @record.host
|
|
124
|
+
end
|
|
204
125
|
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
207
132
|
end
|
|
133
|
+
self.port = p unless sep.empty?
|
|
134
|
+
end
|
|
208
135
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
136
|
+
def hostname
|
|
137
|
+
@record.host.to_s
|
|
138
|
+
end
|
|
212
139
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
216
145
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
146
|
+
def port
|
|
147
|
+
@record.port.nil? ? "" : @record.port.to_s
|
|
148
|
+
end
|
|
220
149
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
224
157
|
end
|
|
158
|
+
end
|
|
225
159
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
href
|
|
230
|
-
when "protocol"
|
|
231
|
-
protocol
|
|
232
|
-
when "host"
|
|
233
|
-
host
|
|
234
|
-
when "hostname"
|
|
235
|
-
hostname
|
|
236
|
-
when "port"
|
|
237
|
-
port
|
|
238
|
-
when "pathname"
|
|
239
|
-
pathname
|
|
240
|
-
when "search"
|
|
241
|
-
search
|
|
242
|
-
when "hash"
|
|
243
|
-
hash
|
|
244
|
-
when "origin"
|
|
245
|
-
origin
|
|
246
|
-
when "username"
|
|
247
|
-
username
|
|
248
|
-
when "password"
|
|
249
|
-
password
|
|
250
|
-
when "searchParams"
|
|
251
|
-
@search_params
|
|
252
|
-
end
|
|
253
|
-
end
|
|
160
|
+
def pathname
|
|
161
|
+
Internal::UrlParser.serialize_path(@record)
|
|
162
|
+
end
|
|
254
163
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
when "href"
|
|
258
|
-
self.href = value
|
|
259
|
-
when "protocol"
|
|
260
|
-
self.protocol = value
|
|
261
|
-
when "host"
|
|
262
|
-
self.host = value
|
|
263
|
-
when "hostname"
|
|
264
|
-
self.hostname = value
|
|
265
|
-
when "port"
|
|
266
|
-
self.port = value
|
|
267
|
-
when "pathname"
|
|
268
|
-
self.pathname = value
|
|
269
|
-
when "search"
|
|
270
|
-
self.search = value
|
|
271
|
-
when "hash"
|
|
272
|
-
self.hash = value
|
|
273
|
-
when "username"
|
|
274
|
-
self.username = value
|
|
275
|
-
when "password"
|
|
276
|
-
self.password = value
|
|
277
|
-
end
|
|
164
|
+
def pathname=(value)
|
|
165
|
+
return if @record.opaque_path?
|
|
278
166
|
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
281
175
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
end
|
|
287
|
-
end
|
|
176
|
+
def search
|
|
177
|
+
q = @record.query
|
|
178
|
+
q.nil? || q.empty? ? "" : "?#{q}"
|
|
179
|
+
end
|
|
288
180
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
294
188
|
end
|
|
189
|
+
@search_params.__internal_replace__(@record.query.to_s)
|
|
190
|
+
end
|
|
295
191
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
# handled specially (inner-URL origin).
|
|
301
|
-
TUPLE_ORIGIN_SCHEMES = %w[http https ws wss ftp].freeze
|
|
302
|
-
|
|
303
|
-
# Default ports per scheme (Ruby URI knows http/https/ftp; we add
|
|
304
|
-
# ws/wss).
|
|
305
|
-
DEFAULT_PORTS = {
|
|
306
|
-
"http" => 80,
|
|
307
|
-
"https" => 443,
|
|
308
|
-
"ws" => 80,
|
|
309
|
-
"wss" => 443,
|
|
310
|
-
"ftp" => 21
|
|
311
|
-
}.freeze
|
|
312
|
-
|
|
313
|
-
# Chars that Ruby URI rejects in the path/query/fragment portion
|
|
314
|
-
# but WHATWG silently percent-encodes.
|
|
315
|
-
UNSAFE_PATH_CHARS = /[ "<>`{}|\\\^\[\]]/
|
|
316
|
-
|
|
317
|
-
private
|
|
318
|
-
|
|
319
|
-
def special_scheme?
|
|
320
|
-
SPECIAL_SCHEMES.include?(@uri.scheme.to_s.downcase)
|
|
321
|
-
end
|
|
192
|
+
def hash
|
|
193
|
+
f = @record.fragment
|
|
194
|
+
f.nil? || f.empty? ? "" : "##{f}"
|
|
195
|
+
end
|
|
322
196
|
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
325
204
|
end
|
|
205
|
+
end
|
|
326
206
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
else
|
|
335
|
-
uri = URI.parse(str)
|
|
336
|
-
raise DOMException::SyntaxError, "Invalid URL: #{str}" unless uri.scheme
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
normalize_path_segments(uri) if special_scheme_for?(uri)
|
|
340
|
-
uri
|
|
341
|
-
rescue URI::InvalidURIError => e
|
|
342
|
-
raise DOMException::SyntaxError, "Invalid URL: #{e.message}"
|
|
343
|
-
rescue Internal::Punycode::Error, Internal::IDNA::Error => e
|
|
344
|
-
raise DOMException::SyntaxError, "Invalid URL host: #{e.message}"
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
# WHATWG URL preprocessing — turn a raw input string into a form
|
|
348
|
-
# Ruby URI accepts. Order matters: each step can depend on
|
|
349
|
-
# earlier normalizations.
|
|
350
|
-
def preprocess(str)
|
|
351
|
-
str = strip_c0_and_space(str)
|
|
352
|
-
str = strip_tab_and_newline(str)
|
|
353
|
-
str = replace_backslashes_for_special_scheme(str)
|
|
354
|
-
str = normalize_idn_host(str)
|
|
355
|
-
str = normalize_ipv4_host(str)
|
|
356
|
-
percent_encode_unsafe(str)
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
# WHATWG §basic-url-parser step 1: strip leading and trailing
|
|
360
|
-
# C0 controls and ASCII space.
|
|
361
|
-
def strip_c0_and_space(str)
|
|
362
|
-
str.sub(/\A[\x00-\x20]+/, "").sub(/[\x00-\x20]+\z/, "")
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
# WHATWG: remove ASCII tab and newline anywhere in the URL.
|
|
366
|
-
def strip_tab_and_newline(str)
|
|
367
|
-
str.delete("\t\n\r")
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
# WHATWG: for special-scheme URLs, treat `\` as `/` in the
|
|
371
|
-
# authority and path portions.
|
|
372
|
-
def replace_backslashes_for_special_scheme(str)
|
|
373
|
-
m = str.match(/\A([a-zA-Z][a-zA-Z0-9+.\-]*):/)
|
|
374
|
-
return str unless m
|
|
375
|
-
return str unless SPECIAL_SCHEMES.include?(m[1].downcase)
|
|
376
|
-
|
|
377
|
-
scheme_end = m.end(0)
|
|
378
|
-
str[0...scheme_end] + str[scheme_end..].tr("\\", "/")
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
# Percent-encode chars after the authority section that Ruby URI
|
|
382
|
-
# would reject (space, `<`, `>`, `{`, `}`, `|`, etc.) and any
|
|
383
|
-
# non-ASCII byte. Preserves already-encoded `%XX` sequences.
|
|
384
|
-
def percent_encode_unsafe(str)
|
|
385
|
-
m = str.match(%r{\A([a-zA-Z][a-zA-Z0-9+.\-]*:(?://[^/?#]*)?)(.*)\z}m)
|
|
386
|
-
return str unless m
|
|
387
|
-
|
|
388
|
-
prefix = m[1]
|
|
389
|
-
tail = m[2]
|
|
390
|
-
out = +""
|
|
391
|
-
i = 0
|
|
392
|
-
while i < tail.length
|
|
393
|
-
c = tail[i]
|
|
394
|
-
if c == "%" && tail[i + 1, 2].to_s.match?(/\A[0-9A-Fa-f]{2}\z/)
|
|
395
|
-
out << tail[i, 3]
|
|
396
|
-
i += 3
|
|
397
|
-
next
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
needs_encoding = c.bytesize > 1 ||
|
|
401
|
-
c.ord < 0x20 ||
|
|
402
|
-
c.ord == 0x7F ||
|
|
403
|
-
UNSAFE_PATH_CHARS.match?(c)
|
|
404
|
-
|
|
405
|
-
if needs_encoding
|
|
406
|
-
c.bytes.each { |b| out << format("%%%02X", b) }
|
|
407
|
-
else
|
|
408
|
-
out << c
|
|
409
|
-
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?
|
|
410
214
|
|
|
411
|
-
|
|
412
|
-
|
|
215
|
+
port_part = @record.port ? ":#{@record.port}" : ""
|
|
216
|
+
"#{scheme}://#{@record.host}#{port_part}"
|
|
217
|
+
end
|
|
413
218
|
|
|
414
|
-
|
|
415
|
-
|
|
219
|
+
def username
|
|
220
|
+
@record.username
|
|
221
|
+
end
|
|
416
222
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
# only; non-special schemes are skipped.
|
|
420
|
-
def normalize_ipv4_host(str)
|
|
421
|
-
m = str.match(%r{\A([a-zA-Z][a-zA-Z0-9+.\-]*://(?:[^@/?#]*@)?)([^/:?#]+)(.*)\z}m)
|
|
422
|
-
return str unless m
|
|
223
|
+
def username=(value)
|
|
224
|
+
return if cannot_have_credentials?
|
|
423
225
|
|
|
424
|
-
|
|
425
|
-
|
|
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
|
|
426
229
|
|
|
427
|
-
|
|
428
|
-
|
|
230
|
+
def password
|
|
231
|
+
@record.password
|
|
232
|
+
end
|
|
429
233
|
|
|
430
|
-
|
|
431
|
-
|
|
234
|
+
def password=(value)
|
|
235
|
+
return if cannot_have_credentials?
|
|
432
236
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
436
240
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
path = uri.path
|
|
441
|
-
return if path.nil? || path.empty?
|
|
241
|
+
def to_s
|
|
242
|
+
href
|
|
243
|
+
end
|
|
442
244
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
case seg
|
|
447
|
-
when ".."
|
|
448
|
-
# Pop unless we'd remove the leading-empty marker.
|
|
449
|
-
result.pop if result.length > 1
|
|
450
|
-
when "."
|
|
451
|
-
# Skip.
|
|
452
|
-
else
|
|
453
|
-
result << seg
|
|
454
|
-
end
|
|
455
|
-
end
|
|
245
|
+
def to_json(*_args)
|
|
246
|
+
href.inspect
|
|
247
|
+
end
|
|
456
248
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
460
263
|
end
|
|
264
|
+
end
|
|
461
265
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
470
284
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
476
291
|
end
|
|
292
|
+
end
|
|
477
293
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
483
299
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
out << ascii_host
|
|
488
|
-
out << ":#{port}" if port
|
|
489
|
-
out
|
|
490
|
-
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
|
|
491
303
|
|
|
492
|
-
|
|
493
|
-
out = +""
|
|
494
|
-
out << "#{@uri.scheme}:" if @uri.scheme
|
|
304
|
+
private
|
|
495
305
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
# — emit the body verbatim, no authority section.
|
|
500
|
-
out << opaque
|
|
501
|
-
else
|
|
502
|
-
if @uri.host
|
|
503
|
-
out << "//"
|
|
504
|
-
if @uri.user
|
|
505
|
-
out << @uri.user
|
|
506
|
-
out << ":#{@uri.password}" if @uri.password
|
|
507
|
-
out << "@"
|
|
508
|
-
end
|
|
306
|
+
def cannot_have_credentials?
|
|
307
|
+
@record.host.nil? || @record.host == "" || @record.scheme == "file"
|
|
308
|
+
end
|
|
509
309
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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?
|
|
514
315
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
# when empty (matches `pathname` accessor).
|
|
518
|
-
path = "/" if path.empty? && special_scheme?
|
|
519
|
-
out << path
|
|
520
|
-
end
|
|
316
|
+
body = @record.path
|
|
317
|
+
return "null" if body.nil? || body.empty?
|
|
521
318
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
out
|
|
525
|
-
end
|
|
319
|
+
inner = URL.new(body)
|
|
320
|
+
return "null" unless %w[http https file].include?(inner.protocol.delete_suffix(":"))
|
|
526
321
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
322
|
+
inner.origin
|
|
323
|
+
rescue Bridge::TypeError
|
|
324
|
+
"null"
|
|
325
|
+
end
|
|
531
326
|
end
|
|
532
327
|
|
|
533
|
-
# `URLSearchParams` — query-string manipulation. Constructed from a
|
|
534
|
-
# raw string (`"a=1&b=2"`), an array of `[k, v]` pairs, or a Hash.
|
|
535
|
-
# Order is preserved. Values are stringified per spec.
|
|
536
328
|
class URLSearchParams
|
|
537
329
|
include Enumerable
|
|
538
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
|
+
|
|
539
336
|
def initialize(input = "", owner: nil)
|
|
540
337
|
@owner = owner
|
|
541
338
|
@pairs = parse(input)
|
|
542
339
|
end
|
|
543
340
|
|
|
544
341
|
def get(name)
|
|
545
|
-
|
|
342
|
+
key = stringify(name)
|
|
343
|
+
pair = @pairs.find { |k, _| k == key }
|
|
546
344
|
pair && pair[1]
|
|
547
345
|
end
|
|
548
346
|
|
|
549
347
|
def get_all(name)
|
|
550
|
-
|
|
348
|
+
key = stringify(name)
|
|
349
|
+
@pairs.select { |k, _| k == key }.map { |_, v| v }
|
|
551
350
|
end
|
|
552
351
|
|
|
553
352
|
alias getAll get_all
|
|
554
353
|
|
|
555
|
-
|
|
556
|
-
|
|
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 }
|
|
557
362
|
end
|
|
558
363
|
|
|
559
364
|
alias has? has
|
|
560
365
|
|
|
561
366
|
def set(name, value)
|
|
562
|
-
key = name
|
|
367
|
+
key = stringify(name)
|
|
563
368
|
first_done = false
|
|
564
369
|
@pairs = @pairs.reject do |k, _|
|
|
565
370
|
next false unless k == key
|
|
@@ -572,33 +377,44 @@ module Dommy
|
|
|
572
377
|
end
|
|
573
378
|
end
|
|
574
379
|
|
|
575
|
-
|
|
576
|
-
@pairs
|
|
380
|
+
val = stringify(value)
|
|
381
|
+
@pairs.map! { |pair| pair[0] == key ? [key, val] : pair }
|
|
382
|
+
@pairs << [key, val] unless first_done
|
|
577
383
|
notify
|
|
578
384
|
nil
|
|
579
385
|
end
|
|
580
386
|
|
|
581
387
|
def append(name, value)
|
|
582
|
-
@pairs << [name
|
|
388
|
+
@pairs << [stringify(name), stringify(value)]
|
|
583
389
|
notify
|
|
584
390
|
nil
|
|
585
391
|
end
|
|
586
392
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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)
|
|
590
398
|
@pairs.reject! { |k, _| k == key }
|
|
591
399
|
else
|
|
592
|
-
|
|
593
|
-
@pairs.reject! { |k, vv| k == key && vv ==
|
|
400
|
+
val = stringify(value)
|
|
401
|
+
@pairs.reject! { |k, vv| k == key && vv == val }
|
|
594
402
|
end
|
|
595
403
|
|
|
596
404
|
notify
|
|
597
405
|
nil
|
|
598
406
|
end
|
|
599
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).
|
|
600
413
|
def sort
|
|
601
|
-
@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)
|
|
602
418
|
notify
|
|
603
419
|
nil
|
|
604
420
|
end
|
|
@@ -636,7 +452,7 @@ module Dommy
|
|
|
636
452
|
@pairs.map { |k, v| "#{encode(k)}=#{encode(v)}" }.join("&")
|
|
637
453
|
end
|
|
638
454
|
|
|
639
|
-
def
|
|
455
|
+
def __internal_replace__(query_string)
|
|
640
456
|
@pairs = parse(query_string)
|
|
641
457
|
nil
|
|
642
458
|
end
|
|
@@ -648,6 +464,8 @@ module Dommy
|
|
|
648
464
|
end
|
|
649
465
|
end
|
|
650
466
|
|
|
467
|
+
include Bridge::Methods
|
|
468
|
+
js_methods %w[get getAll has set append delete sort toString forEach keys values entries]
|
|
651
469
|
def __js_call__(method, args)
|
|
652
470
|
case method
|
|
653
471
|
when "get"
|
|
@@ -655,19 +473,30 @@ module Dommy
|
|
|
655
473
|
when "getAll"
|
|
656
474
|
get_all(args[0])
|
|
657
475
|
when "has"
|
|
658
|
-
has(args[0])
|
|
476
|
+
value_given?(args) ? has(args[0], args[1]) : has(args[0])
|
|
659
477
|
when "set"
|
|
660
478
|
set(args[0], args[1])
|
|
661
479
|
when "append"
|
|
662
480
|
append(args[0], args[1])
|
|
663
481
|
when "delete"
|
|
664
|
-
delete(args[0], args[1])
|
|
482
|
+
value_given?(args) ? delete(args[0], args[1]) : delete(args[0])
|
|
665
483
|
when "sort"
|
|
666
484
|
sort
|
|
667
485
|
when "toString"
|
|
668
486
|
to_s
|
|
669
487
|
when "forEach"
|
|
670
|
-
|
|
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
|
|
671
500
|
when "keys"
|
|
672
501
|
keys
|
|
673
502
|
when "values"
|
|
@@ -679,6 +508,12 @@ module Dommy
|
|
|
679
508
|
|
|
680
509
|
private
|
|
681
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
|
+
|
|
682
517
|
def parse(input)
|
|
683
518
|
case input
|
|
684
519
|
when Array
|
|
@@ -686,22 +521,42 @@ module Dommy
|
|
|
686
521
|
when Hash
|
|
687
522
|
input.map { |k, v| [k.to_s, v.to_s] }
|
|
688
523
|
else
|
|
689
|
-
|
|
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?
|
|
690
530
|
return [] if s.empty?
|
|
691
531
|
|
|
692
|
-
|
|
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|
|
|
693
535
|
k, v = pair.split("=", 2)
|
|
694
|
-
[
|
|
536
|
+
[decode(k.to_s), decode(v.to_s)]
|
|
695
537
|
end
|
|
696
538
|
end
|
|
697
539
|
end
|
|
698
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 "*".)
|
|
699
548
|
def encode(str)
|
|
700
|
-
|
|
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
|
|
701
556
|
end
|
|
702
557
|
|
|
703
558
|
def notify
|
|
704
|
-
@owner&.
|
|
559
|
+
@owner&.__internal_notify_params_changed__
|
|
705
560
|
end
|
|
706
561
|
end
|
|
707
562
|
end
|