@marianmeres/simple-router 2.1.0 → 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 +241 -28
- package/dist/router.js +255 -0
- package/package.json +9 -40
- package/dist/index.cjs +0 -251
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -248
package/dist/router.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { SimpleRoute } from "./route.js";
|
|
2
|
+
import { createPubSub } from "@marianmeres/pubsub";
|
|
3
|
+
const PUBSUB_TOPIC = "current";
|
|
4
|
+
/**
|
|
5
|
+
* A simple, framework-agnostic string pattern matcher and router with support
|
|
6
|
+
* for dynamic parameters, wildcards, query strings, and reactive subscriptions.
|
|
7
|
+
*
|
|
8
|
+
* Can match any string identifiers - URLs, file paths, command names, or custom patterns.
|
|
9
|
+
* Primarily designed for client-side SPA routing, but flexible enough for any pattern
|
|
10
|
+
* matching needs.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { SimpleRouter } from "@marianmeres/simple-router";
|
|
15
|
+
*
|
|
16
|
+
* // URL routing
|
|
17
|
+
* const router = new SimpleRouter({
|
|
18
|
+
* "/": () => console.log("Home"),
|
|
19
|
+
* "/user/[id]": (params) => console.log("User:", params?.id),
|
|
20
|
+
* "*": () => console.log("Not found")
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* router.exec("/user/123"); // Logs: "User: 123"
|
|
24
|
+
*
|
|
25
|
+
* // Command routing
|
|
26
|
+
* const cmdRouter = new SimpleRouter({
|
|
27
|
+
* "user:create": () => createUser(),
|
|
28
|
+
* "user:delete:[id]": ({ id }) => deleteUser(id),
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* cmdRouter.exec("user:delete:123");
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class SimpleRouter {
|
|
35
|
+
/** Enable/disable console debug logging for route matching */
|
|
36
|
+
static debug = false;
|
|
37
|
+
#routes = [];
|
|
38
|
+
#catchAll = null;
|
|
39
|
+
#current = {
|
|
40
|
+
route: null,
|
|
41
|
+
params: null,
|
|
42
|
+
label: null,
|
|
43
|
+
};
|
|
44
|
+
#pubsub = createPubSub();
|
|
45
|
+
/**
|
|
46
|
+
* Creates a new SimpleRouter instance.
|
|
47
|
+
*
|
|
48
|
+
* @param config - Optional configuration object mapping route patterns to callbacks
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* const router = new SimpleRouter({
|
|
53
|
+
* "/": () => HomePage,
|
|
54
|
+
* "/about": () => AboutPage
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
constructor(config = null) {
|
|
59
|
+
Object.entries(config || {}).forEach(([route, cb]) => {
|
|
60
|
+
this.on(route, cb);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
#dbg(...a) {
|
|
64
|
+
SimpleRouter.debug && console.log("[SimpleRouter]", ...a);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns a map of registered route patterns to their labels.
|
|
68
|
+
* Useful for debugging and introspection.
|
|
69
|
+
*
|
|
70
|
+
* @returns Object mapping route patterns to labels
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* router.on("/users", () => {}, { label: "users-list" });
|
|
75
|
+
* console.log(router.info()); // { "/users": "users-list" }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
info() {
|
|
79
|
+
return this.#routes.reduce((m, r) => {
|
|
80
|
+
m[r[0].route] = r[3] || "";
|
|
81
|
+
return m;
|
|
82
|
+
}, {});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Clears all registered routes (except catch-all).
|
|
86
|
+
* Useful for testing or dynamic route reconfiguration.
|
|
87
|
+
*
|
|
88
|
+
* @returns The router instance for method chaining
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* router.reset().on("/new-route", () => {});
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
reset() {
|
|
96
|
+
this.#routes = [];
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Gets the current router state (last matched route and params).
|
|
101
|
+
*
|
|
102
|
+
* @returns Current router state
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* router.exec("/user/123");
|
|
107
|
+
* console.log(router.current); // { route: "/user/[id]", params: { id: "123" }, label: null }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
get current() {
|
|
111
|
+
return this.#current;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Registers one or more route patterns with a callback.
|
|
115
|
+
* Routes are matched in the order they are registered (first match wins).
|
|
116
|
+
* Use "*" as a catch-all route.
|
|
117
|
+
*
|
|
118
|
+
* @param routes - Single route pattern or array of patterns
|
|
119
|
+
* @param cb - Callback function to execute when route matches
|
|
120
|
+
* @param options - Optional configuration (label, allowQueryParams)
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* // Single route
|
|
125
|
+
* router.on("/users", () => UsersPage);
|
|
126
|
+
*
|
|
127
|
+
* // Multiple routes to same handler
|
|
128
|
+
* router.on(["/", "/home", "/index.html"], () => HomePage);
|
|
129
|
+
*
|
|
130
|
+
* // With dynamic parameters
|
|
131
|
+
* router.on("/user/[id]", (params) => UserPage(params?.id));
|
|
132
|
+
*
|
|
133
|
+
* // With regex constraint
|
|
134
|
+
* router.on("/post/[id([0-9]+)]", (params) => PostPage(params?.id));
|
|
135
|
+
*
|
|
136
|
+
* // With label for debugging
|
|
137
|
+
* router.on("/admin", () => AdminPage, { label: "admin-dashboard" });
|
|
138
|
+
*
|
|
139
|
+
* // Catch-all route
|
|
140
|
+
* router.on("*", () => NotFoundPage);
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
on(routes, cb, { label = null, allowQueryParams = true } = {}) {
|
|
144
|
+
if (!Array.isArray(routes))
|
|
145
|
+
routes = [routes];
|
|
146
|
+
routes.forEach((route) => {
|
|
147
|
+
if (route === "*") {
|
|
148
|
+
this.#catchAll = cb;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
this.#routes.push([
|
|
152
|
+
new SimpleRoute(route),
|
|
153
|
+
cb,
|
|
154
|
+
allowQueryParams,
|
|
155
|
+
label,
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Executes pattern matching against the provided string.
|
|
162
|
+
* Returns the value returned by the matched route's callback.
|
|
163
|
+
* Routes are tested in registration order - first match wins.
|
|
164
|
+
*
|
|
165
|
+
* @param url - String to match against registered patterns (can be a URL, file path, command, etc.)
|
|
166
|
+
* @param fallbackFn - Optional fallback function if no route matches
|
|
167
|
+
* @returns The value returned by the matched callback, or false if no match
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* // URL matching
|
|
172
|
+
* router.exec("/users"); // Returns result of callback
|
|
173
|
+
*
|
|
174
|
+
* // With query parameters
|
|
175
|
+
* router.exec("/users?sort=name");
|
|
176
|
+
*
|
|
177
|
+
* // File path matching
|
|
178
|
+
* router.exec("src/components/Button.ts");
|
|
179
|
+
*
|
|
180
|
+
* // Command matching
|
|
181
|
+
* router.exec("user:delete:123");
|
|
182
|
+
*
|
|
183
|
+
* // With fallback
|
|
184
|
+
* router.exec("/unknown", () => console.log("Not found"));
|
|
185
|
+
*
|
|
186
|
+
* // Can return components, values, etc.
|
|
187
|
+
* const component = router.exec("/home");
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
// deno-lint-ignore no-explicit-any
|
|
191
|
+
exec(url, fallbackFn) {
|
|
192
|
+
const dbgPrefix = `'${url}' -> `;
|
|
193
|
+
for (const [route, cb, allowQueryParams, label] of this.#routes) {
|
|
194
|
+
// First match wins
|
|
195
|
+
// Parse returns null or a params object (which can be empty)
|
|
196
|
+
const params = route.parse(url, allowQueryParams);
|
|
197
|
+
if (params !== null) {
|
|
198
|
+
this.#publishCurrent(route.route, params, label);
|
|
199
|
+
this.#dbg(`${dbgPrefix}matches '${route.route}' with`, params);
|
|
200
|
+
return typeof cb === "function" ? cb(params, route.route) : true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (typeof fallbackFn === "function") {
|
|
204
|
+
this.#publishCurrent(null, null, null);
|
|
205
|
+
this.#dbg(`${dbgPrefix}fallback...`);
|
|
206
|
+
return fallbackFn(null, "");
|
|
207
|
+
}
|
|
208
|
+
if (typeof this.#catchAll === "function") {
|
|
209
|
+
this.#publishCurrent("*", null, null);
|
|
210
|
+
this.#dbg(`${dbgPrefix}catchall...`);
|
|
211
|
+
return this.#catchAll(null, "*");
|
|
212
|
+
}
|
|
213
|
+
this.#publishCurrent(null, null, null);
|
|
214
|
+
this.#dbg(`${dbgPrefix}no match...`);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
#publishCurrent(route, params, label) {
|
|
218
|
+
this.#current = { route, params, label };
|
|
219
|
+
this.#pubsub.publish(PUBSUB_TOPIC, this.#current);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Subscribes to router state changes.
|
|
223
|
+
* Follows the Svelte store contract - subscriber is called immediately with current state.
|
|
224
|
+
* Compatible with RxJS Observables (returns object with `unsubscribe` method).
|
|
225
|
+
*
|
|
226
|
+
* @param subscription - Callback function that receives router state changes
|
|
227
|
+
* @returns Subscription object with `unsubscribe()` method
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* const { unsubscribe } = router.subscribe((state) => {
|
|
232
|
+
* console.log("Route changed:", state.route);
|
|
233
|
+
* console.log("Params:", state.params);
|
|
234
|
+
* });
|
|
235
|
+
*
|
|
236
|
+
* // Later, to unsubscribe
|
|
237
|
+
* unsubscribe();
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* @see https://svelte.dev/docs#Store_contract
|
|
241
|
+
*/
|
|
242
|
+
subscribe(subscription) {
|
|
243
|
+
if (typeof subscription !== "function") {
|
|
244
|
+
throw new TypeError("Subscription is not a function");
|
|
245
|
+
}
|
|
246
|
+
const unsubscribe = this.#pubsub.subscribe(PUBSUB_TOPIC, subscription);
|
|
247
|
+
// immediately call with current value (as per store contract)
|
|
248
|
+
subscription(this.#current);
|
|
249
|
+
// For interoperability with RxJS Observables, the .subscribe method is also
|
|
250
|
+
// allowed to return an object with an .unsubscribe method
|
|
251
|
+
return {
|
|
252
|
+
unsubscribe,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
package/package.json
CHANGED
|
@@ -1,51 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/simple-router",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Minimalistic route parser for sapper-like regex routes",
|
|
3
|
+
"version": "2.2.2",
|
|
5
4
|
"type": "module",
|
|
6
|
-
"main": "
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
"import": "./dist/index.js"
|
|
11
|
-
},
|
|
12
|
-
"types": "./dist/index.d.ts",
|
|
13
|
-
"files": [
|
|
14
|
-
"dist"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"clean": "rimraf dist/*",
|
|
18
|
-
"build": "npm run clean && rollup -c",
|
|
19
|
-
"dev": "npm run clean && rollup -c -w",
|
|
20
|
-
"prettier": "prettier --write \"{src,tests,examples}/**/*.{js,ts,json}\"",
|
|
21
|
-
"release": "release -v minor",
|
|
22
|
-
"release:patch": "release -v patch",
|
|
23
|
-
"test": "test-runner",
|
|
24
|
-
"test:watch": "nodemon -q -w dist -w tests -x test-runner --"
|
|
25
|
-
},
|
|
5
|
+
"main": "dist/mod.js",
|
|
6
|
+
"types": "dist/mod.d.ts",
|
|
7
|
+
"author": "Marian Meres",
|
|
8
|
+
"license": "MIT",
|
|
26
9
|
"repository": {
|
|
27
10
|
"type": "git",
|
|
28
11
|
"url": "git+https://github.com/marianmeres/simple-router.git"
|
|
29
12
|
},
|
|
30
|
-
"keywords": [
|
|
31
|
-
"router"
|
|
32
|
-
],
|
|
33
|
-
"author": "Marian Meres <marian@meres.sk>",
|
|
34
|
-
"license": "WTFPL",
|
|
35
13
|
"bugs": {
|
|
36
14
|
"url": "https://github.com/marianmeres/simple-router/issues"
|
|
37
15
|
},
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
"@marianmeres/
|
|
41
|
-
"@marianmeres/test-runner": "^2.0.8",
|
|
42
|
-
"@rollup/plugin-typescript": "^9.0.2",
|
|
43
|
-
"@types/node": "^14.18.33",
|
|
44
|
-
"lodash": "^4.17.21",
|
|
45
|
-
"prettier": "^2.2.1",
|
|
46
|
-
"rimraf": "^3.0.2",
|
|
47
|
-
"rollup": "^3.3.0",
|
|
48
|
-
"tslib": "^2.4.1",
|
|
49
|
-
"typescript": "^4.8.4"
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@marianmeres/pubsub": "^2.2.0",
|
|
18
|
+
"@marianmeres/clog": "^2.3.3"
|
|
50
19
|
}
|
|
51
|
-
}
|
|
20
|
+
}
|
package/dist/index.cjs
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
class SimpleRoute {
|
|
4
|
-
route;
|
|
5
|
-
static SPLITTER = '/';
|
|
6
|
-
static WILDCARD = '*';
|
|
7
|
-
_parsed;
|
|
8
|
-
constructor(route) {
|
|
9
|
-
this.route = route;
|
|
10
|
-
this._parsed = SimpleRoute._parse(route);
|
|
11
|
-
}
|
|
12
|
-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
|
13
|
-
static _escapeRegExp(str) {
|
|
14
|
-
return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
-
}
|
|
16
|
-
static _sanitizeAndSplit(str) {
|
|
17
|
-
const s = SimpleRoute._escapeRegExp(SimpleRoute.SPLITTER);
|
|
18
|
-
return (`${str}`
|
|
19
|
-
.trim()
|
|
20
|
-
// splitter trim left and right
|
|
21
|
-
.replace(new RegExp(`^(${s})+`), '')
|
|
22
|
-
.replace(new RegExp(`(${s})+$`), '')
|
|
23
|
-
.split(SimpleRoute.SPLITTER)
|
|
24
|
-
// remove empty segments... will "normalize" multiple splitters into one
|
|
25
|
-
.filter(Boolean));
|
|
26
|
-
}
|
|
27
|
-
static _parse(route) {
|
|
28
|
-
let wasSpread = false;
|
|
29
|
-
let wasWildcard = false;
|
|
30
|
-
return SimpleRoute._sanitizeAndSplit(route).reduce((memo, segment, idx, all) => {
|
|
31
|
-
let name = null;
|
|
32
|
-
// if optional, remove trailing '?' marker
|
|
33
|
-
let isOptional = segment.endsWith('?');
|
|
34
|
-
if (isOptional)
|
|
35
|
-
segment = segment.slice(0, -1);
|
|
36
|
-
let isSpread = segment.startsWith('[...');
|
|
37
|
-
if (isSpread) {
|
|
38
|
-
if (isOptional) {
|
|
39
|
-
throw new Error('Spread segment must not be marked as optional');
|
|
40
|
-
}
|
|
41
|
-
if (wasSpread) {
|
|
42
|
-
throw new Error('Multiple spread segments are invalid');
|
|
43
|
-
}
|
|
44
|
-
wasSpread = true;
|
|
45
|
-
segment = '[' + segment.slice(4);
|
|
46
|
-
}
|
|
47
|
-
let test = new RegExp('^' + SimpleRoute._escapeRegExp(segment) + '$');
|
|
48
|
-
// catch all wildcard
|
|
49
|
-
if (segment === SimpleRoute.WILDCARD) {
|
|
50
|
-
if (idx < all.length - 1) {
|
|
51
|
-
throw new Error(`Wildcard '${SimpleRoute.WILDCARD}' can be used only as a last segment`);
|
|
52
|
-
}
|
|
53
|
-
wasWildcard = true;
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
// starting with at least one word char within brackets...
|
|
57
|
-
let m = segment.match(/^\[(\w.*)]$/);
|
|
58
|
-
if (m) {
|
|
59
|
-
name = m[1];
|
|
60
|
-
test = /.+/;
|
|
61
|
-
// id([0-9]+)
|
|
62
|
-
let m2 = m[1].match(/^(\w.*)\((.+)\)$/);
|
|
63
|
-
if (m2) {
|
|
64
|
-
name = m2[1];
|
|
65
|
-
test = new RegExp('^' + m2[2] + '$');
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (wasWildcard) {
|
|
70
|
-
isOptional = true;
|
|
71
|
-
test = /.*/;
|
|
72
|
-
}
|
|
73
|
-
memo.push({ segment, name, test, isOptional, isSpread });
|
|
74
|
-
return memo;
|
|
75
|
-
}, []);
|
|
76
|
-
}
|
|
77
|
-
static parseQueryString(str) {
|
|
78
|
-
return `${str}`
|
|
79
|
-
.replace(/^&/, '')
|
|
80
|
-
.replace(/&$/, '')
|
|
81
|
-
.split('&')
|
|
82
|
-
.reduce((memo, kvpair) => {
|
|
83
|
-
const [k, v] = kvpair.split('=').map(decodeURIComponent);
|
|
84
|
-
if (k.length)
|
|
85
|
-
memo[k] = v;
|
|
86
|
-
return memo;
|
|
87
|
-
}, {});
|
|
88
|
-
}
|
|
89
|
-
parse(url, allowQueryParams = true) {
|
|
90
|
-
let matched = {};
|
|
91
|
-
const qPos = url.indexOf('?');
|
|
92
|
-
if (allowQueryParams && ~qPos) {
|
|
93
|
-
const _backup = url;
|
|
94
|
-
url = _backup.slice(0, qPos);
|
|
95
|
-
matched = SimpleRoute.parseQueryString(_backup.slice(qPos + 1));
|
|
96
|
-
}
|
|
97
|
-
let segments = SimpleRoute._sanitizeAndSplit(url);
|
|
98
|
-
// SPREAD PARAMS DANCING BLOCK - if there are "spread" definitions we need to adjust input
|
|
99
|
-
// that is "group" (join) segments that were initially splitted
|
|
100
|
-
const hasSpread = !!this._parsed.filter((v) => v.isSpread).length;
|
|
101
|
-
if (hasSpread) {
|
|
102
|
-
let newSegments = [];
|
|
103
|
-
this._parsed.forEach((p, i) => {
|
|
104
|
-
if (p.isSpread) {
|
|
105
|
-
// there are defined segments after the "spread" definition
|
|
106
|
-
if (this._parsed[i + 1]) {
|
|
107
|
-
// SimpleRoute
|
|
108
|
-
newSegments.push(segments.slice(0, this._parsed.length - i).join(SimpleRoute.SPLITTER));
|
|
109
|
-
segments = segments.slice(this._parsed.length - i);
|
|
110
|
-
}
|
|
111
|
-
// there are no more defined segments
|
|
112
|
-
else {
|
|
113
|
-
newSegments.push(segments.join(SimpleRoute.SPLITTER));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
newSegments = newSegments.concat(segments.slice(0, 1));
|
|
118
|
-
segments = segments.slice(1);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
segments = newSegments;
|
|
122
|
-
}
|
|
123
|
-
// return early no-op special case: first segment is a wildcard
|
|
124
|
-
// (no need to process further)
|
|
125
|
-
if (this._parsed?.[0]?.segment === SimpleRoute.WILDCARD) {
|
|
126
|
-
return matched;
|
|
127
|
-
}
|
|
128
|
-
// minimum required (not optional) segments length
|
|
129
|
-
const reqLen = this._parsed.reduce((memo, p, idx, arr) => {
|
|
130
|
-
const next = arr[idx + 1];
|
|
131
|
-
// if is not optional or has not optional still to come
|
|
132
|
-
if (!p.isOptional || (next && !next.isOptional)) {
|
|
133
|
-
memo++;
|
|
134
|
-
}
|
|
135
|
-
return memo;
|
|
136
|
-
}, 0);
|
|
137
|
-
// quick cheap check: if counts dont match = no match
|
|
138
|
-
if (segments.length < reqLen) {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
for (const [i, s] of segments.entries()) {
|
|
142
|
-
const p = this._parsed[i];
|
|
143
|
-
if (!p || !p.test.test(s)) {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
if (p.name) {
|
|
147
|
-
matched[decodeURIComponent(p.name)] = decodeURIComponent(s);
|
|
148
|
-
}
|
|
149
|
-
if (p.segment === SimpleRoute.WILDCARD) {
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return matched;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
class SimpleRouter {
|
|
158
|
-
// console log debug on/off switch
|
|
159
|
-
static debug = false;
|
|
160
|
-
_routes = [];
|
|
161
|
-
_catchAll;
|
|
162
|
-
// current (last matched) route and params (in the shape { route: "...", params: {} } )
|
|
163
|
-
_current = {
|
|
164
|
-
route: null,
|
|
165
|
-
params: null,
|
|
166
|
-
label: null,
|
|
167
|
-
};
|
|
168
|
-
// https://svelte.dev/docs#Store_contract
|
|
169
|
-
_subscriptions = new Set();
|
|
170
|
-
constructor(config = null) {
|
|
171
|
-
Object.entries(config || {}).forEach(([route, cb]) => {
|
|
172
|
-
this.on(route, cb);
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
_dbg(...a) {
|
|
176
|
-
SimpleRouter.debug && console.log('[SimpleRouter]', ...a);
|
|
177
|
-
}
|
|
178
|
-
/** Will report map of registered routes and their labels. Intended for debugging. */
|
|
179
|
-
info() {
|
|
180
|
-
return this._routes.reduce((m, r) => {
|
|
181
|
-
m[r[0].route] = r[3];
|
|
182
|
-
return m;
|
|
183
|
-
}, {});
|
|
184
|
-
}
|
|
185
|
-
reset() {
|
|
186
|
-
this._routes = [];
|
|
187
|
-
return this;
|
|
188
|
-
}
|
|
189
|
-
get current() {
|
|
190
|
-
return this._current;
|
|
191
|
-
}
|
|
192
|
-
on(routes, cb, { label = null, allowQueryParams = true } = {}) {
|
|
193
|
-
if (!Array.isArray(routes))
|
|
194
|
-
routes = [routes];
|
|
195
|
-
routes.forEach((route) => {
|
|
196
|
-
if (route === '*') {
|
|
197
|
-
this._catchAll = cb;
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
this._routes.push([new SimpleRoute(route), cb, allowQueryParams, label]);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
exec(url, fallbackFn) {
|
|
205
|
-
const dbgPrefix = `'${url}' -> `;
|
|
206
|
-
const isFn = (v) => typeof v === 'function';
|
|
207
|
-
for (const [route, cb, allowQueryParams, label] of this._routes) {
|
|
208
|
-
// first match wins
|
|
209
|
-
// parse returns null or params object (which can be empty)
|
|
210
|
-
const params = route.parse(url, allowQueryParams);
|
|
211
|
-
if (params) {
|
|
212
|
-
this._publishCurrent(route.route, params, label);
|
|
213
|
-
this._dbg(`${dbgPrefix}matches '${route.route}' with`, params);
|
|
214
|
-
return isFn(cb) ? cb(params, route.route) : true;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
if (isFn(fallbackFn)) {
|
|
218
|
-
this._publishCurrent(null, null, null);
|
|
219
|
-
this._dbg(`${dbgPrefix}fallback...`);
|
|
220
|
-
return fallbackFn();
|
|
221
|
-
}
|
|
222
|
-
if (isFn(this._catchAll)) {
|
|
223
|
-
this._publishCurrent('*', null, null);
|
|
224
|
-
this._dbg(`${dbgPrefix}catchall...`);
|
|
225
|
-
return this._catchAll(null, '*');
|
|
226
|
-
}
|
|
227
|
-
this._publishCurrent(null, null, null);
|
|
228
|
-
this._dbg(`${dbgPrefix}no match...`);
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
_publishCurrent(route, params, label) {
|
|
232
|
-
this._current = { route, params, label };
|
|
233
|
-
this._subscriptions.forEach((cb) => cb(this._current));
|
|
234
|
-
}
|
|
235
|
-
// https://svelte.dev/docs#Store_contract
|
|
236
|
-
subscribe(subscription) {
|
|
237
|
-
if (typeof subscription !== 'function') {
|
|
238
|
-
throw new TypeError('Subscription is not a function');
|
|
239
|
-
}
|
|
240
|
-
this._subscriptions.add(subscription);
|
|
241
|
-
subscription(this._current);
|
|
242
|
-
// For interoperability with RxJS Observables, the .subscribe method is also
|
|
243
|
-
// allowed to return an object with an .unsubscribe method
|
|
244
|
-
return {
|
|
245
|
-
unsubscribe: () => this._subscriptions.delete(subscription),
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
exports.SimpleRoute = SimpleRoute;
|
|
251
|
-
exports.SimpleRouter = SimpleRouter;
|
package/dist/index.d.ts
DELETED