summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/url.ts66
-rw-r--r--js/url_test.ts10
2 files changed, 73 insertions, 3 deletions
diff --git a/js/url.ts b/js/url.ts
index 334d1c09b..a2562b9a3 100644
--- a/js/url.ts
+++ b/js/url.ts
@@ -80,6 +80,66 @@ function generateUUID(): string {
// Keep it outside of URL to avoid any attempts of access.
export const blobURLMap = new Map<string, domTypes.Blob>();
+function isAbsolutePath(path: string): boolean {
+ return path.startsWith("/");
+}
+
+// Resolves `.`s and `..`s where possible.
+// Preserves repeating and trailing `/`s by design.
+function normalizePath(path: string): string {
+ const isAbsolute = isAbsolutePath(path);
+ path = path.replace(/^\//, "");
+ const pathSegments = path.split("/");
+
+ const newPathSegments: string[] = [];
+ for (let i = 0; i < pathSegments.length; i++) {
+ const previous = newPathSegments[newPathSegments.length - 1];
+ if (
+ pathSegments[i] == ".." &&
+ previous != ".." &&
+ (previous != undefined || isAbsolute)
+ ) {
+ newPathSegments.pop();
+ } else if (pathSegments[i] != ".") {
+ newPathSegments.push(pathSegments[i]);
+ }
+ }
+
+ let newPath = newPathSegments.join("/");
+ if (!isAbsolute) {
+ if (newPathSegments.length == 0) {
+ newPath = ".";
+ }
+ } else {
+ newPath = `/${newPath}`;
+ }
+ return newPath;
+}
+
+// Standard URL basing logic, applied to paths.
+function resolvePathFromBase(path: string, basePath: string): string {
+ const normalizedPath = normalizePath(path);
+ if (isAbsolutePath(normalizedPath)) {
+ return normalizedPath;
+ }
+ const normalizedBasePath = normalizePath(basePath);
+ if (!isAbsolutePath(normalizedBasePath)) {
+ throw new TypeError("Base path must be absolute.");
+ }
+
+ // Special case.
+ if (path == "") {
+ return normalizedBasePath;
+ }
+
+ // Remove everything after the last `/` in `normalizedBasePath`.
+ const prefix = normalizedBasePath.replace(/[^\/]*$/, "");
+ // If `normalizedPath` ends with `.` or `..`, add a trailing space.
+ const suffix = normalizedPath.replace(/(?<=(^|\/)(\.|\.\.))$/, "/");
+
+ return normalizePath(prefix + suffix);
+}
+
export class URL {
private _parts: URLParts;
private _searchParams!: urlSearchParams.URLSearchParams;
@@ -254,7 +314,7 @@ export class URL {
let baseParts: URLParts | undefined;
if (base) {
baseParts = typeof base === "string" ? parse(base) : base._parts;
- if (!baseParts) {
+ if (!baseParts || baseParts.protocol == "") {
throw new TypeError("Invalid base URL.");
}
}
@@ -273,8 +333,8 @@ export class URL {
password: baseParts.password,
hostname: baseParts.hostname,
port: baseParts.port,
- path: urlParts.path || baseParts.path,
- query: urlParts.query || baseParts.query,
+ path: resolvePathFromBase(urlParts.path, baseParts.path),
+ query: urlParts.query,
hash: urlParts.hash
};
} else {
diff --git a/js/url_test.ts b/js/url_test.ts
index f2a792077..c42025929 100644
--- a/js/url_test.ts
+++ b/js/url_test.ts
@@ -142,6 +142,16 @@ test(function urlBaseString(): void {
assertEquals(url.href, "https://foo:bar@baz.qat:8000/foo/bar?baz=foo#qux");
});
+test(function urlRelativeWithBase(): void {
+ assertEquals(new URL("", "file:///a/a/a").href, "file:///a/a/a");
+ assertEquals(new URL(".", "file:///a/a/a").href, "file:///a/a/");
+ assertEquals(new URL("..", "file:///a/a/a").href, "file:///a/");
+ assertEquals(new URL("b", "file:///a/a/a").href, "file:///a/a/b");
+ assertEquals(new URL("b", "file:///a/a/a/").href, "file:///a/a/a/b");
+ assertEquals(new URL("b/", "file:///a/a/a").href, "file:///a/a/b/");
+ assertEquals(new URL("../b", "file:///a/a/a").href, "file:///a/b");
+});
+
test(function deletingAllParamsRemovesQuestionMarkFromURL(): void {
const url = new URL("http://example.com/?param1&param2");
url.searchParams.delete("param1");