@marianmeres/simple-router 2.0.1 → 2.2.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/LICENSE +21 -0
- package/README.md +347 -66
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +3 -0
- package/dist/route.d.ts +88 -9
- package/dist/route.js +248 -0
- package/dist/router.d.ts +242 -27
- package/dist/router.js +255 -0
- package/package.json +19 -49
- package/dist/index.cjs +0 -244
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -241
package/dist/index.js
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
class SimpleRoute {
|
|
2
|
-
route;
|
|
3
|
-
static SPLITTER = '/';
|
|
4
|
-
static WILDCARD = '*';
|
|
5
|
-
_parsed;
|
|
6
|
-
constructor(route) {
|
|
7
|
-
this.route = route;
|
|
8
|
-
this._parsed = SimpleRoute._parse(route);
|
|
9
|
-
}
|
|
10
|
-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
|
11
|
-
static _escapeRegExp(str) {
|
|
12
|
-
return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
|
|
13
|
-
}
|
|
14
|
-
static _sanitizeAndSplit(str) {
|
|
15
|
-
const s = SimpleRoute._escapeRegExp(SimpleRoute.SPLITTER);
|
|
16
|
-
return (`${str}`
|
|
17
|
-
.trim()
|
|
18
|
-
// splitter trim left and right
|
|
19
|
-
.replace(new RegExp(`^(${s})+`), '')
|
|
20
|
-
.replace(new RegExp(`(${s})+$`), '')
|
|
21
|
-
.split(SimpleRoute.SPLITTER)
|
|
22
|
-
// remove empty segments... will "normalize" multiple splitters into one
|
|
23
|
-
.filter(Boolean));
|
|
24
|
-
}
|
|
25
|
-
static _parse(route) {
|
|
26
|
-
let wasSpread = false;
|
|
27
|
-
let wasWildcard = false;
|
|
28
|
-
return SimpleRoute._sanitizeAndSplit(route).reduce((memo, segment, idx, all) => {
|
|
29
|
-
let name = null;
|
|
30
|
-
// if optional, remove trailing '?' marker
|
|
31
|
-
let isOptional = segment.endsWith('?');
|
|
32
|
-
if (isOptional)
|
|
33
|
-
segment = segment.slice(0, -1);
|
|
34
|
-
let isSpread = segment.startsWith('[...');
|
|
35
|
-
if (isSpread) {
|
|
36
|
-
if (isOptional) {
|
|
37
|
-
throw new Error('Spread segment must not be marked as optional');
|
|
38
|
-
}
|
|
39
|
-
if (wasSpread) {
|
|
40
|
-
throw new Error('Multiple spread segments are invalid');
|
|
41
|
-
}
|
|
42
|
-
wasSpread = true;
|
|
43
|
-
segment = '[' + segment.slice(4);
|
|
44
|
-
}
|
|
45
|
-
let test = new RegExp('^' + SimpleRoute._escapeRegExp(segment) + '$');
|
|
46
|
-
// catch all wildcard
|
|
47
|
-
if (segment === SimpleRoute.WILDCARD) {
|
|
48
|
-
if (idx < all.length - 1) {
|
|
49
|
-
throw new Error(`Wildcard '${SimpleRoute.WILDCARD}' can be used only as a last segment`);
|
|
50
|
-
}
|
|
51
|
-
wasWildcard = true;
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
// starting with at least one word char within brackets...
|
|
55
|
-
let m = segment.match(/^\[(\w.*)]$/);
|
|
56
|
-
if (m) {
|
|
57
|
-
name = m[1];
|
|
58
|
-
test = /.+/;
|
|
59
|
-
// id([0-9]+)
|
|
60
|
-
let m2 = m[1].match(/^(\w.*)\((.+)\)$/);
|
|
61
|
-
if (m2) {
|
|
62
|
-
name = m2[1];
|
|
63
|
-
test = new RegExp('^' + m2[2] + '$');
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (wasWildcard) {
|
|
68
|
-
isOptional = true;
|
|
69
|
-
test = /.*/;
|
|
70
|
-
}
|
|
71
|
-
memo.push({ segment, name, test, isOptional, isSpread });
|
|
72
|
-
return memo;
|
|
73
|
-
}, []);
|
|
74
|
-
}
|
|
75
|
-
static parseQueryString(str) {
|
|
76
|
-
return `${str}`
|
|
77
|
-
.replace(/^&/, '')
|
|
78
|
-
.replace(/&$/, '')
|
|
79
|
-
.split('&')
|
|
80
|
-
.reduce((memo, kvpair) => {
|
|
81
|
-
const [k, v] = kvpair.split('=').map(decodeURIComponent);
|
|
82
|
-
if (k.length)
|
|
83
|
-
memo[k] = v;
|
|
84
|
-
return memo;
|
|
85
|
-
}, {});
|
|
86
|
-
}
|
|
87
|
-
parse(url, allowQueryParams = true) {
|
|
88
|
-
let matched = {};
|
|
89
|
-
const qPos = url.indexOf('?');
|
|
90
|
-
if (allowQueryParams && ~qPos) {
|
|
91
|
-
const _backup = url;
|
|
92
|
-
url = _backup.slice(0, qPos);
|
|
93
|
-
matched = SimpleRoute.parseQueryString(_backup.slice(qPos + 1));
|
|
94
|
-
}
|
|
95
|
-
let segments = SimpleRoute._sanitizeAndSplit(url);
|
|
96
|
-
// SPREAD PARAMS DANCING BLOCK - if there are "spread" definitions we need to adjust input
|
|
97
|
-
// that is "group" (join) segments that were initially splitted
|
|
98
|
-
const hasSpread = !!this._parsed.filter((v) => v.isSpread).length;
|
|
99
|
-
if (hasSpread) {
|
|
100
|
-
let newSegments = [];
|
|
101
|
-
this._parsed.forEach((p, i) => {
|
|
102
|
-
if (p.isSpread) {
|
|
103
|
-
// there are defined segments after the "spread" definition
|
|
104
|
-
if (this._parsed[i + 1]) {
|
|
105
|
-
// SimpleRoute
|
|
106
|
-
newSegments.push(segments.slice(0, this._parsed.length - i).join(SimpleRoute.SPLITTER));
|
|
107
|
-
segments = segments.slice(this._parsed.length - i);
|
|
108
|
-
}
|
|
109
|
-
// there are no more defined segments
|
|
110
|
-
else {
|
|
111
|
-
newSegments.push(segments.join(SimpleRoute.SPLITTER));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
newSegments = newSegments.concat(segments.slice(0, 1));
|
|
116
|
-
segments = segments.slice(1);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
segments = newSegments;
|
|
120
|
-
}
|
|
121
|
-
// return early no-op special case: first segment is a wildcard
|
|
122
|
-
// (no need to process further)
|
|
123
|
-
if (this._parsed?.[0]?.segment === SimpleRoute.WILDCARD) {
|
|
124
|
-
return matched;
|
|
125
|
-
}
|
|
126
|
-
// minimum required (not optional) segments length
|
|
127
|
-
const reqLen = this._parsed.reduce((memo, p, idx, arr) => {
|
|
128
|
-
const next = arr[idx + 1];
|
|
129
|
-
// if is not optional or has not optional still to come
|
|
130
|
-
if (!p.isOptional || (next && !next.isOptional)) {
|
|
131
|
-
memo++;
|
|
132
|
-
}
|
|
133
|
-
return memo;
|
|
134
|
-
}, 0);
|
|
135
|
-
// quick cheap check: if counts dont match = no match
|
|
136
|
-
if (segments.length < reqLen) {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
for (const [i, s] of segments.entries()) {
|
|
140
|
-
const p = this._parsed[i];
|
|
141
|
-
if (!p || !p.test.test(s)) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
if (p.name) {
|
|
145
|
-
matched[decodeURIComponent(p.name)] = decodeURIComponent(s);
|
|
146
|
-
}
|
|
147
|
-
if (p.segment === SimpleRoute.WILDCARD) {
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return matched;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
class SimpleRouter {
|
|
156
|
-
// console log debug on/off switch
|
|
157
|
-
static debug = false;
|
|
158
|
-
_routes = [];
|
|
159
|
-
_catchAll;
|
|
160
|
-
// current (last matched) route and params (in the shape { route: "...", params: {} } )
|
|
161
|
-
_current = {
|
|
162
|
-
route: null,
|
|
163
|
-
params: null,
|
|
164
|
-
label: null,
|
|
165
|
-
};
|
|
166
|
-
// https://svelte.dev/docs#Store_contract
|
|
167
|
-
_subscriptions = new Set();
|
|
168
|
-
constructor(config = null) {
|
|
169
|
-
Object.entries(config || {}).forEach(([route, cb]) => {
|
|
170
|
-
this.on(route, cb);
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
_dbg(...a) {
|
|
174
|
-
SimpleRouter.debug && console.log('[SimpleRouter]', ...a);
|
|
175
|
-
}
|
|
176
|
-
reset() {
|
|
177
|
-
this._routes = [];
|
|
178
|
-
return this;
|
|
179
|
-
}
|
|
180
|
-
get current() {
|
|
181
|
-
return this._current;
|
|
182
|
-
}
|
|
183
|
-
on(routes, cb, { label = null, allowQueryParams = true } = {}) {
|
|
184
|
-
if (!Array.isArray(routes))
|
|
185
|
-
routes = [routes];
|
|
186
|
-
routes.forEach((route) => {
|
|
187
|
-
if (route === '*') {
|
|
188
|
-
this._catchAll = cb;
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
this._routes.push([new SimpleRoute(route), cb, allowQueryParams, label]);
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
exec(url, fallbackFn) {
|
|
196
|
-
const dbgPrefix = `'${url}' -> `;
|
|
197
|
-
const isFn = (v) => typeof v === 'function';
|
|
198
|
-
for (const [route, cb, allowQueryParams, label] of this._routes) {
|
|
199
|
-
// first match wins
|
|
200
|
-
// parse returns null or params object (which can be empty)
|
|
201
|
-
const params = route.parse(url, allowQueryParams);
|
|
202
|
-
if (params) {
|
|
203
|
-
this._publishCurrent(route.route, params, label);
|
|
204
|
-
this._dbg(`${dbgPrefix}matches '${route.route}' with`, params);
|
|
205
|
-
return isFn(cb) ? cb(params, route.route) : true;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
if (isFn(fallbackFn)) {
|
|
209
|
-
this._publishCurrent(null, null, null);
|
|
210
|
-
this._dbg(`${dbgPrefix}fallback...`);
|
|
211
|
-
return fallbackFn();
|
|
212
|
-
}
|
|
213
|
-
if (isFn(this._catchAll)) {
|
|
214
|
-
this._publishCurrent('*', null, null);
|
|
215
|
-
this._dbg(`${dbgPrefix}catchall...`);
|
|
216
|
-
return this._catchAll(null, '*');
|
|
217
|
-
}
|
|
218
|
-
this._publishCurrent(null, null, null);
|
|
219
|
-
this._dbg(`${dbgPrefix}no match...`);
|
|
220
|
-
return false;
|
|
221
|
-
}
|
|
222
|
-
_publishCurrent(route, params, label) {
|
|
223
|
-
this._current = { route, params, label };
|
|
224
|
-
this._subscriptions.forEach((cb) => cb(this._current));
|
|
225
|
-
}
|
|
226
|
-
// https://svelte.dev/docs#Store_contract
|
|
227
|
-
subscribe(subscription) {
|
|
228
|
-
if (typeof subscription !== 'function') {
|
|
229
|
-
throw new TypeError('Subscription is not a function');
|
|
230
|
-
}
|
|
231
|
-
this._subscriptions.add(subscription);
|
|
232
|
-
subscription(this._current);
|
|
233
|
-
// For interoperability with RxJS Observables, the .subscribe method is also
|
|
234
|
-
// allowed to return an object with an .unsubscribe method
|
|
235
|
-
return {
|
|
236
|
-
unsubscribe: () => this._subscriptions.delete(subscription),
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export { SimpleRoute, SimpleRouter };
|