UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

134 lines (133 loc) 5.52 kB
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; }