@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.
- package/README.md +23 -13
- package/THIRD_PARTY_NOTICES.md +70 -23
- package/dist/Text-D5HUf3Fj.d.ts +452 -0
- package/dist/ansi.d.ts +137 -143
- package/dist/ansi.js +87 -2
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
- package/dist/color-policy-SVj1pYTA.js +560 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-Bh4yGP6w.d.ts +186 -0
- package/dist/detect-BuTXtY6e.js +373 -0
- package/dist/{devtools-BhYGjb7h.js → devtools-DbthxoD1.js} +22 -23
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-Bmc2tRPk.d.ts +21 -0
- package/dist/index.d.ts +235 -754
- package/dist/index.js +1362 -3001
- package/dist/osc-CCH7xDoS.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-C19minOS.d.ts +81 -0
- package/dist/query-vaIeGOkH.d.ts +152 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +709 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-BOh__-65.js +324 -0
- package/dist/screen-BReKIheE.d.ts +40 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-BJmzX2NJ.js +664 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-CgrG9K4y.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +118 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-C2sciZOo.js +1335 -0
- package/package.json +43 -17
- package/src/ansi/chalk.ts +12 -53
- package/src/ansi/cursor.ts +2 -4
- package/src/ansi/east-asian-width.ts +44 -0
- package/src/ansi/escapes.ts +18 -26
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +14 -14
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/sgr.ts +2 -5
- package/src/ansi/slice.ts +1 -1
- package/src/ansi/string-width.ts +91 -206
- package/src/ansi/strip.ts +13 -33
- package/src/ansi/tokenize.ts +96 -169
- package/src/ansi/truncate.ts +5 -3
- package/src/ansi/wrap.ts +13 -14
- package/src/ansi-tokenizer.ts +1 -1
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +594 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +657 -0
- package/src/capabilities/store.ts +379 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +109 -22
- package/src/components/BackgroundContext.ts +2 -3
- package/src/components/Box.tsx +43 -60
- package/src/components/CursorContext.ts +1 -1
- package/src/components/ErrorBoundary.tsx +1 -1
- package/src/components/ErrorOverview.tsx +8 -7
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/Spacer.tsx +1 -1
- package/src/components/Static.tsx +1 -1
- package/src/components/StderrContext.ts +0 -2
- package/src/components/StdinContext.ts +0 -1
- package/src/components/StdoutContext.ts +1 -3
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +23 -46
- package/src/components/Transform.tsx +2 -2
- package/src/cursor-position.ts +1 -1
- package/src/devtools.ts +84 -24
- package/src/dom.ts +11 -8
- package/src/env.ts +12 -0
- package/src/get-max-width.ts +1 -1
- package/src/global.d.ts +6 -4
- package/src/{boxes.ts → glyphs.ts} +17 -18
- package/src/hooks/use-animation.ts +1 -1
- package/src/hooks/use-app.ts +1 -1
- package/src/hooks/use-box-metrics.ts +1 -1
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +3 -3
- package/src/hooks/use-focus-manager.ts +1 -1
- package/src/hooks/use-focus.ts +5 -6
- package/src/hooks/use-input.ts +4 -4
- package/src/hooks/use-is-screen-reader-enabled.ts +1 -1
- package/src/hooks/use-paste.ts +2 -2
- package/src/hooks/use-stderr.ts +1 -1
- package/src/hooks/use-stdin.ts +1 -1
- package/src/hooks/use-stdout.ts +1 -1
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/hooks/use-window-size.ts +2 -2
- package/src/index.ts +88 -44
- package/src/ink.tsx +620 -745
- package/src/input-parser.ts +1 -1
- package/src/instances.ts +1 -1
- package/src/kitty-keyboard.ts +128 -0
- package/src/measure-element.ts +1 -1
- package/src/measure-text.ts +2 -2
- package/src/{render-node-to-output.ts → paint-tree.ts} +78 -53
- package/src/parse-keypress.ts +4 -5
- package/src/parse-stack-line.ts +0 -1
- package/src/patch-console.ts +44 -0
- package/src/reconciler.ts +36 -62
- package/src/render-background.ts +37 -16
- package/src/render-border.ts +97 -64
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +24 -9
- package/src/render.ts +24 -16
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/sanitize-ansi.ts +1 -1
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +137 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +272 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/signal-exit.ts +4 -5
- package/src/squash-text-nodes.ts +4 -7
- package/src/stream.ts +2 -4
- package/src/structured-text.ts +321 -0
- package/src/styles.ts +16 -17
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +86 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +188 -0
- package/src/terminal/session.ts +407 -0
- package/src/terminal-size.ts +9 -8
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +349 -0
- package/src/testing/vitest.ts +157 -0
- package/src/transform-adapter.ts +14 -0
- package/src/types.ts +1 -6
- package/src/utils.ts +2 -2
- package/src/wrap-text.ts +4 -4
- package/src/yoga/config.ts +2 -2
- package/src/yoga/core/absoluteLayout.ts +15 -15
- package/src/yoga/core/baseline.ts +3 -3
- package/src/yoga/core/cache.ts +4 -4
- package/src/yoga/core/calculateLayout.ts +33 -27
- package/src/yoga/core/config.ts +1 -1
- package/src/yoga/core/flexLine.ts +3 -3
- package/src/yoga/core/helpers.ts +3 -3
- package/src/yoga/core/layoutResults.ts +4 -4
- package/src/yoga/core/node.ts +16 -16
- package/src/yoga/core/pixelGrid.ts +4 -4
- package/src/yoga/core/style.ts +11 -11
- package/src/yoga/core/types.ts +2 -2
- package/src/yoga/index.ts +6 -6
- package/src/yoga/node.ts +8 -8
- package/dist/sgr-CMfEpjSk.d.ts +0 -91
- package/dist/truncate-CBiyyZzw.js +0 -2156
- package/src/ansi/supports-color.ts +0 -213
- package/src/ansi/widest-line.ts +0 -12
- package/src/auto-bind.ts +0 -41
- package/src/colorize.ts +0 -60
- package/src/devtools-window-polyfill.ts +0 -73
- package/src/indent-string.ts +0 -16
- package/src/is-in-ci.ts +0 -7
- package/src/log-update.ts +0 -370
- package/src/output.ts +0 -308
- package/src/renderer.ts +0 -73
- 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
|
+
}
|
package/src/sanitize-ansi.ts
CHANGED