Class: Dommy::URL
- Inherits:
-
Object
- Object
- Dommy::URL
- 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
-
#search_params ⇒ Object
readonly
Returns the value of attribute search_params.
Class Method Summary collapse
-
.__test_reset_blob_urls__ ⇒ Object
Test seam: drop all registered blob URLs.
-
.__test_resolve_blob_url__(url) ⇒ Object
Resolve a blob: URL back to its Blob, or nil if revoked / unknown.
-
.can_parse(input, base = nil) ⇒ Object
WHATWG URL Standard — ‘URL.canParse(input, base)`.
-
.create_object_url(blob) ⇒ Object
(also: createObjectURL)
Create a unique blob: URL that resolves back to ‘blob` via `URL.test_resolve_blob_url(url)`.
-
.parse(input, base = nil) ⇒ Object
WHATWG URL Standard — ‘URL.parse(input, base)` is the non-throwing static factory.
-
.revoke_object_url(url) ⇒ Object
(also: revokeObjectURL)
Revoke a previously-created blob URL.
Instance Method Summary collapse
-
#__internal_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.
- #__js_call__(method, _args) ⇒ Object
- #__js_get__(key) ⇒ Object
- #__js_set__(key, value) ⇒ Object
- #blob_inner_origin ⇒ Object
- #hash ⇒ Object
- #hash=(value) ⇒ Object
- #host ⇒ Object
- #host=(value) ⇒ Object
- #hostname ⇒ Object
- #hostname=(value) ⇒ Object
- #href ⇒ Object
- #href=(value) ⇒ Object
-
#initialize(input, base = nil) ⇒ URL
constructor
A new instance of URL.
-
#origin ⇒ Object
WHATWG URL §origin.
- #password ⇒ Object
- #password=(value) ⇒ Object
-
#pathname ⇒ Object
WHATWG: for opaque-body schemes (javascript:, mailto:, data:, tel:, blob:) the body sits in ‘URI`’s ‘opaque` slot, not `path`.
- #pathname=(value) ⇒ Object
- #port ⇒ Object
- #port=(value) ⇒ Object
- #protocol ⇒ Object
- #protocol=(value) ⇒ Object
-
#search ⇒ Object
WHATWG: ‘url.search` is the raw query string (with `?` prefix), preserving percent-encoding and stray `?` characters as parsed.
- #search=(value) ⇒ Object
- #to_json(*_args) ⇒ Object
- #to_s ⇒ Object
- #username ⇒ Object
- #username=(value) ⇒ Object
Constructor Details
#initialize(input, base = nil) ⇒ URL
Returns a new instance of URL.
85 86 87 88 89 |
# File 'lib/dommy/url.rb', line 85 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_params ⇒ Object (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 `SyntaxError` 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 DOMException::SyntaxError 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; we need to keep the underlying URI’s query string in sync so subsequent ‘href` is accurate.
309 310 311 |
# File 'lib/dommy/url.rb', line 309 def __internal_notify_params_changed__ sync_uri_query end |
#__js_call__(method, _args) ⇒ Object
299 300 301 302 303 304 |
# File 'lib/dommy/url.rb', line 299 def __js_call__(method, _args) case method when "toString", "toJSON" href end end |
#__js_get__(key) ⇒ Object
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/dommy/url.rb', line 243 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
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/dommy/url.rb', line 272 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 |
#blob_inner_origin ⇒ Object
206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/dommy/url.rb', line 206 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 |
#hash ⇒ Object
182 183 184 185 |
# File 'lib/dommy/url.rb', line 182 def hash f = @uri.fragment.to_s f.empty? ? "" : "##{f}" end |
#hash=(value) ⇒ Object
187 188 189 190 |
# File 'lib/dommy/url.rb', line 187 def hash=(value) f = value.to_s.sub(/^#/, "") @uri.fragment = f.empty? ? nil : f end |
#host ⇒ Object
111 112 113 114 115 116 117 118 |
# File 'lib/dommy/url.rb', line 111 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
120 121 122 123 124 |
# File 'lib/dommy/url.rb', line 120 def host=(value) h, p = value.to_s.split(":", 2) @uri.host = h @uri.port = p.to_i if p end |
#hostname ⇒ Object
126 127 128 |
# File 'lib/dommy/url.rb', line 126 def hostname @uri.host.to_s end |
#hostname=(value) ⇒ Object
130 131 132 133 134 |
# File 'lib/dommy/url.rb', line 130 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 |
#href ⇒ Object
91 92 93 |
# File 'lib/dommy/url.rb', line 91 def href build_href end |
#href=(value) ⇒ Object
95 96 97 98 99 100 |
# File 'lib/dommy/url.rb', line 95 def href=(value) raw = parse_with_base(value.to_s, nil) @uri = raw @search_params.__internal_replace__(raw.query.to_s) build_href end |
#origin ⇒ Object
WHATWG URL §origin. Tuple origins for http(s) / ws(s) / ftp; ‘“null”` for file/data/javascript/etc. Blob URLs unwrap their inner URL recursively.
195 196 197 198 199 200 201 202 203 204 |
# File 'lib/dommy/url.rb', line 195 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 |
#password ⇒ Object
226 227 228 |
# File 'lib/dommy/url.rb', line 226 def password @uri.password.to_s end |
#password=(value) ⇒ Object
230 231 232 |
# File 'lib/dommy/url.rb', line 230 def password=(value) @uri.password = value.to_s.empty? ? nil : value.to_s end |
#pathname ⇒ Object
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 `“/”`.
151 152 153 154 155 156 157 158 159 |
# File 'lib/dommy/url.rb', line 151 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
161 162 163 164 165 |
# File 'lib/dommy/url.rb', line 161 def pathname=(value) v = value.to_s v = "/#{v}" if !v.start_with?("/") && !v.empty? @uri.path = v end |
#port ⇒ Object
136 137 138 139 140 141 |
# File 'lib/dommy/url.rb', line 136 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
143 144 145 |
# File 'lib/dommy/url.rb', line 143 def port=(value) @uri.port = value.to_s.empty? ? nil : value.to_i end |
#protocol ⇒ Object
102 103 104 |
# File 'lib/dommy/url.rb', line 102 def protocol @uri.scheme ? "#{@uri.scheme}:" : "" end |
#protocol=(value) ⇒ Object
106 107 108 109 |
# File 'lib/dommy/url.rb', line 106 def protocol=(value) s = value.to_s.sub(/:$/, "") @uri.scheme = s end |
#search ⇒ Object
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`.
171 172 173 174 |
# File 'lib/dommy/url.rb', line 171 def search q = @uri.query q.nil? || q.empty? ? "" : "?#{q}" end |
#search=(value) ⇒ Object
176 177 178 179 180 |
# File 'lib/dommy/url.rb', line 176 def search=(value) q = value.to_s.sub(/^\?/, "") @uri.query = q.empty? ? nil : q @search_params.__internal_replace__(q) end |
#to_json(*_args) ⇒ Object
238 239 240 241 |
# File 'lib/dommy/url.rb', line 238 def to_json(*_args) # match JSON.stringify(url) -> "\"<href>\"" href.inspect end |
#to_s ⇒ Object
234 235 236 |
# File 'lib/dommy/url.rb', line 234 def to_s href end |
#username ⇒ Object
218 219 220 |
# File 'lib/dommy/url.rb', line 218 def username @uri.user.to_s end |
#username=(value) ⇒ Object
222 223 224 |
# File 'lib/dommy/url.rb', line 222 def username=(value) @uri.user = value.to_s.empty? ? nil : value.to_s end |