@tanstack/router-core
Version:
Modern and scalable routing for React applications
243 lines (242 loc) • 8.26 kB
JavaScript
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