Skip to content

Commit

Permalink
Add a stringify API
Browse files Browse the repository at this point in the history
  • Loading branch information
blakeembrey committed Sep 1, 2024
1 parent ed1095e commit e537daa
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 26 deletions.
113 changes: 90 additions & 23 deletions src/cases.spec.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -34,56 +41,116 @@ 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"',
},
{
data: new TokenData([
{ type: "text", value: "/users" },
{
type: "group",
tokens: [
{ type: "text", value: "/" },
{ type: "param", name: "id" },
],
},
{ type: "text", value: "/delete" },
]),
expected: "/users{/:id}/delete",
},
{
data: new TokenData([{ type: "text", value: "/:+?*" }]),
expected: "/\\:\\+\\?\\*",
},
];

Expand Down
21 changes: 18 additions & 3 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);
});
},
);
Expand Down
34 changes: 34 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ const SIMPLE_TOKENS: Record<string, TokenType> = {
"!": "!",
};

/**
* Escape text for stringify to path.
*/
function escapeText(str: string) {
return str.replace(/[{}()\[\]+?!:*]/g, "\\$&");
}

/**
* Escape a regular expression string.
*/
Expand Down Expand Up @@ -595,3 +602,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) {
return data.tokens.map(stringifyToken).join("");
}

function stringifyToken(token: Token): string {
if (token.type === "text") return escapeText(token.value);
if (token.type === "group") {
return `{${token.tokens.map(stringifyToken).join("")}}`;
}

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));
}

0 comments on commit e537daa

Please sign in to comment.