furi
Version:
File URI manipulation library
504 lines (503 loc) • 14.3 kB
JavaScript
/**
* @module furi
*/
class InvalidFileUri extends TypeError {
constructor(input) {
super();
this.input = input;
this.message = `Invalid file URI: ${input}`;
this.name = "TypeError [ERR_INVALID_FILE_URI]";
this.code = "ERR_INVALID_FILE_URI";
}
}
/**
* A class representing a normalized absolute `file://` URI.
*
* This is a subclass of `url.URL` with the following extra checks:
* - The protocol is `file:`.
* - The pathname does not contain consecutive slashes (`a//b`) ("normalization").
* - The pathname does not contain the `.` or `..` segments (enforced by `url.URL` already).
* - The host `localhost` is represented as the empty string (enforced by `url.URL` already).
*
* This class extends `url.URL`. This means that you can pass it to any
* function expecting a `url.URL`. It also means that the URI is always
* absolute.
*
* Notes:
* - A single trailing slash is allowed.
* - The hostname is allowed to be any string. A non-empty string is used by Windows to represents
* files on a network drive. An empty string means `localhost`.
* - The `username`, `password` and `port` properties are always `""` (enforced by `url.URL`
* already). This implies that `host` only contains `hostname`.
* - The `search` and `hash` properties can have any value.
*/
export class Furi extends URL {
constructor(input) {
const strInput = `${input}`;
super(strInput);
if (this.protocol !== "file:") {
throw new InvalidFileUri(strInput);
}
if (this.pathname.indexOf("//") >= 0) {
this.pathname = this.pathname.replace(/\/+/g, "/");
}
}
// @ts-ignore
get protocol() {
// @ts-ignore
return super.protocol;
}
set protocol(value) {
if (value !== "file:") {
return;
}
// @ts-ignore
super.protocol = value;
}
// @ts-ignore
get pathname() {
// @ts-ignore
return super.pathname;
}
set pathname(value) {
if (value.indexOf("//") >= 0) {
value = value.replace(/\/+/g, "/");
}
// @ts-ignore
super.pathname = value;
}
hasTrailingSlash() {
return this.pathname !== "/" && this.pathname.endsWith("/");
}
setTrailingSlash(hasTrailingSlash) {
if (this.pathname === "/") {
return;
}
if (this.pathname.endsWith("/")) {
if (!hasTrailingSlash) {
this.pathname = this.pathname.substring(0, this.pathname.length - 1);
}
} else if (hasTrailingSlash) {
this.pathname = `${this.pathname}/`;
}
}
toSysPath(windowsLongPath = false) {
return toSysPath(this, windowsLongPath);
}
toPosixPath() {
return toPosixPath(this);
}
toWindowsShortPath() {
return toWindowsShortPath(this);
}
toWindowsLongPath() {
return toWindowsLongPath(this);
}
}
/**
* Normalizes the input to a `Furi` instance.
*
* @param input URL string or instance to normalize.
* @returns `Furi` instance. It is always a new instance.
*/
export function asFuri(input) {
if (input instanceof URL) {
return new Furi(input.toString());
} else {
return new Furi(input);
}
}
/**
* Normalizes the input to a writable `URL` instance.
*
* @param input URL string or instance to normalize.
*/
export function asWritableUrl(input) {
return new URL(typeof input === "string" ? input : input.toString());
}
/**
* Appends the provided components to the pathname of `base`.
*
* It does not mutate the inputs.
* If component list is non-empty, the `hash` and `search` are set to the
* empty string.
*
* @param base Base URL.
* @param paths Paths to append. A path is either a string representing a relative or absolute file URI, or an array
* of components. When passing an array of components, each component will be URI-encoded before being
* appended.
* @returns Joined URL.
*/
export function join(base, ...paths) {
const result = asFuri(base);
if (paths.length === 0) {
return result;
}
let hasTrailingSlash = result.hasTrailingSlash();
const segments = result.pathname.split("/");
for (const p of paths) {
let pathStr;
if (typeof p === "string") {
if (p === "") {
continue;
}
pathStr = p;
} else {
if (p.length === 0) {
continue;
}
pathStr = `./${p.map(encodeURIComponent).join("/")}`;
}
for (const segment of pathStr.split("/")) {
segments.push(segment);
hasTrailingSlash = segment === "";
}
}
result.pathname = segments.join("/");
result.setTrailingSlash(hasTrailingSlash);
result.hash = "";
result.search = "";
return result;
}
/**
* Computes the relative or absolute `file://` URI from `from` to `to`.
*
* The result is an absolute URI only if the arguments have different hosts
* (for example when computing a URI between different Windows networked drives).
*
* If both URIs are equivalent, returns `""`.
*
* Otherwise, returns a relative URI starting with `"./"` or `"../".
*
* @param from Source URI.
* @param to Destination URI.
* @returns Relative (or absolute) URI between the two arguments.
*/
export function relative(from, to) {
if (from === to) {
return "";
}
const fromUri = asFuri(from);
const toUri = asFuri(to);
if (fromUri.host !== toUri.host) {
return toUri.toString();
}
fromUri.setTrailingSlash(false);
const fromSegments = fromUri.pathname === "/" ? [""] : fromUri.pathname.split("/");
const toSegments = toUri.pathname === "/" ? [""] : toUri.pathname.split("/");
let commonSegments = 0;
for (let i = 0; i < Math.min(fromSegments.length, toSegments.length); i++) {
const fromSegment = fromSegments[i];
const toSegment = toSegments[i];
if (fromSegment === toSegment) {
commonSegments++;
} else {
break;
}
}
const resultSegments = [];
if (commonSegments === fromSegments.length) {
if (commonSegments === toSegments.length) {
// TODO: Handle hash and search
return "";
}
resultSegments.push(".");
} else {
for (let i = commonSegments; i < fromSegments.length; i++) {
resultSegments.push("..");
}
}
resultSegments.push(...toSegments.slice(commonSegments));
return resultSegments.join("/");
}
/**
* Returns the basename of the file URI.
*
* This function is similar to Node's `require("path").basename`.
*
* @param furi Absolute `file://` URI.
* @param ext Extension (will be removed if present).
* @returns URI-encoded basename.
*/
export function basename(furi, ext) {
const readable = asFuri(furi);
const components = readable.pathname
.split("/")
.filter(c => c !== "");
const basename = components.length > 0 ? components[components.length - 1] : "";
if (ext !== undefined && ext.length > 0 && ext.length < basename.length) {
if (basename.endsWith(ext)) {
return basename.substring(0, basename.length - ext.length);
}
}
return basename;
}
/**
* Returns the parent URL.
*
* If `input` is the root, it returns itself (saturation).
* If `input` has a trailing separator, it is first removed.
*
* @param input Input URL.
* @returns Parent URL.
*/
export function parent(input) {
const writable = asWritableUrl(input);
const oldPathname = writable.pathname;
const components = oldPathname.split("/");
if (components[components.length - 1] === "") {
// Remove trailing separator
components.pop();
}
components.pop();
writable.pathname = components.join("/");
return writable;
}
/**
* Detect if the current platform uses Windows-style paths.
*
* This used automatically in functions prefixed by `sys` to get system-dependent behavior.
*/
export function isWindows() {
if (!globalThis?.process) {
return false;
}
if (globalThis?.process?.platform === "win32" || globalThis?.process?.platform === "cygwin") {
return true;
}
const osType = globalThis?.process?.env["OSTYPE"];
if (!(typeof osType === "string")) {
return false;
}
return /^(msys|cygwin)$/.test(osType);
}
/**
* Converts a File URI to a system-dependent path.
*
* Use `toPosixPath`, `toWindowsShortPath` or `toWindowsLongPath` if you
* want system-independent results.
*
* Example:
* ```js
* // On a Windows system:
* toSysPath("file:///C:/dir/foo");
* // -> "C:\\dir\\foo";
* toSysPath("file:///C:/dir/foo", true);
* // -> "\\\\?\\C:\\dir\\foo";
*
* // On a Posix system:
* toSysPath("file:///dir/foo");
* // -> "/dir/foo";
* ```
*
* @param furi File URI to convert.
* @param windowsLongPath Use long paths on Windows. (default: `false`)
* @return System-dependent path.
*/
export function toSysPath(furi, windowsLongPath = false) {
if (isWindows()) {
return windowsLongPath ? toWindowsLongPath(furi) : toWindowsShortPath(furi);
} else {
return toPosixPath(furi);
}
}
/**
* Converts a File URI to a Windows short path.
*
* The result is either a short device path or a short UNC server path.
*
* Example:
* ```js
* toSysPath("file:///C:/dir/foo");
* // -> "C:\\dir\\foo";
* toSysPath("file://server/Users/foo");
* // -> "\\\\server\\Users\\foo";
* ```
*
* @param furi File URI to convert.
* @return Windows short path.
*/
export function toWindowsShortPath(furi) {
const urlObj = asFuri(furi);
if (urlObj.host === "") {
// Local drive path
const pathname = urlObj.pathname.substring(1);
const forward = pathname.split("/").map(decodeURIComponent).join("/");
return toBackwardSlashes(forward);
} else {
// Server path
const pathname = urlObj.pathname;
const forward = pathname.split("/").map(decodeURIComponent).join("/");
const backward = toBackwardSlashes(forward);
return `\\\\${urlObj.host}${backward}`;
}
}
/**
* Converts a File URI to a Windows long path.
*
* The result is either a long device path or a long UNC server path.
*
* Example:
* ```js
* toWindowsPath("file:///C:/dir/foo");
* // -> "\\\\?\\C:\\dir\\foo";
* toWindowsPath("file://server/Users/foo");
* // -> "\\\\?\\unc\\server\\Users\\foo";
* ```
*
* @param furi File URI to convert.
* @return Windows long path.
*/
export function toWindowsLongPath(furi) {
const urlObj = asFuri(furi);
if (urlObj.host === "") {
// Local drive path
const pathname = urlObj.pathname.substring(1);
const forward = pathname.split("/").map(decodeURIComponent).join("/");
const backward = toBackwardSlashes(forward);
return `\\\\?\\${backward}`;
} else {
// Server path
const pathname = urlObj.pathname;
const forward = pathname.split("/").map(decodeURIComponent).join("/");
const backward = toBackwardSlashes(forward);
return `\\\\?\\unc\\${urlObj.host}${backward}`;
}
}
/**
* Converts a File URI to a Posix path.
*
* Requires the host to be either an empty string or `"localhost"`.
*
* Example:
* ```js
* toPosixPath("file:///dir/foo");
* // -> "/dir/foo";
* ```
*
* @param furi File URI to convert.
* @return Posix path.
*/
export function toPosixPath(furi) {
const urlObj = asFuri(furi);
if (urlObj.host !== "" && urlObj.host !== "localhost") {
throw new Error(`furi: expected \`host\` to be "" or "localhost": ${furi}`);
}
const pathname = urlObj.pathname;
return pathname.split("/").map(decodeURIComponent).join("/");
}
/**
* Converts an absolute system-dependent path to a frozen URL object.
*
* Use `fromPosixPath` or `fromWindowsPath` if you want system-independent
* results.
*
* Example:
* ```js
* // On a Windows system:
* fromSysPath("C:\\dir\\foo");
* // -> new URL("file:///C:/dir/foo");
*
* // On a Posix system:
* fromSysPath("/dir/foo");
* // -> new URL("file:///dir/foo");
* ```
*
* @param absPath Absolute system-dependent path to convert
* @return Frozen `file://` URL object.
*/
export function fromSysPath(absPath) {
return isWindows() ? fromWindowsPath(absPath) : fromPosixPath(absPath);
}
const WINDOWS_PREFIX_REGEX = /^[\\/]{2,}([^\\/]+)(?:$|[\\/]+)/;
const WINDOWS_UNC_REGEX = /^unc(?:$|[\\/]+)([^\\/]+)(?:$|[\\/]+)/i;
/**
* Converts an absolute Windows path to a frozen URL object.
*
* Example:
* ```js
* fromWindowsPath("C:\\dir\\foo");
* // -> new URL(file:///C:/dir/foo");
* fromWindowsPath("\\\\?\\unc\\server\\Users\\foo");
* // -> new URL("file://server/Users/foo");
* ```
*
* @param absPath Absolute Windows path to convert
* @return Frozen `file://` URL object.
*/
export function fromWindowsPath(absPath) {
const prefixMatch = WINDOWS_PREFIX_REGEX.exec(absPath);
if (prefixMatch === null) {
// Short device path
return formatFileUrl(`/${toForwardSlashes(absPath)}`);
}
const prefix = prefixMatch[1];
const tail = absPath.substring(prefixMatch[0].length);
if (prefix !== "?") {
// Short server path
const result = new URL("file:///");
result.host = prefix;
result.pathname = encodeURI(`/${toForwardSlashes(tail)}`);
return result;
}
// Long path
const uncMatch = WINDOWS_UNC_REGEX.exec(tail);
if (uncMatch === null) {
// Long device path
return formatFileUrl(`/${toForwardSlashes(tail)}`);
} else {
// Long server path
const host = uncMatch[1];
const serverPath = tail.substring(uncMatch[0].length);
const result = new URL("file:///");
result.host = host;
result.pathname = encodeURI(`/${toForwardSlashes(serverPath)}`);
return result;
}
}
/**
* Converts an absolute Posix path to a frozen URL object.
*
* Example:
* ```js
* fromPosixPath("/dir/foo");
* // -> new URL(file:///dir/foo");
* ```
*
* @param absPath Absolute Posix path to convert
* @return Frozen `file://` URL object.
*/
export function fromPosixPath(absPath) {
return formatFileUrl(absPath);
}
/**
* Replaces all the backward slashes by forward slashes.
*
* @param str Input string.
* @internal
*/
function toForwardSlashes(str) {
return str.replace(/\\/g, "/");
}
/**
* Replaces all the forward slashes by backward slashes.
*
* @param str Input string.
* @internal
*/
function toBackwardSlashes(str) {
return str.replace(/\//g, "\\");
}
/**
* Creates a frozen `file://` URL using the supplied `pathname`.
*
* @param pathname Pathname for the URL object.
* @return Frozen `file://` URL object.
* @internal
*/
function formatFileUrl(pathname) {
const result = new URL("file:///");
result.pathname = encodeURI(pathname);
return result;
}
// # sourceMappingURL=index.mjs.map