Skip to content

Commit 01086a0

Browse files
committed
Add wildcard functionality
1 parent db3beff commit 01086a0

File tree

2 files changed

+124
-52
lines changed

2 files changed

+124
-52
lines changed

src/index.spec.ts

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ const PARSER_TESTS: ParserTestSet[] = [
3434
path: "/",
3535
expected: ["/"],
3636
},
37+
{
38+
path: "/:test",
39+
expected: [
40+
{ name: "test", prefix: "/", suffix: "", pattern: "", modifier: "" },
41+
],
42+
},
3743
];
3844

3945
const COMPILE_TESTS: CompileTestSet[] = [
@@ -61,6 +67,14 @@ const COMPILE_TESTS: CompileTestSet[] = [
6167
{ input: { id: "123" }, expected: "/test/" },
6268
],
6369
},
70+
{
71+
path: "/:0",
72+
tests: [
73+
{ input: undefined, expected: null },
74+
{ input: {}, expected: null },
75+
{ input: { 0: "123" }, expected: "/123" },
76+
],
77+
},
6478
{
6579
path: "/:test",
6680
tests: [
@@ -2648,9 +2662,6 @@ const MATCH_TESTS: MatchTestSet[] = [
26482662
},
26492663
{
26502664
path: "#/*",
2651-
testOptions: {
2652-
skip: true,
2653-
},
26542665
tests: [
26552666
{
26562667
input: "#/",
@@ -2675,14 +2686,11 @@ const MATCH_TESTS: MatchTestSet[] = [
26752686
},
26762687
{
26772688
path: "/entity/:id/*",
2678-
testOptions: {
2679-
skip: true,
2680-
},
26812689
tests: [
26822690
{
26832691
input: "/entity/foo",
2684-
matches: ["/entity/foo", "foo", undefined],
2685-
expected: { path: "/entity/foo", index: 0, params: { id: "foo" } },
2692+
matches: null,
2693+
expected: false,
26862694
},
26872695
{
26882696
input: "/entity/foo/",
@@ -2693,14 +2701,11 @@ const MATCH_TESTS: MatchTestSet[] = [
26932701
},
26942702
{
26952703
path: "/test/*",
2696-
testOptions: {
2697-
skip: true,
2698-
},
26992704
tests: [
27002705
{
27012706
input: "/test",
2702-
matches: ["/test", undefined],
2703-
expected: { path: "/test", index: 0, params: {} },
2707+
matches: null,
2708+
expected: false,
27042709
},
27052710
{
27062711
input: "/test/",
@@ -2712,6 +2717,58 @@ const MATCH_TESTS: MatchTestSet[] = [
27122717
matches: ["/test/route", "route"],
27132718
expected: { path: "/test/route", index: 0, params: { "0": ["route"] } },
27142719
},
2720+
{
2721+
input: "/test/route/nested",
2722+
matches: ["/test/route/nested", "route/nested"],
2723+
expected: {
2724+
path: "/test/route/nested",
2725+
index: 0,
2726+
params: { "0": ["route", "nested"] },
2727+
},
2728+
},
2729+
],
2730+
},
2731+
2732+
/**
2733+
* Asterisk wildcard.
2734+
*/
2735+
{
2736+
path: "/*",
2737+
tests: [
2738+
{
2739+
input: "/",
2740+
matches: ["/", undefined],
2741+
expected: { path: "/", index: 0, params: { "0": undefined } },
2742+
},
2743+
{
2744+
input: "/route",
2745+
matches: ["/route", "route"],
2746+
expected: { path: "/route", index: 0, params: { "0": ["route"] } },
2747+
},
2748+
{
2749+
input: "/route/nested",
2750+
matches: ["/route/nested", "route/nested"],
2751+
expected: {
2752+
path: "/route/nested",
2753+
index: 0,
2754+
params: { "0": ["route", "nested"] },
2755+
},
2756+
},
2757+
],
2758+
},
2759+
{
2760+
path: "*",
2761+
tests: [
2762+
{
2763+
input: "/",
2764+
matches: ["/", "/"],
2765+
expected: { path: "/", index: 0, params: { "0": ["", ""] } },
2766+
},
2767+
{
2768+
input: "/test",
2769+
matches: ["/test", "/test"],
2770+
expected: { path: "/test", index: 0, params: { "0": ["", "test"] } },
2771+
},
27152772
],
27162773
},
27172774
];
@@ -2730,7 +2787,7 @@ describe("path-to-regexp", () => {
27302787
prefix: "/",
27312788
suffix: "",
27322789
modifier: "",
2733-
pattern: "[^\\/]+?",
2790+
pattern: "",
27342791
},
27352792
];
27362793

src/index.ts

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
const DEFAULT_PREFIXES = "./";
21
const DEFAULT_DELIMITER = "/";
3-
const GROUPS_RE = /\((?:\?<(.*?)>)?(?!\?)/g;
42
const NOOP_VALUE = (value: string) => value;
5-
const ID_START = /^[$_\p{ID_Start}]$/u;
6-
const ID_CONTINUE = /^[$_\u200C\u200D\p{ID_Continue}]$/u;
3+
const ID_CHAR = /^\p{XID_Continue}$/u;
74

85
/**
96
* Encode a string into another string.
@@ -92,6 +89,7 @@ type TokenType =
9289
| "END"
9390
// Reserved for use.
9491
| "!"
92+
| "@"
9593
| ";";
9694

9795
/**
@@ -105,6 +103,7 @@ interface LexToken {
105103

106104
const SIMPLE_TOKENS: Record<string, TokenType> = {
107105
"!": "!",
106+
"@": "@",
108107
";": ";",
109108
"*": "*",
110109
"+": "+",
@@ -136,14 +135,14 @@ function lexer(str: string) {
136135
}
137136

138137
if (value === ":") {
139-
let name = chars[++i];
138+
let name = "";
140139

141-
if (!ID_START.test(name)) {
142-
throw new TypeError(`Missing parameter name at ${i}`);
140+
while (ID_CHAR.test(chars[++i])) {
141+
name += chars[i];
143142
}
144143

145-
while (ID_CONTINUE.test(chars[++i])) {
146-
name += chars[i];
144+
if (!name) {
145+
throw new TypeError(`Missing parameter name at ${i}`);
147146
}
148147

149148
tokens.push({ type: "NAME", index: i, value: name });
@@ -248,11 +247,10 @@ export class TokenData {
248247
*/
249248
export function parse(str: string, options: ParseOptions = {}): TokenData {
250249
const {
251-
prefixes = DEFAULT_PREFIXES,
250+
prefixes = "./",
252251
delimiter = DEFAULT_DELIMITER,
253252
encodePath = NOOP_VALUE,
254253
} = options;
255-
const defaultPattern = `[^${escape(delimiter)}]+?`;
256254
const tokens: Token[] = [];
257255
const it = lexer(str);
258256
let key = 0;
@@ -265,6 +263,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData {
265263

266264
if (name || pattern) {
267265
let prefix = char || "";
266+
const modifier = it.modifier();
268267

269268
if (!prefixes.includes(prefix)) {
270269
path += prefix;
@@ -281,10 +280,10 @@ export function parse(str: string, options: ParseOptions = {}): TokenData {
281280
encodePath,
282281
delimiter,
283282
name || String(key++),
284-
pattern || defaultPattern,
283+
pattern,
285284
prefix,
286285
"",
287-
it.modifier(),
286+
modifier,
288287
),
289288
);
290289
continue;
@@ -301,6 +300,22 @@ export function parse(str: string, options: ParseOptions = {}): TokenData {
301300
path = "";
302301
}
303302

303+
const asterisk = it.tryConsume("*");
304+
if (asterisk) {
305+
tokens.push(
306+
toKey(
307+
encodePath,
308+
delimiter,
309+
String(key++),
310+
`[^${escape(delimiter)}]*`,
311+
"",
312+
"",
313+
asterisk,
314+
),
315+
);
316+
continue;
317+
}
318+
304319
const open = it.tryConsume("{");
305320
if (open) {
306321
const prefix = it.text();
@@ -315,7 +330,7 @@ export function parse(str: string, options: ParseOptions = {}): TokenData {
315330
encodePath,
316331
delimiter,
317332
name || (pattern ? String(key++) : ""),
318-
name && !pattern ? defaultPattern : pattern || "",
333+
pattern,
319334
prefix,
320335
suffix,
321336
it.modifier(),
@@ -445,14 +460,15 @@ function compileTokens<P extends ParamData>(
445460
} = options;
446461
const reFlags = flags(options);
447462
const stringify = toStringify(loose);
463+
const keyToRegexp = toKeyRegexp(stringify, data.delimiter);
448464

449465
// Compile all the tokens into regexps.
450466
const encoders: Array<(data: ParamData) => string> = data.tokens.map(
451467
(token) => {
452468
const fn = tokenToFunction(token, encode);
453469
if (!validate || typeof token === "string") return fn;
454470

455-
const pattern = keyToRegexp(token, stringify);
471+
const pattern = keyToRegexp(token);
456472
const validRe = new RegExp(`^${pattern}$`, reFlags);
457473

458474
return (data) => {
@@ -516,16 +532,9 @@ function matchRegexp<P extends ParamData>(
516532

517533
const decoders = re.keys.map((key) => {
518534
if (key.separator) {
519-
const re = new RegExp(
520-
`(${key.pattern})(?:${stringify(key.separator)}|$)`,
521-
"g",
522-
);
535+
const re = new RegExp(stringify(key.separator), "g");
523536

524-
return (value: string) => {
525-
const result: string[] = [];
526-
for (const m of value.matchAll(re)) result.push(decode(m[1]));
527-
return result;
528-
};
537+
return (value: string) => value.split(re).map(decode);
529538
}
530539

531540
return decode;
@@ -613,14 +622,15 @@ function tokensToRegexp(
613622
loose = DEFAULT_DELIMITER,
614623
} = options;
615624
const stringify = toStringify(loose);
625+
const keyToRegexp = toKeyRegexp(stringify, data.delimiter);
616626
let pattern = start ? "^" : "";
617627

618628
for (const token of data.tokens) {
619629
if (typeof token === "string") {
620630
pattern += stringify(token);
621631
} else {
622632
if (token.name) keys.push(token);
623-
pattern += keyToRegexp(token, stringify);
633+
pattern += keyToRegexp(token);
624634
}
625635
}
626636

@@ -636,21 +646,26 @@ function tokensToRegexp(
636646
/**
637647
* Convert a token into a regexp string (re-used for path validation).
638648
*/
639-
function keyToRegexp(key: Key, stringify: Encode): string {
640-
const prefix = stringify(key.prefix);
641-
const suffix = stringify(key.suffix);
642-
643-
if (key.name) {
644-
if (key.separator) {
645-
const mod = key.modifier === "*" ? "?" : "";
646-
const split = stringify(key.separator);
647-
return `(?:${prefix}((?:${key.pattern})(?:${split}(?:${key.pattern}))*)${suffix})${mod}`;
648-
} else {
649-
return `(?:${prefix}(${key.pattern})${suffix})${key.modifier}`;
649+
function toKeyRegexp(stringify: Encode, delimiter: string) {
650+
const segmentPattern = `[^${escape(delimiter)}]+?`;
651+
652+
return (key: Key) => {
653+
const prefix = stringify(key.prefix);
654+
const suffix = stringify(key.suffix);
655+
656+
if (key.name) {
657+
const pattern = key.pattern || segmentPattern;
658+
if (key.separator) {
659+
const mod = key.modifier === "*" ? "?" : "";
660+
const split = stringify(key.separator);
661+
return `(?:${prefix}((?:${pattern})(?:${split}(?:${pattern}))*)${suffix})${mod}`;
662+
} else {
663+
return `(?:${prefix}(${pattern})${suffix})${key.modifier}`;
664+
}
650665
}
651-
} else {
666+
652667
return `(?:${prefix}${suffix})${key.modifier}`;
653-
}
668+
};
654669
}
655670

656671
/**

0 commit comments

Comments
 (0)