UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

243 lines (242 loc) 8.26 kB
import { last } from "./utils.js"; import { parseSegment } from "./new-process-route-tree.js"; import { isServer } from "@tanstack/router-core/isServer"; //#region src/path.ts /** Join path segments, cleaning duplicate slashes between parts. */ function joinPaths(paths) { return cleanPath(paths.filter((val) => { return val !== void 0; }).join("/")); } /** Remove repeated slashes from a path string. */ function cleanPath(path) { return path.replace(/\/{2,}/g, "/"); } /** Trim leading slashes (except preserving root '/'). */ function trimPathLeft(path) { return path === "/" ? path : path.replace(/^\/{1,}/, ""); } /** Trim trailing slashes (except preserving root '/'). */ function trimPathRight(path) { const len = path.length; return len > 1 && path[len - 1] === "/" ? path.replace(/\/{1,}$/, "") : path; } /** Trim both leading and trailing slashes. */ function trimPath(path) { return trimPathRight(trimPathLeft(path)); } /** Remove a trailing slash from value when appropriate for comparisons. */ function removeTrailingSlash(value, basepath) { if (value?.endsWith("/") && value !== "/" && value !== `${basepath}/`) return value.slice(0, -1); return value; } /** * Compare two pathnames for exact equality after normalizing trailing slashes * relative to the provided `basepath`. */ function exactPathTest(pathName1, pathName2, basepath) { return removeTrailingSlash(pathName1, basepath) === removeTrailingSlash(pathName2, basepath); } /** * Resolve a destination path against a base, honoring trailing-slash policy * and supporting relative segments (`.`/`..`) and absolute `to` values. */ function resolvePath({ base, to, trailingSlash = "never", cache }) { const isAbsolute = to.startsWith("/"); const isBase = !isAbsolute && to === "."; let key; if (cache) { key = isAbsolute ? to : isBase ? base : base + "\0" + to; const cached = cache.get(key); if (cached) return cached; } let baseSegments; if (isBase) baseSegments = base.split("/"); else if (isAbsolute) baseSegments = to.split("/"); else { baseSegments = base.split("/"); while (baseSegments.length > 1 && last(baseSegments) === "") baseSegments.pop(); const toSegments = to.split("/"); for (let index = 0, length = toSegments.length; index < length; index++) { const value = toSegments[index]; if (value === "") { if (!index) baseSegments = [value]; else if (index === length - 1) baseSegments.push(value); } else if (value === "..") baseSegments.pop(); else if (value === ".") {} else baseSegments.push(value); } } if (baseSegments.length > 1) { if (last(baseSegments) === "") { if (trailingSlash === "never") baseSegments.pop(); } else if (trailingSlash === "always") baseSegments.push(""); } let segment; let joined = ""; for (let i = 0; i < baseSegments.length; i++) { if (i > 0) joined += "/"; const part = baseSegments[i]; if (!part) continue; segment = parseSegment(part, 0, segment); const kind = segment[0]; if (kind === 0) { joined += part; continue; } const end = segment[5]; const prefix = part.substring(0, segment[1]); const suffix = part.substring(segment[4], end); const value = part.substring(segment[2], segment[3]); if (kind === 1) joined += prefix || suffix ? `${prefix}{$${value}}${suffix}` : `$${value}`; else if (kind === 2) joined += prefix || suffix ? `${prefix}{$}${suffix}` : "$"; else joined += `${prefix}{-$${value}}${suffix}`; } joined = cleanPath(joined); const result = joined || "/"; if (key && cache) cache.set(key, result); return result; } /** * Create a pre-compiled decode config from allowed characters. * This should be called once at router initialization. */ function compileDecodeCharMap(pathParamsAllowedCharacters) { const charMap = new Map(pathParamsAllowedCharacters.map((char) => [encodeURIComponent(char), char])); const pattern = Array.from(charMap.keys()).map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"); const regex = new RegExp(pattern, "g"); return (encoded) => encoded.replace(regex, (match) => charMap.get(match) ?? match); } function encodeParam(key, params, decoder) { const value = params[key]; if (typeof value !== "string") return value; if (key === "_splat") { if (/^[a-zA-Z0-9\-._~!/]*$/.test(value)) return value; return value.split("/").map((segment) => encodePathParam(segment, decoder)).join("/"); } else return encodePathParam(value, decoder); } /** * Interpolate params and wildcards into a route path template. * * - Encodes params safely (configurable allowed characters) * - Supports `{-$optional}` segments, `{prefix{$id}suffix}` and `{$}` wildcards */ function interpolatePath({ path, params, decoder, ...rest }) { let isMissingParams = false; const usedParams = Object.create(null); if (!path || path === "/") return { interpolatedPath: "/", usedParams, isMissingParams }; if (!path.includes("$")) return { interpolatedPath: path, usedParams, isMissingParams }; if (isServer ?? rest.server) { if (path.indexOf("{") === -1) { const length = path.length; let cursor = 0; let joined = ""; while (cursor < length) { while (cursor < length && path.charCodeAt(cursor) === 47) cursor++; if (cursor >= length) break; const start = cursor; let end = path.indexOf("/", cursor); if (end === -1) end = length; cursor = end; const part = path.substring(start, end); if (!part) continue; if (part.charCodeAt(0) === 36) if (part.length === 1) { const splat = params._splat; usedParams._splat = splat; usedParams["*"] = splat; if (!splat) { isMissingParams = true; continue; } const value = encodeParam("_splat", params, decoder); joined += "/" + value; } else { const key = part.substring(1); if (!isMissingParams && !(key in params)) isMissingParams = true; usedParams[key] = params[key]; const value = encodeParam(key, params, decoder) ?? "undefined"; joined += "/" + value; } else joined += "/" + part; } if (path.endsWith("/")) joined += "/"; return { usedParams, interpolatedPath: joined || "/", isMissingParams }; } } const length = path.length; let cursor = 0; let segment; let joined = ""; while (cursor < length) { const start = cursor; segment = parseSegment(path, start, segment); const end = segment[5]; cursor = end + 1; if (start === end) continue; const kind = segment[0]; if (kind === 0) { joined += "/" + path.substring(start, end); continue; } if (kind === 2) { const splat = params._splat; usedParams._splat = splat; usedParams["*"] = splat; const prefix = path.substring(start, segment[1]); const suffix = path.substring(segment[4], end); if (!splat) { isMissingParams = true; if (prefix || suffix) joined += "/" + prefix + suffix; continue; } const value = encodeParam("_splat", params, decoder); joined += "/" + prefix + value + suffix; continue; } if (kind === 1) { const key = path.substring(segment[2], segment[3]); if (!isMissingParams && !(key in params)) isMissingParams = true; usedParams[key] = params[key]; const prefix = path.substring(start, segment[1]); const suffix = path.substring(segment[4], end); const value = encodeParam(key, params, decoder) ?? "undefined"; joined += "/" + prefix + value + suffix; continue; } if (kind === 3) { const key = path.substring(segment[2], segment[3]); const valueRaw = params[key]; if (valueRaw == null) continue; usedParams[key] = valueRaw; const prefix = path.substring(start, segment[1]); const suffix = path.substring(segment[4], end); const value = encodeParam(key, params, decoder) ?? ""; joined += "/" + prefix + value + suffix; continue; } } if (path.endsWith("/")) joined += "/"; return { usedParams, interpolatedPath: joined || "/", isMissingParams }; } function encodePathParam(value, decoder) { const encoded = encodeURIComponent(value); return decoder?.(encoded) ?? encoded; } //#endregion export { cleanPath, compileDecodeCharMap, exactPathTest, interpolatePath, joinPaths, removeTrailingSlash, resolvePath, trimPath, trimPathLeft, trimPathRight }; //# sourceMappingURL=path.js.map