Class: Dommy::URL

Inherits:
Object
  • Object
show all
Defined in:
lib/dommy/url.rb

Overview

‘URL` — WHATWG-style URL parsing. Public API mirrors the JS class:

u = Dommy::URL.new("https://x.test/a/b?k=v#h")
u.protocol  # "https:"
u.host      # "x.test"
u.pathname  # "/a/b"
u.search    # "?k=v"
u.hash      # "#h"
u.search_params.get("k")  # "v"

Construction with a base URL is supported for relative inputs:

Dommy::URL.new("/a", "https://x.test").href
  # => "https://x.test/a"

Internally backed by Ruby’s URI library — good enough for the common test cases. Edge cases that URI rejects raise ‘DOMException::SyntaxError` (called `TypeError` in JS but Dommy uses the closest WHATWG name).

Constant Summary collapse

SPECIAL_SCHEMES =
%w[http https ws wss ftp file].freeze
TUPLE_ORIGIN_SCHEMES =

WHATWG: only http(s) / ws(s) / ftp produce a tuple origin. file / data / javascript / etc. resolve to ‘“null”`. `blob:` is handled specially (inner-URL origin).

%w[http https ws wss ftp].freeze
DEFAULT_PORTS =

Default ports per scheme (Ruby URI knows http/https/ftp; we add ws/wss).

{
  "http" => 80,
  "https" => 443,
  "ws" => 80,
  "wss" => 443,
  "ftp" => 21
}.freeze
UNSAFE_PATH_CHARS =

Chars that Ruby URI rejects in the path/query/fragment portion but WHATWG silently percent-encodes.

/[ "<>`{}|\\\^\[\]]/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(input, base = nil) ⇒ URL

Returns a new instance of URL.



68
69
70
71
72
# File 'lib/dommy/url.rb', line 68

def initialize(input, base = nil)
  raw = parse_with_base(input, base)
  @uri = raw
  @search_params = URLSearchParams.new(raw.query.to_s, owner: self)
end

Instance Attribute Details

#search_paramsObject (readonly)

Returns the value of attribute search_params.



66
67
68
# File 'lib/dommy/url.rb', line 66

def search_params
  @search_params
end

Class Method Details

.__reset_blob_urls__Object

Test seam: drop all registered blob URLs.



61
62
63
# File 'lib/dommy/url.rb', line 61

def __reset_blob_urls__
  @blob_urls.clear
end

.__resolve_blob_url__(url) ⇒ Object

Resolve a blob: URL back to its Blob, or nil if revoked / unknown. Internal — used by fetch / XHR implementations that load blob URLs.



56
57
58
# File 'lib/dommy/url.rb', line 56

def __resolve_blob_url__(url)
  @blob_urls[url.to_s]
end

.create_object_url(blob) ⇒ Object Also known as: createObjectURL

Create a unique blob: URL that resolves back to ‘blob` via `URL.resolve_blob_url(url)`. Returns nil for non-Blob input.



34
35
36
37
38
39
40
41
# File 'lib/dommy/url.rb', line 34

def create_object_url(blob)
  return nil unless blob.is_a?(Blob)

  id = "%032x" % rand(2 ** 128)
  url = "blob:dommy/#{id}"
  @blob_urls[url] = blob
  url
end

.revoke_object_url(url) ⇒ Object Also known as: revokeObjectURL

Revoke a previously-created blob URL. No-op for unknown URLs, matching the spec.



47
48
49
50
# File 'lib/dommy/url.rb', line 47

def revoke_object_url(url)
  @blob_urls.delete(url.to_s)
  nil
end

Instance Method Details

#__js_call__(method, _args) ⇒ Object



282
283
284
285
286
287
# File 'lib/dommy/url.rb', line 282

def __js_call__(method, _args)
  case method
  when "toString", "toJSON"
    href
  end
end

#__js_get__(key) ⇒ Object



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/dommy/url.rb', line 226

def __js_get__(key)
  case key
  when "href"
    href
  when "protocol"
    protocol
  when "host"
    host
  when "hostname"
    hostname
  when "port"
    port
  when "pathname"
    pathname
  when "search"
    search
  when "hash"
    hash
  when "origin"
    origin
  when "username"
    username
  when "password"
    password
  when "searchParams"
    @search_params
  end
end

#__js_set__(key, value) ⇒ Object



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/dommy/url.rb', line 255

def __js_set__(key, value)
  case key
  when "href"
    self.href = value
  when "protocol"
    self.protocol = value
  when "host"
    self.host = value
  when "hostname"
    self.hostname = value
  when "port"
    self.port = value
  when "pathname"
    self.pathname = value
  when "search"
    self.search = value
  when "hash"
    self.hash = value
  when "username"
    self.username = value
  when "password"
    self.password = value
  end

  nil
end

#__notify_params_changed__Object

Called by URLSearchParams when it mutates; we need to keep the underlying URI’s query string in sync so subsequent ‘href` is accurate.



292
293
294
# File 'lib/dommy/url.rb', line 292

def __notify_params_changed__
  sync_uri_query
end

#blob_inner_originObject



189
190
191
192
193
194
195
196
197
198
199
# File 'lib/dommy/url.rb', line 189

def blob_inner_origin
  # `blob:<inner-url>` — the body after `blob:` is itself a URL
  # whose origin we adopt. Anything that fails to parse falls
  # back to "null".
  opaque = @uri.respond_to?(:opaque) ? @uri.opaque : nil
  return "null" if opaque.nil? || opaque.empty?

  URL.new(opaque).origin
rescue DOMException::SyntaxError
  "null"
end

#hashObject



165
166
167
168
# File 'lib/dommy/url.rb', line 165

def hash
  f = @uri.fragment.to_s
  f.empty? ? "" : "##{f}"
end

#hash=(value) ⇒ Object



170
171
172
173
# File 'lib/dommy/url.rb', line 170

def hash=(value)
  f = value.to_s.sub(/^#/, "")
  @uri.fragment = f.empty? ? nil : f
end

#hostObject



94
95
96
97
98
99
100
101
# File 'lib/dommy/url.rb', line 94

def host
  port = @uri.port
  default = default_port_for(@uri.scheme.to_s.downcase)
  hostpart = @uri.host.to_s
  return hostpart if port.nil? || port == default

  "#{hostpart}:#{port}"
end

#host=(value) ⇒ Object



103
104
105
106
107
# File 'lib/dommy/url.rb', line 103

def host=(value)
  h, p = value.to_s.split(":", 2)
  @uri.host = h
  @uri.port = p.to_i if p
end

#hostnameObject



109
110
111
# File 'lib/dommy/url.rb', line 109

def hostname
  @uri.host.to_s
end

#hostname=(value) ⇒ Object



113
114
115
116
117
# File 'lib/dommy/url.rb', line 113

def hostname=(value)
  # WHATWG: a non-ASCII hostname assigned through the setter is
  # Punycode-encoded before storage (matches `new URL("...")`).
  @uri.host = Internal::IDNA.to_ascii(value.to_s)
end

#hrefObject



74
75
76
# File 'lib/dommy/url.rb', line 74

def href
  build_href
end

#href=(value) ⇒ Object



78
79
80
81
82
83
# File 'lib/dommy/url.rb', line 78

def href=(value)
  raw = parse_with_base(value.to_s, nil)
  @uri = raw
  @search_params.__replace__(raw.query.to_s)
  build_href
end

#originObject

WHATWG URL §origin. Tuple origins for http(s) / ws(s) / ftp; ‘“null”` for file/data/javascript/etc. Blob URLs unwrap their inner URL recursively.



178
179
180
181
182
183
184
185
186
187
# File 'lib/dommy/url.rb', line 178

def origin
  scheme = @uri.scheme.to_s.downcase
  return blob_inner_origin if scheme == "blob"
  return "null" unless TUPLE_ORIGIN_SCHEMES.include?(scheme)
  return "null" unless @uri.host

  default = default_port_for(scheme)
  port_part = (@uri.port && @uri.port != default) ? ":#{@uri.port}" : ""
  "#{scheme}://#{@uri.host}#{port_part}"
end

#passwordObject



209
210
211
# File 'lib/dommy/url.rb', line 209

def password
  @uri.password.to_s
end

#password=(value) ⇒ Object



213
214
215
# File 'lib/dommy/url.rb', line 213

def password=(value)
  @uri.password = value.to_s.empty? ? nil : value.to_s
end

#pathnameObject

WHATWG: for opaque-body schemes (javascript:, mailto:, data:, tel:, blob:) the body sits in ‘URI`’s ‘opaque` slot, not `path`. For special schemes (http/https/ws/wss/ftp), an empty path is canonicalized to `“/”`.



134
135
136
137
138
139
140
141
142
# File 'lib/dommy/url.rb', line 134

def pathname
  opaque = @uri.respond_to?(:opaque) ? @uri.opaque : nil
  return opaque.to_s if opaque

  path = @uri.path.to_s
  return "/" if path.empty? && special_scheme?

  path
end

#pathname=(value) ⇒ Object



144
145
146
147
148
# File 'lib/dommy/url.rb', line 144

def pathname=(value)
  v = value.to_s
  v = "/#{v}" if !v.start_with?("/") && !v.empty?
  @uri.path = v
end

#portObject



119
120
121
122
123
124
# File 'lib/dommy/url.rb', line 119

def port
  default = default_port_for(@uri.scheme.to_s.downcase)
  return "" if @uri.port.nil? || @uri.port == default

  @uri.port.to_s
end

#port=(value) ⇒ Object



126
127
128
# File 'lib/dommy/url.rb', line 126

def port=(value)
  @uri.port = value.to_s.empty? ? nil : value.to_i
end

#protocolObject



85
86
87
# File 'lib/dommy/url.rb', line 85

def protocol
  @uri.scheme ? "#{@uri.scheme}:" : ""
end

#protocol=(value) ⇒ Object



89
90
91
92
# File 'lib/dommy/url.rb', line 89

def protocol=(value)
  s = value.to_s.sub(/:$/, "")
  @uri.scheme = s
end

#searchObject

WHATWG: ‘url.search` is the raw query string (with `?` prefix), preserving percent-encoding and stray `?` characters as parsed. `url.searchParams.toString()` re-serializes via the form-encoded contract (`+` for space, etc.) — distinct from `url.search`.



154
155
156
157
# File 'lib/dommy/url.rb', line 154

def search
  q = @uri.query
  q.nil? || q.empty? ? "" : "?#{q}"
end

#search=(value) ⇒ Object



159
160
161
162
163
# File 'lib/dommy/url.rb', line 159

def search=(value)
  q = value.to_s.sub(/^\?/, "")
  @uri.query = q.empty? ? nil : q
  @search_params.__replace__(q)
end

#to_json(*_args) ⇒ Object



221
222
223
224
# File 'lib/dommy/url.rb', line 221

def to_json(*_args)
  # match JSON.stringify(url) -> "\"<href>\""
  href.inspect
end

#to_sObject



217
218
219
# File 'lib/dommy/url.rb', line 217

def to_s
  href
end

#usernameObject



201
202
203
# File 'lib/dommy/url.rb', line 201

def username
  @uri.user.to_s
end

#username=(value) ⇒ Object



205
206
207
# File 'lib/dommy/url.rb', line 205

def username=(value)
  @uri.user = value.to_s.empty? ? nil : value.to_s
end