/** * @description 判断给定值是否为字符串。 * * @example * ``` * // Expect: true * const example1 = internalIsString("/app/page") * * // Expect: false * const example2 = internalIsString(["app", "page"]) * ``` */ const internalIsString = (value: unknown): value is string => { return typeof value === "string" } /** * @description 判断给定值是否为数组。 * * @example * ``` * // Expect: true * const example1 = internalIsArray(["app", "page"]) * * // Expect: false * const example2 = internalIsArray("/app/page") * ``` */ const internalIsArray = (value: unknown): value is unknown[] => { return Array.isArray(value) } /** * @description 判断给定值是否为真值。 * * @example * ``` * // Expect: true * const example1 = internalIsTruthy("page") * * // Expect: false * const example2 = internalIsTruthy("") * ``` */ const internalIsTruthy = (value: unknown): boolean => { return Boolean(value) } /** * @description 以字符串形式表示的 pathname。 * * @example * ``` * // Expect: "/app/page" * const example1: PathnameString = "/app/page" * ``` */ export type PathnameString = string /** * @description 以路径段数组形式表示的 pathname。 * * @example * ``` * // Expect: ["", "app", "page"] * const example1: PathnameArray = ["", "app", "page"] * ``` */ export type PathnameArray = string[] /** * @description Pathname 支持的输入或输出形态。 * * @example * ``` * // Expect: "/app/page" * const example1: PathnameUnion = "/app/page" * * // Expect: ["", "app", "page"] * const example2: PathnameUnion = ["", "app", "page"] * ``` */ export type PathnameUnion = PathnameString | PathnameArray /** * @description 移除数组中相邻且重复的字符串。 * * @example * ``` * // Expect: ["", "app", "page", ""] * const example1 = removeConsecutiveRepetition(["", "", "app", "app", "page", ""]) * ``` */ export const removeConsecutiveRepetition = (targetStrings: string[]): string[] => { return targetStrings.filter((item: string, index: number, array: string[]) => { return array[index - 1] !== undefined ? item !== array[index - 1] : true }) } /** * @description 仅对指定目标值移除相邻重复项。 * * @example * ``` * // Expect: ["", "app", "/", "page", "page"] * const example1 = removeConsecutiveRepetitionOf(["", "", "app", "/", "/", "page", "page"], ["", "/"]) * ``` */ export const removeConsecutiveRepetitionOf = (target: string[], ofList: string[]): string[] => { return target.filter((item: string, index: number, array: string[]) => { return array[index - 1] !== undefined && ofList.includes(item) ? item !== array[index - 1] : true }) } /** * @description 移除不在例外名单中的相邻重复项。 * * @example * ``` * // Expect: ["", "", "app", "page", "", ""] * const example1 = removeConsecutiveRepetitionExcept(["", "", "app", "app", "page", "page", "", ""], [""]) * ``` */ export const removeConsecutiveRepetitionExcept = ( target: string[], exceptList: string[], ): string[] => { return target.filter((item: string, index: number, array: string[]) => { return array[index - 1] !== undefined && !exceptList.includes(item) ? item !== array[index - 1] : true }) } /** * @description 移除相邻重复的空字符串。 * * @example * ``` * // Expect: ["", "app", "", "page", ""] * const example1 = removeConsecutiveRepetitionOfEmpty(["", "", "app", "", "", "page", ""]) * ``` */ export const removeConsecutiveRepetitionOfEmpty = (target: string[]): string[] => { return removeConsecutiveRepetitionOf(target, [""]) } /** * @description 移除相邻重复的斜杠字符。 * * @example * ``` * // Expect: ["/", "a", "/", "b"] * const example1 = removeConsecutiveRepetitionOfSlash(["/", "/", "a", "/", "/", "/", "b"]) * ``` */ export const removeConsecutiveRepetitionOfSlash = (target: string[]): string[] => { return removeConsecutiveRepetitionOf(target, ["/"]) } /** * @description 移除路径数组内部的空字符串,但保留首尾位置的空字符串。 * * @example * ``` * // Expect: ["", "app", "page", ""] * const example1 = removeInnerEmptyStringInArray(["", "app", "", "page", ""]) * ``` */ export const removeInnerEmptyStringInArray = (target: string[]): string[] => { return target.filter((item, index, arr) => index === 0 || index === arr.length - 1 ? true : item !== "", ) } /** * @description 保证根路径至少保留为 `["", ""]` 的最小结构。 * * @example * ``` * // Expect: ["", ""] * const example1 = keepMinimumPathArray([""]) * * // Expect: ["", "app"] * const example2 = keepMinimumPathArray(["", "app"]) * ``` */ export const keepMinimumPathArray = (target: string[]): string[] => { return target.length === 1 && target[0] === "" ? ["", ""] : target } /** * @description 规范化 pathname,使其具备稳定的根前缀和重复分隔符语义。 * * @example * ``` * // Expect: ["", "app", "page", ""] * const example1 = neatenPathname(["app", "", "page", ""]) * * // Expect: "/app/page/" * const example2 = neatenPathname("//app//page//") * ``` */ export const neatenPathname = (pathname: PathnameUnion): typeof pathname => { if (internalIsArray(pathname)) { // 第一步:补上根路径前缀,使数组形式始终从空字符串起始。 const _0 = ["", ...pathname] // 第二步:折叠连续的空字符串,避免重复分隔段。 const _1 = removeConsecutiveRepetitionOfEmpty(_0) // 第三步:确保根路径仍至少保留 `['', '']` 的最小表达。 const _2 = keepMinimumPathArray(_1) // 第四步:移除中间无意义的空路径段,同时保留首尾语义。 const _3 = removeInnerEmptyStringInArray(_2) return _3 } else if (internalIsString(pathname)) { // 第一步:拆成字符数组,便于统一处理连续斜杠。 // oxlint-disable-next-line typescript/no-misused-spread const _0 = [...pathname] // 第二步:补上根路径斜杠,确保字符串形式总是绝对路径语义。 const _1 = ["/", ..._0] // 第三步:折叠连续斜杠,避免出现多余分隔符。 const _2 = removeConsecutiveRepetitionOfSlash(_1) // 第四步:回到标准字符串,再按路径段拆分做后续清理。 const _3 = _2.join("") const _4 = _3.split("/") // 第五步:折叠空路径段,并保留根与尾部斜杠语义。 const _5 = removeConsecutiveRepetitionOfEmpty(_4) const _6 = keepMinimumPathArray(_5) // 第六步:重新拼回字符串形式,得到稳定输出。 const _7 = _6.join("/") return _7 } else { throw new TypeError('"pathname" must be string or array.') } } /** * @description 将 pathname 转换为路径段数组形式。 * * @example * ``` * // Expect: ["", "app", "page"] * const example1 = toPathnameArray("app/page") * * // Expect: ["", "app", "page", ""] * const example2 = toPathnameArray(["", "app", "", "page", ""]) * ``` */ export const toPathnameArray = (pathname: PathnameUnion): PathnameArray => { const neatedPathname = neatenPathname(pathname) if (internalIsArray(neatedPathname)) { return neatedPathname } if (internalIsString(neatedPathname)) { return neatedPathname.split("/") } else { throw new TypeError('"pathname" must be string or array.') } } /** * @description 将 pathname 转换为标准字符串形式。 * * @example * ``` * // Expect: "/app/page" * const example1 = toPathnameString(["app", "page"]) * * // Expect: "/app/page/" * const example2 = toPathnameString("//app//page//") * ``` */ export const toPathnameString = (pathname: PathnameUnion): PathnameString => { const neatedPathname = neatenPathname(pathname) if (internalIsString(neatedPathname)) { return neatedPathname } if (internalIsArray(neatedPathname)) { return neatedPathname.join("/") } else { throw new TypeError('"pathname" must be string or array.') } } export const isPathnameLooseIncludes = ( pathname: PathnameUnion, pieces: string | string[], ): boolean => { const pathnameArray = toPathnameArray(pathname) const piecesArray = internalIsArray(pieces) ? pieces : [pieces] return piecesArray.every((piece) => pathnameArray.includes(piece)) } export const isPathnameStrictIncludes = ( pathname: PathnameUnion, pieces: string | string[], ): boolean => { const pathnameString = toPathnameString(pathname) const piecesString = toPathnameString(pieces) return pathnameString.includes(piecesString) } export const isPathnameIncludes = ( mode: "strict" | "loose", pathname: PathnameUnion, pieces: string | string[], ): boolean => { switch (mode) { case "strict": { return isPathnameStrictIncludes(pathname, pieces) } case "loose": { return isPathnameLooseIncludes(pathname, pieces) } default: { throw new TypeError('"mode" must be "strict" or "loose".') } } } /** * @description 以严格模式比较两个 pathname 是否相等。 * * @example * ``` * // Expect: true * const example1 = isPathnameStrictEqual("/app/page", "/app/page") * * // Expect: false * const example2 = isPathnameStrictEqual("/app/page", "/app/page/") * ``` */ export const isPathnameStrictEqual = ( pathname1: PathnameUnion, pathname2: PathnameUnion, ): boolean => { return toPathnameString(pathname1) === toPathnameString(pathname2) } /** * @description 以宽松模式比较两个 pathname 是否相等,并忽略尾部斜杠差异。 * * @example * ``` * // Expect: true * const example1 = isPathnameLooseEqual("/app/page", "/app/page") * * // Expect: true * const example2 = isPathnameLooseEqual("/app/page", "/app/page/") * ``` */ export const isPathnameLooseEqual = ( pathname1: PathnameUnion, pathname2: PathnameUnion, ): boolean => { const preCook = (pathname: PathnameUnion): string => { return toPathnameArray(pathname) .filter((str) => internalIsTruthy(str)) .join("/") } return preCook(pathname1) === preCook(pathname2) } /** * @description 根据指定模式比较两个 pathname 是否相等。 * * @example * ``` * // Expect: true * const example1 = isPathnameEqual("strict", "/app/page", ["", "app", "page"]) * * // Expect: true * const example2 = isPathnameEqual("loose", "/app/page", "/app/page/") * ``` */ export const isPathnameEqual = ( mode: "strict" | "loose", pathname1: PathnameUnion, pathname2: PathnameUnion, ): boolean => { switch (mode) { case "strict": { return isPathnameStrictEqual(pathname1, pathname2) } case "loose": { return isPathnameLooseEqual(pathname1, pathname2) } default: { throw new TypeError('"mode" must be "strict" or "loose".') } } }