summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2018-12-18 11:07:47 +1000
committerRyan Dahl <ry@tinyclouds.org>2018-12-17 20:07:47 -0500
commit91a41ca124ffb6e9c025ca2cb35121cca139083c (patch)
tree60da0ba48c0ffc3d3425840ea7beb2a07a36b344
parent1e54f1d47362308d7eabb02b3ddaa1f670e4ca08 (diff)
Add URL implementation (#1359)
-rw-r--r--BUILD.gn1
-rw-r--r--js/globals.ts3
-rw-r--r--js/unit_tests.ts1
-rw-r--r--js/url.ts261
-rw-r--r--js/url_test.ts131
5 files changed, 397 insertions, 0 deletions
diff --git a/BUILD.gn b/BUILD.gn
index 921754ef1..1f6298ae4 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -100,6 +100,7 @@ ts_sources = [
"js/timers.ts",
"js/truncate.ts",
"js/types.ts",
+ "js/url.ts",
"js/url_search_params.ts",
"js/util.ts",
"js/write_file.ts",
diff --git a/js/globals.ts b/js/globals.ts
index cf45b4239..ac66f77e6 100644
--- a/js/globals.ts
+++ b/js/globals.ts
@@ -16,6 +16,7 @@ import * as fetchTypes from "./fetch";
import * as headers from "./headers";
import * as textEncoding from "./text_encoding";
import * as timers from "./timers";
+import * as url from "./url";
import * as urlSearchParams from "./url_search_params";
// 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;
window.File = file.DenoFile;
export type File = file.DenoFile;
+window.URL = url.URL;
+export type URL = url.URL;
window.URLSearchParams = urlSearchParams.URLSearchParams;
export type URLSearchParams = urlSearchParams.URLSearchParams;
diff --git a/js/unit_tests.ts b/js/unit_tests.ts
index 55877091e..6ac071d3e 100644
--- a/js/unit_tests.ts
+++ b/js/unit_tests.ts
@@ -34,5 +34,6 @@ import "./symlink_test.ts";
import "./text_encoding_test.ts";
import "./timers_test.ts";
import "./truncate_test.ts";
+import "./url_test.ts";
import "./url_search_params_test.ts";
import "./write_file_test.ts";
diff --git a/js/url.ts b/js/url.ts
new file mode 100644
index 000000000..42a2577e1
--- /dev/null
+++ b/js/url.ts
@@ -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;
+ }
+}
diff --git a/js/url_test.ts b/js/url_test.ts
new file mode 100644
index 000000000..f89845274
--- /dev/null
+++ b/js/url_test.ts
@@ -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");
+});