export interface SplitServiceUrl {
url: string;
key: string;
baseUrl: string;
path: string;
[extra: string]: unknown;
}
/**
* Splits a URL of the form `//` into parts.
* Example: `https://host/~FS/a/b` → `{ baseUrl: "https://host/~FS/", key: "FS", path: "a/b" }`.
*/
export function splitServiceUrl(url: URL | string, separator = "~"): SplitServiceUrl {
const str = `${url}`;
const empty = { url: str, key: "", baseUrl: "", path: "" };
// Strip query and fragment to prevent false positives on `?q=~foo` or `#~FS`.
// Extract the prefix (origin) and path from the input verbatim, preserving case
// and form (e.g., `//host` vs. `http://host`, `HTTPS://` vs. `https://`).
const hashIdx = str.indexOf("#");
const queryIdx = str.indexOf("?");
let strippedEnd = str.length;
if (hashIdx >= 0) strippedEnd = Math.min(strippedEnd, hashIdx);
if (queryIdx >= 0) strippedEnd = Math.min(strippedEnd, queryIdx);
const stripped = str.slice(0, strippedEnd);
// Identify where the path begins in the input. Three cases:
// 1. scheme://authority/path — prefix is scheme://authority
// 2. //authority/path — prefix is //authority
// 3. relative path — prefix is empty
let prefixEnd = 0;
const schemeMatch = stripped.match(/^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//);
if (schemeMatch) {
// scheme://authority/path — find the next / after the scheme
prefixEnd = schemeMatch[0].length;
const slashIdx = stripped.indexOf("/", prefixEnd);
if (slashIdx >= 0) {
prefixEnd = slashIdx;
} else {
return empty; // No path
}
} else if (stripped.startsWith("//")) {
// //authority/path — find the next / after the //
prefixEnd = 2;
const slashIdx = stripped.indexOf("/", prefixEnd);
if (slashIdx >= 0) {
prefixEnd = slashIdx;
} else {
return empty; // No path
}
}
// else prefixEnd = 0: relative URL
const prefix = stripped.slice(0, prefixEnd);
const pathPart = stripped.slice(prefixEnd);
// Check if pathPart starts with the separator, anchored at the segment boundary.
// With an authority the path always begins with `/`, so the separator must be
// at `/`. Without one the input is either ROOT-RELATIVE
// (`/~FS/a/b` -- `location.pathname`, or any root-absolute href) or plain
// relative (`~FS/a/b`), and both are accepted: treating "no authority" as
// "relative" made every root-relative caller get nothing back.
let keyStart: number;
let rooted = false;
if (prefix === "") {
if (pathPart.startsWith(`/${separator}`)) {
rooted = true;
keyStart = separator.length + 1;
} else if (pathPart.startsWith(separator)) {
keyStart = separator.length;
} else {
return empty;
}
} else {
if (!pathPart.startsWith(`/${separator}`)) return empty;
keyStart = separator.length + 1;
}
const rest = pathPart.slice(keyStart);
const slash = rest.indexOf("/");
const key = slash < 0 ? rest : rest.slice(0, slash);
if (key === "") return empty;
// The base keeps the input's own form: `https://host/~FS/`, `//host/~FS/`,
// `/~FS/` and `~FS/` each round-trip as written.
const base = prefix === "" ? (rooted ? "/" : "") : `${prefix}/`;
const baseUrl = `${base}${separator}${key}${slash < 0 ? "" : "/"}`;
const path = slash < 0 ? "" : rest.slice(slash + 1);
return { url: str, key, baseUrl, path };
}