UNPKG

@oazmi/kitchensink

Version:

a collection of personal utility functions

1,026 lines 85.4 kB
/** utility tools for manipulating paths and obtaining `URL`s. * * TODO: write document level examples. * * url terminology: * - urls are a subset of uris * - a url protocol is defined as: `[scheme]://` * - a url is defined as: `[scheme]://[host]/[path]?[queryString]#[fragmentHash]` * - or equivalently, a url is: `[protocol][host]/[path]?[queryString]#[fragmentHash]` * - a uri is defined as: `[scheme]:[someIdentifier]` * * @module */ import { array_from, dom_decodeURI, dom_encodeURI, object_entries } from "./alias.js"; import { DEBUG } from "./deps.js"; import { commonPrefix, quote } from "./stringman.js"; import { isObject, isString } from "./struct.js"; // DONE: consider if it would be a good idea to export `uri_protocol_and_scheme_mapping` so that the end user can manually modify it to their needs, // and then have this whole submodule behave according to their custom uri scheme definitions. // of course we're going to encounter issues with the `UriScheme` type, and might even have to turn it into a `string` (there by killing off all typing benefits), // but it's still less painful than having the end user having to redefine all path resolution functions via wrappers. /** this is global mapping of uri-protocol schemes that are identifiable by {@link getUriScheme} and {@link resolveAsUrl}. * you may mutate this 2-tuple array to add or remove custom identifiable uri-schemes. * * @example * adding a new uri protocol scheme named `"inline-scheme"` to our registry: * ```ts * import { assertEquals, assertThrows } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, err = assertThrows * * // initially, our custom "inline" scheme is unidentifiable, and cannot be used in `resolveAsUrl` as a base url * eq(getUriScheme("inline://a/b/c.txt"), "relative") * err(() => resolveAsUrl("./w.xyz", "inline://a/b/c.txt")) // "inline://a/b/c.txt" is identified as a relative path, and cannot be used as a base path * * // registering the custom protocol-scheme mapping. * // note that you will have to declare `as any`, since the schemes are tightly defined by the type `UriScheme`. * uriProtocolSchemeMap.push(["inline://", "inline-scheme" as any]) * * // and now, our custom "inline" scheme becomes identifiable * eq(getUriScheme("inline://a/b/c.txt"), "inline-scheme") * * // it is also now accepted by `resolveAsUrl` as a base uri * eq(resolveAsUrl("./w.xyz", "inline://a/b/c.txt"), new URL("inline://a/b/w.xyz")) * ``` */ export const uriProtocolSchemeMap = /*@__PURE__*/ object_entries({ "node:": "node", "npm:": "npm", "jsr:": "jsr", "blob:": "blob", "data:": "data", "http://": "http", "https://": "https", "file://": "file", "./": "relative", "../": "relative", }); /** here, you can specify which uri schemes cannot be used as a base url for resolving a url via the {@link resolveAsUrl} function. * * @example * adding a new uri protocol scheme named `"base64-scheme"` to our registry, and then forbidding it from being used as a base url: * ```ts * import { assertEquals, assertThrows } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, err = assertThrows * * // initially, our custom "base64" scheme is unidentifiable, and cannot be used in `resolveAsUrl` as a base url * eq(getUriScheme("base64://a/b/c.txt"), "relative") * err(() => resolveAsUrl("./w.xyz", "base64://a/b/c.txt")) // "base64://a/b/c.txt" is identified as a relative path, and cannot be used as a base path * * // registering the custom protocol-scheme mapping. * // note that you will have to declare `as any`, since the schemes are tightly defined by the type `UriScheme`. * uriProtocolSchemeMap.push(["base64://", "base64-scheme" as any]) * * // and now, our custom "base64" scheme becomes identifiable. * eq(getUriScheme("base64://a/b/c.txt"), "base64-scheme") * * // it is also now accepted by `resolveAsUrl` as a base uri * eq(resolveAsUrl("./w.xyz", "base64://a/b/c.txt"), new URL("base64://a/b/w.xyz")) * * // since we don't want to make it possible to have "base64-scheme" as a base uri, so we'll put it in the forbidden list. * // once again `as any` is needed, since the `UriScheme` is tightly defined, and its definition cannot be changed. * forbiddenBaseUriSchemes.push("base64-scheme" as any) * err(() => resolveAsUrl("./w.xyz", "base64://a/b/c.txt")) // "base64://a/b/c.txt" is now amongst the forbidden schemes that cannot be combined with relative paths. * eq(resolveAsUrl("base64://a/b/c.txt"), new URL("base64://a/b/c.txt")) // this is of course not stopping us from building urls with the "base64" scheme, so long as no relative path is attached. * ``` */ export const forbiddenBaseUriSchemes = ["blob", "data", "relative"]; const packageUriSchemes = ["jsr", "npm", "node"], packageUriProtocols = ["jsr:", "npm:", "node:"]; const // posix directory path separator sep = "/", // posix relative directory path navigator dotslash = "./", // posix relative parent directory path navigator dotdotslash = "../", // regex for attaining windows directory path separator ("\\") windows_directory_slash_regex = /\\/g, // regex for detecting if a path is an absolute windows path windows_absolute_path_regex = /^[a-z]\:[\/\\]/i, // regex for correcting an invalid single leading slash in a windows path windows_leading_slash_correction_regex = /^[\/\\]([a-z])\:[\/\\]/i, // regex for attaining leading consecutive slashes leading_slashes_regex = /^\/+/, // regex for attaining trailing consecutive slashes, except for those that are preceded by a dotslash ("/./") or a dotdotslash ("/../") trailing_slashes_regex = /(?<!\/\.\.?)\/+$/, // regex for attaining leading consecutive slashes and dot-slashes leading_slashes_and_dot_slashes_regex = /^(\.?\/)+/, // regex for attaining a reversed path string's trailing consecutive slashes and dot-slashes, but not the slashes preceded by a dotdotslash ("/.."). // this regex is a little complex, so you might want to check out the test cases (of reversed path strings) here: "https://regex101.com/r/IV0twv/1" reversed_trailing_slashes_and_dot_slashes_regex = /^(\/\.?(?![^\/]))*(\/(?!\.\.\/))?/, // regex for attaining the file name of a path, including its leading slash (if there is one) filename_regex = /\/?[^\/]+$/, // regex for attaining the base name and extension name of a file, from its filename (no directories) basename_and_extname_regex = /^(?<basename>.+?)(?<ext>\.[^\.]+)?$/, // an npm or jsr package string parsing regex. see the test cases on regex101 link: "https://regex101.com/r/mX3v1z/2" package_regex = /^(?<protocol>jsr:|npm:|node:)(\/*(@(?<scope>[^\/\s]+)\/)?(?<pkg>[^@\/\s]+)(@(?<version>[^\/\r\n\t\f\v]+))?)?(?<pathname>\/.*)?$/, string_starts_with = (str, starts_with) => str.startsWith(starts_with), string_ends_with = (str, ends_with) => str.endsWith(ends_with); /** test whether a given path is an absolute path (either windows or posix). * * > [!note] * > currently, we do consider the tilde expansion ("~") as an absolute path, even though it is not an os/fs-level path, but rather a shell feature. * > this may result in misclassification on windows, since "~" is a valid starting character for a file or folder name * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = isAbsolutePath * * eq(fn("/a/b/c.txt"), true) * eq(fn("~/a/b/c.txt"), true) * eq(fn("C:/a/b/c.txt"), true) * eq(fn("/c:/a/b/c.txt"), true) * * eq(fn("a/b/c.txt"), false) * eq(fn("./a/b/c.txt"), false) * eq(fn("../a/b/c.txt"), false) * ``` */ export const isAbsolutePath = (path) => { return (string_starts_with(path, sep) || string_starts_with(path, "~") || windows_absolute_path_regex.test(path)); }; /** guesses the scheme of a url string. see {@link UriScheme} for more details. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = getUriScheme * * eq(fn("C:/Users/me/path/to/file.txt"), "local") * eq(fn("~/path/to/file.txt"), "local") * eq(fn("/usr/me/path/to/file.txt"), "local") * eq(fn("path/to/file.txt"), "relative") * eq(fn("./path/to/file.txt"), "relative") * eq(fn("../path/to/file.txt"), "relative") * eq(fn("file:///c://users/me/path/to/file.txt"), "file") * eq(fn("file:///usr/me/path/to/file.txt"), "file") * eq(fn("jsr:@user/path/to/file"), "jsr") * eq(fn("jsr:/@user/path/to/file"), "jsr") * eq(fn("npm:lib/path/to/file"), "npm") * eq(fn("npm:/lib/path/to/file"), "npm") * eq(fn("npm:/@scope/lib/path/to/file"), "npm") * eq(fn("node:http"), "node") * eq(fn("node:fs/promises"), "node") * eq(fn("data:text/plain;charset=utf-8;base64,aGVsbG8="), "data") * eq(fn("blob:https://example.com/4800d2d8-a78c-4895"), "blob") * eq(fn("http://google.com/style.css"), "http") * eq(fn("https://google.com/style.css"), "https") * eq(fn(""), undefined) * ``` */ export const getUriScheme = (path) => { if (!path || path === "") { return undefined; } for (const [protocol, scheme] of uriProtocolSchemeMap) { if (string_starts_with(path, protocol)) { return scheme; } } return isAbsolutePath(path) ? "local" : "relative"; }; /** this function parses npm and jsr package strings, and returns a pseudo URL-like object. * * the regex we use for parsing the input `href` string is quoted below: * > /^(?<protocol>jsr:|npm:|node:)(\/*(@(?<scope>[^\/\s]+)\/)?(?<pkg>[^@\/\s]+)(@(?<version>[^\/\r\n\t\f\v]+))?)?(?<pathname>\/.*)?$/ * * see the regex in action with the test cases on regex101 link: [regex101.com/r/mX3v1z/2](https://regex101.com/r/mX3v1z/2) * * @throws `Error` an error will be thrown if either the package name (`pkg`), or the `protocol` cannot be deduced by the regex. * * @example * ```ts * import { assertEquals, assertThrows } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, err = assertThrows, fn = parsePackageUrl * * // basic breakdown of a package's resource uri * eq(fn("jsr:@scope/package@version/pathname/file.ts"), { * href: "jsr:/@scope/package@version/pathname/file.ts", * protocol: "jsr:", * scope: "scope", * pkg: "package", * version: "version", * pathname: "/pathname/file.ts", * host: "@scope/package@version", * }) * * // showing that jsr package uri's without a scope are perfectly permitted. * // even though it isn't actually possible to do so on "jsr.io". * // thus it is left up to the end-user to make of it what they will. * eq(fn("jsr:package@version/pathname/"), { * href: "jsr:/package@version/pathname/", * protocol: "jsr:", * scope: undefined, * pkg: "package", * version: "version", * pathname: "/pathname/", * host: "package@version", * }) * * // testing a case with multiple slashes ("/") after the protocol colon (":"), and no trailing slash after the version * eq(fn("npm:///@scope/package@version"), { * href: "npm:/@scope/package@version/", * protocol: "npm:", * scope: "scope", * pkg: "package", * version: "version", * pathname: "/", * host: "@scope/package@version", * }) * * // testing a no-scope and no-version case * eq(fn("npm:package"), { * href: "npm:/package/", * protocol: "npm:", * scope: undefined, * pkg: "package", * version: undefined, * pathname: "/", * host: "package", * }) * * // testing the "node:" protocol * eq(fn("node:fs"), { * href: "node:/fs/", * protocol: "node:", * scope: undefined, * pkg: "fs", * version: undefined, * pathname: "/", * host: "fs", * }) * * // testing the "node:" protocol with a certain pathname * eq(fn("node:fs/promises"), { * href: "node:/fs/promises", * protocol: "node:", * scope: undefined, * pkg: "fs", * version: undefined, * pathname: "/promises", * host: "fs", * }) * * // testing a `version` query string that contains whitespaces and url-encoded characters. * // NOTE: the url-encoded characters in vs-code's doc popup appear decoded, so don't be fooled! * // but the `host` is always a url-decoded string. * eq(fn("jsr:@scope/package@1.0.0 - 1.2.0/pathname/file.ts"), { * href: "jsr:/@scope/package@1.0.0%20-%201.2.0/pathname/file.ts", * protocol: "jsr:", * scope: "scope", * pkg: "package", * version: "1.0.0 - 1.2.0", * pathname: "/pathname/file.ts", * host: "@scope/package@1.0.0 - 1.2.0", * }) * * // testing a `version` query string that has its some of its characters (such as whitespaces) url-encoded. * // NOTE: the url-encoded characters in vs-code's doc popup appear decoded, so don't be fooled! * // but the `host` is always a url-decoded string. * eq(fn("jsr:@scope/package@^2%20<2.2%20||%20>%202.3/pathname/file.ts"), { * href: "jsr:/@scope/package@%5E2%20%3C2.2%20%7C%7C%20%3E%202.3/pathname/file.ts", * protocol: "jsr:", * scope: "scope", * pkg: "package", * version: "^2 <2.2 || > 2.3", * pathname: "/pathname/file.ts", * host: "@scope/package@^2 <2.2 || > 2.3", * }) * * // testing cases where an error should be invoked * err(() => fn("npm:@scope/")) // missing a package name * err(() => fn("npm:@scope//package")) // more than one slash after scope * err(() => fn("pnpm:@scope/package@version")) // only "node:", "npm:", and "jsr:" protocols are recognized * ``` */ export const parsePackageUrl = (url_href) => { url_href = dom_decodeURI(isString(url_href) ? url_href : url_href.href); const { protocol, scope: scope_str, pkg, version: version_str, pathname: pathname_str } = package_regex.exec(url_href)?.groups ?? {}; if ((protocol === undefined) || (pkg === undefined)) { throw new Error(DEBUG.ERROR ? ("invalid package url format was provided: " + url_href) : ""); } const scope = scope_str ? scope_str : undefined, // turn empty strings into `undefined` version = version_str ? version_str : undefined, // turn empty strings into `undefined` pathname = pathname_str ? pathname_str : sep, // pathname must always begin with a leading slash, even if it was originally empty host = `${scope ? "@" + scope + sep : ""}${pkg}${version ? "@" + version : ""}`, href = dom_encodeURI(`${protocol}/${host}${pathname}`); return { protocol: protocol, scope, pkg, version, pathname, host, href, }; }; /** convert a url string to an actual `URL` object. * your input `path` url can use any scheme supported by the {@link getUriScheme} function. * and you may also use paths with windows dir-separators ("\\"), as this function implicitly converts them a posix separator ("/"). * * if you pass a `URL` object, then it will be returned as is. * * @throws `Error` an error will be thrown if `base` uri is either a relative path, or uses a data uri scheme, * or if the provided `path` is relative, but no absolute `base` path is provided. * * @example * ```ts * import { assertEquals, assertThrows } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, err = assertThrows, fn = resolveAsUrl * * eq(fn(new URL("some://url:8000/a/b c.txt")), new URL("some://url:8000/a/b%20c.txt")) * * eq(fn("/a/b/c d e.txt"), new URL("file:///a/b/c%20d%20e.txt")) * eq(fn("~/a/b/c.txt"), new URL("file://~/a/b/c.txt")) * eq(fn("C:/a/b/c/d/e.txt"), new URL("file:///C:/a/b/c/d/e.txt")) * eq(fn("C:\\a\\b\\c\\d\\e.txt"), new URL("file:///C:/a/b/c/d/e.txt")) * eq(fn("./e/f g.txt", "C:/a\\b\\c d/"), new URL("file:///C:/a/b/c%20d/e/f%20g.txt")) * eq(fn("../c d/e/f g.txt", "C:/a/b/c d/"), new URL("file:///C:/a/b/c%20d/e/f%20g.txt")) * eq(fn("d/../.././e.txt", "C:/a/b/c/"), new URL("file:///C:/a/b/e.txt")) * eq(fn("d/../.././e.txt", "C:/a/b/c"), new URL("file:///C:/a/e.txt")) * eq(fn("D:/a/b.txt", "C:/c/d.txt"), new URL("file:///D:/a/b.txt")) * eq(fn("/a/b.txt", "C:/c/d.txt"), new URL("file:///C:/a/b.txt")) * eq(fn("/a/b.txt", "/sys/admin/"), new URL("file:///a/b.txt")) * eq(fn("/a/b.txt", ""), new URL("file:///a/b.txt")) * * eq(fn("http://cdn.esm.sh/a/b/c.txt"), new URL("http://cdn.esm.sh/a/b/c.txt")) * eq(fn("http://cdn.esm.sh/a.txt", "file:///b/"), new URL("http://cdn.esm.sh/a.txt")) * eq(fn("http://cdn.esm.sh/a.txt", "/b/"), new URL("http://cdn.esm.sh/a.txt")) * eq(fn("/a/b/c.txt", "http://cdn.esm.sh/"), new URL("http://cdn.esm.sh/a/b/c.txt")) * * eq(fn("b/c.txt", "http://cdn.esm.sh/a/"), new URL("http://cdn.esm.sh/a/b/c.txt")) * eq(fn("b/c.txt", "http://cdn.esm.sh/a"), new URL("http://cdn.esm.sh/b/c.txt")) * eq(fn("./b/c.txt", "http://cdn.esm.sh/a/"), new URL("http://cdn.esm.sh/a/b/c.txt")) * eq(fn("./b/c.txt", "http://cdn.esm.sh/a"), new URL("http://cdn.esm.sh/b/c.txt")) * eq(fn("../b/c.txt", "https://cdn.esm.sh/a/"), new URL("https://cdn.esm.sh/b/c.txt")) * eq(fn("../c/d.txt", "https://cdn.esm.sh/a/b"), new URL("https://cdn.esm.sh/c/d.txt")) * eq(fn("/c/d.txt", "https://cdn.esm.sh/a/b"), new URL("https://cdn.esm.sh/c/d.txt")) * * eq(fn("node:fs"), new URL("node:/fs/")) * eq(fn("node:fs/promises"), new URL("node:/fs/promises")) * eq(fn("promises", "node:fs"), new URL("node:/fs/promises")) * eq(fn("./promises", "node:fs"), new URL("node:/fs/promises")) * eq(fn("./promises", "node:fs/"), new URL("node:/fs/promises")) * eq(fn("mkdir", "node:fs/promises"), new URL("node:/fs/mkdir")) * eq(fn("mkdir", "node:fs/promises/"), new URL("node:/fs/promises/mkdir")) * eq(fn("./mkdir", "node:fs/promises"), new URL("node:/fs/mkdir")) * eq(fn("./mkdir", "node:fs/promises/"), new URL("node:/fs/promises/mkdir")) * eq(fn("/sync", "node:fs/promises/mkdir"), new URL("node:/fs/sync")) * * eq(fn("npm:react"), new URL("npm:/react/")) * eq(fn("npm:react/file.txt"), new URL("npm:/react/file.txt")) * eq(fn("npm:@facebook/react"), new URL("npm:/@facebook/react/")) * eq(fn("./to/file.txt", "npm:react"), new URL("npm:/react/to/file.txt")) * eq(fn("./to/file.txt", "npm:react/"), new URL("npm:/react/to/file.txt")) * eq(fn("/to/file.txt", "npm:react/native/bin"), new URL("npm:/react/to/file.txt")) * eq(fn("npm:react@19/jsx runtime.ts"), new URL("npm:/react@19/jsx%20runtime.ts")) * eq(fn("npm:react@^19 <19.5/jsx.ts"), new URL("npm:/react@%5E19%20%3C19.5/jsx.ts")) * * eq(fn("jsr:@scope/my-lib/b.txt"), new URL("jsr:/@scope/my-lib/b.txt")) * eq(fn("a/b.txt", "jsr:///@scope/my-lib"), new URL("jsr:/@scope/my-lib/a/b.txt")) * eq(fn("./a/b.txt", "jsr:///@scope/my-lib"), new URL("jsr:/@scope/my-lib/a/b.txt")) * eq(fn("a/b.txt", "jsr:///@scope/my-lib/c"), new URL("jsr:/@scope/my-lib/a/b.txt")) * eq(fn("./a/b.txt", "jsr:///@scope/my-lib/c"), new URL("jsr:/@scope/my-lib/a/b.txt")) * eq(fn("./a/b.txt", "jsr:///@scope/my-lib//c"), new URL("jsr:/@scope/my-lib/a/b.txt")) * eq(fn("../a/b.txt", "jsr:/@scope/my-lib///c/"), new URL("jsr:/@scope/my-lib/a/b.txt")) * eq(fn("./a/b.txt", "jsr:///@scope/my-lib/c/"), new URL("jsr:/@scope/my-lib/c/a/b.txt")) * eq(fn("/a/b.txt", "jsr:my-lib/x/y/"), new URL("jsr:/my-lib/a/b.txt")) * eq(fn("/a/b.txt", "jsr:@scope/my-lib/x/y/z"), new URL("jsr:/@scope/my-lib/a/b.txt")) * eq(fn("/a/b.txt", "jsr:my-lib@1 || 2/x/y/z"), new URL("jsr:/my-lib@1%20%7C%7C%202/a/b.txt")) * eq(fn("/a/b.txt", "jsr:@my/lib@1||2/x/y/z"), new URL("jsr:/@my/lib@1%7C%7C2/a/b.txt")) * * eq(fn("C:/a/b.txt", "jsr:@my/lib/x/y"), new URL("file:///C:/a/b.txt")) * eq(fn("jsr:@my/lib/x/y", "C:/a/b.txt"), new URL("jsr:/@my/lib/x/y")) * eq(fn("http://test.io/abc", "C:/a/b.txt"), new URL("http://test.io/abc")) * * eq(fn("blob:https://example.com/480-a78"), new URL("blob:https://example.com/480-a78")) * eq(fn("data:text/plain;utf8,hello"), new URL("data:text/plain;utf8,hello")) * eq(fn("data:text/plain;utf8,hello", "C:/a/b/"), new URL("data:text/plain;utf8,hello")) * * err(() => fn("./a/b.txt", "data:text/plain;charset=utf-8;base64,aGVsbG8=")) * err(() => fn("./a/b.txt", "blob:https://example.com/4800d2d8-a78c-4895-b68b-3690b69a0d6a")) * err(() => fn("./a/b.txt", "./path/")) // a base path must not be relative * err(() => fn("./a/b.txt")) // a relative path cannot be resolved on its own without a base path * err(() => fn("./a/b.txt", "")) // an empty base path is as good as a non-existing one * err(() => fn("fs/promises", "node:")) // the base protocol ("node:") MUST be accompanied with a package name * ``` */ export const resolveAsUrl = (path, base) => { if (!isString(path)) { return path; } path = pathToPosixPath(path); let base_url = base; if (isString(base) && base !== "") { const base_scheme = getUriScheme(base); if (forbiddenBaseUriSchemes.includes(base_scheme)) { throw new Error(DEBUG.ERROR ? ("the following base scheme (url-protocol) is not supported: " + base_scheme) : ""); } base_url = resolveAsUrl(base); } const path_scheme = getUriScheme(path), base_protocol = base_url ? base_url.protocol : undefined, path_is_package = packageUriSchemes.includes(path_scheme), base_is_package = packageUriProtocols.includes(base_protocol), path_is_root = string_starts_with(path, "/"), path_is_local = path_scheme === "local", path_is_relative = path_scheme === "relative"; // handling cases like: `fn("jsr://@hello/world/c/d.txt", "jsr://@scope/lib/a/b.ts") => "jsr://@hello/world/c/d.txt"` if (path_is_package) { // if the `path`'s protocol scheme is that of a package (i.e. "jsr", "npm", or "node"), then we're going to it handle slightly differently, // since it is possible for it to be non-parsable by the `URL` constructor if there is not trailing slash after the "npm:" or "jsr:" protocol. // thus we normalize our `path` by passing it to the `parsePackageUrl` function, and acquiring the normalized `URL` compatible `href` representation of the full `path`. return new URL(parsePackageUrl(path).href); } // if the base protocol's scheme is either "jsr" or "npm", then we're going to handle slightly differently, since it is possible for it to be non-parsable by the `URL` constructor if there is not trailing slash after the "npm:" or "jsr:" protocol. // the path joining rules of packages is different from an http url, which supports the domain name as the host. such an equivalent construction cannot be made for jsr or npm package strings. if (base_url && base_is_package && (path_is_root || path_is_relative)) { // to start off, we parse the `protocol`, `host` (= scope + package_name + version), and any existing `pathname` of the `base_url` using the `parsePackageUrl` function. // note that `pathname` always starts with a leading "/" const { host, protocol, pathname } = parsePackageUrl(base_url); // handling cases like: `fn("/c/d.txt", "jsr://@scope/lib/a/b.ts") => "jsr://@scope/lib/c/d.txt"` if (path_is_root) { return new URL(`${protocol}/${dom_encodeURI(host)}${dom_encodeURI(path)}`); } // handling cases like: `fn("../c/d.txt", "jsr://@scope/lib/a/b.ts") => "jsr://@scope/lib/a/c/d.txt"` if (path_is_relative) { // here, we join the pre-existing base url's `pathname` with the relative `paths`, by exploiting the URL constructor to do the joining part for us, by giving it a fake protocol named "x:". const full_pathname = (new URL(path, "x:" + pathname)).pathname; // `full_pathname` is now url-encoded return new URL(`${protocol}/${dom_encodeURI(host)}${full_pathname}`); } } // handling cases like: // - `fn("/c/de.txt", "https://example.com/a/b.txt") => "https://example.com/c/de.txt"` // - `fn("./c/d.txt", "https://example.com/a/b.txt") => "https://example.com/a/b/c/d.txt"` if (base_url && (path_is_root || path_is_relative)) { return new URL(path, base_url); } // handling cases like: // - `fn("/a/b c d.txt") => "file:///a/b%20c%20d.txt"` // - `fn("C:/a/b d.txt") => "file:///C:/a/b%20d.txt"` // - `fn("~/a/b/cd.txt") => "file://~/a/b%20d.txt"` if (path_is_local) { return new URL("file://" + dom_encodeURI(path)); } // handling all other situations with absolute `path`, such as `http://`, `file://`, `blob:`, and `data:` return new URL(path); }; /** trim the leading forward-slashes at the beginning of a string. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = trimStartSlashes * * eq(fn("///a/b.txt//"), "a/b.txt//") * eq(fn("/.//a/b.txt//"), ".//a/b.txt//") * eq(fn(".///../a/b.txt//"), ".///../a/b.txt//") * eq(fn("file:///a/b.txt//"), "file:///a/b.txt//") * ``` */ export const trimStartSlashes = (str) => { return str.replace(leading_slashes_regex, ""); }; /** trim the trailing forward-slashes at the end of a string, except for those that are preceded by a dotslash ("/./") or a dotdotslash ("/../") * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = trimEndSlashes * * eq(fn("///a/b.zip///"), "///a/b.zip") * eq(fn("///a/b.zip/.///"), "///a/b.zip/./") * eq(fn("///a/b.zip/..///"), "///a/b.zip/../") * eq(fn("///a/b.zip/...///"), "///a/b.zip/...") * eq(fn("///a/b.zip/wut.///"), "///a/b.zip/wut.") * eq(fn("///a/b.zip/wut..///"), "///a/b.zip/wut..") * eq(fn("///a/b.zip/wut...///"), "///a/b.zip/wut...") * eq(fn(".///../a/b.zip/"), ".///../a/b.zip") * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt") * ``` */ export const trimEndSlashes = (str) => { return str.replace(trailing_slashes_regex, ""); }; /** trim leading and trailing forward-slashes, at the beginning and end of a string. * this is a combination of {@link trimStartSlashes} and {@link trimEndSlashes}, so see their doc comments for more precise test cases. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = trimSlashes * * eq(fn("///a/b.zip//"), "a/b.zip") * eq(fn("///a/b.zip/..///"), "a/b.zip/../") * eq(fn("///a/b.zip/...///"), "a/b.zip/...") * eq(fn("///a/b.zip/.//..///"), "a/b.zip/.//../") * eq(fn("///a/b.zip/.//.///"), "a/b.zip/.//./") * eq(fn(".///../a/b.zip//"), ".///../a/b.zip") * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt") * ``` */ export const trimSlashes = (str) => { return trimEndSlashes(trimStartSlashes(str)); }; /** ensure that there is at least one leading slash at the beginning. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = ensureStartSlash * * eq(fn("a/b.zip"), "/a/b.zip") * eq(fn(".///../a/b.zip/"), "/.///../a/b.zip/") * eq(fn("///../a/b.zip/"), "///../a/b.zip/") * eq(fn("file:///a/b.zip//c.txt"), "/file:///a/b.zip//c.txt") * ``` */ export const ensureStartSlash = (str) => { return string_starts_with(str, sep) ? str : sep + str; }; /** ensure that there is at least one leading dot-slash at the beginning. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = ensureStartDotSlash * * eq(fn("a/b.zip"), "./a/b.zip") * eq(fn(".///../a/b.zip/"), ".///../a/b.zip/") * eq(fn("///../a/b.zip/"), ".///../a/b.zip/") * eq(fn("file:///a/b.zip//c.txt"), "./file:///a/b.zip//c.txt") * ``` */ export const ensureStartDotSlash = (str) => { return string_starts_with(str, dotslash) ? str : string_starts_with(str, sep) ? "." + str : dotslash + str; }; /** ensure that there is at least one trailing slash at the end. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = ensureEndSlash * * eq(fn("///a/b.zip//"), "///a/b.zip//") * eq(fn(".///../a/b.zip/"), ".///../a/b.zip/") * eq(fn(".///../a/b.zip/."), ".///../a/b.zip/./") * eq(fn(".///../a/b.zip/./"), ".///../a/b.zip/./") * eq(fn(".///../a/b.zip/.."), ".///../a/b.zip/../") * eq(fn(".///../a/b.zip/../"), ".///../a/b.zip/../") * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt/") * ``` */ export const ensureEndSlash = (str) => { return string_ends_with(str, sep) ? str : str + sep; }; /** trim leading forward-slashes ("/") and dot-slashes ("./"), at the beginning a string. * but exclude non-trivial dotdotslash ("/../") from being wrongfully trimmed. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = trimStartDotSlashes * * eq(fn("///a/b.zip/c.txt"), "a/b.zip/c.txt") * eq(fn("///a/b.zip//"), "a/b.zip//") * eq(fn("//..//a/b.zip//"), "..//a/b.zip//") * eq(fn("/./..//a/b.zip//"), "..//a/b.zip//") * eq(fn("/./.././a/b.zip//"), ".././a/b.zip//") * eq(fn("///././///.////a/b.zip//"), "a/b.zip//") * eq(fn(".///././///.////a/b.zip//"), "a/b.zip//") * eq(fn("./././//././///.////a/b.zip//"), "a/b.zip//") * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt") * ``` */ export const trimStartDotSlashes = (str) => { return str.replace(leading_slashes_and_dot_slashes_regex, ""); }; /** trim all trivial trailing forward-slashes ("/") and dot-slashes ("./"), at the end a string. * but exclude non-trivial dotdotslash ("/../") from being wrongfully trimmed. * * TODO: this operation is somewhat expensive, because: * - the implementation uses regex, however it was not possible for me to design a regex that handles the input string as is, * so I resort to reversing the input string, and using a slightly easier-to-design regex that discovers trivial (dot)slashes in reverse order, * and then after the string replacement, I reverse it again and return it as the output. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = trimEndDotSlashes * * eq(fn("a/b.zip/c.txt"), "a/b.zip/c.txt") * eq(fn("//a/b.zip//"), "//a/b.zip") * eq(fn("/"), "") * eq(fn("./"), "") * eq(fn("//././//./"), "") * eq(fn(".//././//./"), "") * eq(fn(".//./..///./"), ".//./../") * eq(fn("/a/b.zip/./"), "/a/b.zip") * eq(fn("/a/b.zip/../"), "/a/b.zip/../") * eq(fn("/a/b.zip/..//"), "/a/b.zip/../") * eq(fn("/a/b.zip/.././"), "/a/b.zip/../") * eq(fn("a/b.zip///././///.////"), "a/b.zip") * eq(fn("/a/b.zip///././///.////"), "/a/b.zip") * eq(fn("/a/b.zip/.././/.././///.////"), "/a/b.zip/.././/../") * eq(fn("/a/b.zip/././././///.////"), "/a/b.zip") * eq(fn("/a/b.zip./././././///.////"), "/a/b.zip.") * eq(fn("/a/b.zip../././././///.////"), "/a/b.zip..") * eq(fn("/a/b.zip.../././././///.////"), "/a/b.zip...") * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt") * ``` */ export const trimEndDotSlashes = (str) => { const reversed_str = [...str].toReversed().join(""), trimmed_reversed_str = reversed_str.replace(reversed_trailing_slashes_and_dot_slashes_regex, ""); // there is a special case when the entirety of the original `str` is made up of only (dot)slashes and ends with one dotslashes "./" (trailing relative path navigation), // in which case, we will be left with one single "." as our `trimmed_reversed_str`, instead of an empty string. // so we handle this special case below, otherwise we process all other `trimmed_reversed_str` by reversing it once more. return trimmed_reversed_str === "." ? "" : [...trimmed_reversed_str].toReversed().join(""); }; /** trim leading and trailing forward-slashes ("/") and dot-slashes ("./"), at the beginning and end of a string, but keep trailing non-trivial ones intact. * this is a combination of {@link trimStartDotSlashes} and {@link trimEndDotSlashes}, so see their doc comments for more precise test cases. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = trimDotSlashes * * eq(fn("///a/b.zip//"), "a/b.zip") * eq(fn(".///../a/b.zip//"), "../a/b.zip") * eq(fn("//./././///././//../a/b.zip//"), "../a/b.zip") * eq(fn("file:///a/b.zip//c.txt"), "file:///a/b.zip//c.txt") * ``` */ export const trimDotSlashes = (str) => { return trimEndDotSlashes(trimStartDotSlashes(str)); }; /** TODO: purge this function in the future, if you absolutely do not use it anywhere. * @deprecated * * > [!note] * > you'd probably want to use {@link joinPaths} instead of this function, for any realistic set of path segments. * > not only is this more expensive to compute, it does not distinguish between a directory and a file path (intentionally). * * join path segments with forward-slashes in between, and remove redundant slashes ("/") and dotslashes ("./") around each segment (if any). * however, the first segment's leading and trailing slashes are left untouched, * because that would potentially strip away location information (such as relative path ("./"), or absolute path ("/"), or some uri ("file:///")). * * if you want to ensure that your first segment is shortened, use either the {@link normalizePath} or {@link normalizePosixPath} function on it before passing it here. * * > [!warning] * > it is recommended that you use segments with posix path dir-separators ("/"). * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = joinSlash * * eq(fn(".///../a", "b", "c.txt"), ".///../a/b/c.txt") * eq(fn("file:///a/", "b.zip//", "./c.txt"), "file:///a/b.zip/c.txt") * eq(fn("file:///", "a/", "b.zip//", "./c.txt"), "file:///a/b.zip/c.txt") * eq(fn("///a//", "b.//", "zip..//"), "///a//b./zip..") * eq(fn("a/", "b.zip", "./c.txt", ""), "a/b.zip/c.txt/") * eq(fn("a/", "b.zip", "./c.txt", "."), "a/b.zip/c.txt/") * eq(fn("a/", "b.zip", "./c.txt", "./"), "a/b.zip/c.txt/") * eq(fn("a/", "b.zip", "./c.txt", ".."), "a/b.zip/c.txt/..") * eq(fn("a/", "b.zip", "./c.txt", "..."), "a/b.zip/c.txt/...") * eq(fn("", "", ""), "") * eq(fn("/", "", ""), "/") * eq(fn("/", "/", ""), "/") * eq(fn("/", "", "/"), "/") * eq(fn("/", "/", "/"), "/") * eq(fn("./", "", ""), "./") * eq(fn("./", "./", ""), "./") * eq(fn("./", "", "./"), "./") * eq(fn("./", "./", "./"), "./") * eq(fn( * "//./././///././//../a/b.zip/.////", * "///.////././.././c.txt/./../", * "../../d.xyz//.//", * ), "//./././///././//../a/b.zip/.////.././c.txt/./../../../d.xyz") * ``` */ export const joinSlash = (first_segment = "", ...segments) => { return segments .map(trimDotSlashes) .reduce((output, subpath) => ((output === "" ? "" : ensureEndSlash(output)) + subpath), first_segment); }; /** normalize a path by reducing and removing redundant dot-slash ("./" and "../") path navigators from a path. * * if you provide the optional `config` with the `keepRelative` set to `false`, then in the output, there will no be leading dot-slashes ("./"). * read more about the option here: {@link NormalizePathConfig.keepRelative}. * but note that irrespective of what you set this option to be, leading leading dotdot-slashes ("../") and leading slashes ("/") will not be trimmed. * * even though `config` should be of {@link NormalizePathConfig} type, it also accepts `number` so that the function's signature becomes compatible with the `Array.prototype.map` method, * however, unless you pass the correct config object type, only the default action will be taken. * * > [!warning] * > you MUST provide a posix path (i.e. use "/" for dir-separator). * > there will not be any implicit conversion of windows "\\" dir-separator. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = normalizePosixPath * // aliasing the config for disabling the preservation of leading "./" * const remove_rel: NormalizePathConfig = { keepRelative: false } * * eq(fn("../a/./b/../././/c.txt"), "../a//c.txt") * eq(fn("./././a/b/.././././//c.txt"), "./a///c.txt") * eq(fn("./././a/b/.././././//c.txt", remove_rel), "a///c.txt") * eq(fn("/a/b/.././././//c.txt"), "/a///c.txt") * eq(fn("///a/b/.././././//c.txt"), "///a///c.txt") * eq(fn("///a/b/.././.././//c.txt"), "/////c.txt") * eq(fn("file:///./././a/b/.././././c.txt"), "file:///a/c.txt") * eq(fn("/a/../"), "/") * // NOTICE: the test in the next line may seem like a weird behavior. * eq(fn("/a/../../"), "") * eq(fn("/a/../../../"), "../") * eq(fn("./a/../../"), "../") * eq(fn("./a/../../../"), "../../") * eq(fn("/a/b/../.."), "/") * eq(fn("/a/b/."), "/a/b/") * eq(fn("/a/b/./"), "/a/b/") * eq(fn("/a/b/c/.."), "/a/b/") * eq(fn("/a/b/c/../."), "/a/b/") * eq(fn("/a/b/c/d/../.."), "/a/b/") * eq(fn("/a/b/c/../.nomedia"), "/a/b/.nomedia") * eq(fn(""), "") * eq(fn("."), ".") * eq(fn(".."), "..") * eq(fn("./"), "./") * eq(fn("../"), "../") * eq(fn("../."), "../") * eq(fn("../.."), "../../") * eq(fn("./././././"), "./") * eq(fn(".././././"), "../") * eq(fn("./././.././././"), "../") * eq(fn("./././.././.././"), "../../") * eq(fn(".", remove_rel), "") * eq(fn("./", remove_rel), "") * eq(fn("./././././", remove_rel), "") * eq(fn("./././.././././", remove_rel), "../") * eq(fn("./././.././.././", remove_rel), "../../") * ``` */ export const normalizePosixPath = (path, config = {}) => { const { keepRelative = true } = isObject(config) ? config : {}, segments = path.split(sep), last_segment = segments.at(-1), output_segments = [".."], // a flag that specifies whether a "./" should be prepended to final result (assuming that the result does not being with "../") prepend_relative_dotslash_to_output_segments = keepRelative && segments[0] === ".", // a flag that specifies whether the input path ends with a "/." or "/..", in which case we will need to append a final "/" to the output. // this is because our for-loop will strip away the final dot character, without any upcoming replacements. // for instance, an input `path = "/a/b/."` would normalize to `"/a/b/"` with this flag. but without it, it will become `"/a/b"`. ends_with_dir_navigator_without_a_trailing_slash = (segments.length >= 2) && (last_segment === "." || last_segment === ".."); if (ends_with_dir_navigator_without_a_trailing_slash) { segments.push(""); } for (const segment of segments) { if (segment === "..") { if (output_segments.at(-1) !== "..") { output_segments.pop(); } else { output_segments.push(segment); } } else if (segment !== ".") { output_segments.push(segment); } } output_segments.shift(); if (prepend_relative_dotslash_to_output_segments && output_segments[0] !== "..") { output_segments.unshift("."); } return output_segments.join(sep); }; /** normalize a path by reducing and removing redundant dot-slash ("./", "../", ".\\", and "..\\") path navigators from a path. * the returned output is always a posix-style path. * * to read about the optional `config` parameter, refer to the docs of {@link normalizePosixPath}, which is the underlying function that takes care most of the normalization. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = normalizePath * * eq(fn("../a/./b/../././/c.txt"), "../a//c.txt") * eq(fn("./.\\.\\a\\b\\.././.\\.///c.txt"), "./a///c.txt") * eq(fn("/home\\.config/a\\..\\...\\b\\./c.txt"), "/home/.config/.../b/c.txt") * eq(fn("file:///./././a\\b/..\\././.\\c.txt"), "file:///a/c.txt") * ``` */ export const normalizePath = (path, config) => { return normalizePosixPath(pathToPosixPath(path), config); }; /** convert windows directory slash "\\" to posix directory slash "/". * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = pathToPosixPath * * eq(fn("C:\\Users/my name\\file.txt"), "C:/Users/my name/file.txt") * eq(fn("~/path/to/file.txt"), "~/path/to/file.txt") * eq(fn("/path\\to file.txt"), "/path/to file.txt") * ``` */ export const pathToPosixPath = (path) => path.replaceAll(windows_directory_slash_regex, sep); /** convert an array of paths to cli compatible list of paths, suitable for setting as an environment variable. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = pathsToCliArg * * // conversion example with windows separator (";") * eq(fn(";", [ * "./a/b/c.txt", * "C:\\Android Studio\\sdk\\", * "build\\libs\\" * ]), `"./a/b/c.txt;C:/Android Studio/sdk/;build/libs/"`) * * // conversion example with unix separator (":") * eq(fn(":", [ * "./a/b/c.txt", * "~/Android Studio/sdk/", * "build/libs/" * ]), `"./a/b/c.txt:~/Android Studio/sdk/:build/libs/"`) * ``` */ export const pathsToCliArg = (separator, paths) => { return quote(pathToPosixPath(paths.join(separator))); }; /** find the prefix path directory common to all provided `paths`. * > [!warning] * > your paths MUST be normalized beforehand, and use posix dir-separators ("/"). * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = commonNormalizedPosixPath * * eq(fn([ * "C:/Hello/World/This/Is/An/Example/Bla.cs", * "C:/Hello/World/This/Is/Not/An/Example/", * "C:/Hello/Earth/Bla/Bla/Bla", * ]), "C:/Hello/") * * eq(fn([ * "C:/Hello/World/This/Is/An/Example/Bla.cs", * "C:/Hello/World/This/is/an/example/bla.cs", * "C:/Hello/World/This/Is/Not/An/Example/", * ]), "C:/Hello/World/This/") * * eq(fn([ * "./../Hello/World/Users/This/Is/An/Example/Bla.cs", * "./../Hello/World Users/This/Is/An/example/bla.cs", * "./../Hello/World-Users/This/Is/Not/An/Example/", * ]), "./../Hello/") * * eq(fn([ * "./Hello/World/Users/This/Is/An/Example/Bla.cs", * "./Hello/World/", * "./Hello/World", // the "World" here segment is not treated as a directory * ]), "./Hello/") * * eq(fn([ * "C:/Hello/World/", * "/C:/Hello/World/", * "C:/Hello/World/", * ]), "") // no common prefix was identified * ``` */ export const commonNormalizedPosixPath = (paths) => { const common_prefix = commonPrefix(paths), common_prefix_length = common_prefix.length; for (const path of paths) { const remaining_substring = path.slice(common_prefix_length); if (!string_starts_with(remaining_substring, sep)) { // it looks like the `path`'s common prefix is not followed by an immediate "/" separator. // thus, we must now reduce our `common_prefix` to the last available "/" separator. // after we do that, we are guaranteed that this newly created `common_dir_prefix` is indeed common to all `paths`, since its superset, the `common_prefix`, was also common to all `paths`. // thus we can immediately return and ignore the remaining tests in the loop. const common_dir_prefix_length = common_prefix.lastIndexOf(sep) + 1, common_dir_prefix = common_prefix.slice(0, common_dir_prefix_length); return common_dir_prefix; } } // if we have made it to here, it would mean that among all paths, the initial `common_prefix` was indeed also the common directory among all of them. return common_prefix; }; /** find the prefix path directory common to all provided `paths`. * your input `paths` do not need to be normalized nor necessarily use posix-style separator "/". * under the hood, this function normalizes and converts all paths to posix-style, then applies the {@link commonNormalizedPosixPath} onto them. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = commonPath * * eq(fn([ * "C:/Hello/World/This/Is/An/Example/Bla.cs", * "C:\\Hello\\World\\This\\Is\\Not/An/Example/", * "C:/Hello/Earth/Bla/Bla/Bla", * ]), "C:/Hello/") * * eq(fn([ * "./Hello/World/This/Used/to-be-an/example/../../../Is/An/Example/Bla.cs", * ".\\Hello/World/This/Is/an/example/bla.cs", * "./Hello/World/This/Is/Not/An/Example/", * ]), "./Hello/World/This/Is/") * * eq(fn([ * "./../home/Hello/World/Users/This/Is/An/Example/Bla.cs", * "././../home\\Hello\\World Users\\This\\Is/An\\example/bla.cs", * "./../home/./.\\.\\././Hello/World-Users/./././././This/Is/Not/An/Example/", * ]), "../home/Hello/") * * eq(fn([ * "\\C:/Hello/World/Users/This/Is/An/Example/Bla.cs", * "/C:\\Hello\\World Users\\This\\Is/An\\example/bla.cs", * "/C:/Hello/World", // the "World" here segment is not treated as a directory * ]), "/C:/Hello/") * ``` */ export const commonPath = (paths) => { return commonNormalizedPosixPath(paths.map(normalizePath)); }; /** replace the common path among all provided `paths` by transforming it with a custom `map_fn` function. * all `paths` are initially normalized and converted into posix-style (so that no "\\" windows separator is prevelent). * * the `map_fn` function's first argument (`path_info`), is a 2-tuple of the form `[common_dir: string, subpath: string]`, * where `common_dir` represents the directory common to all of the input `paths`, and the `subpath` represents the remaining relative path that comes after common_dir. * - the `common_dir` always ends with a trailing slash ("/"), unless there is absolutely no common directory among the `paths` at all. * - the `subpath` never begins with any slash (nor any dot-slashes), unless of course, you had initially provided a path containing two or more consecutive slashes. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * // aliasing our functions for brevity * const eq = assertEquals, fn = commonPathTransform * * const subpath_map_fn = ([common_dir, subpath]: [string, string]) => (subpath) * * eq(fn([ * "C:/Hello/World/This/Is/An/Example/Bla.cs", * "C:\\Hello\\World\\This\\Is\\Not/An/Example/", * "C:/Hello/Earth/Bla/Bla/Bla", * ], subpath_map_fn), [ * "World/This/Is/An/Example/Bla.cs", * "World/This/Is/Not/An/Example/", * "Earth/Bla/Bla/Bla", * ]) * * eq(fn([ * "./../././home/Hello/World/This/Used/to-be-an/example/../../../Is/An/Example/Bla.cs", * "./././../home/Hello/World/This/Is/an/example/bla.cs", * "./../home/Hello/World/This/Is/Not/An/Example/", * ], subpath_map_fn), [ * "An/Example/Bla.cs", * "an/example/bla.cs", * "Not/An/Example/", * ]) * * eq(fn([ * "/C:/Hello///World/Users/This/Is/An/Example/Bla.cs", * "/C:\\Hello\\World Users\\This\\Is/An\\example/bla.cs", * "/C:/./.\\.\\././Hello/World-Users/./././././This/Is/Not/An/Example/", * ], subpath_map_fn), [ * "//World/Users/This/Is/An/Example/Bla.cs", * "World Users/This/Is/An/example/bla.cs", * "World-Us