You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-Limitation: `{...}+`/ `{...}*` are rejected when group body contains `/` (cross-segment repetition unsupported in radix tree).
91
+
92
+
### URLPattern backslash escaping
93
+
94
+
Two separate escape systems handle `\x` in route patterns:
95
+
96
+
1. **Router escape encoding** (`_utils.ts`): `encodeEscapes()` converts `\:`, `\(`, `\)`, `\{`, `\}` to `\uFFFD` + single-char placeholders (A-E) before segment splitting, preventing these chars from being interpreted as route syntax. `decodeEscaped()` converts them back for static node keys. Other `\x` (like `\*`) are left for existing `segment === "\\*"` handling.
97
+
98
+
2. **Regex escape handling** (`_escape.ts`): `replaceEscapesOutsideGroups()` replaces `\x` outside `(...)` groups with `\uFFFE` placeholder, preserving regex syntax inside groups (e.g., `\d`in`(\d+)`). `resolveEscapePlaceholders()` then converts placeholders to regex-safe literals. Used by `routeToRegExp()` and `getParamRegexp()` in `add.ts`.
99
+
100
+
Key invariant: `\uFFFD` (U+FFFD) is used for router-level escaping, `\uFFFE` (U+FFFE) for regex-level escaping — they must not collide.
101
+
102
+
### Input path normalization
103
+
104
+
`normalizePath()` in `_utils.ts` resolves `.` and `..` segments in lookup paths (fast-path:skipifno`/.`found). Both `findRoute()` and `findAllRoutes()` normalize before matching. The compiler inlines equivalent logic in generated code.
105
+
106
+
### Wildcard segment captures
107
+
108
+
- **Breaking change:** unnamed captures now use URLPattern-style numeric keys (`"0"`, `"1"`, ...) instead of legacy `_0`, `_1`, ...
109
+
-Unescaped`*`insideasegmentistreatedasanunnamedcapture (`"0"`, `"1"`, ...), including mid-pattern forms like `/*.png` and `/file-*-*.png`.
110
+
- Wildcard capture indexing is shared with unnamed regex groups in the same route.
111
+
- `removeRoute()` now treats wildcard-segment patterns as dynamic segments (same classification as add/find/regexp).
-Runasingletest: `pnpm vitest run test/<file>.test.ts`
138
+
-**WPTcompattests:**`test/wpt.test.ts`validatesURLPatterncompatibilityusingWebPlatformTestdata. Knowndiffsaretrackedin`KNOWN_DIFFS`, `REGEXP_ONLY_KNOWN_DIFFS`, and `ROUTER_KNOWN_DIFFS` sets with reason comments.
Copy file name to clipboardExpand all lines: README.md
+66Lines changed: 66 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -94,6 +94,72 @@ findRoute(router, "GET", "/");
94
94
> [!TIP]
95
95
> If you need to register a pattern containing literal `:` or `*`, you can escape them with `\\`. For example, `/static\\:path/\\*\\*` matches only the static `/static:path/**` route.
|`/files/:path*`|`/files` or `/files/a/b`|`{}` or `{ path: "a/b" }`|
115
+
|`/book{s}?`|`/book` or `/books`|`{}`|
116
+
|`/blog/:id(\\d+){-:title}?`|`/blog/123` or `/blog/123-my-post`|`{ id: "123" }` or `{ id: "123", title: "my-post" }`|
117
+
118
+
-**Named params** (`:name`) match a single segment.
119
+
-**Single-segment wildcards** (`*`) capture unnamed params (`0`, `1`, ...) and can be used as full or mid-segment tokens (for example `/*` or `/*.png`).
120
+
-**Wildcards** (`**`) match zero or more segments. Use `**:name` to capture.
121
+
-**Regex constraints** (`:name(regex)`) restrict matching. Constrained and unconstrained params can coexist on the same node (constrained checked first).
122
+
-**Unnamed groups** (`(regex)`) capture into auto-indexed keys `0`, `1`, etc.
123
+
-**Modifiers:**`:name?` (optional), `:name+` (one or more), `:name*` (zero or more). Can combine with regex: `:id(\d+)?`.
124
+
-**Non-capturing groups** (`{...}`): supported with inline (`/foo{bar}`) and optional (`/foo{bar}?`) forms.
125
+
-**Current limitation:** repeating non-capturing groups (`{...}+`, `{...}*`) are supported only within a single segment (no `/` inside the group body).
126
+
-**Backslash escaping** (`\`): escape special characters like `:`, `*`, `(`, `)`, `{`, `}` with a backslash (e.g., `/static\:path` matches literal `/static:path`).
127
+
128
+
### Differences from URLPattern
129
+
130
+
rou3 aims for URLPattern-compatible syntax but has intentional differences due to its radix-tree design:
|`**` (double star) | Literal `**`| Catch-all wildcard (zero or more segments) |
136
+
|`(.*)` in segment | Greedy match across `/`| Segment-scoped (does not cross `/`) |
137
+
|`{...}+` / `{...}*` groups | Cross-segment group repetition | Only supported within a single segment (no `/` in group body) |
138
+
| Path normalization (`.`/`..`) | Resolves `.`/`..` in input paths | Not done by default (opt-in with `{ normalize: true }`) |
139
+
| Case sensitivity | Can be case-insensitive | Always case-sensitive |
140
+
| Non-`/`-prefixed paths | Supported | Paths must start with `/`|
141
+
| Unicode param names | Supports Unicode identifiers | Params use `\w` (ASCII word chars only) |
142
+
| Percent-encoding | Normalizes `%xx` sequences | Does not decode percent-encoded input |
143
+
144
+
### Path normalization
145
+
146
+
By default, `findRoute` and `findAllRoutes` do **not** resolve `.`/`..` segments in input paths. If your input paths may contain relative segments, enable normalization:
? `const _prefix=${JSON.stringify(UNNAMED_GROUP_PREFIX)},_prefixLen=${UNNAMED_GROUP_PREFIX.length};const _normalizeGroups=(g)=>{if(!g)return g;for(const k in g){if(k.startsWith(_prefix)){g[k.slice(_prefixLen)]=g[k];delete g[k]}}return g;};`
113
+
: "";
114
+
115
+
constnormalizePathHelper=ctx.opts?.normalize
116
+
? `if(p.includes("/.")){let _r=[];for(let _v of p.split("/")){if(_v===".")continue;_v===".."&&_r.length>1?_r.pop():_r.push(_v)}p=_r.join("/")||"/"}`
0 commit comments