mirror of
https://github.com/denoland/deno.git
synced 2025-01-05 05:49:20 -05:00
Add URL implementation (#1359)
This commit is contained in:
parent
1e54f1d473
commit
91a41ca124
5 changed files with 397 additions and 0 deletions
1
BUILD.gn
1
BUILD.gn
|
@ -100,6 +100,7 @@ ts_sources = [
|
||||||
"js/timers.ts",
|
"js/timers.ts",
|
||||||
"js/truncate.ts",
|
"js/truncate.ts",
|
||||||
"js/types.ts",
|
"js/types.ts",
|
||||||
|
"js/url.ts",
|
||||||
"js/url_search_params.ts",
|
"js/url_search_params.ts",
|
||||||
"js/util.ts",
|
"js/util.ts",
|
||||||
"js/write_file.ts",
|
"js/write_file.ts",
|
||||||
|
|
|
@ -16,6 +16,7 @@ import * as fetchTypes from "./fetch";
|
||||||
import * as headers from "./headers";
|
import * as headers from "./headers";
|
||||||
import * as textEncoding from "./text_encoding";
|
import * as textEncoding from "./text_encoding";
|
||||||
import * as timers from "./timers";
|
import * as timers from "./timers";
|
||||||
|
import * as url from "./url";
|
||||||
import * as urlSearchParams from "./url_search_params";
|
import * as urlSearchParams from "./url_search_params";
|
||||||
|
|
||||||
// These imports are not exposed and therefore are fine to just import the
|
// These imports are not exposed and therefore are fine to just import the
|
||||||
|
@ -56,6 +57,8 @@ window.Blob = blob.DenoBlob;
|
||||||
export type Blob = blob.DenoBlob;
|
export type Blob = blob.DenoBlob;
|
||||||
window.File = file.DenoFile;
|
window.File = file.DenoFile;
|
||||||
export type File = file.DenoFile;
|
export type File = file.DenoFile;
|
||||||
|
window.URL = url.URL;
|
||||||
|
export type URL = url.URL;
|
||||||
window.URLSearchParams = urlSearchParams.URLSearchParams;
|
window.URLSearchParams = urlSearchParams.URLSearchParams;
|
||||||
export type URLSearchParams = urlSearchParams.URLSearchParams;
|
export type URLSearchParams = urlSearchParams.URLSearchParams;
|
||||||
|
|
||||||
|
|
|
@ -34,5 +34,6 @@ import "./symlink_test.ts";
|
||||||
import "./text_encoding_test.ts";
|
import "./text_encoding_test.ts";
|
||||||
import "./timers_test.ts";
|
import "./timers_test.ts";
|
||||||
import "./truncate_test.ts";
|
import "./truncate_test.ts";
|
||||||
|
import "./url_test.ts";
|
||||||
import "./url_search_params_test.ts";
|
import "./url_search_params_test.ts";
|
||||||
import "./write_file_test.ts";
|
import "./write_file_test.ts";
|
||||||
|
|
261
js/url.ts
Normal file
261
js/url.ts
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
|
||||||
|
import * as urlSearchParams from "./url_search_params";
|
||||||
|
|
||||||
|
interface URLParts {
|
||||||
|
protocol: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
hostname: string;
|
||||||
|
port: string;
|
||||||
|
path: string;
|
||||||
|
query: string;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patterns = {
|
||||||
|
protocol: "(?:([^:/?#]+):)",
|
||||||
|
authority: "(?://([^/?#]*))",
|
||||||
|
path: "([^?#]*)",
|
||||||
|
query: "(\\?[^#]*)",
|
||||||
|
hash: "(#.*)",
|
||||||
|
|
||||||
|
authentication: "(?:([^:]*)(?::([^@]*))?@)",
|
||||||
|
hostname: "([^:]+)",
|
||||||
|
port: "(?::(\\d+))"
|
||||||
|
};
|
||||||
|
|
||||||
|
const urlRegExp = new RegExp(
|
||||||
|
`^${patterns.protocol}?${patterns.authority}?${patterns.path}${
|
||||||
|
patterns.query
|
||||||
|
}?${patterns.hash}?`
|
||||||
|
);
|
||||||
|
|
||||||
|
const authorityRegExp = new RegExp(
|
||||||
|
`^${patterns.authentication}?${patterns.hostname}${patterns.port}?$`
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchParamsMethods: Array<keyof urlSearchParams.URLSearchParams> = [
|
||||||
|
"append",
|
||||||
|
"delete",
|
||||||
|
"set"
|
||||||
|
];
|
||||||
|
|
||||||
|
function parse(url: string): URLParts | undefined {
|
||||||
|
const urlMatch = urlRegExp.exec(url);
|
||||||
|
if (urlMatch) {
|
||||||
|
const [, , authority] = urlMatch;
|
||||||
|
const authorityMatch = authority
|
||||||
|
? authorityRegExp.exec(authority)
|
||||||
|
: [null, null, null, null, null];
|
||||||
|
if (authorityMatch) {
|
||||||
|
return {
|
||||||
|
protocol: urlMatch[1] || "",
|
||||||
|
username: authorityMatch[1] || "",
|
||||||
|
password: authorityMatch[2] || "",
|
||||||
|
hostname: authorityMatch[3] || "",
|
||||||
|
port: authorityMatch[4] || "",
|
||||||
|
path: urlMatch[3] || "",
|
||||||
|
query: urlMatch[4] || "",
|
||||||
|
hash: urlMatch[5] || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class URL {
|
||||||
|
private _parts: URLParts;
|
||||||
|
private _searchParams!: urlSearchParams.URLSearchParams;
|
||||||
|
|
||||||
|
private _updateSearchParams() {
|
||||||
|
const searchParams = new urlSearchParams.URLSearchParams(this.search);
|
||||||
|
|
||||||
|
for (const methodName of searchParamsMethods) {
|
||||||
|
// tslint:disable:no-any
|
||||||
|
const method: (...args: any[]) => any = searchParams[methodName];
|
||||||
|
searchParams[methodName] = (...args: any[]) => {
|
||||||
|
method.apply(searchParams, args);
|
||||||
|
this.search = searchParams.toString();
|
||||||
|
};
|
||||||
|
// tslint:enable
|
||||||
|
}
|
||||||
|
this._searchParams = searchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hash(): string {
|
||||||
|
return this._parts.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
set hash(value: string) {
|
||||||
|
value = unescape(String(value));
|
||||||
|
if (!value) {
|
||||||
|
this._parts.hash = "";
|
||||||
|
} else {
|
||||||
|
if (value.charAt(0) !== "#") {
|
||||||
|
value = `#${value}`;
|
||||||
|
}
|
||||||
|
// hashes can contain % and # unescaped
|
||||||
|
this._parts.hash = escape(value)
|
||||||
|
.replace(/%25/g, "%")
|
||||||
|
.replace(/%23/g, "#");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get host(): string {
|
||||||
|
return `${this.hostname}${this.port ? `:${this.port}` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
set host(value: string) {
|
||||||
|
value = String(value);
|
||||||
|
const url = new URL(`http://${value}`);
|
||||||
|
this._parts.hostname = url.hostname;
|
||||||
|
this._parts.port = url.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hostname(): string {
|
||||||
|
return this._parts.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
set hostname(value: string) {
|
||||||
|
value = String(value);
|
||||||
|
this._parts.hostname = encodeURIComponent(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get href(): string {
|
||||||
|
const authentication =
|
||||||
|
this.username || this.password
|
||||||
|
? `${this.username}${this.password ? ":" + this.password : ""}@`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `${this.protocol}//${authentication}${this.host}${this.pathname}${
|
||||||
|
this.search
|
||||||
|
}${this.hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
set href(value: string) {
|
||||||
|
value = String(value);
|
||||||
|
if (value !== this.href) {
|
||||||
|
const url = new URL(value);
|
||||||
|
this._parts = { ...url._parts };
|
||||||
|
this._updateSearchParams();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get origin(): string {
|
||||||
|
return `${this.protocol}//${this.host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get password(): string {
|
||||||
|
return this._parts.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
set password(value: string) {
|
||||||
|
value = String(value);
|
||||||
|
this._parts.password = encodeURIComponent(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get pathname(): string {
|
||||||
|
return this._parts.path ? this._parts.path : "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
set pathname(value: string) {
|
||||||
|
value = unescape(String(value));
|
||||||
|
if (!value || value.charAt(0) !== "/") {
|
||||||
|
value = `/${value}`;
|
||||||
|
}
|
||||||
|
// paths can contain % unescaped
|
||||||
|
this._parts.path = escape(value).replace(/%25/g, "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
get port(): string {
|
||||||
|
return this._parts.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
set port(value: string) {
|
||||||
|
const port = parseInt(String(value), 10);
|
||||||
|
this._parts.port = isNaN(port)
|
||||||
|
? ""
|
||||||
|
: Math.max(0, port % 2 ** 16).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
get protocol(): string {
|
||||||
|
return `${this._parts.protocol}:`;
|
||||||
|
}
|
||||||
|
|
||||||
|
set protocol(value: string) {
|
||||||
|
value = String(value);
|
||||||
|
if (value) {
|
||||||
|
if (value.charAt(value.length - 1) === ":") {
|
||||||
|
value = value.slice(0, -1);
|
||||||
|
}
|
||||||
|
this._parts.protocol = encodeURIComponent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get search(): string {
|
||||||
|
return this._parts.query;
|
||||||
|
}
|
||||||
|
|
||||||
|
set search(value: string) {
|
||||||
|
value = String(value);
|
||||||
|
if (value.charAt(0) !== "?") {
|
||||||
|
value = `?${value}`;
|
||||||
|
}
|
||||||
|
this._parts.query = value;
|
||||||
|
this._updateSearchParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
get username(): string {
|
||||||
|
return this._parts.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
set username(value: string) {
|
||||||
|
value = String(value);
|
||||||
|
this._parts.username = encodeURIComponent(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get searchParams(): urlSearchParams.URLSearchParams {
|
||||||
|
return this._searchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(url: string, base?: string | URL) {
|
||||||
|
let baseParts: URLParts | undefined;
|
||||||
|
if (base) {
|
||||||
|
baseParts = typeof base === "string" ? parse(base) : base._parts;
|
||||||
|
if (!baseParts) {
|
||||||
|
throw new TypeError("Invalid base URL.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlParts = parse(url);
|
||||||
|
if (!urlParts) {
|
||||||
|
throw new TypeError("Invalid URL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlParts.protocol) {
|
||||||
|
this._parts = urlParts;
|
||||||
|
} else if (baseParts) {
|
||||||
|
this._parts = {
|
||||||
|
protocol: baseParts.protocol,
|
||||||
|
username: baseParts.username,
|
||||||
|
password: baseParts.password,
|
||||||
|
hostname: baseParts.hostname,
|
||||||
|
port: baseParts.port,
|
||||||
|
path: urlParts.path || baseParts.path,
|
||||||
|
query: urlParts.query || baseParts.query,
|
||||||
|
hash: urlParts.hash
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new TypeError("URL requires a base URL.");
|
||||||
|
}
|
||||||
|
this._updateSearchParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): string {
|
||||||
|
return this.href;
|
||||||
|
}
|
||||||
|
}
|
131
js/url_test.ts
Normal file
131
js/url_test.ts
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
|
||||||
|
import { test, assert, assertEqual } from "./test_util.ts";
|
||||||
|
|
||||||
|
test(function urlParsing() {
|
||||||
|
const url = new URL(
|
||||||
|
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
|
||||||
|
);
|
||||||
|
assertEqual(url.hash, "#qat");
|
||||||
|
assertEqual(url.host, "baz.qat:8000");
|
||||||
|
assertEqual(url.hostname, "baz.qat");
|
||||||
|
assertEqual(
|
||||||
|
url.href,
|
||||||
|
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
|
||||||
|
);
|
||||||
|
assertEqual(url.origin, "https://baz.qat:8000");
|
||||||
|
assertEqual(url.password, "bar");
|
||||||
|
assertEqual(url.pathname, "/qux/quux");
|
||||||
|
assertEqual(url.port, "8000");
|
||||||
|
assertEqual(url.protocol, "https:");
|
||||||
|
assertEqual(url.search, "?foo=bar&baz=12");
|
||||||
|
assertEqual(url.searchParams.getAll("foo"), ["bar"]);
|
||||||
|
assertEqual(url.searchParams.getAll("baz"), ["12"]);
|
||||||
|
assertEqual(url.username, "foo");
|
||||||
|
assertEqual(
|
||||||
|
String(url),
|
||||||
|
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
|
||||||
|
);
|
||||||
|
assertEqual(
|
||||||
|
JSON.stringify({ key: url }),
|
||||||
|
`{"key":"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(function urlModifications() {
|
||||||
|
const url = new URL(
|
||||||
|
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
|
||||||
|
);
|
||||||
|
url.hash = "";
|
||||||
|
assertEqual(url.href, "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12");
|
||||||
|
url.host = "qat.baz:8080";
|
||||||
|
assertEqual(url.href, "https://foo:bar@qat.baz:8080/qux/quux?foo=bar&baz=12");
|
||||||
|
url.hostname = "foo.bar";
|
||||||
|
assertEqual(url.href, "https://foo:bar@foo.bar:8080/qux/quux?foo=bar&baz=12");
|
||||||
|
url.password = "qux";
|
||||||
|
assertEqual(url.href, "https://foo:qux@foo.bar:8080/qux/quux?foo=bar&baz=12");
|
||||||
|
url.pathname = "/foo/bar%qat";
|
||||||
|
assertEqual(
|
||||||
|
url.href,
|
||||||
|
"https://foo:qux@foo.bar:8080/foo/bar%qat?foo=bar&baz=12"
|
||||||
|
);
|
||||||
|
url.port = "";
|
||||||
|
assertEqual(url.href, "https://foo:qux@foo.bar/foo/bar%qat?foo=bar&baz=12");
|
||||||
|
url.protocol = "http:";
|
||||||
|
assertEqual(url.href, "http://foo:qux@foo.bar/foo/bar%qat?foo=bar&baz=12");
|
||||||
|
url.search = "?foo=bar&foo=baz";
|
||||||
|
assertEqual(url.href, "http://foo:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz");
|
||||||
|
assertEqual(url.searchParams.getAll("foo"), ["bar", "baz"]);
|
||||||
|
url.username = "foo@bar";
|
||||||
|
assertEqual(
|
||||||
|
url.href,
|
||||||
|
"http://foo%40bar:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz"
|
||||||
|
);
|
||||||
|
url.searchParams.set("bar", "qat");
|
||||||
|
assertEqual(
|
||||||
|
url.href,
|
||||||
|
"http://foo%40bar:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz&bar=qat"
|
||||||
|
);
|
||||||
|
url.searchParams.delete("foo");
|
||||||
|
assertEqual(url.href, "http://foo%40bar:qux@foo.bar/foo/bar%qat?bar=qat");
|
||||||
|
url.searchParams.append("foo", "bar");
|
||||||
|
assertEqual(
|
||||||
|
url.href,
|
||||||
|
"http://foo%40bar:qux@foo.bar/foo/bar%qat?bar=qat&foo=bar"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(function urlModifyHref() {
|
||||||
|
const url = new URL("http://example.com/");
|
||||||
|
url.href = "https://foo:bar@example.com:8080/baz/qat#qux";
|
||||||
|
assertEqual(url.protocol, "https:");
|
||||||
|
assertEqual(url.username, "foo");
|
||||||
|
assertEqual(url.password, "bar");
|
||||||
|
assertEqual(url.host, "example.com:8080");
|
||||||
|
assertEqual(url.hostname, "example.com");
|
||||||
|
assertEqual(url.pathname, "/baz/qat");
|
||||||
|
assertEqual(url.hash, "#qux");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(function urlModifyPathname() {
|
||||||
|
const url = new URL("http://foo.bar/baz%qat/qux%quux");
|
||||||
|
assertEqual(url.pathname, "/baz%qat/qux%quux");
|
||||||
|
url.pathname = url.pathname;
|
||||||
|
assertEqual(url.pathname, "/baz%qat/qux%quux");
|
||||||
|
url.pathname = "baz#qat qux";
|
||||||
|
assertEqual(url.pathname, "/baz%23qat%20qux");
|
||||||
|
url.pathname = url.pathname;
|
||||||
|
assertEqual(url.pathname, "/baz%23qat%20qux");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(function urlModifyHash() {
|
||||||
|
const url = new URL("http://foo.bar");
|
||||||
|
url.hash = "%foo bar/qat%qux#bar";
|
||||||
|
assertEqual(url.hash, "#%foo%20bar/qat%qux#bar");
|
||||||
|
url.hash = url.hash;
|
||||||
|
assertEqual(url.hash, "#%foo%20bar/qat%qux#bar");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(function urlSearchParamsReuse() {
|
||||||
|
const url = new URL(
|
||||||
|
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
|
||||||
|
);
|
||||||
|
const sp = url.searchParams;
|
||||||
|
url.host = "baz.qat";
|
||||||
|
assert(sp === url.searchParams, "Search params should be reused.");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(function urlBaseURL() {
|
||||||
|
const base = new URL(
|
||||||
|
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
|
||||||
|
);
|
||||||
|
const url = new URL("/foo/bar?baz=foo#qux", base);
|
||||||
|
assertEqual(url.href, "https://foo:bar@baz.qat:8000/foo/bar?baz=foo#qux");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(function urlBaseString() {
|
||||||
|
const url = new URL(
|
||||||
|
"/foo/bar?baz=foo#qux",
|
||||||
|
"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
|
||||||
|
);
|
||||||
|
assertEqual(url.href, "https://foo:bar@baz.qat:8000/foo/bar?baz=foo#qux");
|
||||||
|
});
|
Loading…
Reference in a new issue