Skip to content

Commit 5844988

Browse files
authored
Remove internal tokenization during parse (#435)
1 parent 9a78879 commit 5844988

File tree

2 files changed

+98
-116
lines changed

2 files changed

+98
-116
lines changed

src/index.spec.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,17 @@ describe("path-to-regexp", () => {
4848
});
4949

5050
describe("parse errors", () => {
51-
it("should throw on unbalanced group", () => {
52-
expect(() => parse("/{:foo,")).toThrow(
53-
new PathError("Unexpected end at index 7, expected }", "/{:foo,"),
54-
);
55-
});
56-
57-
it("should throw on nested unbalanced group", () => {
58-
expect(() => parse("/{:foo/{x,y}")).toThrow(
59-
new PathError("Unexpected end at index 12, expected }", "/{:foo/{x,y}"),
60-
);
61-
});
51+
it.each(["/{", "/{:foo,", "/{:foo/{x,y}"])(
52+
"should throw on unbalanced group: %s",
53+
(path) => {
54+
expect(() => parse(path)).toThrow(
55+
new PathError(
56+
`Unexpected end at index ${path.length}, expected }`,
57+
path,
58+
),
59+
);
60+
},
61+
);
6262

6363
it("should throw on missing param name", () => {
6464
expect(() => parse("/:/")).toThrow(
@@ -107,6 +107,15 @@ describe("path-to-regexp", () => {
107107
new PathError("Unterminated quote at index 6", '/foo/:"bar\\'),
108108
);
109109
});
110+
111+
it.each(["}", "(", ")", "[", "]", "+", "?", "!"])(
112+
"should throw on unexpected character %s",
113+
(char) => {
114+
expect(() => parse(`/foo/${char}`)).toThrow(
115+
new PathError(`Unexpected ${char} at index 5`, `/foo/${char}`),
116+
);
117+
},
118+
);
110119
});
111120

112121
describe("compile errors", () => {

src/index.ts

Lines changed: 78 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -58,33 +58,6 @@ export interface CompileOptions {
5858
delimiter?: string;
5959
}
6060

61-
type TokenType =
62-
| "{"
63-
| "}"
64-
| "wildcard"
65-
| "param"
66-
| "char"
67-
| "end"
68-
// Reserved for use or ambiguous due to past use.
69-
| "("
70-
| ")"
71-
| "["
72-
| "]"
73-
| "+"
74-
| "?"
75-
| "!";
76-
77-
/**
78-
* Tokenizer results.
79-
*/
80-
interface LexToken {
81-
type: TokenType;
82-
index: number;
83-
value: string;
84-
}
85-
86-
const SIMPLE_TOKENS = "{}()[]+?!";
87-
8861
/**
8962
* Escape text for stringify to path.
9063
*/
@@ -177,116 +150,116 @@ export class PathError extends TypeError {
177150
export function parse(str: string, options: ParseOptions = {}): TokenData {
178151
const { encodePath = NOOP_VALUE } = options;
179152
const chars = [...str];
180-
const tokens: Array<LexToken> = [];
181153
let index = 0;
182-
let pos = 0;
183-
184-
function name() {
185-
let value = "";
186-
187-
if (ID_START.test(chars[index])) {
188-
do {
189-
value += chars[index++];
190-
} while (ID_CONTINUE.test(chars[index]));
191-
} else if (chars[index] === '"') {
192-
let quoteStart = index;
193-
194-
while (index < chars.length) {
195-
if (chars[++index] === '"') {
196-
index++;
197-
quoteStart = 0;
198-
break;
199-
}
200-
201-
// Increment over escape characters.
202-
if (chars[index] === "\\") index++;
203154

204-
value += chars[index];
205-
}
206-
207-
if (quoteStart) {
208-
throw new PathError(`Unterminated quote at index ${quoteStart}`, str);
209-
}
210-
}
211-
212-
if (!value) {
213-
throw new PathError(`Missing parameter name at index ${index}`, str);
155+
function consumeUntil(end: string): Token[] {
156+
const output: Token[] = [];
157+
let path = "";
158+
159+
function writePath() {
160+
if (!path) return;
161+
output.push({
162+
type: "text",
163+
value: encodePath(path),
164+
});
165+
path = "";
214166
}
215167

216-
return value;
217-
}
218-
219-
while (index < chars.length) {
220-
const value = chars[index++];
168+
while (index < chars.length) {
169+
const value = chars[index++];
221170

222-
if (value === "\\") {
223-
if (index === chars.length) {
224-
throw new PathError(`Unexpected end after \\ at index ${index}`, str);
171+
if (value === end) {
172+
writePath();
173+
return output;
225174
}
226175

227-
tokens.push({ type: "char", index, value: chars[index++] });
228-
} else if (SIMPLE_TOKENS.includes(value)) {
229-
tokens.push({ type: value as TokenType, index, value });
230-
} else if (value === ":") {
231-
tokens.push({ type: "param", index, value: name() });
232-
} else if (value === "*") {
233-
tokens.push({ type: "wildcard", index, value: name() });
234-
} else {
235-
tokens.push({ type: "char", index, value });
236-
}
237-
}
176+
if (value === "\\") {
177+
if (index === chars.length) {
178+
throw new PathError(`Unexpected end after \\ at index ${index}`, str);
179+
}
238180

239-
tokens.push({ type: "end", index, value: "" });
181+
path += chars[index++];
182+
continue;
183+
}
240184

241-
function consumeUntil(endType: TokenType): Token[] {
242-
const output: Token[] = [];
185+
if (value === ":" || value === "*") {
186+
const type = value === ":" ? "param" : "wildcard";
187+
let name = "";
188+
189+
if (ID_START.test(chars[index])) {
190+
do {
191+
name += chars[index++];
192+
} while (ID_CONTINUE.test(chars[index]));
193+
} else if (chars[index] === '"') {
194+
let quoteStart = index;
195+
196+
while (index < chars.length) {
197+
if (chars[++index] === '"') {
198+
index++;
199+
quoteStart = 0;
200+
break;
201+
}
243202

244-
while (true) {
245-
const token = tokens[pos++];
246-
if (token.type === endType) break;
203+
// Increment over escape characters.
204+
if (chars[index] === "\\") index++;
247205

248-
if (token.type === "char") {
249-
let path = token.value;
250-
let cur = tokens[pos];
206+
name += chars[index];
207+
}
251208

252-
while (cur.type === "char") {
253-
path += cur.value;
254-
cur = tokens[++pos];
209+
if (quoteStart) {
210+
throw new PathError(
211+
`Unterminated quote at index ${quoteStart}`,
212+
str,
213+
);
214+
}
255215
}
256216

257-
output.push({
258-
type: "text",
259-
value: encodePath(path),
260-
});
261-
continue;
262-
}
217+
if (!name) {
218+
throw new PathError(`Missing parameter name at index ${index}`, str);
219+
}
263220

264-
if (token.type === "param" || token.type === "wildcard") {
265-
output.push({
266-
type: token.type,
267-
name: token.value,
268-
});
221+
writePath();
222+
output.push({ type, name });
269223
continue;
270224
}
271225

272-
if (token.type === "{") {
226+
if (value === "{") {
227+
writePath();
273228
output.push({
274229
type: "group",
275230
tokens: consumeUntil("}"),
276231
});
277232
continue;
278233
}
279234

235+
if (
236+
value === "}" ||
237+
value === "(" ||
238+
value === ")" ||
239+
value === "[" ||
240+
value === "]" ||
241+
value === "+" ||
242+
value === "?" ||
243+
value === "!"
244+
) {
245+
throw new PathError(`Unexpected ${value} at index ${index - 1}`, str);
246+
}
247+
248+
path += value;
249+
}
250+
251+
if (end) {
280252
throw new PathError(
281-
`Unexpected ${token.type} at index ${token.index}, expected ${endType}`,
253+
`Unexpected end at index ${index}, expected ${end}`,
282254
str,
283255
);
284256
}
285257

258+
writePath();
286259
return output;
287260
}
288261

289-
return new TokenData(consumeUntil("end"), str);
262+
return new TokenData(consumeUntil(""), str);
290263
}
291264

292265
/**
@@ -537,7 +510,7 @@ function toRegExpSource(
537510
let hasSegmentCapture = 0;
538511
let index = 0;
539512

540-
function hasInSegment(index: number, type: TokenType) {
513+
function hasInSegment(index: number, type: Token["type"]) {
541514
while (index < tokens.length) {
542515
const token = tokens[index++];
543516
if (token.type === type) return true;

0 commit comments

Comments
 (0)