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
-
.__reset_blob_urls__ ⇒ Object
Test seam: drop all registered blob URLs.
-
.__resolve_blob_url__(url) ⇒ Object
Resolve a blob: URL back to its Blob, or nil if revoked / unknown.
-
.create_object_url(blob) ⇒ Object
(also: createObjectURL)
Create a unique blob: URL that resolves back to ‘blob` via `URL.resolve_blob_url(url)`.
-
.revoke_object_url(url) ⇒ Object
(also: revokeObjectURL)
Revoke a previously-created blob URL.
Instance Method Summary collapse
- #__js_call__(method, _args) ⇒ Object
- #__js_get__(key) ⇒ Object
- #__js_set__(key, value) ⇒ Object
-
#__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.
- #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.
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_params ⇒ Object (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_origin ⇒ Object
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 |
#hash ⇒ Object
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 |
#host ⇒ Object
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 |
#hostname ⇒ Object
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 |
#href ⇒ Object
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 |
#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.
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 |
#password ⇒ Object
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 |
#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 `“/”`.
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 |
#port ⇒ Object
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 |
#protocol ⇒ Object
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 |
#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`.
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_s ⇒ Object
217 218 219 |
# File 'lib/dommy/url.rb', line 217 def to_s href end |
#username ⇒ Object
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 |