Class: Dommy::URL

Inherits:
Object
  • Object
show all
Includes:
Bridge::Methods
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 a WHATWG basic URL parser (‘Internal::UrlParser`). A parse failure in the constructor or the `href` setter raises `Bridge::TypeError` — matching the URL Standard, which throws a JS `TypeError` (not a DOMException) there.

Constant Summary collapse

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.

%w[http https ws wss ftp].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Bridge::Methods

included

Constructor Details

#initialize(input, base = nil) ⇒ URL

Returns a new instance of URL.



86
87
88
89
90
91
92
93
94
95
96
# File 'lib/dommy/url.rb', line 86

def initialize(input, base = nil)
  # An explicit JS `undefined` base means "no base" (WebIDL optional arg),
  # distinct from a string base. (JS null already arrives as nil.)
  base = nil if base.equal?(Bridge::UNDEFINED)
  base_str = base.is_a?(URL) ? base.href : base
  @record = Internal::UrlParser.parse(input.to_s, base_str)
  @search_params = URLSearchParams.new(@record.query.to_s, owner: self)
rescue Internal::UrlParser::Failure => e
  # WHATWG: the URL constructor throws TypeError on a parse failure.
  raise Bridge::TypeError, "Invalid URL: #{e.message}"
end

Instance Attribute Details

#search_paramsObject (readonly)

Returns the value of attribute search_params.



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

def search_params
  @search_params
end

Class Method Details

.__test_reset_blob_urls__Object

Test seam: drop all registered blob URLs.



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

def __test_reset_blob_urls__
  @blob_urls.clear
end

.__test_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.



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

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

.can_parse(input, base = nil) ⇒ Object

WHATWG URL Standard — ‘URL.canParse(input, base)`. Boolean counterpart to `parse`: lets callers peek at validity without rescuing an exception or holding a URL reference.



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

def can_parse(input, base = nil)
  !parse(input, base).nil?
end

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

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



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

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

.parse(input, base = nil) ⇒ Object

WHATWG URL Standard — ‘URL.parse(input, base)` is the non-throwing static factory. Returns a URL on success, `nil` on parse failure. The constructor (`new URL(…)`) raises `TypeError` for the same failure case.



70
71
72
73
74
# File 'lib/dommy/url.rb', line 70

def parse(input, base = nil)
  new(input, base)
rescue Bridge::TypeError
  nil
end

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

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



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

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

Instance Method Details

#__internal_notify_params_changed__Object

Called by URLSearchParams when it mutates; keep the record’s query in sync.



295
296
297
298
# File 'lib/dommy/url.rb', line 295

def __internal_notify_params_changed__
  q = @search_params.to_s
  @record.query = q.empty? ? nil : q
end

#__js_call__(method, _args) ⇒ Object



287
288
289
290
291
292
# File 'lib/dommy/url.rb', line 287

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

#__js_get__(key) ⇒ Object



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/dommy/url.rb', line 249

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

#__js_set__(key, value) ⇒ Object



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/dommy/url.rb', line 266

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

  nil
end

#hashObject



192
193
194
195
# File 'lib/dommy/url.rb', line 192

def hash
  f = @record.fragment
  f.nil? || f.empty? ? "" : "##{f}"
end

#hash=(value) ⇒ Object



197
198
199
200
201
202
203
204
205
# File 'lib/dommy/url.rb', line 197

def hash=(value)
  v = value.to_s.sub(/\A#/, "")
  if v.empty?
    @record.fragment = nil
  else
    set = Internal::UrlParser.method(:fragment_set?)
    @record.fragment = v.each_char.map { |ch| Internal::UrlParser.pe(ch, set) }.join
  end
end

#hostObject



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

def host
  return "" if @record.host.nil?

  @record.port ? "#{@record.host}:#{@record.port}" : @record.host
end

#host=(value) ⇒ Object



126
127
128
129
130
131
132
133
134
# File 'lib/dommy/url.rb', line 126

def host=(value)
  h, sep, p = value.to_s.partition(":")
  begin
    @record.host = Internal::UrlParser.parse_host(h, @record.special?)
  rescue Internal::UrlParser::Failure
    return
  end
  self.port = p unless sep.empty?
end

#hostnameObject



136
137
138
# File 'lib/dommy/url.rb', line 136

def hostname
  @record.host.to_s
end

#hostname=(value) ⇒ Object



140
141
142
143
144
# File 'lib/dommy/url.rb', line 140

def hostname=(value)
  @record.host = Internal::UrlParser.parse_host(value.to_s, @record.special?)
rescue Internal::UrlParser::Failure
  nil
end

#hrefObject



98
99
100
# File 'lib/dommy/url.rb', line 98

def href
  Internal::UrlParser.serialize(@record)
end

#href=(value) ⇒ Object



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

def href=(value)
  @record = Internal::UrlParser.parse(value.to_s, nil)
  @search_params.__internal_replace__(@record.query.to_s)
  href
rescue Internal::UrlParser::Failure => e
  # WHATWG: the href setter throws TypeError on a parse failure.
  raise Bridge::TypeError, "Invalid URL: #{e.message}"
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.



209
210
211
212
213
214
215
216
217
# File 'lib/dommy/url.rb', line 209

def origin
  scheme = @record.scheme
  return blob_inner_origin if scheme == "blob"
  return "null" unless TUPLE_ORIGIN_SCHEMES.include?(scheme)
  return "null" if @record.host.nil?

  port_part = @record.port ? ":#{@record.port}" : ""
  "#{scheme}://#{@record.host}#{port_part}"
end

#passwordObject



230
231
232
# File 'lib/dommy/url.rb', line 230

def password
  @record.password
end

#password=(value) ⇒ Object



234
235
236
237
238
239
# File 'lib/dommy/url.rb', line 234

def password=(value)
  return if cannot_have_credentials?

  set = Internal::UrlParser.method(:userinfo_set?)
  @record.password = value.to_s.each_char.map { |ch| Internal::UrlParser.pe(ch, set) }.join
end

#pathnameObject



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

def pathname
  Internal::UrlParser.serialize_path(@record)
end

#pathname=(value) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
# File 'lib/dommy/url.rb', line 164

def pathname=(value)
  return if @record.opaque_path?

  v = value.to_s
  v = v.tr("\\", "/") if @record.special?
  segs = v.split("/", -1)
  segs.shift if segs.first == ""
  set = Internal::UrlParser.method(:path_set?)
  @record.path = segs.map { |s| s.each_char.map { |ch| Internal::UrlParser.pe(ch, set) }.join }
  @record.path = [""] if @record.path.empty? && @record.special?
end

#portObject



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

def port
  @record.port.nil? ? "" : @record.port.to_s
end

#port=(value) ⇒ Object



150
151
152
153
154
155
156
157
158
# File 'lib/dommy/url.rb', line 150

def port=(value)
  v = value.to_s
  if v.empty?
    @record.port = nil
  elsif v.match?(/\A[0-9]+\z/)
    n = v.to_i
    @record.port = (n == @record.default_port ? nil : n) if n <= 65_535
  end
end

#protocolObject



111
112
113
# File 'lib/dommy/url.rb', line 111

def protocol
  "#{@record.scheme}:"
end

#protocol=(value) ⇒ Object



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

def protocol=(value)
  s = value.to_s.sub(/:\z/, "").downcase
  @record.scheme = s if s.match?(/\A[a-z][a-z0-9+\-.]*\z/)
end

#searchObject



176
177
178
179
# File 'lib/dommy/url.rb', line 176

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

#search=(value) ⇒ Object



181
182
183
184
185
186
187
188
189
190
# File 'lib/dommy/url.rb', line 181

def search=(value)
  v = value.to_s.sub(/\A\?/, "")
  if v.empty?
    @record.query = nil
  else
    set = @record.special? ? Internal::UrlParser.method(:special_query_set?) : Internal::UrlParser.method(:query_set?)
    @record.query = v.each_char.map { |ch| Internal::UrlParser.pe(ch, set) }.join
  end
  @search_params.__internal_replace__(@record.query.to_s)
end

#to_json(*_args) ⇒ Object



245
246
247
# File 'lib/dommy/url.rb', line 245

def to_json(*_args)
  href.inspect
end

#to_sObject



241
242
243
# File 'lib/dommy/url.rb', line 241

def to_s
  href
end

#usernameObject



219
220
221
# File 'lib/dommy/url.rb', line 219

def username
  @record.username
end

#username=(value) ⇒ Object



223
224
225
226
227
228
# File 'lib/dommy/url.rb', line 223

def username=(value)
  return if cannot_have_credentials?

  set = Internal::UrlParser.method(:userinfo_set?)
  @record.username = value.to_s.each_char.map { |ch| Internal::UrlParser.pe(ch, set) }.join
end