Skip to content

Commit e537daa

Browse files
committed
Add a stringify API
1 parent ed1095e commit e537daa

File tree

3 files changed

+142
-26
lines changed

3 files changed

+142
-26
lines changed

src/cases.spec.ts

+90-23
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
import type {
2-
MatchOptions,
3-
Match,
4-
ParseOptions,
5-
Token,
6-
CompileOptions,
7-
ParamData,
1+
import {
2+
type MatchOptions,
3+
type Match,
4+
type ParseOptions,
5+
type Token,
6+
type CompileOptions,
7+
type ParamData,
8+
TokenData,
89
} from "./index.js";
910

1011
export interface ParserTestSet {
1112
path: string;
1213
options?: ParseOptions;
13-
expected: Token[];
14+
expected: TokenData;
15+
}
16+
17+
export interface StringifyTestSet {
18+
data: TokenData;
19+
options?: ParseOptions;
20+
expected: string;
1421
}
1522

1623
export interface CompileTestSet {
@@ -34,56 +41,116 @@ export interface MatchTestSet {
3441
export const PARSER_TESTS: ParserTestSet[] = [
3542
{
3643
path: "/",
37-
expected: [{ type: "text", value: "/" }],
44+
expected: new TokenData([{ type: "text", value: "/" }]),
3845
},
3946
{
4047
path: "/:test",
41-
expected: [
48+
expected: new TokenData([
4249
{ type: "text", value: "/" },
4350
{ type: "param", name: "test" },
44-
],
51+
]),
4552
},
4653
{
4754
path: '/:"0"',
48-
expected: [
55+
expected: new TokenData([
4956
{ type: "text", value: "/" },
5057
{ type: "param", name: "0" },
51-
],
58+
]),
5259
},
5360
{
5461
path: "/:_",
55-
expected: [
62+
expected: new TokenData([
5663
{ type: "text", value: "/" },
5764
{ type: "param", name: "_" },
58-
],
65+
]),
5966
},
6067
{
6168
path: "/:café",
62-
expected: [
69+
expected: new TokenData([
6370
{ type: "text", value: "/" },
6471
{ type: "param", name: "café" },
65-
],
72+
]),
6673
},
6774
{
6875
path: '/:"123"',
69-
expected: [
76+
expected: new TokenData([
7077
{ type: "text", value: "/" },
7178
{ type: "param", name: "123" },
72-
],
79+
]),
7380
},
7481
{
7582
path: '/:"1\\"\\2\\"3"',
76-
expected: [
83+
expected: new TokenData([
7784
{ type: "text", value: "/" },
7885
{ type: "param", name: '1"2"3' },
79-
],
86+
]),
8087
},
8188
{
8289
path: "/*path",
83-
expected: [
90+
expected: new TokenData([
8491
{ type: "text", value: "/" },
8592
{ type: "wildcard", name: "path" },
86-
],
93+
]),
94+
},
95+
];
96+
97+
export const STRINGIFY_TESTS: StringifyTestSet[] = [
98+
{
99+
data: new TokenData([{ type: "text", value: "/" }]),
100+
expected: "/",
101+
},
102+
{
103+
data: new TokenData([
104+
{ type: "text", value: "/" },
105+
{ type: "param", name: "test" },
106+
]),
107+
expected: "/:test",
108+
},
109+
{
110+
data: new TokenData([
111+
{ type: "text", value: "/" },
112+
{ type: "param", name: "café" },
113+
]),
114+
expected: "/:café",
115+
},
116+
{
117+
data: new TokenData([
118+
{ type: "text", value: "/" },
119+
{ type: "param", name: "0" },
120+
]),
121+
expected: '/:"0"',
122+
},
123+
{
124+
data: new TokenData([
125+
{ type: "text", value: "/" },
126+
{ type: "wildcard", name: "test" },
127+
]),
128+
expected: "/*test",
129+
},
130+
{
131+
data: new TokenData([
132+
{ type: "text", value: "/" },
133+
{ type: "wildcard", name: "0" },
134+
]),
135+
expected: '/*"0"',
136+
},
137+
{
138+
data: new TokenData([
139+
{ type: "text", value: "/users" },
140+
{
141+
type: "group",
142+
tokens: [
143+
{ type: "text", value: "/" },
144+
{ type: "param", name: "id" },
145+
],
146+
},
147+
{ type: "text", value: "/delete" },
148+
]),
149+
expected: "/users{/:id}/delete",
150+
},
151+
{
152+
data: new TokenData([{ type: "text", value: "/:+?*" }]),
153+
expected: "/\\:\\+\\?\\*",
87154
},
88155
];
89156

src/index.spec.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { describe, it, expect } from "vitest";
2-
import { parse, compile, match } from "./index.js";
3-
import { PARSER_TESTS, COMPILE_TESTS, MATCH_TESTS } from "./cases.spec.js";
2+
import { parse, compile, match, stringify } from "./index.js";
3+
import {
4+
PARSER_TESTS,
5+
COMPILE_TESTS,
6+
MATCH_TESTS,
7+
STRINGIFY_TESTS,
8+
} from "./cases.spec.js";
49

510
/**
611
* Dynamically generate the entire test suite.
@@ -94,7 +99,17 @@ describe("path-to-regexp", () => {
9499
({ path, options, expected }) => {
95100
it("should parse the path", () => {
96101
const data = parse(path, options);
97-
expect(data.tokens).toEqual(expected);
102+
expect(data).toEqual(expected);
103+
});
104+
},
105+
);
106+
107+
describe.each(STRINGIFY_TESTS)(
108+
"stringify $tokens with $options",
109+
({ data, expected }) => {
110+
it("should stringify the path", () => {
111+
const path = stringify(data);
112+
expect(path).toEqual(expected);
98113
});
99114
},
100115
);

src/index.ts

+34
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ const SIMPLE_TOKENS: Record<string, TokenType> = {
9595
"!": "!",
9696
};
9797

98+
/**
99+
* Escape text for stringify to path.
100+
*/
101+
function escapeText(str: string) {
102+
return str.replace(/[{}()\[\]+?!:*]/g, "\\$&");
103+
}
104+
98105
/**
99106
* Escape a regular expression string.
100107
*/
@@ -595,3 +602,30 @@ function negate(delimiter: string, backtrack: string) {
595602
if (isSimple) return `[^${escape(values.join(""))}]`;
596603
return `(?:(?!${values.map(escape).join("|")}).)`;
597604
}
605+
606+
/**
607+
* Stringify token data into a path string.
608+
*/
609+
export function stringify(data: TokenData) {
610+
return data.tokens.map(stringifyToken).join("");
611+
}
612+
613+
function stringifyToken(token: Token): string {
614+
if (token.type === "text") return escapeText(token.value);
615+
if (token.type === "group") {
616+
return `{${token.tokens.map(stringifyToken).join("")}}`;
617+
}
618+
619+
const isSafe = isNameSafe(token.name);
620+
const key = isSafe ? token.name : JSON.stringify(token.name);
621+
622+
if (token.type === "param") return `:${key}`;
623+
if (token.type === "wildcard") return `*${key}`;
624+
throw new TypeError(`Unexpected token: ${token}`);
625+
}
626+
627+
function isNameSafe(name: string) {
628+
const [first, ...rest] = name;
629+
if (!ID_START.test(first)) return false;
630+
return rest.every((char) => ID_CONTINUE.test(char));
631+
}

0 commit comments

Comments
 (0)