Skip to content

Commit 664eddb

Browse files
committed
Re-add Pattern token, only add regex with pipe special char
1 parent a7e29ab commit 664eddb

File tree

5 files changed

+79
-62
lines changed

5 files changed

+79
-62
lines changed

Readme.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,18 @@ Parameter names must be provided after `:` or `*`, and they must be a valid Java
192192

193193
Parameter names can be wrapped in double quote characters, and this error means you forgot to close the quote character.
194194

195-
### Unterminated parameter pattern
195+
### Unbalanced pattern
196196

197197
Parameter patterns must be wrapped in parentheses, and this error means you forgot to close the parentheses.
198198

199+
### Only '|' is allowed as a special character in patterns
200+
201+
When defining a custom pattern for a parameter (e.g., `:id(<pattern>)`), only the pipe character (`|`) is allowed as a special character inside the pattern.
202+
203+
### Missing pattern
204+
205+
When defining a custom pattern for a parameter (e.g., `:id(<pattern>)`), you must provide a pattern.
206+
199207
### Express <= 4.x
200208

201209
Path-To-RegExp breaks compatibility with Express <= `4.x` in the following ways:

src/cases.spec.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -412,15 +412,6 @@ export const MATCH_TESTS: MatchTestSet[] = [
412412
{ input: "/", expected: false },
413413
],
414414
},
415-
{
416-
path: "/:foo(\\d)",
417-
tests: [
418-
{ input: "/1", expected: { path: "/1", params: { foo: "1" } } },
419-
{ input: "/123", expected: false },
420-
{ input: "/", expected: false },
421-
{ input: "/foo", expected: false },
422-
],
423-
},
424415

425416
/**
426417
* Case-sensitive paths.

src/index.bench.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const PATHS: string[] = [
1212

1313
const STATIC_PATH_MATCH = match("/user");
1414
const SIMPLE_PATH_MATCH = match("/user/:id");
15-
const SIMPLE_PATH_MATCH_WITH_PATTERN = match("/user/:id(\\d+)");
1615
const MULTI_SEGMENT_MATCH = match("/:x/:y");
1716
const MULTI_PATTERN_MATCH = match("/:x-:y");
1817
const TRICKY_PATTERN_MATCH = match("/:foo|:bar|");
@@ -26,10 +25,6 @@ bench("simple path", () => {
2625
for (const path of PATHS) SIMPLE_PATH_MATCH(path);
2726
});
2827

29-
bench("simple path with parameter pattern", () => {
30-
for (const path of PATHS) SIMPLE_PATH_MATCH_WITH_PATTERN(path);
31-
});
32-
3328
bench("multi segment", () => {
3429
for (const path of PATHS) MULTI_SEGMENT_MATCH(path);
3530
});

src/index.spec.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,26 @@ describe("path-to-regexp", () => {
5151
);
5252
});
5353

54-
it("should throw on unterminated parameter pattern", () => {
55-
expect(() => parse("/:foo((bar")).toThrow(
54+
it("should throw on unbalanced pattern", () => {
55+
expect(() => parse("/:foo((bar|sdfsdf)/")).toThrow(
5656
new TypeError(
57-
"Unterminated parameter pattern at 10: https://git.new/pathToRegexpError",
57+
"Unbalanced pattern at 5: https://git.new/pathToRegexpError",
58+
),
59+
);
60+
});
61+
62+
it("should throw on not allowed characters in pattern", () => {
63+
expect(() => parse("/:foo(\\d)")).toThrow(
64+
new TypeError(
65+
`Only "|" is allowed as a special character in patterns at 6: https://git.new/pathToRegexpError`,
66+
),
67+
);
68+
});
69+
70+
it("should throw on missing pattern", () => {
71+
expect(() => parse("//:foo()")).toThrow(
72+
new TypeError(
73+
"Missing pattern at 6: https://git.new/pathToRegexpError",
5874
),
5975
);
6076
});

src/index.ts

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const NOOP_VALUE = (value: string) => value;
33
const ID_START = /^[$_\p{ID_Start}]$/u;
44
const ID_CONTINUE = /^[$\u200c\u200d\p{ID_Continue}]$/u;
55
const DEBUG_URL = "https://git.new/pathToRegexpError";
6+
const INVALID_PATTERN_CHARS = "^$.+*?[]{}\\^";
67

78
/**
89
* Encode a string into another string.
@@ -63,6 +64,7 @@ type TokenType =
6364
| "}"
6465
| "WILDCARD"
6566
| "PARAM"
67+
| "PATTERN"
6668
| "CHAR"
6769
| "ESCAPED"
6870
| "END"
@@ -81,15 +83,15 @@ type TokenType =
8183
interface LexToken {
8284
type: TokenType;
8385
index: number;
84-
value: string | { name: string; pattern?: string };
86+
value: string;
8587
}
8688

8789
const SIMPLE_TOKENS: Record<string, TokenType> = {
8890
// Groups.
8991
"{": "{",
9092
"}": "}",
9193
// Reserved.
92-
"(": "(",
94+
// "(": "(",
9395
")": ")",
9496
"[": "[",
9597
"]": "]",
@@ -119,10 +121,7 @@ function* lexer(str: string): Generator<LexToken, LexToken> {
119121
const chars = [...str];
120122
let i = 0;
121123

122-
function name(options?: { pattern?: boolean }): {
123-
name: string;
124-
pattern?: string;
125-
} {
124+
function name() {
126125
let value = "";
127126

128127
if (ID_START.test(chars[++i])) {
@@ -156,29 +155,46 @@ function* lexer(str: string): Generator<LexToken, LexToken> {
156155
throw new TypeError(`Missing parameter name at ${i}: ${DEBUG_URL}`);
157156
}
158157

159-
if (chars[i] === "(" && options?.pattern) {
160-
let depth = 1;
161-
let pattern = "";
162-
i++;
163-
while (i < chars.length && depth > 0) {
164-
if (chars[i] === "(") {
165-
depth++;
166-
} else if (chars[i] === ")") {
167-
depth--;
168-
}
169-
if (depth > 0) {
170-
pattern += chars[i++];
171-
}
172-
}
173-
if (depth !== 0) {
158+
return value;
159+
}
160+
161+
function pattern() {
162+
const pos = i++;
163+
let depth = 1;
164+
let pattern = "";
165+
166+
while (i < chars.length && depth > 0) {
167+
const char = chars[i];
168+
169+
if (INVALID_PATTERN_CHARS.includes(char)) {
174170
throw new TypeError(
175-
`Unterminated parameter pattern at ${i}: ${DEBUG_URL}`,
171+
`Only "|" is allowed as a special character in patterns at ${i}: ${DEBUG_URL}`,
176172
);
177173
}
174+
175+
if (char === ")") {
176+
depth--;
177+
if (depth === 0) {
178+
i++;
179+
break;
180+
}
181+
} else if (char === "(") {
182+
depth++;
183+
}
184+
185+
pattern += char;
178186
i++;
179-
return { name: value, pattern };
180187
}
181-
return { name: value };
188+
189+
if (depth) {
190+
throw new TypeError(`Unbalanced pattern at ${pos}: ${DEBUG_URL}`);
191+
}
192+
193+
if (!pattern) {
194+
throw new TypeError(`Missing pattern at ${pos}: ${DEBUG_URL}`);
195+
}
196+
197+
return pattern;
182198
}
183199

184200
while (i < chars.length) {
@@ -190,14 +206,13 @@ function* lexer(str: string): Generator<LexToken, LexToken> {
190206
} else if (value === "\\") {
191207
yield { type: "ESCAPED", index: i++, value: chars[i++] };
192208
} else if (value === ":") {
193-
const value = name({ pattern: true });
194-
yield {
195-
type: "PARAM",
196-
index: i,
197-
value,
198-
};
209+
const value = name();
210+
yield { type: "PARAM", index: i, value };
211+
} else if (value === "(") {
212+
const value = pattern();
213+
yield { type: "PATTERN", index: i, value };
199214
} else if (value === "*") {
200-
const { name: value } = name();
215+
const value = name();
201216
yield { type: "WILDCARD", index: i, value };
202217
} else {
203218
yield { type: "CHAR", index: i, value: chars[i++] };
@@ -220,23 +235,15 @@ class Iter {
220235
return this._peek;
221236
}
222237

223-
tryConsume(type: Extract<TokenType, "PARAM">): {
224-
name: string;
225-
pattern?: string;
226-
};
227-
tryConsume(type: Exclude<TokenType, "PARAM">): string;
228-
tryConsume(
229-
type: TokenType,
230-
): string | { name: string; pattern?: string } | undefined {
238+
tryConsume(type: TokenType): string | undefined {
231239
const token = this.peek();
232240
if (token.type !== type) return;
233241
this._peek = undefined; // Reset after consumed.
234242
return token.value;
235243
}
236244

237-
consume(type: TokenType): string | { name: string; pattern?: string } {
238-
const value =
239-
type === "PARAM" ? this.tryConsume(type) : this.tryConsume(type);
245+
consume(type: TokenType): string {
246+
const value = this.tryConsume(type);
240247
if (value !== undefined) return value;
241248
const { type: nextType, index } = this.peek();
242249
throw new TypeError(
@@ -325,10 +332,10 @@ export function parse(str: string, options: ParseOptions = {}): TokenData {
325332

326333
const param = it.tryConsume("PARAM");
327334
if (param) {
328-
const { name, pattern } = param;
335+
const pattern = it.tryConsume("PATTERN");
329336
tokens.push({
330337
type: "param",
331-
name,
338+
name: param,
332339
pattern,
333340
});
334341
continue;

0 commit comments

Comments
 (0)