Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add URL #1359

Merged
merged 1 commit into from
Dec 18, 2018
Merged

Add URL #1359

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions js/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions js/unit_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
261 changes: 261 additions & 0 deletions js/url.ts
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do other URL implementations use RegExp?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node.js uses a combination of splitting strings and regular expressions. It's url is a slightly different beast to the the whatwg URL. JSDom follows the IDL pedantically, so it ingests each character and does loads of branching. While it complies 100% including the logic, it is a lot of code. There are two other polyfills that I found on npm, one ingests per char, the other uses regular expressions.

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;
}
}
Loading