white-label-router 6.1.1 → 6.2.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 CHANGED
@@ -88,7 +88,18 @@ Public navigation should remain a real link:
88
88
  <a href="/products/42" data-pushstate>View product 42</a>
89
89
  ```
90
90
 
91
- `data-pushstate` opts the link into client-side enhancement. Links without it retain native behavior. Modified clicks, non-left clicks, downloads, alternate targets, and cross-origin URLs also remain native.
91
+ `data-pushstate` opts the link into client-side enhancement. Router intercepts a click on an `<a href data-pushstate>` (HTML or SVG; Router reads the `href` attribute, resolved against `document.baseURI`) only when it can actually handle it. These clicks stay native:
92
+
93
+ - links without `data-pushstate`, or clicks whose default action was already prevented;
94
+ - non-left clicks and clicks with Meta, Ctrl, Shift, or Alt held;
95
+ - links with `download`, or a `target` other than `_self`;
96
+ - links to a `#fragment` of the page already shown (same path and query), which the browser handles as in-page fragment navigation; the resulting `popstate` follows the [hash-only rule](#browser-history-behavior);
97
+ - cross-origin or unparseable URLs;
98
+ - URLs that match no route, or only the placeholder `defaultRoute` the constructor installs (a `defaultRoute` you configure yourself does intercept unmatched links);
99
+ - routes with no runnable view behavior;
100
+ - links whose guard returns anything but `true`, or throws (the guard's error still propagates).
101
+
102
+ Once a route has been committed, the click's default action is cancelled even if the route's view throws, so the browser does not navigate a second time.
92
103
 
93
104
  This preserves keyboard behavior, context menus, no-JavaScript navigation, and crawler discovery. Public routes should have directly requestable URLs that return meaningful HTML and appropriate title, description, canonical, and robots metadata.
94
105
 
@@ -145,13 +156,34 @@ router.routes = {
145
156
  };
146
157
  ```
147
158
 
148
- `secure()` must return exactly `true` to allow navigation. Client guards are interface behavior, not server authorization. Router validates the destination guard and runnable view behavior before tearing down the current route. On a successful route change, the previous route's `destroy()` runs before the next route's `initialize()`.
159
+ ### Guards
160
+
161
+ `secure(scope, location)` runs synchronously with the destination location, before anything is torn down. It must return exactly `true` to allow navigation; any other value (including a Promise) rejects it and `navigate()` returns `false`. An error thrown by a guard propagates to the caller and leaves Router state unchanged. Client guards are interface behavior, not server authorization.
162
+
163
+ ### Lifecycle order
164
+
165
+ A successful navigation runs these steps in order:
166
+
167
+ 1. Match the route and run its guard. Rejection stops here with no state change.
168
+ 2. Commit `router.url`, `router.route`, and `router.locationData` for the destination.
169
+ 3. Call the previous route's `destroy(scope, nextLocation)`, if it had one.
170
+ 4. Run the new route: a function route, a `view` function, or `view.initialize(scope, location)`.
171
+ 5. Apply the route's title and focus (browser only; see [Title and focus](#title-and-focus)).
172
+ 6. Set `router.previousRoute`, then push a history entry (browser only; skipped for `initialize()` and back/forward).
173
+
174
+ Router keeps the `destroy` callback of the lifecycle that actually initialized, so replacing or mutating the route table later does not change which teardown runs. That lifecycle stays owned even if `initialize()` throws, or title/focus work throws after it: the route remains committed, the error is rethrown to the caller, and its `destroy()` still runs before the next successful route starts. Title, focus, and history are skipped for a route whose `initialize()` threw.
175
+
176
+ If the previous route's `destroy()` throws, its error propagates out of `navigate()` and that teardown is never called again: Router releases it before calling it. The destination stays committed (`router.url`, `router.route`, `router.locationData`), but its view does not run, so it owns no `destroy()`, and title, focus, `previousRoute`, and history are skipped. The next navigation therefore starts without a teardown; `navigate()` with no URL re-runs the committed destination to initialize it.
177
+
178
+ ### Navigating from inside a route
179
+
180
+ `navigate()` is re-entrant. If `initialize()`, `destroy()`, a focus handler, or a mediator event starts another navigation, the newer navigation wins: the outer one stops at that point, applies no title, focus, or history of its own, and returns the router. For example, navigating to `/two` from inside `/one`'s `initialize()` runs `/one`'s `destroy()` and then `/two`'s `initialize()`, and leaves `/two` as the active route.
149
181
 
150
182
  ## Matching
151
183
 
152
184
  Routes match complete path boundaries. `/products` matches `/products` and `/products/42`, but not `/products-old`.
153
185
 
154
- When multiple route prefixes match, Router selects the **longest matching route**, so specific routes win without depending on object insertion order. Only own route-table properties participate in matching:
186
+ When multiple route prefixes match, Router selects the **longest matching route**, so specific routes win without depending on object insertion order. Only own route-table properties participate in matching. If nothing matches and no own `defaultRoute` is configured, navigation is rejected; in browser click interception the real link remains native.
155
187
 
156
188
  ```js
157
189
  router.routes = {
@@ -163,6 +195,10 @@ router.routes = {
163
195
 
164
196
  `/products/sale` selects `saleRoute` even though `/products` was defined first. When no prefix matches, `defaultRoute` runs if configured.
165
197
 
198
+ Route keys are encoded the same way URL parsing encodes paths, so `'/café'` matches a request for `/caf%C3%A9`. Keys that don't start with `/`, or that contain `?` or `#`, can never match a pathname and are ignored (apart from `defaultRoute`). If two keys normalize to the same path (for example `/x/../same` and `/same`), the key already in normalized form wins, otherwise the alphabetically lowest; insertion order never decides.
199
+
200
+ A new `Router` starts with a placeholder `defaultRoute` that does nothing, so programmatic navigation to an unmatched URL succeeds until you replace `routes`. That placeholder never intercepts link clicks.
201
+
166
202
  ## Location contract
167
203
 
168
204
  Routes receive the configured `scope` and a location object:
@@ -178,7 +214,12 @@ Routes receive the configured `scope` and a location object:
178
214
  }
179
215
  ```
180
216
 
181
- `data.url` contains path segments after the matched prefix, `data.query` contains parsed query values, and `data.mediator` contains navigation data supplied programmatically or through `CustomEvent.detail`. Values are URI-decoded with standard WHATWG URL APIs; applications must still validate untrusted values.
217
+ - `url` is the path, query, and hash (`/products/42?color=blue#reviews`), never a full absolute URL. This holds on the server too: `initialize('https://example.com/products/42?color=blue')` produces `url: '/products/42?color=blue'`. `router.url` holds the same value.
218
+ - `data.url` contains the URI-decoded path segments after the matched route prefix (all segments for `defaultRoute`). A segment with malformed percent-encoding is passed through undecoded.
219
+ - `data.query` contains query values decoded with `URLSearchParams`; for duplicate keys the last value wins.
220
+ - `data.mediator` contains navigation data passed to `navigate()` or through `CustomEvent.detail`. It is `{}` for link clicks, `initialize()`, and back/forward, and `undefined` when `navigate()` is called without data.
221
+
222
+ Applications must still validate these untrusted values.
182
223
 
183
224
  ## Programmatic navigation
184
225
 
@@ -190,9 +231,9 @@ const result = router.navigate('/products/42', {
190
231
 
191
232
  In browsers, successful navigation updates history unless the call represents `popstate`. On servers it dispatches without History API effects.
192
233
 
193
- Rejected navigation is atomic from Router's perspective: malformed or wrong-type URL candidates, cross-origin browser URLs, failed guards, and routes without runnable view behavior return `false` without destroying the current route, pushing browser history, or replacing the last successful URL, selected route, or location payload.
234
+ Rejected navigation is atomic from Router's perspective: malformed or wrong-type URL candidates, cross-origin browser URLs, URLs with no matching route/default route, failed guards, and routes without runnable view behavior return `false` without destroying the current route, pushing browser history, or replacing the last successful URL, selected route, or location payload.
194
235
 
195
- Calling `navigate()` without a URL dispatches the current browser URL. In a non-browser runtime the safe default is `/`; server applications should normally supply a fully qualified request URL explicitly.
236
+ Calling `navigate()` without a URL re-runs Router's last committed URL. In a browser, that re-run adds a history entry only when the address bar does not already show that URL, so it never duplicates the current entry. Before anything has been committed it uses the current browser URL, or `/` on a server; server applications should normally supply a fully qualified request URL explicitly. Errors thrown by a guard or a route callback propagate out of `navigate()`.
196
237
 
197
238
  ## Mediator integration
198
239
 
@@ -214,7 +255,7 @@ mediator.dispatchEvent(new CustomEvent('router:navigate', {
214
255
  }));
215
256
  ```
216
257
 
217
- The `CustomEvent.detail` value becomes `location.data.mediator`. `destroy()` removes the listener owned by Router. Repeated listener initialization is idempotent. Reassigning `router.mediator` while listeners are active moves the `router:navigate` subscription to the replacement.
258
+ The `CustomEvent.detail` object becomes `location.data.mediator`, and its `url` (if any) is the destination. A detail without `url`, or a `null`/missing detail, re-runs the current URL without adding a duplicate history entry; a detail that is not a plain object (a string, number, or array) is ignored. Mediator navigations to a `url` push browser history like `navigate()`. `destroy()` removes the listener owned by Router. Repeated listener initialization is idempotent. Reassigning `router.mediator` while listeners are active moves the `router:navigate` subscription to the replacement.
218
259
 
219
260
  Router does not import or require `white-label-mediator`; the integration is structural so another compatible EventTarget can be used instead.
220
261
 
@@ -259,12 +300,14 @@ router.routes = {
259
300
  };
260
301
  ```
261
302
 
262
- The default focus selector is `main h1`. Set `focus: false` for an in-page refinement that should preserve current focus. Server runtimes retain `pageTitle` as route state but do not mutate a document or focus target.
303
+ After a route renders, Router sets `document.title` from `title` and focuses the first element matching `focus` (default `main h1`), adding `tabindex="-1"` if the element has no `tabindex`. Set `focus: false` for an in-page refinement that should preserve current focus. The first dispatch from `initialize()` sets the title but does not move focus, because the browser already owns focus on a freshly loaded page; later navigations do. Server runtimes retain `pageTitle` as route state but do not mutate a document or focus target.
263
304
 
264
305
  ## Browser history behavior
265
306
 
266
307
  Initialization dispatches the current URL without adding a duplicate history entry. Back/forward navigation reads `window.location`—including path, query, and hash—even when history state is null or belongs to another application. History state is not treated as the authoritative URL. Rejected navigation does not add a history entry.
267
308
 
309
+ When back/forward, or a click on a link to a `#fragment` of the current page, changes only the hash (footnotes, skip links), Router updates `router.url` and `locationData.url` but does not re-run the route, call `destroy()`, or move focus. A change to the path or query re-runs the route as usual.
310
+
268
311
  ## Public API
269
312
 
270
313
  | Member | Behavior | Returns |
@@ -277,12 +320,14 @@ Initialization dispatches the current URL without adding a duplicate history ent
277
320
  | `navigate(url?, data?, isPopState?)` | Match and run a route; update browser history when appropriate. Invalid/malformed URL candidates and other rejected navigation preserve current route state. | The same Router on success; `false` for rejected navigation. |
278
321
  | `addListeners()` | Attach browser and optional mediator listeners once. | The same `Router` instance. |
279
322
  | `removeListeners()` | Release listeners owned by this router. | The same `Router` instance. |
280
- | `destroy()` | Release routing listeners. | The same `Router` instance after cleanup. |
323
+ | `destroy()` | Release routing listeners. Does not call the active route's `destroy()`. | The same `Router` instance after cleanup. |
281
324
  | `parseQueryString(query)` | Decode a query string using `URLSearchParams`. | A plain object containing the last value for each query key. |
282
325
  | `setLocationData(data?)` | Rebuild the current route location payload. | `undefined`; updates `locationData` in place. |
283
326
  | `applyPageContext(route)` | Update route title/focus context when applicable. | The same `Router` instance. |
284
- | `eventPushStateClick(event)` | Handle an eligible browser push-state click. | The same Router after handled navigation; `true` when native/default handling should continue. |
285
- | `eventPopState()` | Dispatch the current browser history location. | The same `Router` instance. |
327
+ | `eventPushStateClick(event)` | Handle an eligible browser push-state click; same-page `#fragment` links stay native (see [Progressive enhancement first](#progressive-enhancement-first)). | The same Router after handled navigation; `true` when native/default handling should continue. |
328
+ | `eventPopState()` | Dispatch the current browser history location; a hash-only change just updates `url`. | The same `Router` instance. |
329
+
330
+ Router records navigation state on public properties that applications should treat as read-only: `url`, `route`, `previousRoute`, `locationData`, and `pageTitle`.
286
331
 
287
332
  ## TypeScript
288
333
 
@@ -304,17 +349,19 @@ router.initialize();
304
349
 
305
350
  ```sh
306
351
  npm ci --ignore-scripts
307
- npm run lint
308
- npm run typecheck
309
- npm test
310
- npm run coverage
311
- npm run audit
312
- npm pack --dry-run
352
+ npm run lint && npm run coverage && npm run audit
313
353
  ```
314
354
 
315
- Tests cover browser behavior, scoped progressive navigation, DOM-free server routing, atomic rejected navigation, mediator reassignment, and parity of the shared route contract. Coverage enforces 100% statements, branches, functions, and lines per implementation file. CI verifies the documented Node minimum and primary Node line, audits dependencies, packs the package, and verifies the public API across npm, Yarn, and pnpm.
355
+ `npm run coverage` builds (which typechecks), runs the consumer type checks, and runs every Node test, so `npm run typecheck` and `npm test` don't need separate runs. For a quick loop while iterating, use `npm run build && node --test test/<file>.test.js`. `npm run lint` covers `src/`, `test/`, and `benchmark/`. `dist/` is generated and not committed; `npm run build` (also run by `prepare` for git installs) creates it.
356
+
357
+ After `npm run build`:
358
+
359
+ - `npm run test:browser` runs `test/browser/` in real Chromium through Playwright (real link clicks, back/forward, fragments, focus, `navigationRoot`) against pages served by a local `node:http` server. It is not part of `npm test`. Install a browser once with `npx playwright install chromium`, or point `PLAYWRIGHT_CHROMIUM_EXECUTABLE` at an existing Chromium.
360
+ - `npm run benchmark` times `matchRoute`, `navigate`, and link clicks with 10 and 1,000 routes. `npm run bench:ci` is a short version that fails if any operation is more than 3x slower than `benchmark/baseline.json` or a deterministic counter differs. Refresh the baseline after an intended performance change with `node benchmark/router.bench.js --ci --update-baseline` and commit the result.
361
+
362
+ Tests cover browser behavior, scoped progressive navigation, DOM-free server routing, atomic rejected navigation, mediator reassignment, and parity of the shared route contract. Coverage enforces 100% statements, branches, functions, and lines per implementation file. CI verifies the documented Node minimum and primary Node line, runs the short benchmark and the Chromium browser tests, audits dependencies, packs the package, and verifies the public API across npm, Yarn, and pnpm.
316
363
 
317
- Edit `src/*.ts` and regenerate `dist`; do not edit generated files directly.
364
+ Edit `src/index.ts`; `dist` is build output and is not edited or committed. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
318
365
 
319
366
  ## Design boundary
320
367
 
package/dist/index.d.ts CHANGED
@@ -1,9 +1,13 @@
1
- /** Navigation payload, including caller-defined metadata. */
1
+ /** Extra data passed with a navigation. `url`, if present, is the destination; any other fields are yours. */
2
2
  interface NavigationData {
3
3
  url?: string;
4
4
  [key: string]: unknown;
5
5
  }
6
- /** URL-derived values supplied to route handlers. */
6
+ /**
7
+ * What a route callback receives about the destination.
8
+ * `url` is the path, query, and hash; `data.url` holds the decoded path segments after the matched route key;
9
+ * `data.query` holds decoded query values; `data.mediator` is the navigation data, if any.
10
+ */
7
11
  interface LocationData {
8
12
  url: string;
9
13
  data: {
@@ -12,24 +16,33 @@ interface LocationData {
12
16
  query: Record<string, string>;
13
17
  };
14
18
  }
19
+ /** A route callback. It receives `router.scope` and the destination location. */
15
20
  type RouteHandler = (scope: Element | null, location: LocationData) => unknown;
16
- /** An object route can own a function or a view lifecycle. */
21
+ /** A view with setup and teardown. `destroy` receives the location being navigated to. */
22
+ interface RouteViewLifecycle {
23
+ initialize?: RouteHandler;
24
+ destroy?: RouteHandler;
25
+ }
26
+ /**
27
+ * A route with optional page context and a guard.
28
+ * `secure` must return exactly `true` to allow navigation; `focus: false` leaves focus where it is.
29
+ */
17
30
  interface RouteObject {
18
31
  title?: string;
19
32
  focus?: string | false;
20
33
  secure?: RouteHandler;
21
- view?: RouteHandler | {
22
- initialize?: RouteHandler;
23
- destroy?: RouteHandler;
24
- };
34
+ view?: RouteHandler | RouteViewLifecycle;
25
35
  }
26
36
  type Route = RouteHandler | RouteObject;
27
- /** Minimal standards-based event target contract required by the router. */
37
+ /** Any EventTarget-like object; Router only adds and removes a `router:navigate` listener on it. */
28
38
  interface NavigationMediator {
29
39
  addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
30
40
  removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
31
41
  }
32
- /** History API router that also dispatches request URLs in non-browser runtimes. */
42
+ /**
43
+ * Maps URLs to route callbacks. In a browser it enhances `data-pushstate` links and back/forward with the History API;
44
+ * on a server it dispatches the request URL you pass, with no DOM required.
45
+ */
33
46
  declare class Router {
34
47
  routes: Record<string, Route>;
35
48
  url: string;
@@ -46,55 +59,114 @@ declare class Router {
46
59
  private boundMediator;
47
60
  private currentNavigationRoot;
48
61
  private boundNavigationRoot;
49
- /** Create an instance with its own state and listener references. */
62
+ private activeDestroy;
63
+ private compiledRoutes;
64
+ private navigationToken;
65
+ private initialDispatch;
66
+ private suppressFocus;
67
+ /** Create a router with a placeholder `defaultRoute` and no listeners attached yet. */
50
68
  constructor();
69
+ /** Event target that can send `router:navigate` events, or `false` for none. */
51
70
  get mediator(): NavigationMediator | false;
52
- /** Move an active router:navigate subscription when the mediator changes. */
71
+ /** Set the mediator. If listeners are active, the `router:navigate` listener moves from the old one to the new one. */
53
72
  set mediator(value: NavigationMediator | false);
73
+ /** Element or document whose `data-pushstate` link clicks this router handles; `null` means the whole document. */
54
74
  get navigationRoot(): Document | Element | null;
55
- /** Scope progressive link interception and move an active click listener when the root changes. */
75
+ /** Set the click root. If listeners are active, the click listener moves to the new root. */
56
76
  set navigationRoot(value: Document | Element | null);
57
- /** True when browser navigation APIs are available. */
77
+ /** True when `window` and `document` exist, i.e. browser-only effects should run. */
58
78
  private isBrowserRuntime;
59
- /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */
79
+ /** Origin used as the base for URL parsing: the page's origin in a browser, a dummy one on a server. */
60
80
  private getOrigin;
61
- /** Read the authoritative browser URL, or retain the supplied server URL. */
81
+ /** Current path, query, and hash: from `window.location` in a browser, or the last committed URL (default `/`) on a server. */
62
82
  private getCurrentUrl;
63
- /** Parse a navigation candidate without changing Router state. */
83
+ /** Parse a URL relative to the current page (or the dummy server origin). Returns `null` if it can't be parsed. */
64
84
  private parseNavigationUrl;
65
- /** Normalize browser navigation to a same-origin path, query, and hash. */
85
+ /** In a browser, reduce a URL to its path, query, and hash, or `null` if it is cross-origin or invalid. */
66
86
  private normalizeBrowserUrl;
67
- /** Return whether a selected route has runnable view behavior. */
87
+ /** True if the route has something to run: a function, a `view` function, or a `view.initialize`. */
68
88
  private isRunnableRoute;
69
- /** Build decoded path, query, and mediator data for a navigation candidate. */
89
+ /** Build (or reuse) the lookup tables for the current route keys. Reused while the own key list is unchanged. */
90
+ private compileRoutes;
91
+ /** Find the route key with the longest matching path prefix, else `'defaultRoute'` if one is set, else `null`. */
92
+ private matchRoute;
93
+ /** Length of the normalized path for a route key, used to slice the matched prefix off `data.url`; 0 for `defaultRoute`. */
94
+ private routePrefixLength;
95
+ /** Parse a URL Router committed earlier. A path is prefixed with the origin first, so '//x' stays a path, not a host. */
96
+ private parseStoredUrl;
97
+ /** Build the location object passed to route callbacks. Path segments that fail to decode are kept as-is. */
70
98
  private buildLocationData;
71
99
  /**
72
- * Start this instance and return it for lifecycle chaining.
73
- * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.
100
+ * Attach listeners and dispatch the starting URL. Does not add a history entry or move focus.
101
+ * @param url - Request URL on a server. In a browser, omit it to use `window.location`.
102
+ * @returns This router.
103
+ * @throws Whatever a guard or route callback throws.
74
104
  */
75
105
  initialize(url?: string): this;
76
- /** Register browser and optional mediator handlers once. */
106
+ /**
107
+ * Attach the click and popstate listeners (browser only) and the mediator listener. Safe to call more than once.
108
+ * @returns This router.
109
+ */
77
110
  addListeners(): this;
78
- /** Release owned state and listeners so the instance can leave the application lifecycle. */
111
+ /**
112
+ * Remove this router's listeners. Does not call the active route's `destroy()`.
113
+ * @returns This router.
114
+ */
79
115
  destroy(): this;
80
- /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */
116
+ /**
117
+ * Remove the listeners `addListeners()` attached. Subclasses can override this to release more.
118
+ * @returns This router.
119
+ */
81
120
  removeListeners(): this;
82
- /** Intercept eligible same-origin push-state links while preserving normal browser actions. */
121
+ /**
122
+ * Click handler for `<a href data-pushstate>` links. Takes over only when Router can handle the link:
123
+ * a plain left click on a same-origin link (no `download`, no other `target`) whose route is app-defined,
124
+ * runnable, and allowed by its guard, and that is not just a `#fragment` of the current page. Anything else is left
125
+ * to the browser.
126
+ * @param e - The click event.
127
+ * @returns This router if it handled the click, otherwise `true`.
128
+ * @throws Whatever a guard or route callback throws.
129
+ */
83
130
  eventPushStateClick(e: MouseEvent): true | this;
84
- /** Restore browser history state without adding a new history entry. */
131
+ /**
132
+ * Popstate handler for back/forward. Runs the route for the new URL without adding a history entry.
133
+ * A hash-only change just updates `url` and `locationData.url`.
134
+ * @returns This router.
135
+ */
85
136
  eventPopState(): this;
86
- /** Decode URL query parameters, keeping the last value for duplicate keys. */
137
+ /**
138
+ * Decode a query string into a plain object. For repeated keys the last value wins.
139
+ * @param queryString - Query string, with or without the leading `?`.
140
+ */
87
141
  parseQueryString(queryString: string): {
88
142
  [k: string]: string;
89
143
  };
90
- /** Build decoded path, query, and mediator data for the selected route. */
144
+ /**
145
+ * Rebuild `locationData` from the current `url` and `route`. Leaves it unchanged if the URL can't be parsed.
146
+ * @param mediatorData - Value to expose as `locationData.data.mediator`.
147
+ */
91
148
  setLocationData(mediatorData?: NavigationData): void;
92
- /** Update page context after a route renders. DOM effects are browser-only. */
149
+ /**
150
+ * Record the route's title in `pageTitle` and, in a browser, set `document.title` and move focus to the route's
151
+ * `focus` selector (default `main h1`), adding `tabindex="-1"` if needed. `focus: false` skips focus.
152
+ * @param route - The route that just ran.
153
+ * @returns This router.
154
+ */
93
155
  applyPageContext(route: Route): this;
94
- /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */
156
+ /**
157
+ * Navigate to a URL: match the route, run its guard, commit the new state, call the previous route's `destroy()`,
158
+ * run the new route, apply title and focus, then push a history entry (browser only, unless `isPopState`).
159
+ * If route code starts another navigation part-way through, that one wins and this call stops early.
160
+ * @param url - Destination URL. Omit to re-run the last committed URL (or the current browser URL / `/` at first).
161
+ * @param mediatorData - Value exposed to the route as `location.data.mediator`.
162
+ * @param isPopState - `true` to skip adding a history entry.
163
+ * @returns This router, or `false` if the navigation was rejected (bad or cross-origin URL, no matching route,
164
+ * guard refused, or nothing to run). A rejected navigation changes nothing.
165
+ * @throws Whatever a guard or route callback throws. A throwing guard changes nothing; a throwing route stays committed.
166
+ */
95
167
  navigate(url?: string, mediatorData?: NavigationData, isPopState?: boolean): false | this;
96
168
  }
97
- /** Public route configuration and navigation payload types. */
169
+ /** Types for route definitions, location data, and navigation data. */
98
170
  declare namespace Router {
99
171
  type Handler = RouteHandler;
100
172
  type Route = RouteHandler | RouteObject;
package/dist/index.js CHANGED
@@ -1,5 +1,25 @@
1
1
  "use strict";
2
- /** History API router that also dispatches request URLs in non-browser runtimes. */
2
+ /** The do-nothing `defaultRoute` objects each constructor installs, so link clicks can tell them apart from app routes. */
3
+ const builtInDefaultRoutes = new WeakSet();
4
+ /** A dummy origin that lets route keys and server URLs go through the standard URL parser. */
5
+ const routeKeyBase = 'http://white-label.invalid';
6
+ /** Encode a route key the way the URL parser encodes paths, so `'/café'` matches `/caf%C3%A9`. */
7
+ function normalizeRouteKey(key) {
8
+ // A key that doesn't start with '/' or contains '?' or '#' can never equal a pathname, so leave it as is (it won't match).
9
+ if (!key.startsWith('/') || /[?#]/.test(key)) {
10
+ return key;
11
+ }
12
+ return new URL(`${routeKeyBase}${key}`).pathname;
13
+ }
14
+ /** Strip the `#fragment`, leaving the path and query. */
15
+ function withoutHash(url) {
16
+ const index = url.indexOf('#');
17
+ return index === -1 ? url : url.slice(0, index);
18
+ }
19
+ /**
20
+ * Maps URLs to route callbacks. In a browser it enhances `data-pushstate` links and back/forward with the History API;
21
+ * on a server it dispatches the request URL you pass, with no DOM required.
22
+ */
3
23
  class Router {
4
24
  routes;
5
25
  url;
@@ -16,16 +36,25 @@ class Router {
16
36
  boundMediator = false;
17
37
  currentNavigationRoot = null;
18
38
  boundNavigationRoot = null;
19
- /** Create an instance with its own state and listener references. */
39
+ activeDestroy;
40
+ compiledRoutes;
41
+ // Bumped by each navigation that passes its guard. If it changes while a navigation is calling out to app code,
42
+ // a newer navigation has started, and the older one stops before touching title, focus, or history.
43
+ navigationToken = 0;
44
+ // True only for the first navigate() call made by initialize(), so that dispatch leaves focus alone.
45
+ initialDispatch = false;
46
+ // Tells applyPageContext() to skip focus for the current call.
47
+ suppressFocus = false;
48
+ /** Create a router with a placeholder `defaultRoute` and no listeners attached yet. */
20
49
  constructor() {
21
- this.routes = {
22
- defaultRoute: {
23
- view: {
24
- initialize: () => true,
25
- destroy: () => true
26
- }
50
+ const defaultRoute = {
51
+ view: {
52
+ initialize: () => true,
53
+ destroy: () => true
27
54
  }
28
55
  };
56
+ builtInDefaultRoutes.add(defaultRoute);
57
+ this.routes = { defaultRoute };
29
58
  this.url = '';
30
59
  this.route = null;
31
60
  this.scope = null;
@@ -47,8 +76,9 @@ class Router {
47
76
  };
48
77
  this.listenersInitialized = false;
49
78
  }
79
+ /** Event target that can send `router:navigate` events, or `false` for none. */
50
80
  get mediator() { return this.currentMediator; }
51
- /** Move an active router:navigate subscription when the mediator changes. */
81
+ /** Set the mediator. If listeners are active, the `router:navigate` listener moves from the old one to the new one. */
52
82
  set mediator(value) {
53
83
  if (value === this.currentMediator) {
54
84
  return;
@@ -63,8 +93,9 @@ class Router {
63
93
  this.boundMediator = value;
64
94
  }
65
95
  }
96
+ /** Element or document whose `data-pushstate` link clicks this router handles; `null` means the whole document. */
66
97
  get navigationRoot() { return this.currentNavigationRoot; }
67
- /** Scope progressive link interception and move an active click listener when the root changes. */
98
+ /** Set the click root. If listeners are active, the click listener moves to the new root. */
68
99
  set navigationRoot(value) {
69
100
  if (value === this.currentNavigationRoot) {
70
101
  return;
@@ -79,22 +110,22 @@ class Router {
79
110
  }
80
111
  this.currentNavigationRoot = value;
81
112
  }
82
- /** True when browser navigation APIs are available. */
113
+ /** True when `window` and `document` exist, i.e. browser-only effects should run. */
83
114
  isBrowserRuntime() {
84
115
  return typeof window !== 'undefined' && typeof document !== 'undefined';
85
116
  }
86
- /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */
117
+ /** Origin used as the base for URL parsing: the page's origin in a browser, a dummy one on a server. */
87
118
  getOrigin() {
88
119
  return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';
89
120
  }
90
- /** Read the authoritative browser URL, or retain the supplied server URL. */
121
+ /** Current path, query, and hash: from `window.location` in a browser, or the last committed URL (default `/`) on a server. */
91
122
  getCurrentUrl() {
92
123
  if (!this.isBrowserRuntime()) {
93
124
  return this.url || '/';
94
125
  }
95
126
  return window.location.pathname + window.location.search + (window.location.hash || '');
96
127
  }
97
- /** Parse a navigation candidate without changing Router state. */
128
+ /** Parse a URL relative to the current page (or the dummy server origin). Returns `null` if it can't be parsed. */
98
129
  parseNavigationUrl(url) {
99
130
  try {
100
131
  const base = this.isBrowserRuntime() ? window.location.href || `${window.location.origin}/` : this.getOrigin();
@@ -104,7 +135,7 @@ class Router {
104
135
  return null;
105
136
  }
106
137
  }
107
- /** Normalize browser navigation to a same-origin path, query, and hash. */
138
+ /** In a browser, reduce a URL to its path, query, and hash, or `null` if it is cross-origin or invalid. */
108
139
  normalizeBrowserUrl(url) {
109
140
  if (!this.isBrowserRuntime()) {
110
141
  return url;
@@ -115,19 +146,81 @@ class Router {
115
146
  }
116
147
  return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
117
148
  }
118
- /** Return whether a selected route has runnable view behavior. */
149
+ /** True if the route has something to run: a function, a `view` function, or a `view.initialize`. */
119
150
  isRunnableRoute(route) {
120
151
  return typeof route === 'function' || typeof route.view === 'function' ||
121
152
  Boolean(route.view && typeof route.view.initialize === 'function');
122
153
  }
123
- /** Build decoded path, query, and mediator data for a navigation candidate. */
154
+ /** Build (or reuse) the lookup tables for the current route keys. Reused while the own key list is unchanged. */
155
+ compileRoutes() {
156
+ const keys = Object.keys(this.routes);
157
+ const cached = this.compiledRoutes;
158
+ if (cached && cached.keys.length === keys.length && cached.keys.every((key, index) => key === keys[index])) {
159
+ return cached;
160
+ }
161
+ const prefixes = new Map();
162
+ const prefixFor = new Map();
163
+ for (const key of keys) {
164
+ if (key === 'defaultRoute') {
165
+ continue;
166
+ }
167
+ const prefix = normalizeRouteKey(key);
168
+ prefixFor.set(key, prefix);
169
+ const existing = prefixes.get(prefix);
170
+ // When two keys normalize to the same path, prefer the one already in normalized form, then the alphabetically
171
+ // lowest, so the winner never depends on insertion order.
172
+ if (existing === undefined || (existing !== prefix && (key === prefix || key < existing))) {
173
+ prefixes.set(prefix, key);
174
+ }
175
+ }
176
+ this.compiledRoutes = { keys, prefixes, prefixFor };
177
+ return this.compiledRoutes;
178
+ }
179
+ /** Find the route key with the longest matching path prefix, else `'defaultRoute'` if one is set, else `null`. */
180
+ matchRoute(pathname) {
181
+ const { prefixes } = this.compileRoutes();
182
+ // Try the full path, then drop one '/segment' at a time; the first hit is the longest match.
183
+ // Cutting only at '/' means '/products' matches '/products/42' but not '/products-old'.
184
+ let candidateRoute = prefixes.get(pathname);
185
+ let index = pathname.lastIndexOf('/');
186
+ while (candidateRoute === undefined && index >= 0) {
187
+ candidateRoute = prefixes.get(pathname.slice(0, index));
188
+ index = index > 0 ? pathname.lastIndexOf('/', index - 1) : -1;
189
+ }
190
+ if (candidateRoute !== undefined) {
191
+ return candidateRoute;
192
+ }
193
+ if (Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {
194
+ return 'defaultRoute';
195
+ }
196
+ return null;
197
+ }
198
+ /** Length of the normalized path for a route key, used to slice the matched prefix off `data.url`; 0 for `defaultRoute`. */
199
+ routePrefixLength(route) {
200
+ if (!route || route === 'defaultRoute') {
201
+ return 0;
202
+ }
203
+ return (this.compileRoutes().prefixFor.get(route) ?? normalizeRouteKey(route)).length;
204
+ }
205
+ /** Parse a URL Router committed earlier. A path is prefixed with the origin first, so '//x' stays a path, not a host. */
206
+ parseStoredUrl(url) {
207
+ if (!url.startsWith('/')) {
208
+ return this.parseNavigationUrl(url);
209
+ }
210
+ try {
211
+ return new URL(`${this.getOrigin()}${url}`);
212
+ }
213
+ catch (_error) {
214
+ return null;
215
+ }
216
+ }
217
+ /** Build the location object passed to route callbacks. Path segments that fail to decode are kept as-is. */
124
218
  buildLocationData(url, route, mediatorData, parsedUrl) {
125
- const matchedRoute = route && route !== 'defaultRoute' ? route : '';
126
219
  return {
127
220
  url,
128
221
  data: {
129
222
  url: parsedUrl.pathname
130
- .slice(matchedRoute.length)
223
+ .slice(this.routePrefixLength(route))
131
224
  .split('/')
132
225
  .filter(Boolean)
133
226
  .map((fragment) => {
@@ -144,15 +237,27 @@ class Router {
144
237
  };
145
238
  }
146
239
  /**
147
- * Start this instance and return it for lifecycle chaining.
148
- * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.
240
+ * Attach listeners and dispatch the starting URL. Does not add a history entry or move focus.
241
+ * @param url - Request URL on a server. In a browser, omit it to use `window.location`.
242
+ * @returns This router.
243
+ * @throws Whatever a guard or route callback throws.
149
244
  */
150
245
  initialize(url) {
151
246
  this.addListeners();
152
- this.navigate(url ?? this.getCurrentUrl(), {}, true);
247
+ // The browser has already placed focus on the page it just loaded, so only later navigations move it.
248
+ this.initialDispatch = true;
249
+ try {
250
+ this.navigate(url ?? this.getCurrentUrl(), {}, true);
251
+ }
252
+ finally {
253
+ this.initialDispatch = false;
254
+ }
153
255
  return this;
154
256
  }
155
- /** Register browser and optional mediator handlers once. */
257
+ /**
258
+ * Attach the click and popstate listeners (browser only) and the mediator listener. Safe to call more than once.
259
+ * @returns This router.
260
+ */
156
261
  addListeners() {
157
262
  if (this.listenersInitialized) {
158
263
  return this;
@@ -170,12 +275,18 @@ class Router {
170
275
  this.listenersInitialized = true;
171
276
  return this;
172
277
  }
173
- /** Release owned state and listeners so the instance can leave the application lifecycle. */
278
+ /**
279
+ * Remove this router's listeners. Does not call the active route's `destroy()`.
280
+ * @returns This router.
281
+ */
174
282
  destroy() {
175
283
  this.removeListeners();
176
284
  return this;
177
285
  }
178
- /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */
286
+ /**
287
+ * Remove the listeners `addListeners()` attached. Subclasses can override this to release more.
288
+ * @returns This router.
289
+ */
179
290
  removeListeners() {
180
291
  if (!this.listenersInitialized) {
181
292
  return this;
@@ -192,7 +303,15 @@ class Router {
192
303
  this.listenersInitialized = false;
193
304
  return this;
194
305
  }
195
- /** Intercept eligible same-origin push-state links while preserving normal browser actions. */
306
+ /**
307
+ * Click handler for `<a href data-pushstate>` links. Takes over only when Router can handle the link:
308
+ * a plain left click on a same-origin link (no `download`, no other `target`) whose route is app-defined,
309
+ * runnable, and allowed by its guard, and that is not just a `#fragment` of the current page. Anything else is left
310
+ * to the browser.
311
+ * @param e - The click event.
312
+ * @returns This router if it handled the click, otherwise `true`.
313
+ * @throws Whatever a guard or route callback throws.
314
+ */
196
315
  eventPushStateClick(e) {
197
316
  if (typeof window === 'undefined') {
198
317
  return true;
@@ -210,44 +329,89 @@ class Router {
210
329
  if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {
211
330
  return true;
212
331
  }
213
- const href = anchor.href || anchor.getAttribute('href') || '';
332
+ // Read the attribute, because on an SVG <a> the href property is an SVGAnimatedString, not a URL.
333
+ const href = anchor.getAttribute('href') ?? '';
214
334
  const baseUrl = typeof document !== 'undefined' && document.baseURI
215
335
  ? document.baseURI
216
336
  : window.location.href || `${window.location.origin}/`;
217
- const location = new URL(href, baseUrl);
337
+ let location;
338
+ try {
339
+ location = new URL(href, baseUrl);
340
+ }
341
+ catch {
342
+ return true;
343
+ }
218
344
  if (location.origin !== window.location.origin) {
219
345
  return true;
220
346
  }
221
- e.preventDefault();
222
- this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false);
223
- return this;
347
+ // A link to a #fragment of the page already shown is native fragment navigation: let the browser scroll to it.
348
+ // Its popstate then only updates `url`, matching the fragment-only back/forward rule.
349
+ if (location.href.includes('#') && location.pathname === window.location.pathname && location.search === window.location.search) {
350
+ return true;
351
+ }
352
+ const candidateRoute = this.matchRoute(location.pathname);
353
+ const selected = candidateRoute ? this.routes[candidateRoute] : undefined;
354
+ if (!selected || !this.isRunnableRoute(selected) || builtInDefaultRoutes.has(selected)) {
355
+ return true;
356
+ }
357
+ // Guards are synchronous, so try the navigation first and cancel the browser's default only if Router took it.
358
+ // A rejected (or throwing) guard therefore falls back to a normal page load. If the route committed and then
359
+ // threw, the token has moved, so the default is still cancelled and the browser doesn't navigate twice.
360
+ const token = this.navigationToken;
361
+ let handled = false;
362
+ try {
363
+ handled = this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false) !== false;
364
+ }
365
+ finally {
366
+ if (handled || this.navigationToken !== token) {
367
+ e.preventDefault();
368
+ }
369
+ }
370
+ return handled ? this : true;
224
371
  }
225
- /** Restore browser history state without adding a new history entry. */
372
+ /**
373
+ * Popstate handler for back/forward. Runs the route for the new URL without adding a history entry.
374
+ * A hash-only change just updates `url` and `locationData.url`.
375
+ * @returns This router.
376
+ */
226
377
  eventPopState() {
227
378
  if (typeof window === 'undefined') {
228
379
  return this;
229
380
  }
230
- this.navigate(this.getCurrentUrl(), {}, true);
381
+ const currentUrl = this.getCurrentUrl();
382
+ // Moving between #fragments on the same page (footnotes, skip links) keeps the current view and focus.
383
+ if (this.route !== null && withoutHash(currentUrl) === withoutHash(this.url)) {
384
+ this.url = currentUrl;
385
+ this.locationData = { ...this.locationData, url: currentUrl };
386
+ return this;
387
+ }
388
+ this.navigate(currentUrl, {}, true);
231
389
  return this;
232
390
  }
233
- /** Decode URL query parameters, keeping the last value for duplicate keys. */
391
+ /**
392
+ * Decode a query string into a plain object. For repeated keys the last value wins.
393
+ * @param queryString - Query string, with or without the leading `?`.
394
+ */
234
395
  parseQueryString(queryString) {
235
396
  return Object.fromEntries(new URLSearchParams(queryString));
236
397
  }
237
- /** Build decoded path, query, and mediator data for the selected route. */
398
+ /**
399
+ * Rebuild `locationData` from the current `url` and `route`. Leaves it unchanged if the URL can't be parsed.
400
+ * @param mediatorData - Value to expose as `locationData.data.mediator`.
401
+ */
238
402
  setLocationData(mediatorData) {
239
- const url = this.url || '/';
240
- let parsedUrl;
241
- try {
242
- const value = this.isBrowserRuntime() ? `${window.location.origin}${url}` : url;
243
- parsedUrl = new URL(value, this.getOrigin());
244
- }
245
- catch (_error) {
403
+ const parsedUrl = this.parseStoredUrl(this.url || '/');
404
+ if (!parsedUrl) {
246
405
  return;
247
406
  }
248
407
  this.locationData = this.buildLocationData(this.url, this.route, mediatorData, parsedUrl);
249
408
  }
250
- /** Update page context after a route renders. DOM effects are browser-only. */
409
+ /**
410
+ * Record the route's title in `pageTitle` and, in a browser, set `document.title` and move focus to the route's
411
+ * `focus` selector (default `main h1`), adding `tabindex="-1"` if needed. `focus: false` skips focus.
412
+ * @param route - The route that just ran.
413
+ * @returns This router.
414
+ */
251
415
  applyPageContext(route) {
252
416
  if (typeof route === 'function') {
253
417
  this.pageTitle = null;
@@ -260,7 +424,7 @@ class Router {
260
424
  if (this.pageTitle) {
261
425
  document.title = this.pageTitle;
262
426
  }
263
- if (route.focus !== false) {
427
+ if (route.focus !== false && !this.suppressFocus) {
264
428
  const target = document.querySelector(route.focus || 'main h1');
265
429
  if (target) {
266
430
  if (!target.hasAttribute('tabindex')) {
@@ -271,64 +435,97 @@ class Router {
271
435
  }
272
436
  return this;
273
437
  }
274
- /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */
438
+ /**
439
+ * Navigate to a URL: match the route, run its guard, commit the new state, call the previous route's `destroy()`,
440
+ * run the new route, apply title and focus, then push a history entry (browser only, unless `isPopState`).
441
+ * If route code starts another navigation part-way through, that one wins and this call stops early.
442
+ * @param url - Destination URL. Omit to re-run the last committed URL (or the current browser URL / `/` at first).
443
+ * @param mediatorData - Value exposed to the route as `location.data.mediator`.
444
+ * @param isPopState - `true` to skip adding a history entry.
445
+ * @returns This router, or `false` if the navigation was rejected (bad or cross-origin URL, no matching route,
446
+ * guard refused, or nothing to run). A rejected navigation changes nothing.
447
+ * @throws Whatever a guard or route callback throws. A throwing guard changes nothing; a throwing route stays committed.
448
+ */
275
449
  navigate(url, mediatorData, isPopState = false) {
450
+ const skipFocus = this.initialDispatch;
451
+ this.initialDispatch = false;
276
452
  if (url !== undefined && typeof url !== 'string') {
277
453
  return false;
278
454
  }
279
- let candidateInput = url !== undefined ? url : this.url;
455
+ const candidateInput = url !== undefined ? url : this.url;
456
+ let parsedUrl;
280
457
  if (!candidateInput) {
281
- candidateInput = this.getCurrentUrl();
458
+ parsedUrl = this.parseNavigationUrl(this.getCurrentUrl());
459
+ }
460
+ else {
461
+ parsedUrl = url === undefined ? this.parseStoredUrl(candidateInput) : this.parseNavigationUrl(candidateInput);
282
462
  }
283
- const parsedUrl = this.parseNavigationUrl(candidateInput);
284
463
  if (!parsedUrl) {
285
464
  return false;
286
465
  }
287
466
  if (this.isBrowserRuntime() && parsedUrl.origin !== window.location.origin) {
288
467
  return false;
289
468
  }
290
- const candidateUrl = this.isBrowserRuntime()
291
- ? `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`
292
- : candidateInput;
293
- let candidateRoute = null;
294
- for (const route of Object.keys(this.routes)) {
295
- if (route === 'defaultRoute' || (parsedUrl.pathname !== route && !parsedUrl.pathname.startsWith(`${route}/`))) {
296
- continue;
297
- }
298
- if (!candidateRoute || route.length > candidateRoute.length) {
299
- candidateRoute = route;
300
- }
301
- }
302
- if (!candidateRoute && Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {
303
- candidateRoute = 'defaultRoute';
469
+ // Store only path, query, and hash, even when a server passed an absolute URL.
470
+ const candidateUrl = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
471
+ const candidateRoute = this.matchRoute(parsedUrl.pathname);
472
+ if (!candidateRoute) {
473
+ return false;
304
474
  }
305
475
  const candidateLocationData = this.buildLocationData(candidateUrl, candidateRoute, mediatorData, parsedUrl);
306
- const selected = candidateRoute ? this.routes[candidateRoute] : undefined;
307
- if (selected && ((typeof selected !== 'function' && selected.secure && selected.secure(this.scope, candidateLocationData) !== true) ||
308
- !this.isRunnableRoute(selected))) {
476
+ const selected = this.routes[candidateRoute];
477
+ if (!selected || (typeof selected !== 'function' && selected.secure && selected.secure(this.scope, candidateLocationData) !== true) ||
478
+ !this.isRunnableRoute(selected)) {
309
479
  return false;
310
480
  }
481
+ const token = ++this.navigationToken;
311
482
  this.url = candidateUrl;
312
483
  this.route = candidateRoute;
313
484
  this.locationData = candidateLocationData;
314
- if (selected) {
315
- const previous = this.previousRoute ? this.routes[this.previousRoute] : undefined;
316
- if (previous && typeof previous !== 'function' && typeof previous.view === 'object' && previous.view.destroy) {
317
- previous.view.destroy(this.scope, this.locationData);
318
- }
319
- if (typeof selected === 'function') {
320
- selected(this.scope, this.locationData);
321
- }
322
- else if (typeof selected.view === 'function') {
323
- selected.view(this.scope, this.locationData);
324
- }
325
- else {
326
- selected.view.initialize(this.scope, this.locationData);
485
+ // Clear the stored teardown before calling it, so a navigation started inside destroy() can't call it again,
486
+ // and a destroy() that throws is not retried: its error propagates with the destination committed but not run.
487
+ const previous = this.activeDestroy;
488
+ this.activeDestroy = undefined;
489
+ if (previous) {
490
+ previous.callback.call(previous.receiver, this.scope, candidateLocationData);
491
+ if (token !== this.navigationToken) {
492
+ return this;
327
493
  }
494
+ }
495
+ const selectedView = typeof selected !== 'function' && typeof selected.view === 'object'
496
+ ? selected.view
497
+ : undefined;
498
+ // Store the teardown before initialize() runs, so it still runs later if initialize() throws or navigates away.
499
+ this.activeDestroy = selectedView?.destroy
500
+ ? { receiver: selectedView, callback: selectedView.destroy }
501
+ : undefined;
502
+ if (typeof selected === 'function') {
503
+ selected(this.scope, candidateLocationData);
504
+ }
505
+ else if (typeof selected.view === 'function') {
506
+ selected.view(this.scope, candidateLocationData);
507
+ }
508
+ else {
509
+ selectedView.initialize(this.scope, candidateLocationData);
510
+ }
511
+ // If initialize() started another navigation, that one has already applied its own title, focus, and history.
512
+ if (token !== this.navigationToken) {
513
+ return this;
514
+ }
515
+ this.suppressFocus = skipFocus;
516
+ try {
328
517
  this.applyPageContext(selected);
329
- this.previousRoute = this.route;
330
518
  }
331
- if (!isPopState && this.isBrowserRuntime()) {
519
+ finally {
520
+ this.suppressFocus = false;
521
+ }
522
+ if (token !== this.navigationToken) {
523
+ return this;
524
+ }
525
+ this.previousRoute = this.route;
526
+ // Re-running the current URL (no `url` given, e.g. a mediator event without a destination) must not add a
527
+ // duplicate history entry for the address already shown.
528
+ if (!isPopState && this.isBrowserRuntime() && !(url === undefined && parsedUrl.href === window.location.href)) {
332
529
  window.history.pushState(this.url, this.pageTitle || '', parsedUrl.href);
333
530
  }
334
531
  return this;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/** Navigation payload, including caller-defined metadata. */\ninterface NavigationData {url?: string; [key: string]: unknown}\n/** URL-derived values supplied to route handlers. */\ninterface LocationData {url: string; data: {url: string[]; mediator: NavigationData | undefined; query: Record<string, string>}}\ntype RouteHandler = (scope: Element | null, location: LocationData) => unknown;\n/** An object route can own a function or a view lifecycle. */\ninterface RouteObject {title?: string; focus?: string | false; secure?: RouteHandler; view?: RouteHandler | {initialize?: RouteHandler; destroy?: RouteHandler}}\ntype Route = RouteHandler | RouteObject;\n/** Minimal standards-based event target contract required by the router. */\ninterface NavigationMediator {\n addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;\n removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;\n}\n/** History API router that also dispatches request URLs in non-browser runtimes. */\nclass Router {\n routes: Record<string, Route>;\n url: string;\n route: string | null;\n scope: Element | null;\n previousRoute: string | null;\n pageTitle: string | null;\n boundPushStateClick: (event: MouseEvent) => unknown;\n boundPopState: (event: PopStateEvent) => unknown;\n boundMediatorNavigate: EventListener;\n listenersInitialized: boolean;\n locationData: LocationData = {url: '', data: {url: [], mediator: undefined, query: {}}};\n private currentMediator: NavigationMediator | false = false;\n private boundMediator: NavigationMediator | false = false;\n private currentNavigationRoot: Document | Element | null = null;\n private boundNavigationRoot: Document | Element | null = null;\n\n /** Create an instance with its own state and listener references. */\n constructor() {\n this.routes = {\n defaultRoute: {\n view: {\n initialize: () => true,\n destroy: () => true\n }\n }\n };\n this.url = '';\n this.route = null;\n this.scope = null;\n this.previousRoute = null;\n this.pageTitle = null;\n this.boundPushStateClick = this.eventPushStateClick.bind(this);\n this.boundPopState = this.eventPopState.bind(this);\n this.boundMediatorNavigate = (event: Event) => {\n const detail = (event as CustomEvent<unknown>).detail;\n if (detail === undefined || detail === null) {\n this.navigate(undefined, {}, false);\n return;\n }\n if (typeof detail !== 'object' || Array.isArray(detail)) {return;}\n const data = detail as NavigationData;\n this.navigate(data.url, data, false);\n };\n this.listenersInitialized = false;\n }\n\n get mediator(): NavigationMediator | false {return this.currentMediator;}\n\n /** Move an active router:navigate subscription when the mediator changes. */\n set mediator(value: NavigationMediator | false) {\n if (value === this.currentMediator) {return;}\n if (this.listenersInitialized && this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.currentMediator = value;\n if (this.listenersInitialized && value) {\n value.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = value;\n }\n }\n\n get navigationRoot(): Document | Element | null {return this.currentNavigationRoot;}\n\n /** Scope progressive link interception and move an active click listener when the root changes. */\n set navigationRoot(value: Document | Element | null) {\n if (value === this.currentNavigationRoot) {return;}\n if (this.listenersInitialized && this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.currentNavigationRoot = value;\n const nextRoot = value || document;\n nextRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = nextRoot;\n return;\n }\n this.currentNavigationRoot = value;\n }\n\n /** True when browser navigation APIs are available. */\n private isBrowserRuntime() {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n }\n\n /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */\n private getOrigin() {\n return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';\n }\n\n /** Read the authoritative browser URL, or retain the supplied server URL. */\n private getCurrentUrl() {\n if (!this.isBrowserRuntime()) {return this.url || '/';}\n return window.location.pathname + window.location.search + (window.location.hash || '');\n }\n\n /** Parse a navigation candidate without changing Router state. */\n private parseNavigationUrl(url: string) {\n try {\n const base = this.isBrowserRuntime() ? window.location.href || `${window.location.origin}/` : this.getOrigin();\n return new URL(url || '/', base);\n } catch (_error) {\n return null;\n }\n }\n\n /** Normalize browser navigation to a same-origin path, query, and hash. */\n private normalizeBrowserUrl(url: string) {\n if (!this.isBrowserRuntime()) {return url;}\n const parsedUrl = this.parseNavigationUrl(url);\n if (!parsedUrl || parsedUrl.origin !== window.location.origin) {return null;}\n return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;\n }\n\n /** Return whether a selected route has runnable view behavior. */\n private isRunnableRoute(route: Route): boolean {\n return typeof route === 'function' || typeof route.view === 'function' ||\n Boolean(route.view && typeof route.view.initialize === 'function');\n }\n\n /** Build decoded path, query, and mediator data for a navigation candidate. */\n private buildLocationData(url: string, route: string | null, mediatorData: NavigationData | undefined, parsedUrl: URL): LocationData {\n const matchedRoute = route && route !== 'defaultRoute' ? route : '';\n return {\n url,\n data: {\n url: parsedUrl.pathname\n .slice(matchedRoute.length)\n .split('/')\n .filter(Boolean)\n .map((fragment) => {\n try {return decodeURIComponent(fragment);} catch (_error) {return fragment;}\n }),\n mediator: mediatorData,\n query: this.parseQueryString(parsedUrl.search)\n }\n };\n }\n\n /**\n * Start this instance and return it for lifecycle chaining.\n * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.\n */\n initialize(url?: string) {\n this.addListeners();\n this.navigate(url ?? this.getCurrentUrl(), {}, true);\n return this;\n }\n\n /** Register browser and optional mediator handlers once. */\n addListeners() {\n if (this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n const navigationRoot = this.currentNavigationRoot || document;\n navigationRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = navigationRoot;\n window.addEventListener('popstate', this.boundPopState);\n }\n if (this.currentMediator) {\n this.currentMediator.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = this.currentMediator;\n }\n this.listenersInitialized = true;\n return this;\n }\n\n /** Release owned state and listeners so the instance can leave the application lifecycle. */\n destroy() {\n this.removeListeners();\n return this;\n }\n\n /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */\n removeListeners() {\n if (!this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = null;\n window.removeEventListener('popstate', this.boundPopState);\n }\n if (this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.listenersInitialized = false;\n return this;\n }\n\n /** Intercept eligible same-origin push-state links while preserving normal browser actions. */\n eventPushStateClick(e: MouseEvent) {\n if (typeof window === 'undefined') {return true;}\n if (e.defaultPrevented || (e.button !== undefined && e.button !== 0) || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return true;\n }\n const eventTarget = e.target as Element | null;\n const source = eventTarget && (typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement);\n const anchor = source && typeof source.closest === 'function' ? source.closest('a[href][data-pushstate]') : null;\n if (!anchor) {return true;}\n const target = anchor.getAttribute('target');\n if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {return true;}\n const href = (anchor as HTMLAnchorElement).href || anchor.getAttribute('href') || '';\n const baseUrl = typeof document !== 'undefined' && document.baseURI\n ? document.baseURI\n : window.location.href || `${window.location.origin}/`;\n const location = new URL(href, baseUrl);\n if (location.origin !== window.location.origin) {return true;}\n e.preventDefault();\n this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false);\n return this;\n }\n\n /** Restore browser history state without adding a new history entry. */\n eventPopState() {\n if (typeof window === 'undefined') {return this;}\n this.navigate(this.getCurrentUrl(), {}, true);\n return this;\n }\n\n /** Decode URL query parameters, keeping the last value for duplicate keys. */\n parseQueryString(queryString: string) {\n return Object.fromEntries(new URLSearchParams(queryString));\n }\n\n /** Build decoded path, query, and mediator data for the selected route. */\n setLocationData(mediatorData?: NavigationData) {\n const url = this.url || '/';\n let parsedUrl: URL;\n try {\n const value = this.isBrowserRuntime() ? `${window.location.origin}${url}` : url;\n parsedUrl = new URL(value, this.getOrigin());\n } catch (_error) {\n return;\n }\n this.locationData = this.buildLocationData(this.url, this.route, mediatorData, parsedUrl);\n }\n\n /** Update page context after a route renders. DOM effects are browser-only. */\n applyPageContext(route: Route) {\n if (typeof route === 'function') {\n this.pageTitle = null;\n return this;\n }\n this.pageTitle = typeof route.title === 'string' ? route.title : null;\n if (!this.isBrowserRuntime()) {return this;}\n if (this.pageTitle) {document.title = this.pageTitle;}\n if (route.focus !== false) {\n const target = document.querySelector<HTMLElement>(route.focus || 'main h1');\n if (target) {\n if (!target.hasAttribute('tabindex')) {target.setAttribute('tabindex', '-1');}\n target.focus();\n }\n }\n return this;\n }\n\n /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */\n navigate(url?: string, mediatorData?: NavigationData, isPopState = false) {\n if (url !== undefined && typeof url !== 'string') {return false;}\n\n let candidateInput = url !== undefined ? url : this.url;\n if (!candidateInput) {candidateInput = this.getCurrentUrl();}\n const parsedUrl = this.parseNavigationUrl(candidateInput);\n if (!parsedUrl) {return false;}\n if (this.isBrowserRuntime() && parsedUrl.origin !== window.location.origin) {return false;}\n\n const candidateUrl = this.isBrowserRuntime()\n ? `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`\n : candidateInput;\n let candidateRoute: string | null = null;\n for (const route of Object.keys(this.routes)) {\n if (route === 'defaultRoute' || (parsedUrl.pathname !== route && !parsedUrl.pathname.startsWith(`${route}/`))) {continue;}\n if (!candidateRoute || route.length > candidateRoute.length) {candidateRoute = route;}\n }\n if (!candidateRoute && Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {\n candidateRoute = 'defaultRoute';\n }\n\n const candidateLocationData = this.buildLocationData(candidateUrl, candidateRoute, mediatorData, parsedUrl);\n const selected = candidateRoute ? this.routes[candidateRoute] : undefined;\n if (selected && ((typeof selected !== 'function' && selected.secure && selected.secure(this.scope, candidateLocationData) !== true) ||\n !this.isRunnableRoute(selected))) {\n return false;\n }\n\n this.url = candidateUrl;\n this.route = candidateRoute;\n this.locationData = candidateLocationData;\n if (selected) {\n const previous = this.previousRoute ? this.routes[this.previousRoute] : undefined;\n if (previous && typeof previous !== 'function' && typeof previous.view === 'object' && previous.view.destroy) {\n previous.view.destroy(this.scope, this.locationData);\n }\n if (typeof selected === 'function') {\n selected(this.scope, this.locationData);\n } else if (typeof selected.view === 'function') {\n selected.view(this.scope, this.locationData);\n } else {\n selected.view!.initialize!(this.scope, this.locationData);\n }\n this.applyPageContext(selected);\n this.previousRoute = this.route;\n }\n if (!isPopState && this.isBrowserRuntime()) {\n window.history.pushState(this.url, this.pageTitle || '', parsedUrl.href);\n }\n return this;\n }\n}\n\n/** Public route configuration and navigation payload types. */\nnamespace Router {\n export type Handler = RouteHandler;\n export type Route = RouteHandler | RouteObject;\n export type Location = LocationData;\n export type Navigation = NavigationData;\n}\nexport = Router;\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA+BA,2HAA2H;AAC3H,MAAM,oBAAoB,GAAG,IAAI,OAAO,EAAU,CAAC;AACnD,8FAA8F;AAC9F,MAAM,YAAY,GAAG,4BAA4B,CAAC;AAElD,kGAAkG;AAClG,SAAS,iBAAiB,CAAC,GAAW;IAClC,2HAA2H;IAC3H,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAAA,OAAO,GAAG,CAAC;IAAA,CAAC;IAC3D,OAAO,IAAI,GAAG,CAAC,GAAG,YAAY,GAAG,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC;AACrD,CAAC;AAED,yDAAyD;AACzD,SAAS,WAAW,CAAC,GAAW;IAC5B,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/B,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AACpD,CAAC;AAED;;;GAGG;AACH,MAAM,MAAM;IACR,MAAM,CAAwB;IAC9B,GAAG,CAAS;IACZ,KAAK,CAAgB;IACrB,KAAK,CAAiB;IACtB,aAAa,CAAgB;IAC7B,SAAS,CAAgB;IACzB,mBAAmB,CAAiC;IACpD,aAAa,CAAoC;IACjD,qBAAqB,CAAgB;IACrC,oBAAoB,CAAU;IAC9B,YAAY,GAAiB,EAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAC,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAC,EAAC,CAAC;IAChF,eAAe,GAA+B,KAAK,CAAC;IACpD,aAAa,GAA+B,KAAK,CAAC;IAClD,qBAAqB,GAA8B,IAAI,CAAC;IACxD,mBAAmB,GAA8B,IAAI,CAAC;IACtD,aAAa,CAA4B;IACzC,cAAc,CAA6B;IACnD,gHAAgH;IAChH,oGAAoG;IAC5F,eAAe,GAAG,CAAC,CAAC;IAC5B,qGAAqG;IAC7F,eAAe,GAAG,KAAK,CAAC;IAChC,+DAA+D;IACvD,aAAa,GAAG,KAAK,CAAC;IAE9B,uFAAuF;IACvF;QACI,MAAM,YAAY,GAAgB;YAC9B,IAAI,EAAE;gBACF,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI;gBACtB,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI;aACtB;SACJ,CAAC;QACF,oBAAoB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QACvC,IAAI,CAAC,MAAM,GAAG,EAAC,YAAY,EAAC,CAAC;QAC7B,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC;QACd,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/D,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,qBAAqB,GAAG,CAAC,KAAY,EAAE,EAAE;YAC1C,MAAM,MAAM,GAAI,KAA8B,CAAC,MAAM,CAAC;YACtD,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBAC1C,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;gBACpC,OAAO;YACX,CAAC;YACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAAA,OAAO;YAAA,CAAC;YAClE,MAAM,IAAI,GAAG,MAAwB,CAAC;YACtC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACzC,CAAC,CAAC;QACF,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;IACtC,CAAC;IAED,gFAAgF;IAChF,IAAI,QAAQ,KAAgC,OAAO,IAAI,CAAC,eAAe,CAAC,CAAA,CAAC;IAEzE,uHAAuH;IACvH,IAAI,QAAQ,CAAC,KAAiC;QAC1C,IAAI,KAAK,KAAK,IAAI,CAAC,eAAe,EAAE,CAAC;YAAA,OAAO;QAAA,CAAC;QAC7C,IAAI,IAAI,CAAC,oBAAoB,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YAClD,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,iBAAiB,EAAE,IAAI,CAAC,qBAAqB,CAAC,CAAC;YACtF,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC/B,CAAC;QACD,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;QAC7B,IAAI,IAAI,CAAC,oBAAoB,IAAI,KAAK,EAAE,CAAC;YACrC,KAAK,CAAC,gBAAgB,CAAC,iBAAiB,EAAE,IAAI,CAAC,qBAAqB,CAAC,CAAC;YACtE,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC/B,CAAC;IACL,CAAC;IAED,mHAAmH;IACnH,IAAI,cAAc,KAA+B,OAAO,IAAI,CAAC,qBAAqB,CAAC,CAAA,CAAC;IAEpF,6FAA6F;IAC7F,IAAI,cAAc,CAAC,KAAgC;QAC/C,IAAI,KAAK,KAAK,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAAA,OAAO;QAAA,CAAC;QACnD,IAAI,IAAI,CAAC,oBAAoB,IAAI,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YACvD,IAAI,CAAC,mBAAmB,EAAE,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,mBAAoC,CAAC,CAAC;YAClG,IAAI,CAAC,qBAAqB,GAAG,KAAK,CAAC;YACnC,MAAM,QAAQ,GAAG,KAAK,IAAI,QAAQ,CAAC;YACnC,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,mBAAoC,CAAC,CAAC;YAC9E,IAAI,CAAC,mBAAmB,GAAG,QAAQ,CAAC;YACpC,OAAO;QACX,CAAC;QACD,IAAI,CAAC,qBAAqB,GAAG,KAAK,CAAC;IACvC,CAAC;IAED,qFAAqF;IAC7E,gBAAgB;QACpB,OAAO,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,QAAQ,KAAK,WAAW,CAAC;IAC5E,CAAC;IAED,wGAAwG;IAChG,SAAS;QACb,OAAO,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,4BAA4B,CAAC;IAC3F,CAAC;IAED,+HAA+H;IACvH,aAAa;QACjB,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC;QAAA,CAAC;QACvD,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC5F,CAAC;IAED,mHAAmH;IAC3G,kBAAkB,CAAC,GAAW;QAClC,IAAI,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YAC/G,OAAO,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,IAAI,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,MAAM,EAAE,CAAC;YACd,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IAED,2GAA2G;IACnG,mBAAmB,CAAC,GAAW;QACnC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAAA,OAAO,GAAG,CAAC;QAAA,CAAC;QAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAC/C,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC7E,OAAO,GAAG,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;IACvE,CAAC;IAED,qGAAqG;IAC7F,eAAe,CAAC,KAAY;QAChC,OAAO,OAAO,KAAK,KAAK,UAAU,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU;YAClE,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,OAAO,KAAK,CAAC,IAAI,CAAC,UAAU,KAAK,UAAU,CAAC,CAAC;IAC3E,CAAC;IACD,iHAAiH;IACzG,aAAa;QACjB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC;QACnC,IAAI,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACzG,OAAO,MAAM,CAAC;QAClB,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;QAC3C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;QAC5C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,IAAI,GAAG,KAAK,cAAc,EAAE,CAAC;gBAAA,SAAS;YAAA,CAAC;YACvC,MAAM,MAAM,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;YACtC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAC3B,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACtC,+GAA+G;YAC/G,0DAA0D;YAC1D,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,QAAQ,KAAK,MAAM,IAAI,CAAC,GAAG,KAAK,MAAM,IAAI,GAAG,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC;gBAAA,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAAA,CAAC;QAC3H,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,EAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAC,CAAC;QAClD,OAAO,IAAI,CAAC,cAAc,CAAC;IAC/B,CAAC;IAED,kHAAkH;IAC1G,UAAU,CAAC,QAAgB;QAC/B,MAAM,EAAC,QAAQ,EAAC,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QACxC,6FAA6F;QAC7F,wFAAwF;QACxF,IAAI,cAAc,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC5C,IAAI,KAAK,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACtC,OAAO,cAAc,KAAK,SAAS,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YAChD,cAAc,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;YACxD,KAAK,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClE,CAAC;QACD,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;YAAA,OAAO,cAAc,CAAC;QAAA,CAAC;QAC1D,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YAChG,OAAO,cAAc,CAAC;QAC1B,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,4HAA4H;IACpH,iBAAiB,CAAC,KAAoB;QAC1C,IAAI,CAAC,KAAK,IAAI,KAAK,KAAK,cAAc,EAAE,CAAC;YAAA,OAAO,CAAC,CAAC;QAAA,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;IAC1F,CAAC;IAED,yHAAyH;IACjH,cAAc,CAAC,GAAW;QAC9B,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAAA,CAAC;QAChE,IAAI,CAAC;YACD,OAAO,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;QAChD,CAAC;QAAC,OAAO,MAAM,EAAE,CAAC;YACd,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IAED,6GAA6G;IACrG,iBAAiB,CAAC,GAAW,EAAE,KAAoB,EAAE,YAAwC,EAAE,SAAc;QACjH,OAAO;YACH,GAAG;YACH,IAAI,EAAE;gBACF,GAAG,EAAE,SAAS,CAAC,QAAQ;qBAClB,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;qBACpC,KAAK,CAAC,GAAG,CAAC;qBACV,MAAM,CAAC,OAAO,CAAC;qBACf,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;oBACd,IAAI,CAAC;wBAAA,OAAO,kBAAkB,CAAC,QAAQ,CAAC,CAAC;oBAAA,CAAC;oBAAC,OAAO,MAAM,EAAE,CAAC;wBAAA,OAAO,QAAQ,CAAC;oBAAA,CAAC;gBAChF,CAAC,CAAC;gBACN,QAAQ,EAAE,YAAY;gBACtB,KAAK,EAAE,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,MAAM,CAAC;aACjD;SACJ,CAAC;IACN,CAAC;IAED;;;;;OAKG;IACH,UAAU,CAAC,GAAY;QACnB,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,sGAAsG;QACtG,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,IAAI,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,GAAG,IAAI,IAAI,CAAC,aAAa,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QACzD,CAAC;gBAAS,CAAC;YACP,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;QACjC,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;OAGG;IACH,YAAY;QACR,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC7C,IAAI,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAC1B,MAAM,cAAc,GAAG,IAAI,CAAC,qBAAqB,IAAI,QAAQ,CAAC;YAC9D,cAAc,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,mBAAoC,CAAC,CAAC;YACpF,IAAI,CAAC,mBAAmB,GAAG,cAAc,CAAC;YAC1C,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAC5D,CAAC;QACD,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,iBAAiB,EAAE,IAAI,CAAC,qBAAqB,CAAC,CAAC;YACrF,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,eAAe,CAAC;QAC9C,CAAC;QACD,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;OAGG;IACH,OAAO;QACH,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;OAGG;IACH,eAAe;QACX,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC9C,IAAI,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAC1B,IAAI,CAAC,mBAAmB,EAAE,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,mBAAoC,CAAC,CAAC;YAClG,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC;YAChC,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAC/D,CAAC;QACD,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACrB,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,iBAAiB,EAAE,IAAI,CAAC,qBAAqB,CAAC,CAAC;YACtF,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC/B,CAAC;QACD,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;QAClC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;;;;;OAQG;IACH,mBAAmB,CAAC,CAAa;QAC7B,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QACjD,IAAI,CAAC,CAAC,gBAAgB,IAAI,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;YACvH,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,MAAM,WAAW,GAAG,CAAC,CAAC,MAAwB,CAAC;QAC/C,MAAM,MAAM,GAAG,WAAW,IAAI,CAAC,OAAO,WAAW,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC;QACpH,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACjH,IAAI,CAAC,MAAM,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,WAAW,EAAE,KAAK,OAAO,CAAC,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QACnG,kGAAkG;QAClG,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAC/C,MAAM,OAAO,GAAG,OAAO,QAAQ,KAAK,WAAW,IAAI,QAAQ,CAAC,OAAO;YAC/D,CAAC,CAAC,QAAQ,CAAC,OAAO;YAClB,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;QAC3D,IAAI,QAAa,CAAC;QAClB,IAAI,CAAC;YAAA,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAAA,CAAC;QAAC,MAAM,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC7D,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC9D,+GAA+G;QAC/G,sFAAsF;QACtF,IAAI,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAC9H,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC1D,MAAM,QAAQ,GAAG,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1E,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,IAAI,oBAAoB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QACtG,+GAA+G;QAC/G,6GAA6G;QAC7G,wGAAwG;QACxG,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC;QACnC,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC;YACD,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,KAAK,KAAK,CAAC;QAC3G,CAAC;gBAAS,CAAC;YACP,IAAI,OAAO,IAAI,IAAI,CAAC,eAAe,KAAK,KAAK,EAAE,CAAC;gBAAA,CAAC,CAAC,cAAc,EAAE,CAAC;YAAA,CAAC;QACxE,CAAC;QACD,OAAO,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACjC,CAAC;IAED;;;;OAIG;IACH,aAAa;QACT,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QACjD,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QACxC,uGAAuG;QACvG,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,WAAW,CAAC,UAAU,CAAC,KAAK,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3E,IAAI,CAAC,GAAG,GAAG,UAAU,CAAC;YACtB,IAAI,CAAC,YAAY,GAAG,EAAC,GAAG,IAAI,CAAC,YAAY,EAAE,GAAG,EAAE,UAAU,EAAC,CAAC;YAC5D,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;OAGG;IACH,gBAAgB,CAAC,WAAmB;QAChC,OAAO,MAAM,CAAC,WAAW,CAAC,IAAI,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC;IAChE,CAAC;IAED;;;OAGG;IACH,eAAe,CAAC,YAA6B;QACzC,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;QACvD,IAAI,CAAC,SAAS,EAAE,CAAC;YAAA,OAAO;QAAA,CAAC;QACzB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC;IAC9F,CAAC;IAED;;;;;OAKG;IACH,gBAAgB,CAAC,KAAY;QACzB,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;YAC9B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC5C,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAAA,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC;QAAA,CAAC;QACtD,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YAC/C,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAc,KAAK,CAAC,KAAK,IAAI,SAAS,CAAC,CAAC;YAC7E,IAAI,MAAM,EAAE,CAAC;gBACT,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;oBAAA,MAAM,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;gBAAA,CAAC;gBAC9E,MAAM,CAAC,KAAK,EAAE,CAAC;YACnB,CAAC;QACL,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,GAAY,EAAE,YAA6B,EAAE,UAAU,GAAG,KAAK;QACpE,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC;QACvC,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;QAC7B,IAAI,GAAG,KAAK,SAAS,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;YAAA,OAAO,KAAK,CAAC;QAAA,CAAC;QAEjE,MAAM,cAAc,GAAG,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QAC1D,IAAI,SAAqB,CAAC;QAC1B,IAAI,CAAC,cAAc,EAAE,CAAC;YAClB,SAAS,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;QAC9D,CAAC;aAAM,CAAC;YACJ,SAAS,GAAG,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC;QAClH,CAAC;QACD,IAAI,CAAC,SAAS,EAAE,CAAC;YAAA,OAAO,KAAK,CAAC;QAAA,CAAC;QAC/B,IAAI,IAAI,CAAC,gBAAgB,EAAE,IAAI,SAAS,CAAC,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAAA,OAAO,KAAK,CAAC;QAAA,CAAC;QAE3F,+EAA+E;QAC/E,MAAM,YAAY,GAAG,GAAG,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;QACjF,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC3D,IAAI,CAAC,cAAc,EAAE,CAAC;YAAA,OAAO,KAAK,CAAC;QAAA,CAAC;QAEpC,MAAM,qBAAqB,GAAG,IAAI,CAAC,iBAAiB,CAAC,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC;QAC5G,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QAC7C,IAAI,CAAC,QAAQ,IAAI,CAAC,OAAO,QAAQ,KAAK,UAAU,IAAI,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,qBAAqB,CAAC,KAAK,IAAI,CAAC;YAC/H,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClC,OAAO,KAAK,CAAC;QACjB,CAAC;QAED,MAAM,KAAK,GAAG,EAAE,IAAI,CAAC,eAAe,CAAC;QACrC,IAAI,CAAC,GAAG,GAAG,YAAY,CAAC;QACxB,IAAI,CAAC,KAAK,GAAG,cAAc,CAAC;QAC5B,IAAI,CAAC,YAAY,GAAG,qBAAqB,CAAC;QAC1C,6GAA6G;QAC7G,+GAA+G;QAC/G,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC;QACpC,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;QAC/B,IAAI,QAAQ,EAAE,CAAC;YACX,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC;YAC7E,IAAI,KAAK,KAAK,IAAI,CAAC,eAAe,EAAE,CAAC;gBAAA,OAAO,IAAI,CAAC;YAAA,CAAC;QACtD,CAAC;QACD,MAAM,YAAY,GAAG,OAAO,QAAQ,KAAK,UAAU,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ;YACpF,CAAC,CAAC,QAAQ,CAAC,IAAI;YACf,CAAC,CAAC,SAAS,CAAC;QAChB,gHAAgH;QAChH,IAAI,CAAC,aAAa,GAAG,YAAY,EAAE,OAAO;YACtC,CAAC,CAAC,EAAC,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,CAAC,OAAO,EAAC;YAC1D,CAAC,CAAC,SAAS,CAAC;QAChB,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE,CAAC;YACjC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC;QAChD,CAAC;aAAM,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC7C,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC;QACrD,CAAC;aAAM,CAAC;YACJ,YAAa,CAAC,UAAW,CAAC,IAAI,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC;QACjE,CAAC;QACD,8GAA8G;QAC9G,IAAI,KAAK,KAAK,IAAI,CAAC,eAAe,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAClD,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;QAC/B,IAAI,CAAC;YACD,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QACpC,CAAC;gBAAS,CAAC;YACP,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC/B,CAAC;QACD,IAAI,KAAK,KAAK,IAAI,CAAC,eAAe,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAClD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC;QAChC,0GAA0G;QAC1G,yDAAyD;QACzD,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC,CAAC,GAAG,KAAK,SAAS,IAAI,SAAS,CAAC,IAAI,KAAK,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5G,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,IAAI,EAAE,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QAC7E,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;CACJ;AASD,iBAAS,MAAM,CAAC","sourcesContent":["/** Extra data passed with a navigation. `url`, if present, is the destination; any other fields are yours. */\ninterface NavigationData {url?: string; [key: string]: unknown}\n/**\n * What a route callback receives about the destination.\n * `url` is the path, query, and hash; `data.url` holds the decoded path segments after the matched route key;\n * `data.query` holds decoded query values; `data.mediator` is the navigation data, if any.\n */\ninterface LocationData {url: string; data: {url: string[]; mediator: NavigationData | undefined; query: Record<string, string>}}\n/** A route callback. It receives `router.scope` and the destination location. */\ntype RouteHandler = (scope: Element | null, location: LocationData) => unknown;\n/** A view with setup and teardown. `destroy` receives the location being navigated to. */\ninterface RouteViewLifecycle {initialize?: RouteHandler; destroy?: RouteHandler}\n/**\n * A route with optional page context and a guard.\n * `secure` must return exactly `true` to allow navigation; `focus: false` leaves focus where it is.\n */\ninterface RouteObject {title?: string; focus?: string | false; secure?: RouteHandler; view?: RouteHandler | RouteViewLifecycle}\ntype Route = RouteHandler | RouteObject;\n/** The teardown captured from the view that actually initialized, so later route-table edits can't change it. */\ninterface ActiveDestroy {receiver: RouteViewLifecycle; callback: RouteHandler}\n/** Any EventTarget-like object; Router only adds and removes a `router:navigate` listener on it. */\ninterface NavigationMediator {\n addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;\n removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;\n}\n/**\n * Route keys prepared for fast matching. `prefixes` maps a normalized path to the route key that owns it;\n * `prefixFor` maps each key to its normalized path. Rebuilt only when the list of own keys changes.\n */\ninterface CompiledRoutes {keys: string[]; prefixes: Map<string, string>; prefixFor: Map<string, string>}\n\n/** The do-nothing `defaultRoute` objects each constructor installs, so link clicks can tell them apart from app routes. */\nconst builtInDefaultRoutes = new WeakSet<object>();\n/** A dummy origin that lets route keys and server URLs go through the standard URL parser. */\nconst routeKeyBase = 'http://white-label.invalid';\n\n/** Encode a route key the way the URL parser encodes paths, so `'/café'` matches `/caf%C3%A9`. */\nfunction normalizeRouteKey(key: string) {\n // A key that doesn't start with '/' or contains '?' or '#' can never equal a pathname, so leave it as is (it won't match).\n if (!key.startsWith('/') || /[?#]/.test(key)) {return key;}\n return new URL(`${routeKeyBase}${key}`).pathname;\n}\n\n/** Strip the `#fragment`, leaving the path and query. */\nfunction withoutHash(url: string) {\n const index = url.indexOf('#');\n return index === -1 ? url : url.slice(0, index);\n}\n\n/**\n * Maps URLs to route callbacks. In a browser it enhances `data-pushstate` links and back/forward with the History API;\n * on a server it dispatches the request URL you pass, with no DOM required.\n */\nclass Router {\n routes: Record<string, Route>;\n url: string;\n route: string | null;\n scope: Element | null;\n previousRoute: string | null;\n pageTitle: string | null;\n boundPushStateClick: (event: MouseEvent) => unknown;\n boundPopState: (event: PopStateEvent) => unknown;\n boundMediatorNavigate: EventListener;\n listenersInitialized: boolean;\n locationData: LocationData = {url: '', data: {url: [], mediator: undefined, query: {}}};\n private currentMediator: NavigationMediator | false = false;\n private boundMediator: NavigationMediator | false = false;\n private currentNavigationRoot: Document | Element | null = null;\n private boundNavigationRoot: Document | Element | null = null;\n private activeDestroy: ActiveDestroy | undefined;\n private compiledRoutes: CompiledRoutes | undefined;\n // Bumped by each navigation that passes its guard. If it changes while a navigation is calling out to app code,\n // a newer navigation has started, and the older one stops before touching title, focus, or history.\n private navigationToken = 0;\n // True only for the first navigate() call made by initialize(), so that dispatch leaves focus alone.\n private initialDispatch = false;\n // Tells applyPageContext() to skip focus for the current call.\n private suppressFocus = false;\n\n /** Create a router with a placeholder `defaultRoute` and no listeners attached yet. */\n constructor() {\n const defaultRoute: RouteObject = {\n view: {\n initialize: () => true,\n destroy: () => true\n }\n };\n builtInDefaultRoutes.add(defaultRoute);\n this.routes = {defaultRoute};\n this.url = '';\n this.route = null;\n this.scope = null;\n this.previousRoute = null;\n this.pageTitle = null;\n this.boundPushStateClick = this.eventPushStateClick.bind(this);\n this.boundPopState = this.eventPopState.bind(this);\n this.boundMediatorNavigate = (event: Event) => {\n const detail = (event as CustomEvent<unknown>).detail;\n if (detail === undefined || detail === null) {\n this.navigate(undefined, {}, false);\n return;\n }\n if (typeof detail !== 'object' || Array.isArray(detail)) {return;}\n const data = detail as NavigationData;\n this.navigate(data.url, data, false);\n };\n this.listenersInitialized = false;\n }\n\n /** Event target that can send `router:navigate` events, or `false` for none. */\n get mediator(): NavigationMediator | false {return this.currentMediator;}\n\n /** Set the mediator. If listeners are active, the `router:navigate` listener moves from the old one to the new one. */\n set mediator(value: NavigationMediator | false) {\n if (value === this.currentMediator) {return;}\n if (this.listenersInitialized && this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.currentMediator = value;\n if (this.listenersInitialized && value) {\n value.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = value;\n }\n }\n\n /** Element or document whose `data-pushstate` link clicks this router handles; `null` means the whole document. */\n get navigationRoot(): Document | Element | null {return this.currentNavigationRoot;}\n\n /** Set the click root. If listeners are active, the click listener moves to the new root. */\n set navigationRoot(value: Document | Element | null) {\n if (value === this.currentNavigationRoot) {return;}\n if (this.listenersInitialized && this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.currentNavigationRoot = value;\n const nextRoot = value || document;\n nextRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = nextRoot;\n return;\n }\n this.currentNavigationRoot = value;\n }\n\n /** True when `window` and `document` exist, i.e. browser-only effects should run. */\n private isBrowserRuntime() {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n }\n\n /** Origin used as the base for URL parsing: the page's origin in a browser, a dummy one on a server. */\n private getOrigin() {\n return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';\n }\n\n /** Current path, query, and hash: from `window.location` in a browser, or the last committed URL (default `/`) on a server. */\n private getCurrentUrl() {\n if (!this.isBrowserRuntime()) {return this.url || '/';}\n return window.location.pathname + window.location.search + (window.location.hash || '');\n }\n\n /** Parse a URL relative to the current page (or the dummy server origin). Returns `null` if it can't be parsed. */\n private parseNavigationUrl(url: string) {\n try {\n const base = this.isBrowserRuntime() ? window.location.href || `${window.location.origin}/` : this.getOrigin();\n return new URL(url || '/', base);\n } catch (_error) {\n return null;\n }\n }\n\n /** In a browser, reduce a URL to its path, query, and hash, or `null` if it is cross-origin or invalid. */\n private normalizeBrowserUrl(url: string) {\n if (!this.isBrowserRuntime()) {return url;}\n const parsedUrl = this.parseNavigationUrl(url);\n if (!parsedUrl || parsedUrl.origin !== window.location.origin) {return null;}\n return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;\n }\n\n /** True if the route has something to run: a function, a `view` function, or a `view.initialize`. */\n private isRunnableRoute(route: Route): boolean {\n return typeof route === 'function' || typeof route.view === 'function' ||\n Boolean(route.view && typeof route.view.initialize === 'function');\n }\n /** Build (or reuse) the lookup tables for the current route keys. Reused while the own key list is unchanged. */\n private compileRoutes(): CompiledRoutes {\n const keys = Object.keys(this.routes);\n const cached = this.compiledRoutes;\n if (cached && cached.keys.length === keys.length && cached.keys.every((key, index) => key === keys[index])) {\n return cached;\n }\n const prefixes = new Map<string, string>();\n const prefixFor = new Map<string, string>();\n for (const key of keys) {\n if (key === 'defaultRoute') {continue;}\n const prefix = normalizeRouteKey(key);\n prefixFor.set(key, prefix);\n const existing = prefixes.get(prefix);\n // When two keys normalize to the same path, prefer the one already in normalized form, then the alphabetically\n // lowest, so the winner never depends on insertion order.\n if (existing === undefined || (existing !== prefix && (key === prefix || key < existing))) {prefixes.set(prefix, key);}\n }\n this.compiledRoutes = {keys, prefixes, prefixFor};\n return this.compiledRoutes;\n }\n\n /** Find the route key with the longest matching path prefix, else `'defaultRoute'` if one is set, else `null`. */\n private matchRoute(pathname: string): string | null {\n const {prefixes} = this.compileRoutes();\n // Try the full path, then drop one '/segment' at a time; the first hit is the longest match.\n // Cutting only at '/' means '/products' matches '/products/42' but not '/products-old'.\n let candidateRoute = prefixes.get(pathname);\n let index = pathname.lastIndexOf('/');\n while (candidateRoute === undefined && index >= 0) {\n candidateRoute = prefixes.get(pathname.slice(0, index));\n index = index > 0 ? pathname.lastIndexOf('/', index - 1) : -1;\n }\n if (candidateRoute !== undefined) {return candidateRoute;}\n if (Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {\n return 'defaultRoute';\n }\n return null;\n }\n\n /** Length of the normalized path for a route key, used to slice the matched prefix off `data.url`; 0 for `defaultRoute`. */\n private routePrefixLength(route: string | null) {\n if (!route || route === 'defaultRoute') {return 0;}\n return (this.compileRoutes().prefixFor.get(route) ?? normalizeRouteKey(route)).length;\n }\n\n /** Parse a URL Router committed earlier. A path is prefixed with the origin first, so '//x' stays a path, not a host. */\n private parseStoredUrl(url: string) {\n if (!url.startsWith('/')) {return this.parseNavigationUrl(url);}\n try {\n return new URL(`${this.getOrigin()}${url}`);\n } catch (_error) {\n return null;\n }\n }\n\n /** Build the location object passed to route callbacks. Path segments that fail to decode are kept as-is. */\n private buildLocationData(url: string, route: string | null, mediatorData: NavigationData | undefined, parsedUrl: URL): LocationData {\n return {\n url,\n data: {\n url: parsedUrl.pathname\n .slice(this.routePrefixLength(route))\n .split('/')\n .filter(Boolean)\n .map((fragment) => {\n try {return decodeURIComponent(fragment);} catch (_error) {return fragment;}\n }),\n mediator: mediatorData,\n query: this.parseQueryString(parsedUrl.search)\n }\n };\n }\n\n /**\n * Attach listeners and dispatch the starting URL. Does not add a history entry or move focus.\n * @param url - Request URL on a server. In a browser, omit it to use `window.location`.\n * @returns This router.\n * @throws Whatever a guard or route callback throws.\n */\n initialize(url?: string) {\n this.addListeners();\n // The browser has already placed focus on the page it just loaded, so only later navigations move it.\n this.initialDispatch = true;\n try {\n this.navigate(url ?? this.getCurrentUrl(), {}, true);\n } finally {\n this.initialDispatch = false;\n }\n return this;\n }\n\n /**\n * Attach the click and popstate listeners (browser only) and the mediator listener. Safe to call more than once.\n * @returns This router.\n */\n addListeners() {\n if (this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n const navigationRoot = this.currentNavigationRoot || document;\n navigationRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = navigationRoot;\n window.addEventListener('popstate', this.boundPopState);\n }\n if (this.currentMediator) {\n this.currentMediator.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = this.currentMediator;\n }\n this.listenersInitialized = true;\n return this;\n }\n\n /**\n * Remove this router's listeners. Does not call the active route's `destroy()`.\n * @returns This router.\n */\n destroy() {\n this.removeListeners();\n return this;\n }\n\n /**\n * Remove the listeners `addListeners()` attached. Subclasses can override this to release more.\n * @returns This router.\n */\n removeListeners() {\n if (!this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = null;\n window.removeEventListener('popstate', this.boundPopState);\n }\n if (this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.listenersInitialized = false;\n return this;\n }\n\n /**\n * Click handler for `<a href data-pushstate>` links. Takes over only when Router can handle the link:\n * a plain left click on a same-origin link (no `download`, no other `target`) whose route is app-defined,\n * runnable, and allowed by its guard, and that is not just a `#fragment` of the current page. Anything else is left\n * to the browser.\n * @param e - The click event.\n * @returns This router if it handled the click, otherwise `true`.\n * @throws Whatever a guard or route callback throws.\n */\n eventPushStateClick(e: MouseEvent) {\n if (typeof window === 'undefined') {return true;}\n if (e.defaultPrevented || (e.button !== undefined && e.button !== 0) || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return true;\n }\n const eventTarget = e.target as Element | null;\n const source = eventTarget && (typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement);\n const anchor = source && typeof source.closest === 'function' ? source.closest('a[href][data-pushstate]') : null;\n if (!anchor) {return true;}\n const target = anchor.getAttribute('target');\n if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {return true;}\n // Read the attribute, because on an SVG <a> the href property is an SVGAnimatedString, not a URL.\n const href = anchor.getAttribute('href') ?? '';\n const baseUrl = typeof document !== 'undefined' && document.baseURI\n ? document.baseURI\n : window.location.href || `${window.location.origin}/`;\n let location: URL;\n try {location = new URL(href, baseUrl);} catch {return true;}\n if (location.origin !== window.location.origin) {return true;}\n // A link to a #fragment of the page already shown is native fragment navigation: let the browser scroll to it.\n // Its popstate then only updates `url`, matching the fragment-only back/forward rule.\n if (location.href.includes('#') && location.pathname === window.location.pathname && location.search === window.location.search) {\n return true;\n }\n const candidateRoute = this.matchRoute(location.pathname);\n const selected = candidateRoute ? this.routes[candidateRoute] : undefined;\n if (!selected || !this.isRunnableRoute(selected) || builtInDefaultRoutes.has(selected)) {return true;}\n // Guards are synchronous, so try the navigation first and cancel the browser's default only if Router took it.\n // A rejected (or throwing) guard therefore falls back to a normal page load. If the route committed and then\n // threw, the token has moved, so the default is still cancelled and the browser doesn't navigate twice.\n const token = this.navigationToken;\n let handled = false;\n try {\n handled = this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false) !== false;\n } finally {\n if (handled || this.navigationToken !== token) {e.preventDefault();}\n }\n return handled ? this : true;\n }\n\n /**\n * Popstate handler for back/forward. Runs the route for the new URL without adding a history entry.\n * A hash-only change just updates `url` and `locationData.url`.\n * @returns This router.\n */\n eventPopState() {\n if (typeof window === 'undefined') {return this;}\n const currentUrl = this.getCurrentUrl();\n // Moving between #fragments on the same page (footnotes, skip links) keeps the current view and focus.\n if (this.route !== null && withoutHash(currentUrl) === withoutHash(this.url)) {\n this.url = currentUrl;\n this.locationData = {...this.locationData, url: currentUrl};\n return this;\n }\n this.navigate(currentUrl, {}, true);\n return this;\n }\n\n /**\n * Decode a query string into a plain object. For repeated keys the last value wins.\n * @param queryString - Query string, with or without the leading `?`.\n */\n parseQueryString(queryString: string) {\n return Object.fromEntries(new URLSearchParams(queryString));\n }\n\n /**\n * Rebuild `locationData` from the current `url` and `route`. Leaves it unchanged if the URL can't be parsed.\n * @param mediatorData - Value to expose as `locationData.data.mediator`.\n */\n setLocationData(mediatorData?: NavigationData) {\n const parsedUrl = this.parseStoredUrl(this.url || '/');\n if (!parsedUrl) {return;}\n this.locationData = this.buildLocationData(this.url, this.route, mediatorData, parsedUrl);\n }\n\n /**\n * Record the route's title in `pageTitle` and, in a browser, set `document.title` and move focus to the route's\n * `focus` selector (default `main h1`), adding `tabindex=\"-1\"` if needed. `focus: false` skips focus.\n * @param route - The route that just ran.\n * @returns This router.\n */\n applyPageContext(route: Route) {\n if (typeof route === 'function') {\n this.pageTitle = null;\n return this;\n }\n this.pageTitle = typeof route.title === 'string' ? route.title : null;\n if (!this.isBrowserRuntime()) {return this;}\n if (this.pageTitle) {document.title = this.pageTitle;}\n if (route.focus !== false && !this.suppressFocus) {\n const target = document.querySelector<HTMLElement>(route.focus || 'main h1');\n if (target) {\n if (!target.hasAttribute('tabindex')) {target.setAttribute('tabindex', '-1');}\n target.focus();\n }\n }\n return this;\n }\n\n /**\n * Navigate to a URL: match the route, run its guard, commit the new state, call the previous route's `destroy()`,\n * run the new route, apply title and focus, then push a history entry (browser only, unless `isPopState`).\n * If route code starts another navigation part-way through, that one wins and this call stops early.\n * @param url - Destination URL. Omit to re-run the last committed URL (or the current browser URL / `/` at first).\n * @param mediatorData - Value exposed to the route as `location.data.mediator`.\n * @param isPopState - `true` to skip adding a history entry.\n * @returns This router, or `false` if the navigation was rejected (bad or cross-origin URL, no matching route,\n * guard refused, or nothing to run). A rejected navigation changes nothing.\n * @throws Whatever a guard or route callback throws. A throwing guard changes nothing; a throwing route stays committed.\n */\n navigate(url?: string, mediatorData?: NavigationData, isPopState = false) {\n const skipFocus = this.initialDispatch;\n this.initialDispatch = false;\n if (url !== undefined && typeof url !== 'string') {return false;}\n\n const candidateInput = url !== undefined ? url : this.url;\n let parsedUrl: URL | null;\n if (!candidateInput) {\n parsedUrl = this.parseNavigationUrl(this.getCurrentUrl());\n } else {\n parsedUrl = url === undefined ? this.parseStoredUrl(candidateInput) : this.parseNavigationUrl(candidateInput);\n }\n if (!parsedUrl) {return false;}\n if (this.isBrowserRuntime() && parsedUrl.origin !== window.location.origin) {return false;}\n\n // Store only path, query, and hash, even when a server passed an absolute URL.\n const candidateUrl = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;\n const candidateRoute = this.matchRoute(parsedUrl.pathname);\n if (!candidateRoute) {return false;}\n\n const candidateLocationData = this.buildLocationData(candidateUrl, candidateRoute, mediatorData, parsedUrl);\n const selected = this.routes[candidateRoute];\n if (!selected || (typeof selected !== 'function' && selected.secure && selected.secure(this.scope, candidateLocationData) !== true) ||\n !this.isRunnableRoute(selected)) {\n return false;\n }\n\n const token = ++this.navigationToken;\n this.url = candidateUrl;\n this.route = candidateRoute;\n this.locationData = candidateLocationData;\n // Clear the stored teardown before calling it, so a navigation started inside destroy() can't call it again,\n // and a destroy() that throws is not retried: its error propagates with the destination committed but not run.\n const previous = this.activeDestroy;\n this.activeDestroy = undefined;\n if (previous) {\n previous.callback.call(previous.receiver, this.scope, candidateLocationData);\n if (token !== this.navigationToken) {return this;}\n }\n const selectedView = typeof selected !== 'function' && typeof selected.view === 'object'\n ? selected.view\n : undefined;\n // Store the teardown before initialize() runs, so it still runs later if initialize() throws or navigates away.\n this.activeDestroy = selectedView?.destroy\n ? {receiver: selectedView, callback: selectedView.destroy}\n : undefined;\n if (typeof selected === 'function') {\n selected(this.scope, candidateLocationData);\n } else if (typeof selected.view === 'function') {\n selected.view(this.scope, candidateLocationData);\n } else {\n selectedView!.initialize!(this.scope, candidateLocationData);\n }\n // If initialize() started another navigation, that one has already applied its own title, focus, and history.\n if (token !== this.navigationToken) {return this;}\n this.suppressFocus = skipFocus;\n try {\n this.applyPageContext(selected);\n } finally {\n this.suppressFocus = false;\n }\n if (token !== this.navigationToken) {return this;}\n this.previousRoute = this.route;\n // Re-running the current URL (no `url` given, e.g. a mediator event without a destination) must not add a\n // duplicate history entry for the address already shown.\n if (!isPopState && this.isBrowserRuntime() && !(url === undefined && parsedUrl.href === window.location.href)) {\n window.history.pushState(this.url, this.pageTitle || '', parsedUrl.href);\n }\n return this;\n }\n}\n\n/** Types for route definitions, location data, and navigation data. */\nnamespace Router {\n export type Handler = RouteHandler;\n export type Route = RouteHandler | RouteObject;\n export type Location = LocationData;\n export type Navigation = NavigationData;\n}\nexport = Router;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "white-label-router",
3
- "version": "6.1.1",
3
+ "version": "6.2.0",
4
4
  "description": "Framework-independent TypeScript router for progressive enhancement in browser, server-rendered, and Node.js applications, with scoped links, guards, and accessible navigation.",
5
5
  "sideEffects": false,
6
6
  "main": "dist/index.js",
@@ -54,21 +54,25 @@
54
54
  "audit": "npm audit --audit-level=high",
55
55
  "typecheck": "tsc -p tsconfig.json --noEmit",
56
56
  "test:unit": "node --test test/*.test.js",
57
+ "test:browser": "node --test test/browser/*.test.js",
57
58
  "coverage": "node --run build && node --run test:types && c8 node --run test:unit",
59
+ "benchmark": "node benchmark/router.bench.js",
60
+ "bench:ci": "node benchmark/router.bench.js --ci",
58
61
  "prepare": "node --run build",
59
62
  "test:types": "tsc -p tsconfig.consumer.json",
60
- "lint": "eslint \"src/**/*.ts\" --max-warnings=0",
63
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.js\" \"benchmark/**/*.js\" --max-warnings=0",
61
64
  "lint:fix": "node --run lint -- --fix"
62
65
  },
63
66
  "devDependencies": {
64
- "@types/node": "24.13.3",
65
- "c8": "12.0.0",
66
- "typescript": "7.0.2",
67
67
  "@babel/core": "8.0.5",
68
68
  "@babel/eslint-parser": "8.0.5",
69
69
  "@babel/preset-typescript": "8.0.1",
70
70
  "@eslint/js": "10.0.1",
71
- "eslint": "10.10.0"
71
+ "@types/node": "24.13.3",
72
+ "c8": "12.0.0",
73
+ "eslint": "10.10.0",
74
+ "playwright": "1.64.1",
75
+ "typescript": "7.0.2"
72
76
  },
73
77
  "types": "dist/index.d.ts",
74
78
  "c8": {
@@ -89,7 +93,9 @@
89
93
  "branches": 100,
90
94
  "statements": 100,
91
95
  "perFile": true,
92
- "exclude": ["**/*.d.ts"]
96
+ "exclude": [
97
+ "**/*.d.ts"
98
+ ]
93
99
  },
94
100
  "exports": {
95
101
  ".": {