@marianmeres/simple-router 1.9.0
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 +75 -0
- package/dist/index.cjs +221 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +218 -0
- package/dist/route.d.ts +18 -0
- package/dist/router.d.ts +31 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @marianmeres/simple-router
|
|
2
|
+
|
|
3
|
+
Small path-like string parser. Originally inspired by
|
|
4
|
+
[sapper-like regex routes](https://sapper.svelte.dev/docs#Regexes_in_routes).
|
|
5
|
+
Intended primarily for - but not limited to - client side SPA routing.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
npm i @marianmeres/simple-router
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick example
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// routes definitions can be added via ctor config object
|
|
17
|
+
const router = new SimpleRouter({
|
|
18
|
+
'/': () => pageIndex(),
|
|
19
|
+
'*': () => page404(), // catch all (last resort fallback)
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// or via "on" api
|
|
23
|
+
router.on(
|
|
24
|
+
'/article/[id([0-9]+)]/[slug]',
|
|
25
|
+
({ id, slug }) => pageArticle(id, slug)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// finally, perform route match (execute router)
|
|
29
|
+
// (example here returns "component" which is then "rendered")
|
|
30
|
+
window.onhashchange = () => render(router.exec(location.hash));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
See [tests](tests) or [examples](examples) for more.
|
|
34
|
+
|
|
35
|
+
## Route matching
|
|
36
|
+
|
|
37
|
+
Route segments:
|
|
38
|
+
- `exact` matches `exact`
|
|
39
|
+
- `[name]` matches `any` and is resolved as `{ name: 'any' }` param
|
|
40
|
+
- `[name(regex)]` matches if `regex.test(segment)` is truthy
|
|
41
|
+
- `exact?` or `[name]?` marks segment as optional
|
|
42
|
+
- `[...name]` matches "rest segments"
|
|
43
|
+
|
|
44
|
+
Few notes on segments separators (which is slash `/` by default):
|
|
45
|
+
- multiple ones are always normalized to single,
|
|
46
|
+
- separator is always trimmed (both left and right) before matching
|
|
47
|
+
|
|
48
|
+
| Example route definition | Example input | Result |
|
|
49
|
+
| ------------------------- | ------------------ | --------------------------- |
|
|
50
|
+
| `/foo` | `/bar` | `null` |
|
|
51
|
+
| `/foo` | (empty string) | `null` |
|
|
52
|
+
| `/foo/[bar]` | `/foo` | `null` |
|
|
53
|
+
| `/[foo]/bar` | `/foo` | `null` |
|
|
54
|
+
| `/[foo]/[bar]` | `/foo` | `null` |
|
|
55
|
+
| `/` | (empty string) | `{}` |
|
|
56
|
+
| (empty string) | `///` | `{}` |
|
|
57
|
+
| `foo` | `foo` | `{}` |
|
|
58
|
+
| `//foo///bar.baz/` | `foo/bar.baz` | `{}` |
|
|
59
|
+
| `foo/bar/baz` | `//foo//bar/baz//` | `{}` |
|
|
60
|
+
| `/[foo]` | `/bar` | `{"foo":"bar"}` |
|
|
61
|
+
| `#/foo/[bar]` | `#/foo/bat` | `{"bar":"bat"}` |
|
|
62
|
+
| `#/[foo]/[bar]` | `#/baz/bat` | `{"foo":"baz","bar":"bat"}` |
|
|
63
|
+
| `#/[foo]/bar` | `#/baz/bar` | `{"foo":"baz"}` |
|
|
64
|
+
| `/[id([0-9]+)]` | `/123` | `{"id":"123"}` |
|
|
65
|
+
| `/[id(\d-\d)]` | `/1-2` | `{"id":"1-2"}` |
|
|
66
|
+
| `/[id([0-9]+)]` | `/foo` | `null` |
|
|
67
|
+
| `/foo/[bar]/[id([0-9]+)]` | `/foo/baz/123` | `{"bar":"baz","id":"123"}` |
|
|
68
|
+
| `/foo/[id([0-9]+)]/[bar]` | `/foo/bar/baz` | `null` |
|
|
69
|
+
| `/foo/[([0-9]+)]` | `/foo/123` | `null` (missing name before regex) |
|
|
70
|
+
| `/foo/[bar]?` | `/foo` | `{}` |
|
|
71
|
+
| `/foo/[bar]?` | `/foo/bar` | `{ bar: 'bar' }` |
|
|
72
|
+
| `/foo/[bar]?/baz` | `/foo/bar/baz` | `{ bar: 'bar' }` |
|
|
73
|
+
| `/[...path]/[file]` | `/foo/bar/baz.js` | `{ path': 'foo/bar', file: 'baz.js' }`|
|
|
74
|
+
See [tests](tests) or [examples](examples) for more.
|
|
75
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class SimpleRoute {
|
|
4
|
+
route;
|
|
5
|
+
static SPLITTER = '/';
|
|
6
|
+
_parsed;
|
|
7
|
+
constructor(route) {
|
|
8
|
+
this.route = route;
|
|
9
|
+
this._parsed = SimpleRoute._parse(route);
|
|
10
|
+
}
|
|
11
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
|
12
|
+
static _escapeRegExp(str) {
|
|
13
|
+
return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
|
|
14
|
+
}
|
|
15
|
+
static _sanitizeAndSplit(str) {
|
|
16
|
+
const s = SimpleRoute._escapeRegExp(SimpleRoute.SPLITTER);
|
|
17
|
+
return (`${str}`
|
|
18
|
+
.trim()
|
|
19
|
+
// splitter trim left and right
|
|
20
|
+
.replace(new RegExp(`^(${s})+`), '')
|
|
21
|
+
.replace(new RegExp(`(${s})+$`), '')
|
|
22
|
+
.split(SimpleRoute.SPLITTER)
|
|
23
|
+
// remove empty segments... will "normalize" multiple splitters into one
|
|
24
|
+
.filter(Boolean));
|
|
25
|
+
}
|
|
26
|
+
static _parse(route) {
|
|
27
|
+
let wasSpread = false;
|
|
28
|
+
return SimpleRoute._sanitizeAndSplit(route).reduce((memo, segment) => {
|
|
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.substr(4);
|
|
44
|
+
}
|
|
45
|
+
let test = new RegExp('^' + SimpleRoute._escapeRegExp(segment) + '$');
|
|
46
|
+
// starting with at least one word char within brackets...
|
|
47
|
+
let m = segment.match(/^\[(\w.+)]$/);
|
|
48
|
+
if (m) {
|
|
49
|
+
name = m[1];
|
|
50
|
+
test = /.+/;
|
|
51
|
+
// id([0-9]+)
|
|
52
|
+
let m2 = m[1].match(/^(\w.*)\((.+)\)$/);
|
|
53
|
+
if (m2) {
|
|
54
|
+
name = m2[1];
|
|
55
|
+
test = new RegExp('^' + m2[2] + '$');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
memo.push({ segment, name, test, isOptional, isSpread });
|
|
59
|
+
return memo;
|
|
60
|
+
}, []);
|
|
61
|
+
}
|
|
62
|
+
static parseQueryString(str) {
|
|
63
|
+
return `${str}`
|
|
64
|
+
.replace(/^&/, '')
|
|
65
|
+
.replace(/&$/, '')
|
|
66
|
+
.split('&')
|
|
67
|
+
.reduce((memo, kvpair) => {
|
|
68
|
+
const [k, v] = kvpair.split('=').map(decodeURIComponent);
|
|
69
|
+
if (k.length)
|
|
70
|
+
memo[k] = v;
|
|
71
|
+
return memo;
|
|
72
|
+
}, {});
|
|
73
|
+
}
|
|
74
|
+
parse(url, allowQueryParams = true) {
|
|
75
|
+
let matched = {};
|
|
76
|
+
const qPos = url.indexOf('?');
|
|
77
|
+
if (allowQueryParams && ~qPos) {
|
|
78
|
+
const _backup = url;
|
|
79
|
+
url = _backup.slice(0, qPos);
|
|
80
|
+
matched = SimpleRoute.parseQueryString(_backup.slice(qPos + 1));
|
|
81
|
+
}
|
|
82
|
+
let segments = SimpleRoute._sanitizeAndSplit(url);
|
|
83
|
+
// SPREAD PARAMS DANCING BLOCK - if there are "spread" definitions we need to adjust input
|
|
84
|
+
// that is "group" (join) segments that were initially splitted
|
|
85
|
+
const hasSpread = !!this._parsed.filter((v) => v.isSpread).length;
|
|
86
|
+
if (hasSpread) {
|
|
87
|
+
let newSegments = [];
|
|
88
|
+
this._parsed.forEach((p, i) => {
|
|
89
|
+
if (p.isSpread) {
|
|
90
|
+
// there are defined segments after the "spread" definition
|
|
91
|
+
if (this._parsed[i + 1]) {
|
|
92
|
+
// SimpleRoute
|
|
93
|
+
newSegments.push(segments.slice(0, this._parsed.length - i).join(SimpleRoute.SPLITTER));
|
|
94
|
+
segments = segments.slice(this._parsed.length - i);
|
|
95
|
+
}
|
|
96
|
+
// there are no more defined segments
|
|
97
|
+
else {
|
|
98
|
+
newSegments.push(segments.join(SimpleRoute.SPLITTER));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
newSegments = newSegments.concat(segments.slice(0, 1));
|
|
103
|
+
segments = segments.slice(1);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
segments = newSegments;
|
|
107
|
+
}
|
|
108
|
+
// minimum required (not optional) segments length
|
|
109
|
+
const reqLen = this._parsed.reduce((memo, p, idx, arr) => {
|
|
110
|
+
const next = arr[idx + 1];
|
|
111
|
+
// if is not optional or has not optional still to come
|
|
112
|
+
if (!p.isOptional || (next && !next.isOptional)) {
|
|
113
|
+
memo++;
|
|
114
|
+
}
|
|
115
|
+
return memo;
|
|
116
|
+
}, 0);
|
|
117
|
+
// quick cheap check: if counts dont match = no match
|
|
118
|
+
if (segments.length < reqLen) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
for (const [i, s] of segments.entries()) {
|
|
122
|
+
const p = this._parsed[i];
|
|
123
|
+
if (!p || !p.test.test(s)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
if (p.name) {
|
|
127
|
+
matched[decodeURIComponent(p.name)] = decodeURIComponent(s);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return matched;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
class SimpleRouter {
|
|
135
|
+
// console log debug on/off switch
|
|
136
|
+
static debug = false;
|
|
137
|
+
_routes = [];
|
|
138
|
+
_catchAll;
|
|
139
|
+
// current (last matched) route and params (in the shape { route: "...", params: {} } )
|
|
140
|
+
_current = {
|
|
141
|
+
route: null,
|
|
142
|
+
params: null,
|
|
143
|
+
label: null,
|
|
144
|
+
};
|
|
145
|
+
// https://svelte.dev/docs#Store_contract
|
|
146
|
+
_subscriptions = new Set();
|
|
147
|
+
constructor(config = null) {
|
|
148
|
+
Object.entries(config || {}).forEach(([route, cb]) => {
|
|
149
|
+
this.on(route, cb);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
_dbg(...a) {
|
|
153
|
+
SimpleRouter.debug && console.log('[SimpleRouter]', ...a);
|
|
154
|
+
}
|
|
155
|
+
reset() {
|
|
156
|
+
this._routes = [];
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
get current() {
|
|
160
|
+
return this._current;
|
|
161
|
+
}
|
|
162
|
+
on(routes, cb, { label = null, allowQueryParams = true } = {}) {
|
|
163
|
+
if (!Array.isArray(routes))
|
|
164
|
+
routes = [routes];
|
|
165
|
+
routes.forEach((route) => {
|
|
166
|
+
if (route === '*') {
|
|
167
|
+
this._catchAll = cb;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
this._routes.push([new SimpleRoute(route), cb, allowQueryParams, label]);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
exec(url, fallbackFn) {
|
|
175
|
+
const dbgPrefix = `'${url}' -> `;
|
|
176
|
+
const isFn = (v) => typeof v === 'function';
|
|
177
|
+
for (const [route, cb, allowQueryParams, label] of this._routes) {
|
|
178
|
+
// first match wins
|
|
179
|
+
// parse returns null or params object (which can be empty)
|
|
180
|
+
const params = route.parse(url, allowQueryParams);
|
|
181
|
+
if (params) {
|
|
182
|
+
this._publishCurrent(route.route, params, label);
|
|
183
|
+
this._dbg(`${dbgPrefix}matches '${route.route}' with`, params);
|
|
184
|
+
return isFn(cb) ? cb(params, route.route) : true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (isFn(fallbackFn)) {
|
|
188
|
+
this._publishCurrent(null, null, null);
|
|
189
|
+
this._dbg(`${dbgPrefix}fallback...`);
|
|
190
|
+
return fallbackFn();
|
|
191
|
+
}
|
|
192
|
+
if (isFn(this._catchAll)) {
|
|
193
|
+
this._publishCurrent('*', null, null);
|
|
194
|
+
this._dbg(`${dbgPrefix}catchall...`);
|
|
195
|
+
return this._catchAll(null, '*');
|
|
196
|
+
}
|
|
197
|
+
this._publishCurrent(null, null, null);
|
|
198
|
+
this._dbg(`${dbgPrefix}no match...`);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
_publishCurrent(route, params, label) {
|
|
202
|
+
this._current = { route, params, label };
|
|
203
|
+
this._subscriptions.forEach((cb) => cb(this._current));
|
|
204
|
+
}
|
|
205
|
+
// https://svelte.dev/docs#Store_contract
|
|
206
|
+
subscribe(subscription) {
|
|
207
|
+
if (typeof subscription !== 'function') {
|
|
208
|
+
throw new TypeError('Subscription is not a function');
|
|
209
|
+
}
|
|
210
|
+
this._subscriptions.add(subscription);
|
|
211
|
+
subscription(this._current);
|
|
212
|
+
// For interoperability with RxJS Observables, the .subscribe method is also
|
|
213
|
+
// allowed to return an object with an .unsubscribe method
|
|
214
|
+
return {
|
|
215
|
+
unsubscribe: () => this._subscriptions.delete(subscription),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
exports.SimpleRoute = SimpleRoute;
|
|
221
|
+
exports.SimpleRouter = SimpleRouter;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
class SimpleRoute {
|
|
2
|
+
route;
|
|
3
|
+
static SPLITTER = '/';
|
|
4
|
+
_parsed;
|
|
5
|
+
constructor(route) {
|
|
6
|
+
this.route = route;
|
|
7
|
+
this._parsed = SimpleRoute._parse(route);
|
|
8
|
+
}
|
|
9
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
|
10
|
+
static _escapeRegExp(str) {
|
|
11
|
+
return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
|
|
12
|
+
}
|
|
13
|
+
static _sanitizeAndSplit(str) {
|
|
14
|
+
const s = SimpleRoute._escapeRegExp(SimpleRoute.SPLITTER);
|
|
15
|
+
return (`${str}`
|
|
16
|
+
.trim()
|
|
17
|
+
// splitter trim left and right
|
|
18
|
+
.replace(new RegExp(`^(${s})+`), '')
|
|
19
|
+
.replace(new RegExp(`(${s})+$`), '')
|
|
20
|
+
.split(SimpleRoute.SPLITTER)
|
|
21
|
+
// remove empty segments... will "normalize" multiple splitters into one
|
|
22
|
+
.filter(Boolean));
|
|
23
|
+
}
|
|
24
|
+
static _parse(route) {
|
|
25
|
+
let wasSpread = false;
|
|
26
|
+
return SimpleRoute._sanitizeAndSplit(route).reduce((memo, segment) => {
|
|
27
|
+
let name = null;
|
|
28
|
+
// if optional, remove trailing '?' marker
|
|
29
|
+
let isOptional = segment.endsWith('?');
|
|
30
|
+
if (isOptional)
|
|
31
|
+
segment = segment.slice(0, -1);
|
|
32
|
+
let isSpread = segment.startsWith('[...');
|
|
33
|
+
if (isSpread) {
|
|
34
|
+
if (isOptional) {
|
|
35
|
+
throw new Error('Spread segment must not be marked as optional');
|
|
36
|
+
}
|
|
37
|
+
if (wasSpread) {
|
|
38
|
+
throw new Error('Multiple spread segments are invalid');
|
|
39
|
+
}
|
|
40
|
+
wasSpread = true;
|
|
41
|
+
segment = '[' + segment.substr(4);
|
|
42
|
+
}
|
|
43
|
+
let test = new RegExp('^' + SimpleRoute._escapeRegExp(segment) + '$');
|
|
44
|
+
// starting with at least one word char within brackets...
|
|
45
|
+
let m = segment.match(/^\[(\w.+)]$/);
|
|
46
|
+
if (m) {
|
|
47
|
+
name = m[1];
|
|
48
|
+
test = /.+/;
|
|
49
|
+
// id([0-9]+)
|
|
50
|
+
let m2 = m[1].match(/^(\w.*)\((.+)\)$/);
|
|
51
|
+
if (m2) {
|
|
52
|
+
name = m2[1];
|
|
53
|
+
test = new RegExp('^' + m2[2] + '$');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
memo.push({ segment, name, test, isOptional, isSpread });
|
|
57
|
+
return memo;
|
|
58
|
+
}, []);
|
|
59
|
+
}
|
|
60
|
+
static parseQueryString(str) {
|
|
61
|
+
return `${str}`
|
|
62
|
+
.replace(/^&/, '')
|
|
63
|
+
.replace(/&$/, '')
|
|
64
|
+
.split('&')
|
|
65
|
+
.reduce((memo, kvpair) => {
|
|
66
|
+
const [k, v] = kvpair.split('=').map(decodeURIComponent);
|
|
67
|
+
if (k.length)
|
|
68
|
+
memo[k] = v;
|
|
69
|
+
return memo;
|
|
70
|
+
}, {});
|
|
71
|
+
}
|
|
72
|
+
parse(url, allowQueryParams = true) {
|
|
73
|
+
let matched = {};
|
|
74
|
+
const qPos = url.indexOf('?');
|
|
75
|
+
if (allowQueryParams && ~qPos) {
|
|
76
|
+
const _backup = url;
|
|
77
|
+
url = _backup.slice(0, qPos);
|
|
78
|
+
matched = SimpleRoute.parseQueryString(_backup.slice(qPos + 1));
|
|
79
|
+
}
|
|
80
|
+
let segments = SimpleRoute._sanitizeAndSplit(url);
|
|
81
|
+
// SPREAD PARAMS DANCING BLOCK - if there are "spread" definitions we need to adjust input
|
|
82
|
+
// that is "group" (join) segments that were initially splitted
|
|
83
|
+
const hasSpread = !!this._parsed.filter((v) => v.isSpread).length;
|
|
84
|
+
if (hasSpread) {
|
|
85
|
+
let newSegments = [];
|
|
86
|
+
this._parsed.forEach((p, i) => {
|
|
87
|
+
if (p.isSpread) {
|
|
88
|
+
// there are defined segments after the "spread" definition
|
|
89
|
+
if (this._parsed[i + 1]) {
|
|
90
|
+
// SimpleRoute
|
|
91
|
+
newSegments.push(segments.slice(0, this._parsed.length - i).join(SimpleRoute.SPLITTER));
|
|
92
|
+
segments = segments.slice(this._parsed.length - i);
|
|
93
|
+
}
|
|
94
|
+
// there are no more defined segments
|
|
95
|
+
else {
|
|
96
|
+
newSegments.push(segments.join(SimpleRoute.SPLITTER));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
newSegments = newSegments.concat(segments.slice(0, 1));
|
|
101
|
+
segments = segments.slice(1);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
segments = newSegments;
|
|
105
|
+
}
|
|
106
|
+
// minimum required (not optional) segments length
|
|
107
|
+
const reqLen = this._parsed.reduce((memo, p, idx, arr) => {
|
|
108
|
+
const next = arr[idx + 1];
|
|
109
|
+
// if is not optional or has not optional still to come
|
|
110
|
+
if (!p.isOptional || (next && !next.isOptional)) {
|
|
111
|
+
memo++;
|
|
112
|
+
}
|
|
113
|
+
return memo;
|
|
114
|
+
}, 0);
|
|
115
|
+
// quick cheap check: if counts dont match = no match
|
|
116
|
+
if (segments.length < reqLen) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
for (const [i, s] of segments.entries()) {
|
|
120
|
+
const p = this._parsed[i];
|
|
121
|
+
if (!p || !p.test.test(s)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (p.name) {
|
|
125
|
+
matched[decodeURIComponent(p.name)] = decodeURIComponent(s);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return matched;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class SimpleRouter {
|
|
133
|
+
// console log debug on/off switch
|
|
134
|
+
static debug = false;
|
|
135
|
+
_routes = [];
|
|
136
|
+
_catchAll;
|
|
137
|
+
// current (last matched) route and params (in the shape { route: "...", params: {} } )
|
|
138
|
+
_current = {
|
|
139
|
+
route: null,
|
|
140
|
+
params: null,
|
|
141
|
+
label: null,
|
|
142
|
+
};
|
|
143
|
+
// https://svelte.dev/docs#Store_contract
|
|
144
|
+
_subscriptions = new Set();
|
|
145
|
+
constructor(config = null) {
|
|
146
|
+
Object.entries(config || {}).forEach(([route, cb]) => {
|
|
147
|
+
this.on(route, cb);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
_dbg(...a) {
|
|
151
|
+
SimpleRouter.debug && console.log('[SimpleRouter]', ...a);
|
|
152
|
+
}
|
|
153
|
+
reset() {
|
|
154
|
+
this._routes = [];
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
get current() {
|
|
158
|
+
return this._current;
|
|
159
|
+
}
|
|
160
|
+
on(routes, cb, { label = null, allowQueryParams = true } = {}) {
|
|
161
|
+
if (!Array.isArray(routes))
|
|
162
|
+
routes = [routes];
|
|
163
|
+
routes.forEach((route) => {
|
|
164
|
+
if (route === '*') {
|
|
165
|
+
this._catchAll = cb;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
this._routes.push([new SimpleRoute(route), cb, allowQueryParams, label]);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
exec(url, fallbackFn) {
|
|
173
|
+
const dbgPrefix = `'${url}' -> `;
|
|
174
|
+
const isFn = (v) => typeof v === 'function';
|
|
175
|
+
for (const [route, cb, allowQueryParams, label] of this._routes) {
|
|
176
|
+
// first match wins
|
|
177
|
+
// parse returns null or params object (which can be empty)
|
|
178
|
+
const params = route.parse(url, allowQueryParams);
|
|
179
|
+
if (params) {
|
|
180
|
+
this._publishCurrent(route.route, params, label);
|
|
181
|
+
this._dbg(`${dbgPrefix}matches '${route.route}' with`, params);
|
|
182
|
+
return isFn(cb) ? cb(params, route.route) : true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (isFn(fallbackFn)) {
|
|
186
|
+
this._publishCurrent(null, null, null);
|
|
187
|
+
this._dbg(`${dbgPrefix}fallback...`);
|
|
188
|
+
return fallbackFn();
|
|
189
|
+
}
|
|
190
|
+
if (isFn(this._catchAll)) {
|
|
191
|
+
this._publishCurrent('*', null, null);
|
|
192
|
+
this._dbg(`${dbgPrefix}catchall...`);
|
|
193
|
+
return this._catchAll(null, '*');
|
|
194
|
+
}
|
|
195
|
+
this._publishCurrent(null, null, null);
|
|
196
|
+
this._dbg(`${dbgPrefix}no match...`);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
_publishCurrent(route, params, label) {
|
|
200
|
+
this._current = { route, params, label };
|
|
201
|
+
this._subscriptions.forEach((cb) => cb(this._current));
|
|
202
|
+
}
|
|
203
|
+
// https://svelte.dev/docs#Store_contract
|
|
204
|
+
subscribe(subscription) {
|
|
205
|
+
if (typeof subscription !== 'function') {
|
|
206
|
+
throw new TypeError('Subscription is not a function');
|
|
207
|
+
}
|
|
208
|
+
this._subscriptions.add(subscription);
|
|
209
|
+
subscription(this._current);
|
|
210
|
+
// For interoperability with RxJS Observables, the .subscribe method is also
|
|
211
|
+
// allowed to return an object with an .unsubscribe method
|
|
212
|
+
return {
|
|
213
|
+
unsubscribe: () => this._subscriptions.delete(subscription),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export { SimpleRoute, SimpleRouter };
|
package/dist/route.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RouteConfig {
|
|
2
|
+
segment: string;
|
|
3
|
+
name: string;
|
|
4
|
+
test: RegExp;
|
|
5
|
+
isOptional: boolean;
|
|
6
|
+
isSpread: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class SimpleRoute {
|
|
9
|
+
readonly route: any;
|
|
10
|
+
static SPLITTER: string;
|
|
11
|
+
protected _parsed: RouteConfig[];
|
|
12
|
+
constructor(route: any);
|
|
13
|
+
protected static _escapeRegExp(str: string): string;
|
|
14
|
+
protected static _sanitizeAndSplit(str: any): string[];
|
|
15
|
+
protected static _parse(route: any): any[];
|
|
16
|
+
static parseQueryString(str: any): {};
|
|
17
|
+
parse(url: any, allowQueryParams?: boolean): {};
|
|
18
|
+
}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SimpleRoute } from './route';
|
|
2
|
+
export declare class SimpleRouter {
|
|
3
|
+
static debug: boolean;
|
|
4
|
+
protected _routes: [SimpleRoute, Function, boolean, string][];
|
|
5
|
+
protected _catchAll: Function;
|
|
6
|
+
protected _current: {
|
|
7
|
+
route: string;
|
|
8
|
+
params: any;
|
|
9
|
+
label: string;
|
|
10
|
+
};
|
|
11
|
+
protected _subscriptions: Set<Function>;
|
|
12
|
+
constructor(config?: {
|
|
13
|
+
[route: string]: Function;
|
|
14
|
+
});
|
|
15
|
+
protected _dbg(...a: any[]): void;
|
|
16
|
+
reset(): this;
|
|
17
|
+
get current(): {
|
|
18
|
+
route: string;
|
|
19
|
+
params: any;
|
|
20
|
+
label: string;
|
|
21
|
+
};
|
|
22
|
+
on(routes: string | string[], cb: Function, { label, allowQueryParams }?: {
|
|
23
|
+
label?: any;
|
|
24
|
+
allowQueryParams?: boolean;
|
|
25
|
+
}): void;
|
|
26
|
+
exec(url: string, fallbackFn?: Function): any;
|
|
27
|
+
protected _publishCurrent(route: any, params: any, label: any): void;
|
|
28
|
+
subscribe(subscription: Function): {
|
|
29
|
+
unsubscribe: () => boolean;
|
|
30
|
+
};
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@marianmeres/simple-router",
|
|
3
|
+
"version": "1.9.0",
|
|
4
|
+
"description": "Minimalistic route parser for sapper-like regex routes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
"require": "./dist/index.cjs",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"clean": "rimraf dist/*",
|
|
17
|
+
"build": "npm run clean && rollup -c",
|
|
18
|
+
"dev": "npm run clean && rollup -c -w",
|
|
19
|
+
"prettier": "prettier --write \"{src,tests,examples}/**/*.{js,ts,json}\"",
|
|
20
|
+
"release": "release",
|
|
21
|
+
"test": "test-runner",
|
|
22
|
+
"test:watch": "nodemon -q -w dist -w tests -x test-runner --"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/marianmeres/simple-router.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"router"
|
|
30
|
+
],
|
|
31
|
+
"author": "Marian Meres <marian@meres.sk>",
|
|
32
|
+
"license": "WTFPL",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/marianmeres/simple-router/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/marianmeres/simple-router#readme",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@marianmeres/release": "^0.0.7",
|
|
39
|
+
"@marianmeres/test-runner": "^2.0.8",
|
|
40
|
+
"@rollup/plugin-typescript": "^9.0.2",
|
|
41
|
+
"@types/node": "^14.18.33",
|
|
42
|
+
"lodash": "^4.17.21",
|
|
43
|
+
"nodemon": "^2.0.20",
|
|
44
|
+
"prettier": "^2.2.1",
|
|
45
|
+
"rimraf": "^3.0.2",
|
|
46
|
+
"rollup": "^3.3.0",
|
|
47
|
+
"tslib": "^2.4.1",
|
|
48
|
+
"typescript": "^4.8.4"
|
|
49
|
+
}
|
|
50
|
+
}
|