/** * @description 判断给定值是否为字符串。 * * @example * ``` * // Expect: true * const example1 = internalIsString("#page=1") * * // Expect: false * const example2 = internalIsString({ page: "1" }) * ``` */ const internalIsString = (value: unknown): value is string => { return typeof value === "string" } /** * @description 判断给定值是否为普通对象。 * * @example * ``` * // Expect: true * const example1 = internalIsObject({ page: "1" }) * * // Expect: false * const example2 = internalIsObject(["page"]) * ``` */ const internalIsObject = (value: unknown): value is Record => { return typeof value === "object" && value !== null && Array.isArray(value) === false } /** * @description 将单个 hash 条目拆分为 key 和 value。 * * @example * ``` * // Expect: ["page", "1"] * const example1 = internalSplitHashEntry("page=1") * * // Expect: ["formula", "a=b"] * const example2 = internalSplitHashEntry("formula=a=b") * ``` */ const internalSplitHashEntry = (hashEntry: string): [key: string, value: string] => { const internalEqualSignIndex = hashEntry.indexOf("=") if (internalEqualSignIndex === -1) { const key = hashEntry const value = "" return [key, value] } const key = hashEntry.slice(0, internalEqualSignIndex) const value = hashEntry.slice(internalEqualSignIndex + 1) return [key, value] } export type HashStringLoose = string /** * @description 带 `#` 前缀的 hash 字符串。 * * @example * ``` * // Expect: "#page=1" * const example1: HashString = "#page=1" * ``` */ export type HashStringStrict = `#${string}` /** * @description Hash 参数对象。 * * @example * ``` * // Expect: { page: "1" } * const example1: HashObject = { page: "1" } * ``` */ export type HashObject = Record /** * @description Hash 支持的输入或输出形态。 * * @example * ``` * // Expect: "#page=1" * const example1: HashUnion = "#page=1" * * // Expect: { page: "1" } * const example2: HashUnion = { page: "1" } * ``` */ export type HashUnion = HashStringLoose | HashStringStrict | HashObject /** * @description 规范化 hash 字符串,确保其带有 `#` 前缀。 * * @example * ``` * // Expect: "#name=cigaret" * const example1 = neatenHash("#name=cigaret") * * // Expect: "#name=cigaret" * const example2 = neatenHash("name=cigaret") * ``` */ export const neatenHash = (target: string): HashStringStrict => { return target.startsWith("#") ? (target as HashStringStrict) : `#${target}` } /** * @description 将 hash 字符串解析为 hash 参数对象。 * * @example * ``` * // Expect: { name: "cigaret" } * const example1 = hashStringToHashObject("#name=cigaret") * * // Expect: { formula: "a=b" } * const example2 = hashStringToHashObject("#formula=a%3Db") * ``` */ export const hashStringToHashObject = (target: HashStringLoose): HashObject => { const hashString = neatenHash(target) const hashEntries = hashString.slice(1).split("&") const hashObject: HashObject = {} hashEntries.forEach((hashEntry) => { if (hashEntry !== "") { const [rawKey, rawValue] = internalSplitHashEntry(hashEntry) hashObject[decodeURIComponent(rawKey)] = decodeURIComponent(rawValue) } }) return hashObject } /** * @description 将 hash 参数对象序列化为 hash 字符串。 * * @example * ``` * // Expect: "#name=cigaret" * const example1 = hashObjectToHashString({ name: "cigaret" }) * * // Expect: "#formula=a%3Db" * const example2 = hashObjectToHashString({ formula: "a=b" }) * ``` */ export const hashObjectToHashString = (target: HashObject): HashStringStrict => { const hashString = Object.entries(target) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join("&") return neatenHash(hashString) } /** * @description 将 hash 统一转换为 hash 字符串。 * * @example * ``` * // Expect: "#page=1" * const example1 = toHashString("#page=1") * * // Expect: "#page=1" * const example2 = toHashString({ page: "1" }) * ``` */ export const toHashString = (target: HashUnion): HashStringStrict => { if (internalIsString(target)) { return neatenHash(target) } else if (internalIsObject(target)) { return hashObjectToHashString(target) } else { throw new TypeError('"target" is expected to be type of "string" | "object".') } } /** * @description 将 hash 统一转换为 hash 参数对象。 * * @example * ``` * // Expect: { page: "1" } * const example1 = toHashObject("#page=1") * * // Expect: { page: "1" } * const example2 = toHashObject({ page: "1" }) * ``` */ export const toHashObject = (target: HashUnion): HashObject => { if (internalIsString(target)) { return hashStringToHashObject(target) } else if (internalIsObject(target)) { return target } else { throw new TypeError('"target" is expected to be type of "string" | "object".') } } /** * @description 宽松包含关系:hash 参数的键集合是目标 hash 参数键集合的子集。 * * @example * ``` * // Expect: true * const example1 = isHashLooseIncludes("#a=1&b=2", ["a"]) * * // Expect: false * const example2 = isHashLooseIncludes("#a=1&b=2", ["a", "c"]) * ``` */ export const isHashLooseIncludes = (hash: HashUnion, keys: string | string[]): boolean => { const existKeys = Object.keys(toHashObject(hash)) const targetKeys = internalIsString(keys) ? [keys] : keys return targetKeys.every((targetKey) => existKeys.includes(targetKey)) } /** * @description 严格包含关系:hash 参数的键集合与目标 hash 参数键集合完全相同。 * * @example * ``` * // Expect: true * const example1 = isHashStrictIncludes("#a=1&b=2", ["a", "b"]) * * // Expect: false * const example2 = isHashStrictIncludes("#a=1&b=2", ["a"]) * ``` */ export const isHashStrictIncludes = (hash: HashUnion, keys: string | string[]): boolean => { const existKeys = Object.keys(toHashObject(hash)).toSorted() const targetKeys = (internalIsString(keys) ? [keys] : keys).toSorted() if (existKeys.length !== targetKeys.length) { return false } return existKeys.every((key, index) => key === targetKeys[index]) } /** * @description 包含关系:hash 参数的键集合必须包含目标 hash 参数键集合,并且不能包含除目标 hash 参数键集合之外的其他键。 * * @example * ``` * // Expect: true * const example1 = isHashIncludes("#a=1&b=2", { a: "required", b: "optional" }) * * // Expect: false * const example2 = isHashIncludes("#a=1&b=2", { a: "required" }) * ``` */ export const isHashIncludes = ( hash: HashUnion, keys: Record, ): boolean => { const existKeys = Object.keys(toHashObject(hash)) const requiredKeys = Object.entries(keys) .filter(([, value]) => value === "required") .map(([key]) => key) const optionalKeys = Object.entries(keys) .filter(([, value]) => value === "optional") .map(([key]) => key) const requiredKeysExist = requiredKeys.every((requiredKey) => existKeys.includes(requiredKey)) const extraKeysExist = existKeys.some( (existKey) => !requiredKeys.includes(existKey) && !optionalKeys.includes(existKey), ) return requiredKeysExist && !extraKeysExist } /** * @description 宽松相等:hash 参数的键集合是目标 hash 参数键集合的子集,并且对应的值相等。 * * @example * ``` * // Expect: true * const example1 = isHashLooseEqual("#a=1&b=2", "#a=1") * * // Expect: false * const example2 = isHashLooseEqual("#a=1&b=2", "#a=1&c=3") * ``` */ export const isHashLooseEqual = (hashA: HashUnion, hashB: HashUnion): boolean => { const hashObjectA = toHashObject(hashA) const hashObjectB = toHashObject(hashB) return Object.entries(hashObjectB).every(([key, value]) => hashObjectA[key] === value) } /** * @description 严格相等:hash 参数的键集合与目标 hash 参数键集合完全相同,并且对应的值相等。 * * @example * ``` * // Expect: true * const example1 = isHashStrictEqual("#a=1&b=2", "#b=2&a=1") * * // Expect: false * const example2 = isHashStrictEqual("#a=1&b=2", "#a=1") * ``` */ export const isHashStrictEqual = (hashA: HashUnion, hashB: HashUnion): boolean => { const hashObjectA = toHashObject(hashA) const hashObjectB = toHashObject(hashB) const lengthEqual = Object.keys(hashObjectA).length === Object.keys(hashObjectB).length const valueEqual = Object.entries(hashObjectB).every(([key, value]) => hashObjectA[key] === value) return lengthEqual && valueEqual }