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.



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

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.



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

def search_params
  @search_params
end

Class Method Details

.__test_reset_blob_urls__Object

Test seam: drop all registered blob URLs.



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

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.



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

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.



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

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.



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

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



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

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.



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

#__internal_notify_params_changed__Object

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



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

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

#__js_call__(method, _args) ⇒ Object



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

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

#__js_get__(key) ⇒ Object



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

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
  else
    Bridge::ABSENT
  end
end

#__js_set__(key, value) ⇒ Object



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

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



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

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

#hash=(value) ⇒ Object



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

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



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

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

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

#host=(value) ⇒ Object



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

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



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

def hostname
  @record.host.to_s
end

#hostname=(value) ⇒ Object



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

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

#hrefObject



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

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

#href=(value) ⇒ Object



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

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.



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

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



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

def password
  @record.password
end

#password=(value) ⇒ Object



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

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



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

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

#pathname=(value) ⇒ Object



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

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



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

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

#port=(value) ⇒ Object



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

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



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

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

#protocol=(value) ⇒ Object



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

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



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

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

#search=(value) ⇒ Object



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

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



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

def to_json(*_args)
  href.inspect
end

#to_sObject



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

def to_s
  href
end

#usernameObject



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

def username
  @record.username
end

#username=(value) ⇒ Object



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

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