Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"size-limit": [
{
"path": "dist/index.js",
"limit": "2 kB"
"limit": "2.1 kB"
}
],
"ts-scripts": {
Expand Down
50 changes: 50 additions & 0 deletions src/cases.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,26 @@ export const PARSER_TESTS: ParserTestSet[] = [
"\\\\:test",
),
},
{
path: "/:username([a-zA-Z]+)",
expected: new TokenData(
[
{ type: "text", value: "/" },
{ type: "param", name: "username", pattern: "[a-zA-Z]+" },
],
"/:username([a-zA-Z]+)",
),
},
{
path: "/:id(\\.(json|xml))",
expected: new TokenData(
[
{ type: "text", value: "/" },
{ type: "param", name: "id", pattern: "\\.(json|xml)" },
],
"/:id(\\.(json|xml))",
),
},
];

export const STRINGIFY_TESTS: StringifyTestSet[] = [
Expand Down Expand Up @@ -231,6 +251,15 @@ export const STRINGIFY_TESTS: StringifyTestSet[] = [
},
expected: "/:test",
},
{
data: {
tokens: [
{ type: "text", value: "/" },
{ type: "param", name: "username", pattern: "[a-zA-Z]+" },
],
},
expected: "/:username([a-zA-Z]+)",
},
];

export const COMPILE_TESTS: CompileTestSet[] = [
Expand Down Expand Up @@ -1741,4 +1770,25 @@ export const MATCH_TESTS: MatchTestSet[] = [
{ input: "/123", expected: { path: "/123", params: { test: "123" } } },
],
},

/**
* Pattern.
*/
{
path: "/:test(abc|123)",
tests: [
{
input: "/abc",
expected: { path: "/abc", params: { test: "abc" } },
},
{
input: "/123",
expected: { path: "/123", params: { test: "123" } },
},
{
input: "/xyz",
expected: false,
},
],
},
];
29 changes: 29 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ describe("path-to-regexp", () => {
new PathError("Unterminated quote at index 2", '/:"foo'),
);
});

it("should throw on incomplete pattern", () => {
expect(() => parse("/(")).toThrow(
new PathError("Unbalanced pattern at index 2", "/("),
);
});

it("should throw on missing opening pattern", () => {
expect(() => parse("/)")).toThrow(
new PathError("Unexpected ) at index 1, expected end", "/)"),
);
});
});

describe("compile errors", () => {
Expand Down Expand Up @@ -166,6 +178,23 @@ describe("path-to-regexp", () => {
expect(stack).toContain("index.spec.ts");
}
});

describe("patterns", () => {
it("should throw on unsupported pattern", () => {
expect(() => pathToRegexp("/:foo(??)")).toThrow(
new PathError(
'Unsupported pattern "??" for "foo" param',
"/:foo(??)",
),
);
});

it("should throw on empty pattern", () => {
expect(() => pathToRegexp("/:foo()")).toThrow(
new PathError('Unsupported pattern "" for "foo" param', "/:foo()"),
);
});
});
});

describe("stringify errors", () => {
Expand Down
83 changes: 75 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type TokenType =
| "}"
| "wildcard"
| "param"
| "pattern"
| "char"
| "escape"
| "end"
Expand All @@ -88,7 +89,6 @@ const SIMPLE_TOKENS: Record<string, TokenType> = {
"{": "{",
"}": "}",
// Reserved.
"(": "(",
")": ")",
"[": "[",
"]": "]",
Expand Down Expand Up @@ -125,6 +125,7 @@ export interface Text {
export interface Parameter {
type: "param";
name: string;
pattern?: string;
}

/**
Expand All @@ -133,6 +134,7 @@ export interface Parameter {
export interface Wildcard {
type: "wildcard";
name: string;
pattern?: string;
}

/**
Expand Down Expand Up @@ -228,6 +230,35 @@ export function parse(str: string, options: ParseOptions = {}): TokenData {
return value;
}

function pattern() {
let count = 1;
let value = "";

while (index < chars.length) {
const char = chars[index++];

if (char === "\\") {
value += char + chars[index++];
continue;
}

if (char === "(") {
count++;
} else if (char === ")") {
count--;
if (count === 0) break;
}

value += char;
}

if (count) {
throw new PathError(`Unbalanced pattern at index ${index}`, str);
}

return value;
}

while (index < chars.length) {
const value = chars[index];
const type = SIMPLE_TOKENS[value];
Expand All @@ -240,6 +271,8 @@ export function parse(str: string, options: ParseOptions = {}): TokenData {
tokens.push({ type: "param", index: index++, value: name() });
} else if (value === "*") {
tokens.push({ type: "wildcard", index: index++, value: name() });
} else if (value === "(") {
tokens.push({ type: "pattern", index: index++, value: pattern() });
} else {
tokens.push({ type: "char", index: index++, value });
}
Expand Down Expand Up @@ -271,9 +304,13 @@ export function parse(str: string, options: ParseOptions = {}): TokenData {
}

if (token.type === "param" || token.type === "wildcard") {
const pattern =
tokens[pos].type === "pattern" ? tokens[pos++].value : undefined;

output.push({
type: token.type,
name: token.value,
pattern,
});
continue;
}
Expand Down Expand Up @@ -562,11 +599,12 @@ function toRegExpSource(
);
}

if (token.type === "param") {
result += `(${negate(delimiter, isSafeSegmentParam ? "" : backtrack)}+)`;
} else {
result += `([\\s\\S]+)`;
}
result += safePattern(
token,
delimiter,
isSafeSegmentParam ? "" : backtrack,
originalPath,
);

keys.push(token);
backtrack = "";
Expand All @@ -578,6 +616,33 @@ function toRegExpSource(
return result;
}

/**
* Validate supported pattern characters.
*/
function safePattern(
token: Parameter | Wildcard,
delimiter: string,
backtrack: string,
originalPath: string | undefined,
): string {
if (token.pattern !== undefined) {
if (!/^[a-zA-Z0-9\|]+$/.test(token.pattern)) {
throw new PathError(
`Unsupported pattern "${token.pattern}" for "${token.name}" ${token.type}`,
originalPath,
);
}

return `(${token.pattern})`;
}

if (token.type === "param") {
return `(${negate(delimiter, backtrack)}+)`;
}

return `([\\s\\S]+)`; // Wildcard (.+)
}

/**
* Block backtracking on previous text and ignore delimiter string.
*/
Expand Down Expand Up @@ -618,12 +683,14 @@ function stringifyTokens(tokens: Token[]): string {
}

if (token.type === "param") {
value += `:${name(token.name)}`;
value +=
`:${name(token.name)}` + (token.pattern ? `(${token.pattern})` : "");
continue;
}

if (token.type === "wildcard") {
value += `*${name(token.name)}`;
value +=
`*${name(token.name)}` + (token.pattern ? `(${token.pattern})` : "");
continue;
}

Expand Down
Loading