@alchemy.run/sigil 0.0.0-alpha.3 → 0.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +23 -13
  2. package/THIRD_PARTY_NOTICES.md +70 -23
  3. package/dist/Text-D5HUf3Fj.d.ts +452 -0
  4. package/dist/ansi.d.ts +137 -143
  5. package/dist/ansi.js +87 -2
  6. package/dist/capabilities.d.ts +5 -0
  7. package/dist/capabilities.js +3 -0
  8. package/dist/cell-_ZVhbfl0.js +44 -0
  9. package/dist/color-CkbalRqK.js +2 -0
  10. package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
  11. package/dist/color-policy-SVj1pYTA.js +560 -0
  12. package/dist/color-profile-DHhQHY55.js +36 -0
  13. package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
  14. package/dist/color.d.ts +21 -0
  15. package/dist/color.js +3 -0
  16. package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
  17. package/dist/detect-Bh4yGP6w.d.ts +186 -0
  18. package/dist/detect-BuTXtY6e.js +373 -0
  19. package/dist/{devtools-BhYGjb7h.js → devtools-DbthxoD1.js} +22 -23
  20. package/dist/env-YVw64yZS.js +9 -0
  21. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  22. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  23. package/dist/index-Bmc2tRPk.d.ts +21 -0
  24. package/dist/index.d.ts +235 -754
  25. package/dist/index.js +1362 -3001
  26. package/dist/osc-CCH7xDoS.js +71 -0
  27. package/dist/osc-Cn0fw77g.d.ts +23 -0
  28. package/dist/paint-C19minOS.d.ts +81 -0
  29. package/dist/query-vaIeGOkH.d.ts +152 -0
  30. package/dist/router.d.ts +392 -0
  31. package/dist/router.js +709 -0
  32. package/dist/sample-Cqw1bjUL.js +445 -0
  33. package/dist/screen-BOh__-65.js +324 -0
  34. package/dist/screen-BReKIheE.d.ts +40 -0
  35. package/dist/screen.d.ts +5 -0
  36. package/dist/screen.js +5 -0
  37. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  38. package/dist/serialize-BTkAZgw1.js +79 -0
  39. package/dist/session-BJmzX2NJ.js +664 -0
  40. package/dist/sgr-BhwaWAJB.js +246 -0
  41. package/dist/store-CgrG9K4y.d.ts +72 -0
  42. package/dist/string-width-CijQwpIk.js +69 -0
  43. package/dist/strip-BvU4toXG.js +6 -0
  44. package/dist/terminal.d.ts +118 -0
  45. package/dist/terminal.js +2 -0
  46. package/dist/tokenize-AjqbvtiT.js +1242 -0
  47. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  48. package/dist/truncate-D31fhU6i.js +562 -0
  49. package/dist/use-focus-C2sciZOo.js +1335 -0
  50. package/package.json +43 -17
  51. package/src/ansi/chalk.ts +12 -53
  52. package/src/ansi/cursor.ts +2 -4
  53. package/src/ansi/east-asian-width.ts +44 -0
  54. package/src/ansi/escapes.ts +18 -26
  55. package/src/ansi/graphemes.ts +8 -0
  56. package/src/ansi/hyperlink.ts +44 -0
  57. package/src/ansi/index.ts +14 -14
  58. package/src/ansi/osc.ts +77 -0
  59. package/src/ansi/sgr.ts +2 -5
  60. package/src/ansi/slice.ts +1 -1
  61. package/src/ansi/string-width.ts +91 -206
  62. package/src/ansi/strip.ts +13 -33
  63. package/src/ansi/tokenize.ts +96 -169
  64. package/src/ansi/truncate.ts +5 -3
  65. package/src/ansi/wrap.ts +13 -14
  66. package/src/ansi-tokenizer.ts +1 -1
  67. package/src/capabilities/color-policy.ts +34 -0
  68. package/src/capabilities/detect.ts +594 -0
  69. package/src/capabilities/index.ts +37 -0
  70. package/src/capabilities/query.ts +657 -0
  71. package/src/capabilities/store.ts +379 -0
  72. package/src/color/index.ts +3 -0
  73. package/src/color/paint.ts +169 -0
  74. package/src/color/palette.ts +48 -0
  75. package/src/color/sample.ts +323 -0
  76. package/src/color.ts +1 -0
  77. package/src/components/AnsiText.tsx +42 -0
  78. package/src/components/App.tsx +109 -22
  79. package/src/components/BackgroundContext.ts +2 -3
  80. package/src/components/Box.tsx +43 -60
  81. package/src/components/CursorContext.ts +1 -1
  82. package/src/components/ErrorBoundary.tsx +1 -1
  83. package/src/components/ErrorOverview.tsx +8 -7
  84. package/src/components/Hyperlink.tsx +56 -0
  85. package/src/components/Spacer.tsx +1 -1
  86. package/src/components/Static.tsx +1 -1
  87. package/src/components/StderrContext.ts +0 -2
  88. package/src/components/StdinContext.ts +0 -1
  89. package/src/components/StdoutContext.ts +1 -3
  90. package/src/components/TerminalOscContext.ts +25 -0
  91. package/src/components/Text.tsx +23 -46
  92. package/src/components/Transform.tsx +2 -2
  93. package/src/cursor-position.ts +1 -1
  94. package/src/devtools.ts +84 -24
  95. package/src/dom.ts +11 -8
  96. package/src/env.ts +12 -0
  97. package/src/get-max-width.ts +1 -1
  98. package/src/global.d.ts +6 -4
  99. package/src/{boxes.ts → glyphs.ts} +17 -18
  100. package/src/hooks/use-animation.ts +1 -1
  101. package/src/hooks/use-app.ts +1 -1
  102. package/src/hooks/use-box-metrics.ts +1 -1
  103. package/src/hooks/use-capabilities.ts +73 -0
  104. package/src/hooks/use-cursor.ts +3 -3
  105. package/src/hooks/use-focus-manager.ts +1 -1
  106. package/src/hooks/use-focus.ts +5 -6
  107. package/src/hooks/use-input.ts +4 -4
  108. package/src/hooks/use-is-screen-reader-enabled.ts +1 -1
  109. package/src/hooks/use-paste.ts +2 -2
  110. package/src/hooks/use-stderr.ts +1 -1
  111. package/src/hooks/use-stdin.ts +1 -1
  112. package/src/hooks/use-stdout.ts +1 -1
  113. package/src/hooks/use-terminal-osc.ts +59 -0
  114. package/src/hooks/use-window-size.ts +2 -2
  115. package/src/index.ts +88 -44
  116. package/src/ink.tsx +620 -745
  117. package/src/input-parser.ts +1 -1
  118. package/src/instances.ts +1 -1
  119. package/src/kitty-keyboard.ts +128 -0
  120. package/src/measure-element.ts +1 -1
  121. package/src/measure-text.ts +2 -2
  122. package/src/{render-node-to-output.ts → paint-tree.ts} +78 -53
  123. package/src/parse-keypress.ts +4 -5
  124. package/src/parse-stack-line.ts +0 -1
  125. package/src/patch-console.ts +44 -0
  126. package/src/reconciler.ts +36 -62
  127. package/src/render-background.ts +37 -16
  128. package/src/render-border.ts +97 -64
  129. package/src/render-frame.ts +83 -0
  130. package/src/render-to-string.ts +24 -9
  131. package/src/render.ts +24 -16
  132. package/src/router/components.tsx +343 -0
  133. package/src/router/context.ts +41 -0
  134. package/src/router/history.ts +194 -0
  135. package/src/router/hooks.tsx +391 -0
  136. package/src/router/index.ts +34 -0
  137. package/src/router/matcher.ts +571 -0
  138. package/src/sanitize-ansi.ts +1 -1
  139. package/src/screen/ansi.ts +184 -0
  140. package/src/screen/canvas.ts +137 -0
  141. package/src/screen/cell.ts +138 -0
  142. package/src/screen/color-profile.ts +47 -0
  143. package/src/screen/geometry.ts +9 -0
  144. package/src/screen/index.ts +6 -0
  145. package/src/screen/screen.ts +272 -0
  146. package/src/screen/serialize.ts +129 -0
  147. package/src/screen.ts +1 -0
  148. package/src/semantic-text-style.ts +118 -0
  149. package/src/signal-exit.ts +4 -5
  150. package/src/squash-text-nodes.ts +4 -7
  151. package/src/stream.ts +2 -4
  152. package/src/structured-text.ts +321 -0
  153. package/src/styles.ts +16 -17
  154. package/src/terminal/index.ts +2 -0
  155. package/src/terminal/inline-presenter.ts +120 -0
  156. package/src/terminal/input.ts +86 -0
  157. package/src/terminal/render-scheduler.ts +37 -0
  158. package/src/terminal/screen-presenter.ts +188 -0
  159. package/src/terminal/session.ts +407 -0
  160. package/src/terminal-size.ts +9 -8
  161. package/src/terminal.ts +1 -0
  162. package/src/testing/browser.ts +588 -0
  163. package/src/testing/emulators.ts +205 -0
  164. package/src/testing/explorer-app/index.html +12 -0
  165. package/src/testing/explorer-app/main.ts +381 -0
  166. package/src/testing/explorer-app/style.css +194 -0
  167. package/src/testing/explorer-app/tsconfig.json +15 -0
  168. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  169. package/src/testing/index.ts +26 -0
  170. package/src/testing/keys.ts +56 -0
  171. package/src/testing/live.ts +85 -0
  172. package/src/testing/matchers.ts +70 -0
  173. package/src/testing/public.ts +94 -0
  174. package/src/testing/terminal.ts +349 -0
  175. package/src/testing/vitest.ts +157 -0
  176. package/src/transform-adapter.ts +14 -0
  177. package/src/types.ts +1 -6
  178. package/src/utils.ts +2 -2
  179. package/src/wrap-text.ts +4 -4
  180. package/src/yoga/config.ts +2 -2
  181. package/src/yoga/core/absoluteLayout.ts +15 -15
  182. package/src/yoga/core/baseline.ts +3 -3
  183. package/src/yoga/core/cache.ts +4 -4
  184. package/src/yoga/core/calculateLayout.ts +33 -27
  185. package/src/yoga/core/config.ts +1 -1
  186. package/src/yoga/core/flexLine.ts +3 -3
  187. package/src/yoga/core/helpers.ts +3 -3
  188. package/src/yoga/core/layoutResults.ts +4 -4
  189. package/src/yoga/core/node.ts +16 -16
  190. package/src/yoga/core/pixelGrid.ts +4 -4
  191. package/src/yoga/core/style.ts +11 -11
  192. package/src/yoga/core/types.ts +2 -2
  193. package/src/yoga/index.ts +6 -6
  194. package/src/yoga/node.ts +8 -8
  195. package/dist/sgr-CMfEpjSk.d.ts +0 -91
  196. package/dist/truncate-CBiyyZzw.js +0 -2156
  197. package/src/ansi/supports-color.ts +0 -213
  198. package/src/ansi/widest-line.ts +0 -12
  199. package/src/auto-bind.ts +0 -41
  200. package/src/colorize.ts +0 -60
  201. package/src/devtools-window-polyfill.ts +0 -73
  202. package/src/indent-string.ts +0 -16
  203. package/src/is-in-ci.ts +0 -7
  204. package/src/log-update.ts +0 -370
  205. package/src/output.ts +0 -308
  206. package/src/renderer.ts +0 -73
  207. package/src/write-synchronized.ts +0 -9
@@ -0,0 +1,571 @@
1
+ // Route matching, ranking, and path resolution. Ported from react-router's
2
+ // agnostic matcher (lib/router/utils.ts, MIT) with the URL-specific parts
3
+ // removed: no basename, no percent-encoding/decoding, and matching is always
4
+ // case-sensitive — route paths are code constants, not user-typed URLs.
5
+ import { type ReactNode } from "react";
6
+
7
+ import { parsePath, type Location, type Path, type To } from "#/router/history.ts";
8
+
9
+ /**
10
+ A route definition. Used as JSX via `<Route>` or passed as plain objects to
11
+ `useRoutes`.
12
+ */
13
+ export type RouteObject = {
14
+ /**
15
+ The path pattern to match against the current location, relative to the
16
+ parent route. Supports `:param` dynamic segments, optional segments
17
+ (`:param?`, `edit?`), and a trailing `*` splat.
18
+ */
19
+ path?: string;
20
+
21
+ /**
22
+ An index route renders in its parent's `<Outlet>` at the parent's exact path.
23
+ Index routes cannot have children.
24
+ */
25
+ index?: boolean;
26
+
27
+ /**
28
+ The element to render when this route matches.
29
+ */
30
+ element?: ReactNode;
31
+
32
+ /**
33
+ Nested child routes, rendered into this route's `<Outlet>`.
34
+ */
35
+ children?: RouteObject[];
36
+ };
37
+
38
+ /**
39
+ Parsed params from dynamic segments of a matched path. The splat segment, if
40
+ any, is available under the `"*"` key.
41
+ */
42
+ export type Params<Key extends string = string> = {
43
+ readonly [key in Key]: string | undefined;
44
+ };
45
+
46
+ /**
47
+ A route object matched against a location, along with the params and the
48
+ portion of the pathname it consumed.
49
+ */
50
+ export type RouteMatch<RouteObjectType extends RouteObject = RouteObject> = {
51
+ params: Params;
52
+ pathname: string;
53
+ pathnameBase: string;
54
+ route: RouteObjectType;
55
+ };
56
+
57
+ /**
58
+ A pattern for matching some portion of a pathname with `matchPath`.
59
+ */
60
+ export type PathPattern = {
61
+ /**
62
+ The path pattern to match against.
63
+ */
64
+ path: string;
65
+
66
+ /**
67
+ Should be `true` (the default) if the pattern must consume the entire
68
+ pathname; `false` allows matching a prefix.
69
+ */
70
+ end?: boolean;
71
+ };
72
+
73
+ /**
74
+ The result of matching a `PathPattern` against a pathname.
75
+ */
76
+ export type PathMatch<ParamKey extends string = string> = {
77
+ params: Params<ParamKey>;
78
+ pathname: string;
79
+ pathnameBase: string;
80
+ pattern: PathPattern;
81
+ };
82
+
83
+ export const joinPaths = (paths: string[]): string => paths.join("/").replace(/\/\/+/g, "/");
84
+
85
+ export const normalizePathname = (pathname: string): string =>
86
+ pathname.replace(/\/+$/, "").replace(/^\/*/, "/");
87
+
88
+ export const normalizeSearch = (search: string): string =>
89
+ !search || search === "?" ? "" : search.startsWith("?") ? search : `?${search}`;
90
+
91
+ type RouteMeta<RouteObjectType extends RouteObject> = {
92
+ relativePath: string;
93
+ childrenIndex: number;
94
+ route: RouteObjectType;
95
+ };
96
+
97
+ type RouteBranch<RouteObjectType extends RouteObject> = {
98
+ path: string;
99
+ score: number;
100
+ routesMeta: RouteMeta<RouteObjectType>[];
101
+ };
102
+
103
+ /**
104
+ Matches a set of (possibly nested) routes against a location and returns the
105
+ chain of matches from the root route down to the leaf, or `null` if nothing
106
+ matches.
107
+ */
108
+ export function matchRoutes<RouteObjectType extends RouteObject = RouteObject>(
109
+ routes: RouteObjectType[],
110
+ locationArg: Partial<Location> | string,
111
+ ): RouteMatch<RouteObjectType>[] | null {
112
+ const location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
113
+ const pathname = location.pathname ?? "/";
114
+
115
+ const branches = flattenRoutes(routes);
116
+ rankRouteBranches(branches);
117
+
118
+ let matches = null;
119
+ for (let index = 0; matches == null && index < branches.length; index++) {
120
+ matches = matchRouteBranch(branches[index]!, pathname);
121
+ }
122
+
123
+ return matches;
124
+ }
125
+
126
+ function flattenRoutes<RouteObjectType extends RouteObject>(
127
+ routes: RouteObjectType[],
128
+ branches: RouteBranch<RouteObjectType>[] = [],
129
+ parentsMeta: RouteMeta<RouteObjectType>[] = [],
130
+ parentPath = "",
131
+ hasParentOptionalSegments = false,
132
+ ): RouteBranch<RouteObjectType>[] {
133
+ const flattenRoute = (
134
+ route: RouteObjectType,
135
+ index: number,
136
+ insideOptional: boolean,
137
+ relativePath = route.path ?? "",
138
+ ): void => {
139
+ const meta: RouteMeta<RouteObjectType> = {
140
+ relativePath,
141
+ childrenIndex: index,
142
+ route,
143
+ };
144
+
145
+ if (meta.relativePath.startsWith("/")) {
146
+ if (!meta.relativePath.startsWith(parentPath)) {
147
+ // Optional-segment explosion produces sibling variants that can't all
148
+ // line up with an absolute child path; discard the mismatches.
149
+ if (insideOptional) {
150
+ return;
151
+ }
152
+
153
+ throw new Error(
154
+ `Absolute route path "${meta.relativePath}" nested under path "${parentPath}" is not valid. ` +
155
+ `An absolute child route path must start with the combined path of all its parent routes.`,
156
+ );
157
+ }
158
+
159
+ meta.relativePath = meta.relativePath.slice(parentPath.length);
160
+ }
161
+
162
+ const path = joinPaths([parentPath, meta.relativePath]);
163
+ const routesMeta = parentsMeta.concat(meta);
164
+
165
+ // Traverse depth-first so child routes rank before their parents in the
166
+ // flattened list.
167
+ if (route.children && route.children.length > 0) {
168
+ if (route.index) {
169
+ throw new Error(
170
+ `Index routes must not have child routes. Please remove all child routes from route path "${path}".`,
171
+ );
172
+ }
173
+
174
+ flattenRoutes(
175
+ route.children as RouteObjectType[],
176
+ branches,
177
+ routesMeta,
178
+ path,
179
+ insideOptional,
180
+ );
181
+ }
182
+
183
+ // Routes without a path never match by themselves unless they are index
184
+ // routes, so don't add them to the list of possible branches.
185
+ if (route.path == null && !route.index) {
186
+ return;
187
+ }
188
+
189
+ branches.push({ path, score: computeScore(path, route.index), routesMeta });
190
+ };
191
+
192
+ routes.forEach((route, index) => {
193
+ if (route.path === "" || !route.path?.includes("?")) {
194
+ flattenRoute(route, index, hasParentOptionalSegments);
195
+ } else {
196
+ for (const exploded of explodeOptionalSegments(route.path)) {
197
+ flattenRoute(route, index, true, exploded);
198
+ }
199
+ }
200
+ });
201
+
202
+ return branches;
203
+ }
204
+
205
+ // Computes all combinations of optional path segments for a given path.
206
+ // For example, `/one/:two?/three/:four?` explodes to `/one/three`,
207
+ // `/one/:two/three`, `/one/three/:four`, and `/one/:two/three/:four`.
208
+ function explodeOptionalSegments(path: string): string[] {
209
+ const segments = path.split("/");
210
+ if (segments.length === 0) {
211
+ return [];
212
+ }
213
+
214
+ const [first = "", ...rest] = segments;
215
+ const isOptional = first.endsWith("?");
216
+ const required = first.replace(/\?$/, "");
217
+
218
+ if (rest.length === 0) {
219
+ // An empty string means "omit this optional segment".
220
+ return isOptional ? [required, ""] : [required];
221
+ }
222
+
223
+ const restExploded = explodeOptionalSegments(rest.join("/"));
224
+ const result: string[] = [];
225
+
226
+ // Emit the required variant for all children before the omitted variant so
227
+ // parent optional segments rank as required ahead of deeper ones.
228
+ result.push(
229
+ ...restExploded.map((subpath) => (subpath === "" ? required : [required, subpath].join("/"))),
230
+ );
231
+
232
+ if (isOptional) {
233
+ result.push(...restExploded);
234
+ }
235
+
236
+ return result.map((exploded) => (path.startsWith("/") && exploded === "" ? "/" : exploded));
237
+ }
238
+
239
+ const paramRe = /^:[\w-]+$/;
240
+ const dynamicSegmentValue = 3;
241
+ const indexRouteValue = 2;
242
+ const emptySegmentValue = 1;
243
+ const staticSegmentValue = 10;
244
+ const splatPenalty = -2;
245
+ const isSplat = (segment: string) => segment === "*";
246
+
247
+ function computeScore(path: string, index: boolean | undefined): number {
248
+ const segments = path.split("/");
249
+ let initialScore = segments.length;
250
+ if (segments.some(isSplat)) {
251
+ initialScore += splatPenalty;
252
+ }
253
+
254
+ if (index) {
255
+ initialScore += indexRouteValue;
256
+ }
257
+
258
+ return segments
259
+ .filter((segment) => !isSplat(segment))
260
+ .reduce(
261
+ (score, segment) =>
262
+ score +
263
+ (paramRe.test(segment)
264
+ ? dynamicSegmentValue
265
+ : segment === ""
266
+ ? emptySegmentValue
267
+ : staticSegmentValue),
268
+ initialScore,
269
+ );
270
+ }
271
+
272
+ function rankRouteBranches(branches: RouteBranch<RouteObject>[]): void {
273
+ branches.sort((a, b) =>
274
+ a.score !== b.score
275
+ ? b.score - a.score
276
+ : compareIndexes(
277
+ a.routesMeta.map((meta) => meta.childrenIndex),
278
+ b.routesMeta.map((meta) => meta.childrenIndex),
279
+ ),
280
+ );
281
+ }
282
+
283
+ function compareIndexes(a: number[], b: number[]): number {
284
+ const siblings = a.length === b.length && a.slice(0, -1).every((n, index) => n === b[index]);
285
+
286
+ // Sibling routes with identical scores match in source order; non-siblings
287
+ // rank equally.
288
+ return siblings ? a[a.length - 1]! - b[b.length - 1]! : 0;
289
+ }
290
+
291
+ function matchRouteBranch<RouteObjectType extends RouteObject>(
292
+ branch: RouteBranch<RouteObjectType>,
293
+ pathname: string,
294
+ ): RouteMatch<RouteObjectType>[] | null {
295
+ const { routesMeta } = branch;
296
+
297
+ const matchedParams: Record<string, string | undefined> = {};
298
+ let matchedPathname = "/";
299
+ const matches: RouteMatch<RouteObjectType>[] = [];
300
+ for (let index = 0; index < routesMeta.length; index++) {
301
+ const meta = routesMeta[index]!;
302
+ const end = index === routesMeta.length - 1;
303
+ const remainingPathname =
304
+ matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/";
305
+ const match = matchPath({ path: meta.relativePath, end }, remainingPathname);
306
+
307
+ if (!match) {
308
+ return null;
309
+ }
310
+
311
+ Object.assign(matchedParams, match.params);
312
+
313
+ matches.push({
314
+ params: matchedParams,
315
+ pathname: joinPaths([matchedPathname, match.pathname]),
316
+ pathnameBase: normalizePathname(joinPaths([matchedPathname, match.pathnameBase])),
317
+ route: meta.route,
318
+ });
319
+
320
+ if (match.pathnameBase !== "/") {
321
+ matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
322
+ }
323
+ }
324
+
325
+ return matches;
326
+ }
327
+
328
+ /**
329
+ Matches a single path pattern against a pathname. Returns the match with
330
+ extracted params, or `null` if the pattern does not match.
331
+ */
332
+ export function matchPath<ParamKey extends string = string>(
333
+ pattern: PathPattern | string,
334
+ pathname: string,
335
+ ): PathMatch<ParamKey> | null {
336
+ if (typeof pattern === "string") {
337
+ pattern = { path: pattern, end: true };
338
+ }
339
+
340
+ const [matcher, compiledParams] = compilePath(pattern.path, pattern.end);
341
+
342
+ const match = pathname.match(matcher);
343
+ if (!match) {
344
+ return null;
345
+ }
346
+
347
+ const matchedPathname = match[0];
348
+ let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
349
+ const captureGroups = match.slice(1);
350
+ const params = compiledParams.reduce<Record<string, string | undefined>>(
351
+ (memo, { paramName, isOptional }, index) => {
352
+ // Compute pathnameBase from the raw splat value: everything before the
353
+ // splat is the base consumed by this pattern.
354
+ const value = captureGroups[index];
355
+ if (paramName === "*") {
356
+ const splatValue = value || "";
357
+ pathnameBase = matchedPathname
358
+ .slice(0, matchedPathname.length - splatValue.length)
359
+ .replace(/(.)\/+$/, "$1");
360
+ }
361
+
362
+ memo[paramName] = isOptional && !value ? undefined : value || "";
363
+ return memo;
364
+ },
365
+ {},
366
+ );
367
+
368
+ return {
369
+ params: params as Params<ParamKey>,
370
+ pathname: matchedPathname,
371
+ pathnameBase,
372
+ pattern,
373
+ };
374
+ }
375
+
376
+ type CompiledPathParam = { paramName: string; isOptional?: boolean };
377
+
378
+ function compilePath(path: string, end = true): [RegExp, CompiledPathParam[]] {
379
+ if (path !== "*" && path.endsWith("*") && !path.endsWith("/*")) {
380
+ throw new Error(
381
+ `Route path "${path}" is invalid because the \`*\` character must always follow a \`/\` in the pattern. ` +
382
+ `Please change the route path to "${path.replace(/\*$/, "/*")}".`,
383
+ );
384
+ }
385
+
386
+ const params: CompiledPathParam[] = [];
387
+ let regexpSource =
388
+ "^" +
389
+ path
390
+ .replace(/\/*\*?$/, "") // Ignore trailing / and /*, handled below
391
+ .replace(/^\/*/, "/") // Make sure it has a leading /
392
+ .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
393
+ .replace(/\/:([\w-]+)(\?)?/g, (_: string, paramName: string, isOptional) => {
394
+ params.push({ paramName, isOptional: isOptional != null });
395
+ return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)";
396
+ }) // Dynamic segment
397
+ .replace(/\/([\w-]+)\?(\/|$)/g, "(/$1)?$2"); // Optional static segment
398
+
399
+ if (path.endsWith("*")) {
400
+ params.push({ paramName: "*" });
401
+ regexpSource +=
402
+ path === "*" || path === "/*"
403
+ ? "(.*)$" // Already matched the initial /, just match the rest
404
+ : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
405
+ } else if (end) {
406
+ // When matching to the end, ignore trailing slashes
407
+ regexpSource += "\\/*$";
408
+ } else if (path !== "" && path !== "/") {
409
+ // Match a prefix only up to a segment boundary, so "/user" does not
410
+ // match "/user-preferences".
411
+ regexpSource += "(?:(?=\\/|$))";
412
+ }
413
+
414
+ return [new RegExp(regexpSource), params];
415
+ }
416
+
417
+ /**
418
+ Interpolates params into a route path pattern.
419
+
420
+ ```ts
421
+ generatePath("/users/:id", { id: "42" }); // "/users/42"
422
+ ```
423
+ */
424
+ export function generatePath(
425
+ originalPath: string,
426
+ params: Record<string, string | null> = {},
427
+ ): string {
428
+ const path = originalPath;
429
+ if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
430
+ throw new Error(
431
+ `Route path "${path}" is invalid because the \`*\` character must always follow a \`/\` in the pattern. ` +
432
+ `Please change the route path to "${path.replace(/\*$/, "/*")}".`,
433
+ );
434
+ }
435
+
436
+ const prefix = path.startsWith("/") ? "/" : "";
437
+
438
+ const segments = path
439
+ .split(/\/+/)
440
+ .map((segment, index, array) => {
441
+ const isLastSegment = index === array.length - 1;
442
+
443
+ // Only apply the splat if it's the last segment
444
+ if (isLastSegment && segment === "*") {
445
+ return params["*"] ?? "";
446
+ }
447
+
448
+ const keyMatch = segment.match(/^:([\w-]+)(\??)$/);
449
+ if (keyMatch) {
450
+ const key = keyMatch[1]!;
451
+ const optional = keyMatch[2];
452
+ const param = params[key];
453
+ if (optional !== "?" && param == null) {
454
+ throw new Error(`Missing ":${key}" param`);
455
+ }
456
+
457
+ return param ?? "";
458
+ }
459
+
460
+ // Remove any optional markers from optional static segments
461
+ return segment.replace(/\?$/g, "");
462
+ })
463
+ .filter((segment) => !!segment);
464
+
465
+ return prefix + segments.join("/");
466
+ }
467
+
468
+ /**
469
+ Resolves a `To` value against a starting pathname, handling `.` and `..`
470
+ segments.
471
+ */
472
+ export function resolvePath(to: To, fromPathname = "/"): Path {
473
+ const { pathname: toPathname, search = "" } = typeof to === "string" ? parsePath(to) : to;
474
+
475
+ const pathname = toPathname
476
+ ? toPathname.startsWith("/")
477
+ ? toPathname
478
+ : resolvePathname(toPathname, fromPathname)
479
+ : fromPathname;
480
+
481
+ return {
482
+ pathname,
483
+ search: normalizeSearch(search),
484
+ };
485
+ }
486
+
487
+ function resolvePathname(relativePath: string, fromPathname: string): string {
488
+ const segments = fromPathname.replace(/\/+$/, "").split("/");
489
+
490
+ for (const segment of relativePath.split("/")) {
491
+ if (segment === "..") {
492
+ // Keep the root "" segment so the pathname starts at /
493
+ if (segments.length > 1) {
494
+ segments.pop();
495
+ }
496
+ } else if (segment !== ".") {
497
+ segments.push(segment);
498
+ }
499
+ }
500
+
501
+ return segments.length > 1 ? segments.join("/") : "/";
502
+ }
503
+
504
+ // Ancestor routes that do not contribute a path segment (index routes,
505
+ // pathless layouts) are ignored for relative navigation, so `..` always means
506
+ // "up one route that has a path".
507
+ function getPathContributingMatches<T extends RouteMatch>(matches: T[]): T[] {
508
+ return matches.filter(
509
+ (match, index) => index === 0 || (match.route.path && match.route.path.length > 0),
510
+ );
511
+ }
512
+
513
+ export function getResolveToMatches(matches: RouteMatch[]): string[] {
514
+ const pathMatches = getPathContributingMatches(matches);
515
+
516
+ // Use the full pathname for the leaf match so splat values are included in
517
+ // "." links.
518
+ return pathMatches.map((match, index) =>
519
+ index === pathMatches.length - 1 ? match.pathname : match.pathnameBase,
520
+ );
521
+ }
522
+
523
+ export function resolveTo(toArg: To, routePathnames: string[], locationPathname: string): Path {
524
+ const to: Partial<Path> = typeof toArg === "string" ? parsePath(toArg) : { ...toArg };
525
+
526
+ if (to.pathname?.includes("?")) {
527
+ throw new Error(
528
+ `Cannot include a '?' character in a manually specified \`to.pathname\` field ` +
529
+ `[${JSON.stringify(to)}]. Please separate it out to the \`to.search\` field.`,
530
+ );
531
+ }
532
+
533
+ const isEmptyPath = toArg === "" || to.pathname === "";
534
+ const toPathname = isEmptyPath ? "/" : to.pathname;
535
+
536
+ let from: string;
537
+ if (toPathname == null) {
538
+ // A search-only `to` is relative to the current location, not the route.
539
+ from = locationPathname;
540
+ } else {
541
+ let routePathnameIndex = routePathnames.length - 1;
542
+
543
+ // Each leading `..` segment means "go up one route", not "up one URL
544
+ // segment" — routes are the unit of navigation here.
545
+ if (toPathname.startsWith("..")) {
546
+ const toSegments = toPathname.split("/");
547
+
548
+ while (toSegments[0] === "..") {
549
+ toSegments.shift();
550
+ routePathnameIndex -= 1;
551
+ }
552
+
553
+ to.pathname = toSegments.join("/");
554
+ }
555
+
556
+ from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex]! : "/";
557
+ }
558
+
559
+ const path = resolvePath(to, from);
560
+
561
+ // Preserve an explicit trailing slash from the original `to` value, or from
562
+ // the current location for "."-style links.
563
+ const hasExplicitTrailingSlash = toPathname && toPathname !== "/" && toPathname.endsWith("/");
564
+ const hasCurrentTrailingSlash =
565
+ (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/");
566
+ if (!path.pathname.endsWith("/") && (hasExplicitTrailingSlash || hasCurrentTrailingSlash)) {
567
+ path.pathname += "/";
568
+ }
569
+
570
+ return path;
571
+ }
@@ -1,4 +1,4 @@
1
- import { hasAnsiControlCharacters, tokenizeAnsi } from "./ansi-tokenizer.ts";
1
+ import { hasAnsiControlCharacters, tokenizeAnsi } from "#/ansi-tokenizer.ts";
2
2
 
3
3
  const sgrParametersRegex = /^[\d:;]*$/;
4
4