@bquery/bquery 1.7.0 → 1.8.2
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 +760 -716
- package/dist/{a11y-C5QOVvRn.js → a11y-DVBCy09c.js} +3 -3
- package/dist/a11y-DVBCy09c.js.map +1 -0
- package/dist/a11y.es.mjs +1 -1
- package/dist/component/library.d.ts.map +1 -1
- package/dist/{component-CuuTijA6.js → component-L3-JfOFz.js} +5 -5
- package/dist/component-L3-JfOFz.js.map +1 -0
- package/dist/component.es.mjs +1 -1
- package/dist/{config-BW35FKuA.js → config-DhT9auRm.js} +1 -1
- package/dist/{config-BW35FKuA.js.map → config-DhT9auRm.js.map} +1 -1
- package/dist/{constraints-3lV9yyBw.js → constraints-D5RHQLmP.js} +1 -1
- package/dist/constraints-D5RHQLmP.js.map +1 -0
- package/dist/core/collection.d.ts +86 -0
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/element.d.ts +28 -0
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/shared.d.ts +6 -0
- package/dist/core/shared.d.ts.map +1 -1
- package/dist/core-DdtZHzsS.js +168 -0
- package/dist/core-DdtZHzsS.js.map +1 -0
- package/dist/{core-Cjl7GUu8.js → core-EMYSLzaT.js} +289 -259
- package/dist/core-EMYSLzaT.js.map +1 -0
- package/dist/core.es.mjs +48 -47
- package/dist/{custom-directives-7wAShnnd.js → custom-directives-Dr4C5lVV.js} +1 -1
- package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
- package/dist/{devtools-D2fQLhDN.js → devtools-BhB2iDPT.js} +2 -2
- package/dist/devtools-BhB2iDPT.js.map +1 -0
- package/dist/devtools.es.mjs +1 -1
- package/dist/{dnd-B8EgyzaI.js → dnd-NwZBYh4l.js} +1 -1
- package/dist/dnd-NwZBYh4l.js.map +1 -0
- package/dist/dnd.es.mjs +1 -1
- package/dist/{env-NeVmr4Gf.js → env-CTdvLaH2.js} +1 -1
- package/dist/env-CTdvLaH2.js.map +1 -0
- package/dist/forms/create-form.d.ts.map +1 -1
- package/dist/forms/index.d.ts +3 -2
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/types.d.ts +46 -0
- package/dist/forms/types.d.ts.map +1 -1
- package/dist/forms/use-field.d.ts +34 -0
- package/dist/forms/use-field.d.ts.map +1 -0
- package/dist/forms/validators.d.ts +25 -0
- package/dist/forms/validators.d.ts.map +1 -1
- package/dist/forms-UcRHsYxC.js +227 -0
- package/dist/forms-UcRHsYxC.js.map +1 -0
- package/dist/forms.es.mjs +14 -12
- package/dist/full.d.ts +17 -26
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +206 -181
- package/dist/full.iife.js +33 -33
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +33 -33
- package/dist/full.umd.js.map +1 -1
- package/dist/function-Cybd57JV.js +33 -0
- package/dist/function-Cybd57JV.js.map +1 -0
- package/dist/{i18n-BnnhTFOS.js → i18n-kuF6Ekj6.js} +3 -3
- package/dist/i18n-kuF6Ekj6.js.map +1 -0
- package/dist/i18n.es.mjs +1 -1
- package/dist/index.es.mjs +251 -228
- package/dist/media/breakpoints.d.ts.map +1 -1
- package/dist/media/types.d.ts +2 -2
- package/dist/media/types.d.ts.map +1 -1
- package/dist/{media-Di2Ta22s.js → media-i-fB5WxI.js} +3 -3
- package/dist/media-i-fB5WxI.js.map +1 -0
- package/dist/media.es.mjs +1 -1
- package/dist/{motion-qPj_TYGv.js → motion-BJsAuULb.js} +2 -2
- package/dist/motion-BJsAuULb.js.map +1 -0
- package/dist/motion.es.mjs +1 -1
- package/dist/{mount-SM07RUa6.js → mount-B4Y8bk8Z.js} +5 -5
- package/dist/mount-B4Y8bk8Z.js.map +1 -0
- package/dist/{platform-CPbCprb6.js → platform-Dw2gE3zI.js} +3 -3
- package/dist/{platform-CPbCprb6.js.map → platform-Dw2gE3zI.js.map} +1 -1
- package/dist/platform.es.mjs +2 -2
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/{plugin-cPoOHFLY.js → plugin-C2WuC8SF.js} +20 -18
- package/dist/plugin-C2WuC8SF.js.map +1 -0
- package/dist/plugin.es.mjs +1 -1
- package/dist/reactive/async-data.d.ts +28 -3
- package/dist/reactive/async-data.d.ts.map +1 -1
- package/dist/reactive/computed.d.ts +3 -0
- package/dist/reactive/computed.d.ts.map +1 -1
- package/dist/reactive/effect.d.ts +3 -0
- package/dist/reactive/effect.d.ts.map +1 -1
- package/dist/reactive/http.d.ts +194 -0
- package/dist/reactive/http.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/pagination.d.ts +126 -0
- package/dist/reactive/pagination.d.ts.map +1 -0
- package/dist/reactive/polling.d.ts +55 -0
- package/dist/reactive/polling.d.ts.map +1 -0
- package/dist/reactive/readonly.d.ts +20 -1
- package/dist/reactive/readonly.d.ts.map +1 -1
- package/dist/reactive/rest.d.ts +293 -0
- package/dist/reactive/rest.d.ts.map +1 -0
- package/dist/reactive/scope.d.ts +140 -0
- package/dist/reactive/scope.d.ts.map +1 -0
- package/dist/reactive/signal.d.ts +16 -2
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive/to-value.d.ts +57 -0
- package/dist/reactive/to-value.d.ts.map +1 -0
- package/dist/reactive/websocket.d.ts +285 -0
- package/dist/reactive/websocket.d.ts.map +1 -0
- package/dist/reactive-DwkhUJfP.js +1148 -0
- package/dist/reactive-DwkhUJfP.js.map +1 -0
- package/dist/reactive.es.mjs +38 -19
- package/dist/{registry-CWf368tT.js → registry-B08iilIh.js} +1 -1
- package/dist/{registry-CWf368tT.js.map → registry-B08iilIh.js.map} +1 -1
- package/dist/router/constraints.d.ts.map +1 -1
- package/dist/router/index.d.ts +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/state.d.ts +25 -2
- package/dist/router/state.d.ts.map +1 -1
- package/dist/router-CQikC9Ed.js +492 -0
- package/dist/router-CQikC9Ed.js.map +1 -0
- package/dist/router.es.mjs +9 -8
- package/dist/ssr/hydrate.d.ts.map +1 -1
- package/dist/{ssr-B2qd_WBB.js → ssr-_dAcGdzu.js} +4 -4
- package/dist/ssr-_dAcGdzu.js.map +1 -0
- package/dist/ssr.es.mjs +1 -1
- package/dist/store/persisted.d.ts.map +1 -1
- package/dist/{store-DWpyH6p5.js → store-Cb3gPRve.js} +7 -7
- package/dist/store-Cb3gPRve.js.map +1 -0
- package/dist/store.es.mjs +2 -2
- package/dist/storybook.es.mjs.map +1 -1
- package/dist/{testing-CsqjNUyy.js → testing-C5Sjfsna.js} +8 -8
- package/dist/testing-C5Sjfsna.js.map +1 -0
- package/dist/testing.es.mjs +1 -1
- package/dist/{type-guards-Do9DWgNp.js → type-guards-BMX2c0LP.js} +1 -1
- package/dist/{type-guards-Do9DWgNp.js.map → type-guards-BMX2c0LP.js.map} +1 -1
- package/dist/untrack-D0fnO5k2.js +36 -0
- package/dist/untrack-D0fnO5k2.js.map +1 -0
- package/dist/view/custom-directives.d.ts.map +1 -1
- package/dist/view.es.mjs +4 -4
- package/package.json +178 -177
- package/src/a11y/announce.ts +131 -131
- package/src/a11y/audit.ts +314 -314
- package/src/a11y/index.ts +68 -68
- package/src/a11y/media-preferences.ts +255 -255
- package/src/a11y/roving-tab-index.ts +164 -164
- package/src/a11y/skip-link.ts +255 -255
- package/src/a11y/trap-focus.ts +184 -184
- package/src/a11y/types.ts +183 -183
- package/src/component/component.ts +599 -599
- package/src/component/html.ts +153 -153
- package/src/component/index.ts +52 -52
- package/src/component/library.ts +540 -542
- package/src/component/scope.ts +212 -212
- package/src/component/types.ts +310 -310
- package/src/core/collection.ts +876 -707
- package/src/core/element.ts +1015 -981
- package/src/core/env.ts +60 -60
- package/src/core/index.ts +49 -49
- package/src/core/shared.ts +77 -62
- package/src/core/utils/index.ts +148 -148
- package/src/devtools/devtools.ts +410 -410
- package/src/devtools/index.ts +48 -48
- package/src/devtools/types.ts +104 -104
- package/src/dnd/draggable.ts +296 -296
- package/src/dnd/droppable.ts +228 -228
- package/src/dnd/index.ts +62 -62
- package/src/dnd/sortable.ts +307 -307
- package/src/dnd/types.ts +293 -293
- package/src/forms/create-form.ts +320 -278
- package/src/forms/index.ts +70 -65
- package/src/forms/types.ts +203 -154
- package/src/forms/use-field.ts +231 -0
- package/src/forms/validators.ts +294 -265
- package/src/full.ts +554 -480
- package/src/i18n/formatting.ts +67 -67
- package/src/i18n/i18n.ts +200 -200
- package/src/i18n/index.ts +67 -67
- package/src/i18n/translate.ts +182 -182
- package/src/i18n/types.ts +171 -171
- package/src/index.ts +108 -108
- package/src/media/battery.ts +116 -116
- package/src/media/breakpoints.ts +129 -131
- package/src/media/clipboard.ts +80 -80
- package/src/media/device-sensors.ts +158 -158
- package/src/media/geolocation.ts +119 -119
- package/src/media/index.ts +76 -76
- package/src/media/media-query.ts +92 -92
- package/src/media/network.ts +115 -115
- package/src/media/types.ts +177 -177
- package/src/media/viewport.ts +84 -84
- package/src/motion/index.ts +57 -57
- package/src/motion/morph.ts +151 -151
- package/src/motion/parallax.ts +120 -120
- package/src/motion/reduced-motion.ts +66 -66
- package/src/motion/types.ts +271 -271
- package/src/motion/typewriter.ts +164 -164
- package/src/plugin/index.ts +37 -37
- package/src/plugin/registry.ts +284 -269
- package/src/plugin/types.ts +137 -137
- package/src/reactive/async-data.ts +250 -29
- package/src/reactive/computed.ts +144 -130
- package/src/reactive/effect.ts +29 -6
- package/src/reactive/http.ts +790 -0
- package/src/reactive/index.ts +60 -0
- package/src/reactive/pagination.ts +317 -0
- package/src/reactive/polling.ts +179 -0
- package/src/reactive/readonly.ts +52 -8
- package/src/reactive/rest.ts +859 -0
- package/src/reactive/scope.ts +276 -0
- package/src/reactive/signal.ts +61 -1
- package/src/reactive/to-value.ts +71 -0
- package/src/reactive/websocket.ts +849 -0
- package/src/router/bq-link.ts +279 -279
- package/src/router/constraints.ts +204 -201
- package/src/router/index.ts +49 -49
- package/src/router/match.ts +312 -312
- package/src/router/path-pattern.ts +52 -52
- package/src/router/query.ts +38 -38
- package/src/router/router.ts +421 -402
- package/src/router/state.ts +51 -3
- package/src/router/types.ts +139 -139
- package/src/router/use-route.ts +68 -68
- package/src/router/utils.ts +157 -157
- package/src/security/index.ts +12 -12
- package/src/ssr/hydrate.ts +84 -82
- package/src/ssr/index.ts +70 -70
- package/src/ssr/render.ts +508 -508
- package/src/ssr/serialize.ts +296 -296
- package/src/ssr/types.ts +81 -81
- package/src/store/create-store.ts +467 -467
- package/src/store/index.ts +27 -27
- package/src/store/persisted.ts +245 -249
- package/src/store/types.ts +247 -247
- package/src/store/utils.ts +135 -135
- package/src/storybook/index.ts +480 -480
- package/src/testing/index.ts +42 -42
- package/src/testing/testing.ts +593 -593
- package/src/testing/types.ts +170 -170
- package/src/view/custom-directives.ts +28 -30
- package/src/view/evaluate.ts +292 -292
- package/src/view/process.ts +108 -108
- package/dist/a11y-C5QOVvRn.js.map +0 -1
- package/dist/component-CuuTijA6.js.map +0 -1
- package/dist/constraints-3lV9yyBw.js.map +0 -1
- package/dist/core-Cjl7GUu8.js.map +0 -1
- package/dist/core-DnlyjbF2.js +0 -112
- package/dist/core-DnlyjbF2.js.map +0 -1
- package/dist/custom-directives-7wAShnnd.js.map +0 -1
- package/dist/devtools-D2fQLhDN.js.map +0 -1
- package/dist/dnd-B8EgyzaI.js.map +0 -1
- package/dist/env-NeVmr4Gf.js.map +0 -1
- package/dist/forms-C3yovgH9.js +0 -141
- package/dist/forms-C3yovgH9.js.map +0 -1
- package/dist/i18n-BnnhTFOS.js.map +0 -1
- package/dist/media-Di2Ta22s.js.map +0 -1
- package/dist/motion-qPj_TYGv.js.map +0 -1
- package/dist/mount-SM07RUa6.js.map +0 -1
- package/dist/plugin-cPoOHFLY.js.map +0 -1
- package/dist/reactive-Cfv0RK6x.js +0 -233
- package/dist/reactive-Cfv0RK6x.js.map +0 -1
- package/dist/router-BrthaP_z.js +0 -473
- package/dist/router-BrthaP_z.js.map +0 -1
- package/dist/ssr-B2qd_WBB.js.map +0 -1
- package/dist/store-DWpyH6p5.js.map +0 -1
- package/dist/testing-CsqjNUyy.js.map +0 -1
- package/dist/untrack-DJVQQ2WM.js +0 -33
- package/dist/untrack-DJVQQ2WM.js.map +0 -1
package/src/router/match.ts
CHANGED
|
@@ -1,312 +1,312 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route matching helpers.
|
|
3
|
-
* @module bquery/router
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { parseQuery } from './query';
|
|
7
|
-
import { getNormalizedRouteConstraint, getRouteConstraintRegex } from './constraints';
|
|
8
|
-
import { isParamChar, isParamStart, readConstraint } from './path-pattern';
|
|
9
|
-
import type { Route, RouteDefinition } from './types';
|
|
10
|
-
|
|
11
|
-
const readConstraintOrThrow = (
|
|
12
|
-
path: string,
|
|
13
|
-
startIndex: number
|
|
14
|
-
): { constraint: string; endIndex: number } => {
|
|
15
|
-
const parsedConstraint = readConstraint(path, startIndex);
|
|
16
|
-
if (!parsedConstraint) {
|
|
17
|
-
throw new Error(
|
|
18
|
-
`bQuery router: Invalid route param constraint syntax in path "${path}" at index ${startIndex}.`
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
return parsedConstraint;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type RouteParamDescriptor = {
|
|
25
|
-
name: string;
|
|
26
|
-
constraint?: string;
|
|
27
|
-
nextIndex: number;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const validatedRoutePathCache = new Set<string>();
|
|
31
|
-
|
|
32
|
-
const readParamDescriptor = (path: string, index: number): RouteParamDescriptor | null => {
|
|
33
|
-
if (path[index] !== ':' || !isParamStart(path[index + 1])) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let nameEnd = index + 2;
|
|
38
|
-
while (nameEnd < path.length && isParamChar(path[nameEnd])) {
|
|
39
|
-
nameEnd++;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let nextIndex = nameEnd;
|
|
43
|
-
let constraint: string | undefined;
|
|
44
|
-
|
|
45
|
-
if (path[nameEnd] === '(') {
|
|
46
|
-
const parsedConstraint = readConstraintOrThrow(path, nameEnd);
|
|
47
|
-
constraint = parsedConstraint.constraint;
|
|
48
|
-
nextIndex = parsedConstraint.endIndex;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
name: path.slice(index + 1, nameEnd),
|
|
53
|
-
constraint,
|
|
54
|
-
nextIndex,
|
|
55
|
-
};
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const validateRoutePathPattern = (path: string): void => {
|
|
59
|
-
if (validatedRoutePathCache.has(path)) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
for (let i = 0; i < path.length; ) {
|
|
64
|
-
const char = path[i];
|
|
65
|
-
|
|
66
|
-
if (char === ':' && isParamStart(path[i + 1])) {
|
|
67
|
-
const param = readParamDescriptor(path, i);
|
|
68
|
-
if (param?.constraint) {
|
|
69
|
-
getNormalizedRouteConstraint(param.constraint);
|
|
70
|
-
}
|
|
71
|
-
i = param?.nextIndex ?? i + 1;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
i++;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
validatedRoutePathCache.add(path);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const findSegmentBoundary = (value: string, startIndex: number): number => {
|
|
82
|
-
const slashIndex = value.indexOf('/', startIndex);
|
|
83
|
-
return slashIndex === -1 ? value.length : slashIndex;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const readNextStaticChunk = (path: string, startIndex: number): string => {
|
|
87
|
-
let chunkEnd = startIndex;
|
|
88
|
-
|
|
89
|
-
while (chunkEnd < path.length) {
|
|
90
|
-
if (path[chunkEnd] === '*') {
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (path[chunkEnd] === ':' && isParamStart(path[chunkEnd + 1])) {
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
chunkEnd++;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return path.slice(startIndex, chunkEnd);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const findAnchoredCandidateEnds = (
|
|
105
|
-
actualPath: string,
|
|
106
|
-
startIndex: number,
|
|
107
|
-
limit: number,
|
|
108
|
-
nextStaticChunk: string
|
|
109
|
-
): number[] => {
|
|
110
|
-
const candidates: number[] = [];
|
|
111
|
-
let searchIndex = startIndex;
|
|
112
|
-
|
|
113
|
-
while (searchIndex <= limit) {
|
|
114
|
-
const candidateEnd = actualPath.indexOf(nextStaticChunk, searchIndex);
|
|
115
|
-
if (candidateEnd === -1 || candidateEnd > limit) {
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
candidates.push(candidateEnd);
|
|
120
|
-
searchIndex = candidateEnd + 1;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return candidates.reverse();
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const matchPathPattern = (routePath: string, actualPath: string): Record<string, string> | null => {
|
|
127
|
-
// Memoization keeps wildcard/param backtracking linear for repeated subproblems
|
|
128
|
-
// within a single route/path match attempt.
|
|
129
|
-
const memo = new Map<string, Record<string, string> | null>();
|
|
130
|
-
|
|
131
|
-
const matchFrom = (routeIndex: number, pathIndex: number): Record<string, string> | null => {
|
|
132
|
-
const memoKey = `${routeIndex}:${pathIndex}`;
|
|
133
|
-
if (memo.has(memoKey)) {
|
|
134
|
-
return memo.get(memoKey) ?? null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (routeIndex === routePath.length) {
|
|
138
|
-
const result = pathIndex === actualPath.length ? {} : null;
|
|
139
|
-
memo.set(memoKey, result);
|
|
140
|
-
return result;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const routeChar = routePath[routeIndex];
|
|
144
|
-
|
|
145
|
-
if (routeChar === '*') {
|
|
146
|
-
if (routeIndex === routePath.length - 1) {
|
|
147
|
-
const result = {};
|
|
148
|
-
memo.set(memoKey, result);
|
|
149
|
-
return result;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const nextStaticChunk = readNextStaticChunk(routePath, routeIndex + 1);
|
|
153
|
-
const anchoredCandidateEnds =
|
|
154
|
-
nextStaticChunk.length > 0
|
|
155
|
-
? findAnchoredCandidateEnds(actualPath, pathIndex, actualPath.length, nextStaticChunk)
|
|
156
|
-
: null;
|
|
157
|
-
|
|
158
|
-
const iterateCandidateEnds = anchoredCandidateEnds
|
|
159
|
-
? (callback: (candidateEnd: number) => Record<string, string> | null) => {
|
|
160
|
-
for (const candidateEnd of anchoredCandidateEnds) {
|
|
161
|
-
const result = callback(candidateEnd);
|
|
162
|
-
if (result) {
|
|
163
|
-
return result;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
: (callback: (candidateEnd: number) => Record<string, string> | null) => {
|
|
169
|
-
for (let candidateEnd = actualPath.length; candidateEnd >= pathIndex; candidateEnd--) {
|
|
170
|
-
const result = callback(candidateEnd);
|
|
171
|
-
if (result) {
|
|
172
|
-
return result;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return null;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const wildcardMatch = iterateCandidateEnds((candidateEnd) => {
|
|
179
|
-
const suffixMatch = matchFrom(routeIndex + 1, candidateEnd);
|
|
180
|
-
if (suffixMatch) {
|
|
181
|
-
memo.set(memoKey, suffixMatch);
|
|
182
|
-
return suffixMatch;
|
|
183
|
-
}
|
|
184
|
-
return null;
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
if (wildcardMatch) {
|
|
188
|
-
return wildcardMatch;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
memo.set(memoKey, null);
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const param = readParamDescriptor(routePath, routeIndex);
|
|
196
|
-
if (param) {
|
|
197
|
-
const constraintRegex = param.constraint
|
|
198
|
-
? getRouteConstraintRegex(param.constraint)
|
|
199
|
-
: undefined;
|
|
200
|
-
const candidateLimit = param.constraint
|
|
201
|
-
? actualPath.length
|
|
202
|
-
: findSegmentBoundary(actualPath, pathIndex);
|
|
203
|
-
const nextStaticChunk = readNextStaticChunk(routePath, param.nextIndex);
|
|
204
|
-
const anchoredCandidateEnds =
|
|
205
|
-
nextStaticChunk.length > 0
|
|
206
|
-
? findAnchoredCandidateEnds(actualPath, pathIndex, candidateLimit, nextStaticChunk)
|
|
207
|
-
: null;
|
|
208
|
-
|
|
209
|
-
const iterateCandidateEnds = anchoredCandidateEnds
|
|
210
|
-
? (callback: (candidateEnd: number) => Record<string, string> | null) => {
|
|
211
|
-
for (const candidateEnd of anchoredCandidateEnds) {
|
|
212
|
-
if (candidateEnd <= pathIndex) {
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
const result = callback(candidateEnd);
|
|
216
|
-
if (result) {
|
|
217
|
-
return result;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
: (callback: (candidateEnd: number) => Record<string, string> | null) => {
|
|
223
|
-
for (let candidateEnd = candidateLimit; candidateEnd > pathIndex; candidateEnd--) {
|
|
224
|
-
const result = callback(candidateEnd);
|
|
225
|
-
if (result) {
|
|
226
|
-
return result;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return null;
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
const paramMatch = iterateCandidateEnds((candidateEnd) => {
|
|
233
|
-
const candidateValue = actualPath.slice(pathIndex, candidateEnd);
|
|
234
|
-
|
|
235
|
-
if (constraintRegex) {
|
|
236
|
-
if (!constraintRegex.test(candidateValue)) {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const suffixMatch = matchFrom(param.nextIndex, candidateEnd);
|
|
242
|
-
if (suffixMatch) {
|
|
243
|
-
const result = {
|
|
244
|
-
[param.name]: candidateValue,
|
|
245
|
-
...suffixMatch,
|
|
246
|
-
};
|
|
247
|
-
memo.set(memoKey, result);
|
|
248
|
-
return result;
|
|
249
|
-
}
|
|
250
|
-
return null;
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
if (paramMatch) {
|
|
254
|
-
return paramMatch;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
memo.set(memoKey, null);
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (pathIndex >= actualPath.length || routeChar !== actualPath[pathIndex]) {
|
|
262
|
-
memo.set(memoKey, null);
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const result = matchFrom(routeIndex + 1, pathIndex + 1);
|
|
267
|
-
memo.set(memoKey, result);
|
|
268
|
-
return result;
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
return matchFrom(0, 0);
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Matches a path against route definitions and extracts params.
|
|
276
|
-
* @internal
|
|
277
|
-
*/
|
|
278
|
-
export const matchRoute = (
|
|
279
|
-
path: string,
|
|
280
|
-
routes: RouteDefinition[]
|
|
281
|
-
): { matched: RouteDefinition; params: Record<string, string> } | null => {
|
|
282
|
-
for (const route of routes) {
|
|
283
|
-
validateRoutePathPattern(route.path);
|
|
284
|
-
const params = matchPathPattern(route.path, path);
|
|
285
|
-
if (params) {
|
|
286
|
-
return { matched: route, params };
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return null;
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Creates a Route object from the current URL.
|
|
295
|
-
* @internal
|
|
296
|
-
*/
|
|
297
|
-
export const createRoute = (
|
|
298
|
-
pathname: string,
|
|
299
|
-
search: string,
|
|
300
|
-
hash: string,
|
|
301
|
-
routes: RouteDefinition[]
|
|
302
|
-
): Route => {
|
|
303
|
-
const result = matchRoute(pathname, routes);
|
|
304
|
-
|
|
305
|
-
return {
|
|
306
|
-
path: pathname,
|
|
307
|
-
params: result?.params ?? {},
|
|
308
|
-
query: parseQuery(search),
|
|
309
|
-
matched: result?.matched ?? null,
|
|
310
|
-
hash: hash.replace(/^#/, ''),
|
|
311
|
-
};
|
|
312
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Route matching helpers.
|
|
3
|
+
* @module bquery/router
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parseQuery } from './query';
|
|
7
|
+
import { getNormalizedRouteConstraint, getRouteConstraintRegex } from './constraints';
|
|
8
|
+
import { isParamChar, isParamStart, readConstraint } from './path-pattern';
|
|
9
|
+
import type { Route, RouteDefinition } from './types';
|
|
10
|
+
|
|
11
|
+
const readConstraintOrThrow = (
|
|
12
|
+
path: string,
|
|
13
|
+
startIndex: number
|
|
14
|
+
): { constraint: string; endIndex: number } => {
|
|
15
|
+
const parsedConstraint = readConstraint(path, startIndex);
|
|
16
|
+
if (!parsedConstraint) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`bQuery router: Invalid route param constraint syntax in path "${path}" at index ${startIndex}.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return parsedConstraint;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type RouteParamDescriptor = {
|
|
25
|
+
name: string;
|
|
26
|
+
constraint?: string;
|
|
27
|
+
nextIndex: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const validatedRoutePathCache = new Set<string>();
|
|
31
|
+
|
|
32
|
+
const readParamDescriptor = (path: string, index: number): RouteParamDescriptor | null => {
|
|
33
|
+
if (path[index] !== ':' || !isParamStart(path[index + 1])) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let nameEnd = index + 2;
|
|
38
|
+
while (nameEnd < path.length && isParamChar(path[nameEnd])) {
|
|
39
|
+
nameEnd++;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let nextIndex = nameEnd;
|
|
43
|
+
let constraint: string | undefined;
|
|
44
|
+
|
|
45
|
+
if (path[nameEnd] === '(') {
|
|
46
|
+
const parsedConstraint = readConstraintOrThrow(path, nameEnd);
|
|
47
|
+
constraint = parsedConstraint.constraint;
|
|
48
|
+
nextIndex = parsedConstraint.endIndex;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
name: path.slice(index + 1, nameEnd),
|
|
53
|
+
constraint,
|
|
54
|
+
nextIndex,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const validateRoutePathPattern = (path: string): void => {
|
|
59
|
+
if (validatedRoutePathCache.has(path)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < path.length; ) {
|
|
64
|
+
const char = path[i];
|
|
65
|
+
|
|
66
|
+
if (char === ':' && isParamStart(path[i + 1])) {
|
|
67
|
+
const param = readParamDescriptor(path, i);
|
|
68
|
+
if (param?.constraint) {
|
|
69
|
+
getNormalizedRouteConstraint(param.constraint);
|
|
70
|
+
}
|
|
71
|
+
i = param?.nextIndex ?? i + 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
validatedRoutePathCache.add(path);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const findSegmentBoundary = (value: string, startIndex: number): number => {
|
|
82
|
+
const slashIndex = value.indexOf('/', startIndex);
|
|
83
|
+
return slashIndex === -1 ? value.length : slashIndex;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const readNextStaticChunk = (path: string, startIndex: number): string => {
|
|
87
|
+
let chunkEnd = startIndex;
|
|
88
|
+
|
|
89
|
+
while (chunkEnd < path.length) {
|
|
90
|
+
if (path[chunkEnd] === '*') {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (path[chunkEnd] === ':' && isParamStart(path[chunkEnd + 1])) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
chunkEnd++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return path.slice(startIndex, chunkEnd);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const findAnchoredCandidateEnds = (
|
|
105
|
+
actualPath: string,
|
|
106
|
+
startIndex: number,
|
|
107
|
+
limit: number,
|
|
108
|
+
nextStaticChunk: string
|
|
109
|
+
): number[] => {
|
|
110
|
+
const candidates: number[] = [];
|
|
111
|
+
let searchIndex = startIndex;
|
|
112
|
+
|
|
113
|
+
while (searchIndex <= limit) {
|
|
114
|
+
const candidateEnd = actualPath.indexOf(nextStaticChunk, searchIndex);
|
|
115
|
+
if (candidateEnd === -1 || candidateEnd > limit) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
candidates.push(candidateEnd);
|
|
120
|
+
searchIndex = candidateEnd + 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return candidates.reverse();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const matchPathPattern = (routePath: string, actualPath: string): Record<string, string> | null => {
|
|
127
|
+
// Memoization keeps wildcard/param backtracking linear for repeated subproblems
|
|
128
|
+
// within a single route/path match attempt.
|
|
129
|
+
const memo = new Map<string, Record<string, string> | null>();
|
|
130
|
+
|
|
131
|
+
const matchFrom = (routeIndex: number, pathIndex: number): Record<string, string> | null => {
|
|
132
|
+
const memoKey = `${routeIndex}:${pathIndex}`;
|
|
133
|
+
if (memo.has(memoKey)) {
|
|
134
|
+
return memo.get(memoKey) ?? null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (routeIndex === routePath.length) {
|
|
138
|
+
const result = pathIndex === actualPath.length ? {} : null;
|
|
139
|
+
memo.set(memoKey, result);
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const routeChar = routePath[routeIndex];
|
|
144
|
+
|
|
145
|
+
if (routeChar === '*') {
|
|
146
|
+
if (routeIndex === routePath.length - 1) {
|
|
147
|
+
const result = {};
|
|
148
|
+
memo.set(memoKey, result);
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const nextStaticChunk = readNextStaticChunk(routePath, routeIndex + 1);
|
|
153
|
+
const anchoredCandidateEnds =
|
|
154
|
+
nextStaticChunk.length > 0
|
|
155
|
+
? findAnchoredCandidateEnds(actualPath, pathIndex, actualPath.length, nextStaticChunk)
|
|
156
|
+
: null;
|
|
157
|
+
|
|
158
|
+
const iterateCandidateEnds = anchoredCandidateEnds
|
|
159
|
+
? (callback: (candidateEnd: number) => Record<string, string> | null) => {
|
|
160
|
+
for (const candidateEnd of anchoredCandidateEnds) {
|
|
161
|
+
const result = callback(candidateEnd);
|
|
162
|
+
if (result) {
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
: (callback: (candidateEnd: number) => Record<string, string> | null) => {
|
|
169
|
+
for (let candidateEnd = actualPath.length; candidateEnd >= pathIndex; candidateEnd--) {
|
|
170
|
+
const result = callback(candidateEnd);
|
|
171
|
+
if (result) {
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const wildcardMatch = iterateCandidateEnds((candidateEnd) => {
|
|
179
|
+
const suffixMatch = matchFrom(routeIndex + 1, candidateEnd);
|
|
180
|
+
if (suffixMatch) {
|
|
181
|
+
memo.set(memoKey, suffixMatch);
|
|
182
|
+
return suffixMatch;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (wildcardMatch) {
|
|
188
|
+
return wildcardMatch;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
memo.set(memoKey, null);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const param = readParamDescriptor(routePath, routeIndex);
|
|
196
|
+
if (param) {
|
|
197
|
+
const constraintRegex = param.constraint
|
|
198
|
+
? getRouteConstraintRegex(param.constraint)
|
|
199
|
+
: undefined;
|
|
200
|
+
const candidateLimit = param.constraint
|
|
201
|
+
? actualPath.length
|
|
202
|
+
: findSegmentBoundary(actualPath, pathIndex);
|
|
203
|
+
const nextStaticChunk = readNextStaticChunk(routePath, param.nextIndex);
|
|
204
|
+
const anchoredCandidateEnds =
|
|
205
|
+
nextStaticChunk.length > 0
|
|
206
|
+
? findAnchoredCandidateEnds(actualPath, pathIndex, candidateLimit, nextStaticChunk)
|
|
207
|
+
: null;
|
|
208
|
+
|
|
209
|
+
const iterateCandidateEnds = anchoredCandidateEnds
|
|
210
|
+
? (callback: (candidateEnd: number) => Record<string, string> | null) => {
|
|
211
|
+
for (const candidateEnd of anchoredCandidateEnds) {
|
|
212
|
+
if (candidateEnd <= pathIndex) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const result = callback(candidateEnd);
|
|
216
|
+
if (result) {
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
: (callback: (candidateEnd: number) => Record<string, string> | null) => {
|
|
223
|
+
for (let candidateEnd = candidateLimit; candidateEnd > pathIndex; candidateEnd--) {
|
|
224
|
+
const result = callback(candidateEnd);
|
|
225
|
+
if (result) {
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const paramMatch = iterateCandidateEnds((candidateEnd) => {
|
|
233
|
+
const candidateValue = actualPath.slice(pathIndex, candidateEnd);
|
|
234
|
+
|
|
235
|
+
if (constraintRegex) {
|
|
236
|
+
if (!constraintRegex.test(candidateValue)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const suffixMatch = matchFrom(param.nextIndex, candidateEnd);
|
|
242
|
+
if (suffixMatch) {
|
|
243
|
+
const result = {
|
|
244
|
+
[param.name]: candidateValue,
|
|
245
|
+
...suffixMatch,
|
|
246
|
+
};
|
|
247
|
+
memo.set(memoKey, result);
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (paramMatch) {
|
|
254
|
+
return paramMatch;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
memo.set(memoKey, null);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (pathIndex >= actualPath.length || routeChar !== actualPath[pathIndex]) {
|
|
262
|
+
memo.set(memoKey, null);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result = matchFrom(routeIndex + 1, pathIndex + 1);
|
|
267
|
+
memo.set(memoKey, result);
|
|
268
|
+
return result;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return matchFrom(0, 0);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Matches a path against route definitions and extracts params.
|
|
276
|
+
* @internal
|
|
277
|
+
*/
|
|
278
|
+
export const matchRoute = (
|
|
279
|
+
path: string,
|
|
280
|
+
routes: RouteDefinition[]
|
|
281
|
+
): { matched: RouteDefinition; params: Record<string, string> } | null => {
|
|
282
|
+
for (const route of routes) {
|
|
283
|
+
validateRoutePathPattern(route.path);
|
|
284
|
+
const params = matchPathPattern(route.path, path);
|
|
285
|
+
if (params) {
|
|
286
|
+
return { matched: route, params };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return null;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Creates a Route object from the current URL.
|
|
295
|
+
* @internal
|
|
296
|
+
*/
|
|
297
|
+
export const createRoute = (
|
|
298
|
+
pathname: string,
|
|
299
|
+
search: string,
|
|
300
|
+
hash: string,
|
|
301
|
+
routes: RouteDefinition[]
|
|
302
|
+
): Route => {
|
|
303
|
+
const result = matchRoute(pathname, routes);
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
path: pathname,
|
|
307
|
+
params: result?.params ?? {},
|
|
308
|
+
query: parseQuery(search),
|
|
309
|
+
matched: result?.matched ?? null,
|
|
310
|
+
hash: hash.replace(/^#/, ''),
|
|
311
|
+
};
|
|
312
|
+
};
|