Skip to content

Commit bf78005

Browse files
committed
update
1 parent 16cfb44 commit bf78005

File tree

5 files changed

+63
-85
lines changed

5 files changed

+63
-85
lines changed

README.md

Lines changed: 20 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -96,81 +96,26 @@ findRoute(router, "GET", "/");
9696
9797
## Route Patterns
9898

99-
rou3 supports a variety of route patterns, including [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API)-compatible syntax.
100-
101-
### Static
102-
103-
```
104-
/path/to/resource
105-
```
106-
107-
### Named Parameters
108-
109-
`:name` matches a single path segment.
110-
111-
```
112-
/users/:name → /users/foo → { name: "foo" }
113-
```
114-
115-
### Wildcards
116-
117-
`**` matches zero or more segments. Use `**:name` to capture the matched path.
118-
119-
```
120-
/path/** → /path/foo/bar → {}
121-
/path/**:rest → /path/foo/bar → { rest: "foo/bar" }
122-
```
123-
124-
### Regex Constraints
125-
126-
`:name(regex)` restricts a named parameter to match only the given pattern.
127-
128-
```
129-
/users/:id(\d+) → /users/123 → { id: "123" }
130-
→ /users/abc → (no match)
131-
/files/:ext(png|jpg|gif) → /files/png → { ext: "png" }
132-
→ /files/pdf → (no match)
133-
```
134-
135-
Regex-constrained and unconstrained params can coexist on the same path node (constrained routes are checked first):
136-
137-
```
138-
/users/:id(\d+) + /users/:slug
139-
/users/123 → { id: "123" }
140-
/users/abc → { slug: "abc" }
141-
```
142-
143-
### Unnamed Regex Groups
144-
145-
`(regex)` without a parameter name captures into auto-indexed keys `_0`, `_1`, etc.
146-
147-
```
148-
/path/(\d+) → /path/123 → { _0: "123" }
149-
/files/(png|jpg|gif) → /files/png → { _0: "png" }
150-
```
151-
152-
### Modifiers
153-
154-
- `:name?` — optional (zero or one segment)
155-
- `:name+` — one or more segments
156-
- `:name*` — zero or more segments
157-
158-
```
159-
/users/:id? → /users/123 → { id: "123" }
160-
→ /users → {}
161-
/files/:path+ → /files/a/b/c → { path: "a/b/c" }
162-
→ /files → (no match)
163-
/files/:path* → /files/a/b/c → { path: "a/b/c" }
164-
→ /files → {}
165-
```
166-
167-
Modifiers can be combined with regex constraints:
168-
169-
```
170-
/users/:id(\d+)? → /users/123 → { id: "123" }
171-
→ /users → {}
172-
→ /users/abc → (no match)
173-
```
99+
rou3 supports [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API)-compatible syntax.
100+
101+
| Pattern | Example Match | Params |
102+
| --- | --- | --- |
103+
| `/path/to/resource` | `/path/to/resource` | `{}` |
104+
| `/users/:name` | `/users/foo` | `{ name: "foo" }` |
105+
| `/path/**` | `/path/foo/bar` | `{}` |
106+
| `/path/**:rest` | `/path/foo/bar` | `{ rest: "foo/bar" }` |
107+
| `/users/:id(\\d+)` | `/users/123` | `{ id: "123" }` |
108+
| `/files/:ext(png\|jpg)` | `/files/png` | `{ ext: "png" }` |
109+
| `/path/(\\d+)` | `/path/123` | `{ _0: "123" }` |
110+
| `/users/:id?` | `/users` or `/users/123` | `{}` or `{ id: "123" }` |
111+
| `/files/:path+` | `/files/a/b/c` | `{ path: "a/b/c" }` |
112+
| `/files/:path*` | `/files` or `/files/a/b` | `{}` or `{ path: "a/b" }` |
113+
114+
- **Named params** (`:name`) match a single segment.
115+
- **Wildcards** (`**`) match zero or more segments. Use `**:name` to capture.
116+
- **Regex constraints** (`:name(regex)`) restrict matching. Constrained and unconstrained params can coexist on the same node (constrained checked first).
117+
- **Unnamed groups** (`(regex)`) capture into auto-indexed keys `_0`, `_1`, etc.
118+
- **Modifiers:** `:name?` (optional), `:name+` (one or more), `:name*` (zero or more). Can combine with regex: `:id(\d+)?`.
174119

175120
## Compiler
176121

src/operations/add.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export function addRoute<T>(
6565
if (segment === "*") {
6666
paramsMap.push([i, `_${_unnamedParamIndex++}`, true /* optional */]);
6767
} else if (segment.includes(":", 1) || segment.includes("(")) {
68-
const regexp = getParamRegexp(segment);
68+
const [regexp, nextIndex] = getParamRegexp(segment, _unnamedParamIndex);
69+
_unnamedParamIndex = nextIndex;
6970
paramsRegexp[i] = regexp;
7071
node.hasRegexParam = true;
7172
paramsMap.push([i, regexp, false]);
@@ -125,19 +126,20 @@ function _expandModifiers(segments: string[]): string[] | undefined {
125126
];
126127
}
127128
const name = m[1].match(/:(\w+)/)?.[1] || "_";
128-
const wc = "/" + pre.concat(`**:${name}`).join("/");
129-
return m[2] === "+" ? [wc] : [wc, "/" + pre.join("/")];
129+
const wc = "/" + [...pre, `**:${name}`, ...suf].join("/");
130+
const without = "/" + [...pre, ...suf].join("/");
131+
return m[2] === "+" ? [wc] : [wc, without];
130132
}
131133
}
132134

133-
function getParamRegexp(segment: string): RegExp {
134-
let _i = 0;
135+
function getParamRegexp(segment: string, unnamedStart = 0): [RegExp, number] {
136+
let _i = unnamedStart;
135137
const regex = segment
136138
.replace(
137139
/:(\w+)(?:\(([^)]*)\))?/g,
138140
(_, id, pattern) => `(?<${id}>${pattern || "[^/]+"})`,
139141
)
140142
.replace(/\((?![?<])/g, () => `(?<_${_i++}>`)
141143
.replace(/\./g, "\\.");
142-
return new RegExp(`^${regex}$`);
144+
return [new RegExp(`^${regex}$`), _i];
143145
}

src/regexp.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function routeToRegExp(route: string = "/"): RegExp {
99
reSegments.push(
1010
segment === "**" ? "?(?<_>.*)" : `?(?<${segment.slice(3)}>.+)`,
1111
);
12-
} else if (segment.includes(":") || segment.includes("(")) {
12+
} else if (segment.includes(":") || /(^|[^\\])\(/.test(segment)) {
1313
const modMatch = segment.match(/^(.*:\w+(?:\([^)]*\))?)([?+*])$/);
1414
if (modMatch) {
1515
const [, base, mod] = modMatch;
@@ -24,8 +24,18 @@ export function routeToRegExp(route: string = "/"): RegExp {
2424
reSegments.push(`?${inner}?`);
2525
continue;
2626
}
27-
// + or *
28-
reSegments.push(mod === "+" ? `?(?<${name}>.+)` : `?(?<${name}>.*)`);
27+
// + or * (preserve inline constraint when present)
28+
const pattern = base.match(/:(\w+)(?:\(([^)]*)\))?/)?.[2];
29+
if (pattern) {
30+
const repeated = `${pattern}(?:/${pattern})*`;
31+
reSegments.push(
32+
mod === "+"
33+
? `?(?<${name}>${repeated})`
34+
: `?(?<${name}>${repeated})?`,
35+
);
36+
} else {
37+
reSegments.push(mod === "+" ? `?(?<${name}>.+)` : `?(?<${name}>.*)`);
38+
}
2939
continue;
3040
}
3141
reSegments.push(
@@ -34,7 +44,7 @@ export function routeToRegExp(route: string = "/"): RegExp {
3444
/:(\w+)(?:\(([^)]*)\))?/g,
3545
(_, id, pattern) => `(?<${id}>${pattern || "[^/]+"})`,
3646
)
37-
.replace(/\((?![?<])/g, () => `(?<_${idCtr++}>`)
47+
.replace(/(^|[^\\])\((?![?<])/g, (_, p) => `${p}(?<_${idCtr++}>`)
3848
.replace(/\./g, "\\."),
3949
);
4050
} else {

test/regexp.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ describe("routeToRegExp", () => {
9595
regex: /^\/path\/(?<_0>png|jpg|gif)\/?$/,
9696
match: [["/path/png", { _0: "png" }]],
9797
},
98+
"/path/:id(\\d+)+": {
99+
regex: /^\/path\/?(?<id>\d+(?:\/\d+)*)\/?$/,
100+
match: [
101+
["/path/123", { id: "123" }],
102+
["/path/123/456", { id: "123/456" }],
103+
],
104+
},
105+
"/path/:id(\\d+)*": {
106+
regex: /^\/path\/?(?<id>\d+(?:\/\d+)*)?\/?$/,
107+
match: [["/path/123", { id: "123" }], ["/path"]],
108+
},
98109
} as const;
99110

100111
for (const [route, expected] of Object.entries(routes)) {

test/router.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,16 @@ describe("Router lookup", function () {
494494
"/path/abc/foo": undefined,
495495
});
496496

497+
// Multi-unnamed groups across segments
498+
testRouter(["/path/(\\d+)/(\\w+)"], undefined, {
499+
"/path/123/abc": {
500+
data: { path: "/path/(\\d+)/(\\w+)" },
501+
params: { _0: "123", _1: "abc" },
502+
},
503+
"/path/abc/abc": undefined,
504+
"/path/123/!": undefined,
505+
});
506+
497507
// Coexistence: unnamed regex + unconstrained param
498508
testRouter(["/path/(\\d+)", "/path/:slug"], undefined, {
499509
"/path/123": {

0 commit comments

Comments
 (0)