| /** | |
| * @fileoverview The Path class. | |
| * @author Nicholas C. Zakas | |
| */ | |
| /* globals URL */ | |
| //----------------------------------------------------------------------------- | |
| // Types | |
| //----------------------------------------------------------------------------- | |
| /** @typedef{import("@humanfs/types").HfsImpl} HfsImpl */ | |
| /** @typedef{import("@humanfs/types").HfsDirectoryEntry} HfsDirectoryEntry */ | |
| //----------------------------------------------------------------------------- | |
| // Helpers | |
| //----------------------------------------------------------------------------- | |
| /** | |
| * Normalizes a path to use forward slashes. | |
| * @param {string} filePath The path to normalize. | |
| * @returns {string} The normalized path. | |
| */ | |
| function normalizePath(filePath) { | |
| let startIndex = 0; | |
| let endIndex = filePath.length; | |
| if (/[a-z]:\//i.test(filePath)) { | |
| startIndex = 3; | |
| } | |
| if (filePath.startsWith("./")) { | |
| startIndex = 2; | |
| } | |
| if (filePath.startsWith("/")) { | |
| startIndex = 1; | |
| } | |
| if (filePath.endsWith("/")) { | |
| endIndex = filePath.length - 1; | |
| } | |
| return filePath.slice(startIndex, endIndex).replace(/\\/g, "/"); | |
| } | |
| /** | |
| * Asserts that the given name is a non-empty string, no equal to "." or "..", | |
| * and does not contain a forward slash or backslash. | |
| * @param {string} name The name to check. | |
| * @returns {void} | |
| * @throws {TypeError} When name is not valid. | |
| */ | |
| function assertValidName(name) { | |
| if (typeof name !== "string") { | |
| throw new TypeError("name must be a string"); | |
| } | |
| if (!name) { | |
| throw new TypeError("name cannot be empty"); | |
| } | |
| if (name === ".") { | |
| throw new TypeError(`name cannot be "."`); | |
| } | |
| if (name === "..") { | |
| throw new TypeError(`name cannot be ".."`); | |
| } | |
| if (name.includes("/") || name.includes("\\")) { | |
| throw new TypeError( | |
| `name cannot contain a slash or backslash: "${name}"`, | |
| ); | |
| } | |
| } | |
| //----------------------------------------------------------------------------- | |
| // Exports | |
| //----------------------------------------------------------------------------- | |
| export class Path { | |
| /** | |
| * The steps in the path. | |
| * @type {Array<string>} | |
| */ | |
| #steps; | |
| /** | |
| * Creates a new instance. | |
| * @param {Iterable<string>} [steps] The steps to use for the path. | |
| * @throws {TypeError} When steps is not iterable. | |
| */ | |
| constructor(steps = []) { | |
| if (typeof steps[Symbol.iterator] !== "function") { | |
| throw new TypeError("steps must be iterable"); | |
| } | |
| this.#steps = [...steps]; | |
| this.#steps.forEach(assertValidName); | |
| } | |
| /** | |
| * Adds steps to the end of the path. | |
| * @param {...string} steps The steps to add to the path. | |
| * @returns {void} | |
| */ | |
| push(...steps) { | |
| steps.forEach(assertValidName); | |
| this.#steps.push(...steps); | |
| } | |
| /** | |
| * Removes the last step from the path. | |
| * @returns {string} The last step in the path. | |
| */ | |
| pop() { | |
| return this.#steps.pop(); | |
| } | |
| /** | |
| * Returns an iterator for steps in the path. | |
| * @returns {IterableIterator<string>} An iterator for the steps in the path. | |
| */ | |
| steps() { | |
| return this.#steps.values(); | |
| } | |
| /** | |
| * Returns an iterator for the steps in the path. | |
| * @returns {IterableIterator<string>} An iterator for the steps in the path. | |
| */ | |
| [Symbol.iterator]() { | |
| return this.steps(); | |
| } | |
| /** | |
| * Retrieves the name (the last step) of the path. | |
| * @type {string} | |
| */ | |
| get name() { | |
| return this.#steps[this.#steps.length - 1]; | |
| } | |
| /** | |
| * Sets the name (the last step) of the path. | |
| * @type {string} | |
| */ | |
| set name(value) { | |
| assertValidName(value); | |
| this.#steps[this.#steps.length - 1] = value; | |
| } | |
| /** | |
| * Retrieves the size of the path. | |
| * @type {number} | |
| */ | |
| get size() { | |
| return this.#steps.length; | |
| } | |
| /** | |
| * Returns the path as a string. | |
| * @returns {string} The path as a string. | |
| */ | |
| toString() { | |
| return this.#steps.join("/"); | |
| } | |
| /** | |
| * Creates a new path based on the argument type. If the argument is a string, | |
| * it is assumed to be a file or directory path and is converted to a Path | |
| * instance. If the argument is a URL, it is assumed to be a file URL and is | |
| * converted to a Path instance. If the argument is a Path instance, it is | |
| * copied into a new Path instance. If the argument is an array, it is assumed | |
| * to be the steps of a path and is used to create a new Path instance. | |
| * @param {string|URL|Path|Array<string>} pathish The value to convert to a Path instance. | |
| * @returns {Path} A new Path instance. | |
| * @throws {TypeError} When pathish is not a string, URL, Path, or Array. | |
| * @throws {TypeError} When pathish is a string and is empty. | |
| */ | |
| static from(pathish) { | |
| if (typeof pathish === "string") { | |
| if (!pathish) { | |
| throw new TypeError("argument cannot be empty"); | |
| } | |
| return Path.fromString(pathish); | |
| } | |
| if (pathish instanceof URL) { | |
| return Path.fromURL(pathish); | |
| } | |
| if (pathish instanceof Path || Array.isArray(pathish)) { | |
| return new Path(pathish); | |
| } | |
| throw new TypeError("argument must be a string, URL, Path, or Array"); | |
| } | |
| /** | |
| * Creates a new Path instance from a string. | |
| * @param {string} fileOrDirPath The file or directory path to convert. | |
| * @returns {Path} A new Path instance. | |
| * @deprecated Use Path.from() instead. | |
| */ | |
| static fromString(fileOrDirPath) { | |
| return new Path(normalizePath(fileOrDirPath).split("/")); | |
| } | |
| /** | |
| * Creates a new Path instance from a URL. | |
| * @param {URL} url The URL to convert. | |
| * @returns {Path} A new Path instance. | |
| * @throws {TypeError} When url is not a URL instance. | |
| * @throws {TypeError} When url.pathname is empty. | |
| * @throws {TypeError} When url.protocol is not "file:". | |
| * @deprecated Use Path.from() instead. | |
| */ | |
| static fromURL(url) { | |
| if (!(url instanceof URL)) { | |
| throw new TypeError("url must be a URL instance"); | |
| } | |
| if (!url.pathname || url.pathname === "/") { | |
| throw new TypeError("url.pathname cannot be empty"); | |
| } | |
| if (url.protocol !== "file:") { | |
| throw new TypeError(`url.protocol must be "file:"`); | |
| } | |
| // Remove leading slash in pathname | |
| return new Path(normalizePath(url.pathname.slice(1)).split("/")); | |
| } | |
| } | |