shelving
Version:
Toolkit for using data in JavaScript.
134 lines (133 loc) • 5.52 kB
JavaScript
import { RequiredError } from "../error/RequiredError.js";
export const ImmutableURL = URL;
/**
* Is an unknown value a URL object?
* - Must be a `URL` instance and its origin must start with `scheme://`
*/
export function isURL(value) {
return value instanceof URL && _isURL(value);
}
function _isURL(uri) {
return uri.href.startsWith(`${uri.protocol}//`);
}
/** Assert that an unknown value is a URL object. */
export function assertURL(value, caller = assertURL) {
if (!isURL(value))
throw new RequiredError("Invalid URL", { received: value, caller });
}
/**
* Resolve a possible URI relative to a base, or return `undefined` if conversion fails.
* - Returns any kind of URI — not just hierarchical `scheme://host` URLs. Use `getURL()` when a true URL is specifically required.
* - A `URL` instance is returned as-is (already absolute, base ignored).
*
* Note: the base is normalised with `getBaseURL()`, so it is always treated as if it ends in a slash.
* - e.g. if `base` is `http://p.com/a/b/c` the path resolves relative to `c/` as if a trailing slash was present.
* - This differs from the default behaviour of `new URL()`, but is the more natural expected result.
*/
export function getBasedURI(input, base) {
if (!input)
return;
if (input instanceof URL)
return input;
try {
return new URL(input, getBaseURL(base));
}
catch {
//
}
}
/**
* Resolve a possible URL relative to a base URL, or return `undefined` if conversion fails.
* - Like `getBasedURI()` but only succeeds for true `scheme://host` URLs — other URIs (e.g. `mailto:`) return `undefined`.
*/
export function getURL(target, base) {
const uri = getBasedURI(target, base);
if (uri && _isURL(uri))
return uri;
}
/** Convert a possible URL to a URL, or throw `RequiredError` if conversion fails. */
export function requireURL(target, base, caller = requireURL) {
const url = getURL(target, base);
assertURL(url, caller);
return url;
}
/**
* Resolve and match a target URL/path against a base URL and return the remaining path.
*
* - Need to be valid _URLs_ not just _URIs_, i.e. needs to have `protocol://` at the start.
* - Origins need to match, i.e. `http://localhost` !== `http://localhost:4020`
* - Relative targets are resolved against the normalized base URL.
*
* @param target URL to match against `base` — if this is a relative path it will be resolved against `base`
*
* @returns Absolute path starting with `/`, or `undefined` for origin mismatches or non-matching paths.
*/
export function matchURLPrefix(target, base, caller = matchURLPrefix) {
if (!target || !base)
return;
const baseURL = requireURL(base, undefined, caller);
const targetURL = requireURL(target, baseURL, caller);
if (targetURL.origin !== baseURL.origin)
return;
const basePath = baseURL.pathname;
const targetPath = targetURL.pathname;
if (basePath === "/")
return targetPath;
// `basePath` may or may not have a trailing slash, so strip it and re-assert the directory boundary explicitly.
const bareBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
if (targetPath === bareBase)
return "/"; // e.g. `/abc` matches base `/abc` or `/abc/`
if (targetPath.startsWith(bareBase) && targetPath[bareBase.length] === "/")
return targetPath.slice(bareBase.length);
}
/**
* Is a target URL active relative to a base URL?
* - Active means `target` and `base` resolve to the exact same URL (same origin, same path).
* - Origin mismatches return `false`.
*
* @param target URL whose status to test — relative paths resolve against `base`.
* @param base Base URL to test against.
*/
export function isURLActive(target, base, caller = isURLActive) {
return !!target && !!base && matchURLPrefix(target, base, caller) === "/";
}
/**
* Is a target URL proud relative to a base URL?
* - Proud means `target` is `base` or a descendant of `base` — i.e. `base` is at or above `target` in the URL hierarchy.
* - Useful for marking a menu item as "current branch" when the user is somewhere deeper in its sub-tree.
* - Origin mismatches return `false`.
*
* @param target URL whose status to test — relative paths resolve against `base`.
* @param base Base URL to test against.
*/
export function isURLProud(target, base, caller = isURLProud) {
return !!target && !!base && matchURLPrefix(target, base, caller) !== undefined;
}
/** Is an unknown value a valid Base URL. */
export function isBaseURL(value) {
return isURL(value) && _isBaseURL(value);
}
function _isBaseURL(uri) {
return uri.pathname.endsWith("/");
}
/** Get a Base URL. */
export function getBaseURL(input) {
if (!input)
return;
const uri = getBasedURI(input, undefined);
if (!uri || !_isURL(uri))
return;
if (_isBaseURL(uri))
return uri;
// Clone before mutating: when `input` was a `URL` instance, `getBasedURI` returned that same instance, so mutating it would corrupt the caller's object. A string input always yields a fresh `uri`.
const base = typeof input === "string" ? uri : new URL(uri);
base.pathname = `${uri.pathname}/`; // Add a trailing slash.
return base;
}
/** Require a Base URL. */
export function requireBaseURL(value, caller) {
const url = getBaseURL(value);
if (!url)
throw new RequiredError("Invalid base URL", { received: value, caller });
return url;
}