Skip to content

Commit 14d2b55

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

File tree

4 files changed

+80
-57
lines changed

4 files changed

+80
-57
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.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: 52 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,47 @@ 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)) {
170+
console.log({ char });
174171
throw new TypeError(
175-
`Unterminated parameter pattern at ${i}: ${DEBUG_URL}`,
172+
`Only "|" is allowed as a special character in patterns at ${i}: ${DEBUG_URL}`,
176173
);
177174
}
175+
176+
if (char === ")") {
177+
depth--;
178+
if (depth === 0) {
179+
i++;
180+
break;
181+
}
182+
} else if (char === "(") {
183+
depth++;
184+
}
185+
186+
pattern += char;
178187
i++;
179-
return { name: value, pattern };
180188
}
181-
return { name: value };
189+
190+
if (depth) {
191+
throw new TypeError(`Unbalanced pattern at ${pos}: ${DEBUG_URL}`);
192+
}
193+
194+
if (!pattern) {
195+
throw new TypeError(`Missing pattern at ${pos}: ${DEBUG_URL}`);
196+
}
197+
198+
return pattern;
182199
}
183200

184201
while (i < chars.length) {
@@ -190,14 +207,13 @@ function* lexer(str: string): Generator<LexToken, LexToken> {
190207
} else if (value === "\\") {
191208
yield { type: "ESCAPED", index: i++, value: chars[i++] };
192209
} else if (value === ":") {
193-
const value = name({ pattern: true });
194-
yield {
195-
type: "PARAM",
196-
index: i,
197-
value,
198-
};
210+
const value = name();
211+
yield { type: "PARAM", index: i, value };
212+
} else if (value === "(") {
213+
const value = pattern();
214+
yield { type: "PATTERN", index: i, value };
199215
} else if (value === "*") {
200-
const { name: value } = name();
216+
const value = name();
201217
yield { type: "WILDCARD", index: i, value };
202218
} else {
203219
yield { type: "CHAR", index: i, value: chars[i++] };
@@ -220,23 +236,15 @@ class Iter {
220236
return this._peek;
221237
}
222238

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 {
239+
tryConsume(type: TokenType): string | undefined {
231240
const token = this.peek();
232241
if (token.type !== type) return;
233242
this._peek = undefined; // Reset after consumed.
234243
return token.value;
235244
}
236245

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

326334
const param = it.tryConsume("PARAM");
327335
if (param) {
328-
const { name, pattern } = param;
336+
const pattern = it.tryConsume("PATTERN");
329337
tokens.push({
330338
type: "param",
331-
name,
339+
name: param,
332340
pattern,
333341
});
334342
continue;

0 commit comments

Comments
 (0)