diff --git a/src/cases.spec.ts b/src/cases.spec.ts index ef06e1f..0482777 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -1,16 +1,23 @@ -import type { - MatchOptions, - Match, - ParseOptions, - Token, - CompileOptions, - ParamData, +import { + type MatchOptions, + type Match, + type ParseOptions, + type Token, + type CompileOptions, + type ParamData, + TokenData, } from "./index.js"; export interface ParserTestSet { path: string; options?: ParseOptions; - expected: Token[]; + expected: TokenData; +} + +export interface StringifyTestSet { + data: TokenData; + options?: ParseOptions; + expected: string; } export interface CompileTestSet { @@ -34,56 +41,98 @@ export interface MatchTestSet { export const PARSER_TESTS: ParserTestSet[] = [ { path: "/", - expected: [{ type: "text", value: "/" }], + expected: new TokenData([{ type: "text", value: "/" }]), }, { path: "/:test", - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: "test" }, - ], + ]), }, { path: '/:"0"', - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: "0" }, - ], + ]), }, { path: "/:_", - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: "_" }, - ], + ]), }, { path: "/:café", - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: "café" }, - ], + ]), }, { path: '/:"123"', - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: "123" }, - ], + ]), }, { path: '/:"1\\"\\2\\"3"', - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "param", name: '1"2"3' }, - ], + ]), }, { path: "/*path", - expected: [ + expected: new TokenData([ { type: "text", value: "/" }, { type: "wildcard", name: "path" }, - ], + ]), + }, +]; + +export const STRINGIFY_TESTS: StringifyTestSet[] = [ + { + data: new TokenData([{ type: "text", value: "/" }]), + expected: "/", + }, + { + data: new TokenData([ + { type: "text", value: "/" }, + { type: "param", name: "test" }, + ]), + expected: "/:test", + }, + { + data: new TokenData([ + { type: "text", value: "/" }, + { type: "param", name: "café" }, + ]), + expected: "/:café", + }, + { + data: new TokenData([ + { type: "text", value: "/" }, + { type: "param", name: "0" }, + ]), + expected: '/:"0"', + }, + { + data: new TokenData([ + { type: "text", value: "/" }, + { type: "wildcard", name: "test" }, + ]), + expected: "/*test", + }, + { + data: new TokenData([ + { type: "text", value: "/" }, + { type: "wildcard", name: "0" }, + ]), + expected: '/*"0"', }, ]; diff --git a/src/index.spec.ts b/src/index.spec.ts index c6da631..cef557f 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,6 +1,11 @@ import { describe, it, expect } from "vitest"; -import { parse, compile, match } from "./index.js"; -import { PARSER_TESTS, COMPILE_TESTS, MATCH_TESTS } from "./cases.spec.js"; +import { parse, compile, match, stringify } from "./index.js"; +import { + PARSER_TESTS, + COMPILE_TESTS, + MATCH_TESTS, + STRINGIFY_TESTS, +} from "./cases.spec.js"; /** * Dynamically generate the entire test suite. @@ -94,7 +99,17 @@ describe("path-to-regexp", () => { ({ path, options, expected }) => { it("should parse the path", () => { const data = parse(path, options); - expect(data.tokens).toEqual(expected); + expect(data).toEqual(expected); + }); + }, + ); + + describe.each(STRINGIFY_TESTS)( + "stringify $tokens with $options", + ({ data, expected }) => { + it("should stringify the path", () => { + const path = stringify(data); + expect(path).toEqual(expected); }); }, ); diff --git a/src/index.ts b/src/index.ts index a63e365..614fe6f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -595,3 +595,30 @@ function negate(delimiter: string, backtrack: string) { if (isSimple) return `[^${escape(values.join(""))}]`; return `(?:(?!${values.map(escape).join("|")}).)`; } + +/** + * Stringify token data into a path string. + */ +export function stringify(data: TokenData) { + let result = ""; + for (const token of data.tokens) result += stringifyToken(token); + return result; +} + +function stringifyToken(token: Token) { + if (token.type === "text") return token.value; + if (token.type === "group") return `{${stringifyToken(token)}}`; + + const isSafe = isNameSafe(token.name); + const key = isSafe ? token.name : JSON.stringify(token.name); + + if (token.type === "param") return `:${key}`; + if (token.type === "wildcard") return `*${key}`; + throw new TypeError(`Unexpected token: ${token}`); +} + +function isNameSafe(name: string) { + const [first, ...rest] = name; + if (!ID_START.test(first)) return false; + return rest.every((char) => ID_CONTINUE.test(char)); +}