@zvndev/darkmown 2.12.1 → 2.12.3

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/src/cli.js CHANGED
@@ -16,7 +16,8 @@ import {
16
16
  devClientPath,
17
17
  devClientScript,
18
18
  devEventsPath,
19
- injectDevClient
19
+ injectDevClient,
20
+ refuseLocalRequest
20
21
  } from "./dev.js";
21
22
  import { createDispatcher, serveMcp } from "./mcp.js";
22
23
  import { mcpConfig } from "./mcp-tools.js";
@@ -421,6 +422,7 @@ function startDevServer({ cwd, log, warn, error }) {
421
422
 
422
423
  const apiDir = path.join(cwd, "api");
423
424
  const server = http.createServer((req, res) => {
425
+ if (refuseForeign(req, res, host)) return;
424
426
  const url = req.url || "/";
425
427
  if (url.split("?")[0] === devEventsPath) {
426
428
  res.writeHead(200, {
@@ -486,6 +488,25 @@ function startDevServer({ cwd, log, warn, error }) {
486
488
  });
487
489
  }
488
490
 
491
+ /**
492
+ * Answer 403 when `refuseLocalRequest` rejects the request (DNS rebinding or a
493
+ * cross-site call into `api/`). Shared by `dev` and `serve`.
494
+ * @param {import("node:http").IncomingMessage} req
495
+ * @param {import("node:http").ServerResponse} res
496
+ * @param {string} bindHost
497
+ * @returns {boolean} true when the request was refused and answered
498
+ */
499
+ function refuseForeign(req, res, bindHost) {
500
+ const reason = refuseLocalRequest(req, {
501
+ bindHost,
502
+ allowedHosts: process.env.DARKMOWN_ALLOWED_HOSTS || ""
503
+ });
504
+ if (!reason) return false;
505
+ res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
506
+ res.end(`${reason}\n`);
507
+ return true;
508
+ }
509
+
489
510
  /**
490
511
  * Start the static preview server for an existing `dist/`. Throws `CliError`
491
512
  * when there is nothing built to serve.
@@ -510,6 +531,7 @@ function startPreviewServer({ cwd, log, error }) {
510
531
  }
511
532
  const apiDir = path.join(cwd, "api");
512
533
  const server = http.createServer((req, res) => {
534
+ if (refuseForeign(req, res, host)) return;
513
535
  const acceptEncoding = req.headers["accept-encoding"];
514
536
  // Serverless parity, exactly as in dev: run a project api/*.js function when
515
537
  // the path matches; otherwise serve static (compressed when negotiated).
@@ -67,8 +67,9 @@ const dateOnlyTZ = (v) =>
67
67
  export const AGGREGATES = {
68
68
  sum: (list, f) => nums(list, f || "").reduce((a, n) => a + n, 0),
69
69
  avg: (list, f) => (list.length ? AGGREGATES.sum(list, f) / list.length : 0),
70
- min: (list, f) => (list.length ? Math.min(...nums(list, f || "")) : 0),
71
- max: (list, f) => (list.length ? Math.max(...nums(list, f || "")) : 0),
70
+ // reduce, not spread: `Math.max(...list)` throws RangeError past ~100k items.
71
+ min: (list, f) => (list.length ? nums(list, f || "").reduce((a, n) => Math.min(a, n)) : 0),
72
+ max: (list, f) => (list.length ? nums(list, f || "").reduce((a, n) => Math.max(a, n)) : 0),
72
73
  count: (list) => list.length
73
74
  };
74
75
 
@@ -56,7 +56,8 @@ export function parseFrontmatter(raw, file) {
56
56
  const quotedKeys = new Set();
57
57
  for (const line of front.split("\n")) {
58
58
  const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
59
- if (!match) continue;
59
+ // `__proto__: [a]` would swap meta's prototype rather than set a key.
60
+ if (!match || match[1] === "__proto__") continue;
60
61
  const parsed = parseFrontmatterValue(match[2]);
61
62
  meta[match[1]] = parsed.value;
62
63
  if (parsed.quoted) quotedKeys.add(match[1]);
@@ -11,6 +11,7 @@ import { highlightCode } from "../highlight.js";
11
11
  import { LOOP_META, lineOf, recordRead, wdError } from "./context.js";
12
12
  import { applyPipeline, fmtAttr, validatePipes } from "./format.js";
13
13
  import { imageSize } from "./image-size.js";
14
+ import { isAllowedInclude } from "./includes.js";
14
15
  import {
15
16
  escapeHtml,
16
17
  getPath,
@@ -487,11 +488,14 @@ function buildSrcset(src, ctx) {
487
488
  return null;
488
489
  }
489
490
  const reader = ctx.comp.reader;
491
+ // Same sandbox as measureImage/@include: a data-driven `/../../x.png` must not
492
+ // probe (or read the header of) files outside site/pages and site/_.
493
+ if (!isAllowedInclude(path.join(root, `${relBase}${ext}`), ctx.context, reader)) return null;
490
494
  /** @type {{ href: string, width: number }[]} */
491
495
  const candidates = [];
492
496
  for (const width of VARIANT_WIDTHS) {
493
497
  const abs = path.join(root, `${relBase}@${width}${ext}`);
494
- if (!reader.exists(abs)) continue;
498
+ if (!isAllowedInclude(abs, ctx.context, reader) || !reader.exists(abs)) continue;
495
499
  candidates.push({ href: `${base}@${width}${ext}`, width: measureWidth(abs, reader) ?? width });
496
500
  }
497
501
  // Builder-generated variants (2.9.0): resized into dist by the pre-pass, so
@@ -681,8 +685,24 @@ function rawHtmlPlugin(mdInstance) {
681
685
  // happened to resolve to. Escaping is no defense here: `href` and friends
682
686
  // resolve `javascript:` without any quote breakout, and unlike a markdown
683
687
  // destination nothing upstream vets an author's own raw `<a href="…">`.
684
- const URL_ATTR_TAIL =
685
- /\b(?:href|src|action|formaction|xlink:href)\s*=\s*(?:"([^"]*)|'([^']*)|([^\s>"']*))$/i;
688
+ const URL_ATTRS = new Set([
689
+ "href",
690
+ "src",
691
+ "action",
692
+ "formaction",
693
+ "xlink:href",
694
+ "data",
695
+ "poster",
696
+ "cite",
697
+ "background",
698
+ "ping"
699
+ ]);
700
+
701
+ // The tag a fragment is inside of, as far as it has been written (no `>` yet),
702
+ // and within it the attribute whose value is open: name, then the value so far
703
+ // as group 2 ("…), 3 ('…) or 4 (unquoted).
704
+ const OPEN_TAG = /<[A-Za-z][^<>]*$/;
705
+ const OPEN_ATTR = /\s([^\s"'<>/=]+)\s*=\s*(?:"([^"]*)|'([^']*)|([^\s"'=<>`]*))$/;
686
706
 
687
707
  /**
688
708
  * Warn that a value in URL position carries a scheme the browser would execute.
@@ -698,6 +718,22 @@ function warnUnsafeUrl(ctx, index, expr) {
698
718
  );
699
719
  }
700
720
 
721
+ /**
722
+ * Warn that a value landed where HTML escaping cannot contain it (an event
723
+ * handler, `srcdoc`, or bare tag position), so it was dropped.
724
+ * @param {Ctx} ctx
725
+ * @param {number} index 0-based line index, for `file:line`.
726
+ * @param {string} expr
727
+ * @param {string} where
728
+ * @returns {void}
729
+ */
730
+ function warnUnsafeSlot(ctx, index, expr, where) {
731
+ ctx.comp.warnings.push(
732
+ `${ctx.file}:${lineOf(ctx, index)}: "{ ${expr} }" sits ${where}, where escaping cannot stop a value from running as code, so it is emitted empty. ` +
733
+ `Put the value in a quoted attribute such as data-*, or render it as text, and read it from a colocated .js file.`
734
+ );
735
+ }
736
+
701
737
  /**
702
738
  * Substitute every `{ expr }` in a raw-HTML fragment with its escaped value.
703
739
  *
@@ -736,14 +772,32 @@ function fillRawHtml(html, ctx, index) {
736
772
  continue;
737
773
  }
738
774
  if (found.kind === "state") warnUnbindable(ctx, index, expr, true);
739
- const open = URL_ATTR_TAIL.exec(out);
740
- const head = open ? (open[1] ?? open[2] ?? open[3]) : "";
741
- if (open && unsafeUrlValue(head + found.text)) {
775
+ // Escaping only holds inside a quoted attribute or text. In a script-valued
776
+ // attribute the browser entity-decodes `&#39;` back into a live quote, and
777
+ // in bare tag position (`<div { x }>`) the value IS markup.
778
+ const tag = OPEN_TAG.exec(out);
779
+ const open = tag && OPEN_ATTR.exec(tag[0]);
780
+ const name = open ? open[1].toLowerCase() : "";
781
+ if (tag && !open) {
782
+ warnUnsafeSlot(ctx, index, expr, "inside a tag but outside any attribute value");
783
+ continue;
784
+ }
785
+ if (name.startsWith("on") || name === "srcdoc") {
786
+ warnUnsafeSlot(ctx, index, expr, `in the ${name}= attribute`);
787
+ continue;
788
+ }
789
+ const head = open ? (open[2] ?? open[3] ?? open[4]) : "";
790
+ if (URL_ATTRS.has(name) && unsafeUrlValue(head + found.text)) {
742
791
  warnUnsafeUrl(ctx, index, expr);
743
792
  out = out.slice(0, out.length - head.length); // the head belongs to the refused value
744
793
  continue;
745
794
  }
746
- out += escapeHtml(found.text);
795
+ // An unquoted value ends at whitespace, so `x onmouseover=…` would open a
796
+ // new attribute. Entities keep it one value and decode back to the text.
797
+ out +=
798
+ open?.[4] !== undefined
799
+ ? escapeHtml(found.text).replace(/[\s=`]/g, (c) => `&#${c.charCodeAt(0)};`)
800
+ : escapeHtml(found.text);
747
801
  }
748
802
  return out + html.slice(last);
749
803
  }
@@ -183,25 +183,22 @@ export function compilePage(file, context, options = {}) {
183
183
  // (graceful, transition-free fallback) and only applies to same-origin
184
184
  // navigations where both the outgoing and incoming page opt in.
185
185
  //
186
- // The default is a quick out-then-in: the outgoing snapshot fades to 0 in
187
- // 90ms, the incoming one fades in after an 80ms delay. A simultaneous
188
- // cross-fade was measured on a real site and read as flicker: the incoming
189
- // snapshot stacks over the outgoing one, so image-heavy pages double-expose
190
- // (outgoing opaque, incoming ghosting through). Sequencing keeps at most a
191
- // ~10ms overlap near background opacity, so nothing ever ghosts. The delay
192
- // fills `both`, keeping the incoming snapshot invisible until its turn.
186
+ // The default is no animation at all: both snapshots drop and swap at
187
+ // commit. Three animated defaults were field-tested on a real site and each
188
+ // failed somewhere: the directional slide read as a bounce, the simultaneous
189
+ // cross-fade double-exposed (the incoming snapshot stacks over the outgoing
190
+ // one), and the sequenced out-then-in was ignored by a shipping browser in
191
+ // favor of its own cross-fade. View-transition animation compositing varies
192
+ // too much across engines to ship as a default. Zero animation is the only
193
+ // choice with no possible artifact, and the speculation-rules prerender
194
+ // below is what makes navigation feel instant anyway. Authors who want a
195
+ // fade can add ::view-transition-old/new rules in their own skin.
193
196
  const wantTransitions =
194
197
  compiled.meta.transitions === true || compiled.meta.transitions === "true";
195
198
  const transitions = wantTransitions
196
199
  ? `\n <style>
197
200
  @view-transition { navigation: auto; }
198
- ::view-transition-old(root) { animation: wd-nav-out 90ms cubic-bezier(0.4, 0, 0.2, 1) both; }
199
- ::view-transition-new(root) { animation: wd-nav-in 130ms cubic-bezier(0.4, 0, 0.2, 1) 80ms both; }
200
- @keyframes wd-nav-out { to { opacity: 0; } }
201
- @keyframes wd-nav-in { from { opacity: 0; } }
202
- @media (prefers-reduced-motion: reduce) {
203
- ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 1ms; animation-delay: 0ms; }
204
- }
201
+ ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
205
202
  </style>`
206
203
  : "";
207
204
 
package/src/dev.js CHANGED
@@ -1,8 +1,71 @@
1
+ import { isIP } from "node:net";
1
2
  import { escapeHtml } from "./compiler.js";
2
3
 
3
4
  export const devClientPath = "/__wd/dev-client.js";
4
5
  export const devEventsPath = "/__wd/dev-events";
5
6
 
7
+ /** @param {string} h */
8
+ const isLoopbackName = (h) =>
9
+ h === "localhost" || h.endsWith(".localhost") || h === "::1" || /^127\.\d+\.\d+\.\d+$/.test(h);
10
+
11
+ /** `Host`/`Origin` → bare lowercase hostname (`[::1]:5173` → `::1`), or null. @param {string} value */
12
+ function hostnameOf(value) {
13
+ try {
14
+ return new URL(value.includes("://") ? value : `http://${value}`).hostname
15
+ .toLowerCase()
16
+ .replace(/^\[|\]$/g, "");
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * The local dev/preview servers bind to loopback, but loopback alone does not
24
+ * keep a website you are visiting out. Two attacks reach it from a browser:
25
+ *
26
+ * - DNS rebinding: `evil.example` re-resolves to 127.0.0.1, after which the
27
+ * page can READ every response (drafts, the dep map, the SSE stream, and
28
+ * `api/*` handlers, which run with the full `process.env`). The browser still
29
+ * sends `Host: evil.example`, so only hostnames that cannot be rebound pass:
30
+ * `localhost`/`*.localhost`, IP literals (a rebinding needs a domain), the
31
+ * bind address, and the `DARKMOWN_ALLOWED_HOSTS` list (a tunnel's domain).
32
+ * - Cross-site requests: any page can fire a simple POST at
33
+ * `http://127.0.0.1:5173/api/x` without reading the reply, so the handler's
34
+ * side effects run. `/api/*` therefore also refuses `Sec-Fetch-Site:
35
+ * cross-site` and an `Origin` that is not loopback, the request's own host,
36
+ * or allow-listed.
37
+ *
38
+ * `DARKMOWN_ALLOWED_HOSTS=*` turns the guard off.
39
+ * @param {{ url?: string, headers: Record<string, string | string[] | undefined> }} req
40
+ * @param {{ bindHost?: string, allowedHosts?: string }} [options]
41
+ * @returns {string | null} why the request is refused, or null to serve it
42
+ */
43
+ export function refuseLocalRequest(req, { bindHost = "", allowedHosts = "" } = {}) {
44
+ const allowed = allowedHosts
45
+ .split(",")
46
+ .map((h) => hostnameOf(h.trim()) ?? "")
47
+ .filter(Boolean);
48
+ if (allowedHosts.split(",").some((h) => h.trim() === "*")) return null;
49
+ const bind = hostnameOf(bindHost) ?? "";
50
+ const host = hostnameOf(String(req.headers.host ?? ""));
51
+ if (!host || !(isLoopbackName(host) || isIP(host) || host === bind || allowed.includes(host))) {
52
+ return `Host "${req.headers.host ?? ""}" is not allowed. Set DARKMOWN_ALLOWED_HOSTS to permit it.`;
53
+ }
54
+ if (!(req.url || "/").startsWith("/api/")) return null;
55
+ if (req.headers["sec-fetch-site"] === "cross-site") return "Cross-site request to /api/ refused.";
56
+ const origin = req.headers.origin;
57
+ if (origin !== undefined) {
58
+ const from = origin === "null" ? null : hostnameOf(String(origin));
59
+ if (
60
+ !from ||
61
+ !(isLoopbackName(from) || from === host || from === bind || allowed.includes(from))
62
+ ) {
63
+ return `Origin "${origin}" may not call /api/ on this server.`;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+
6
69
  /**
7
70
  * Serialize the dev server's rebuilds: at most ONE child build runs at a time.
8
71
  * Every build read-modify-writes the dependency map (`dist/.wd-dev-deps.json`),
package/src/feeds.js CHANGED
@@ -27,14 +27,23 @@ import { wdError } from "./compiler/context.js";
27
27
  * @property {string} lastmod ISO date (`yyyy-mm-dd`).
28
28
  */
29
29
 
30
+ // Characters XML 1.0 cannot carry (see escapeXml).
31
+ const XML_INVALID =
32
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: these are exactly the characters being removed.
33
+ /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
34
+
30
35
  /**
31
36
  * XML-escape a text value for use inside an element or attribute. Covers the
32
- * five predefined XML entities — the same set RSS/sitemap consumers expect.
37
+ * five predefined XML entities — the same set RSS/sitemap consumers expect —
38
+ * and drops the characters XML 1.0 cannot carry at all (C0 controls other than
39
+ * tab/LF/CR, U+FFFE/U+FFFF, lone surrogates): one in a single post title makes
40
+ * the whole feed malformed, and readers reject it outright.
33
41
  * @param {string} value
34
42
  * @returns {string}
35
43
  */
36
44
  export function escapeXml(value) {
37
45
  return String(value)
46
+ .replace(XML_INVALID, "")
38
47
  .replaceAll("&", "&amp;")
39
48
  .replaceAll("<", "&lt;")
40
49
  .replaceAll(">", "&gt;")
@@ -174,14 +183,20 @@ export function firstParagraph(body) {
174
183
  /** @type {string[]} */
175
184
  const collected = [];
176
185
  for (let j = i; j < lines.length && lines[j].trim(); j++) collected.push(lines[j].trim());
177
- return collected
178
- .join(" ")
179
- .replace(/`([^`]+)`/g, "$1") // inline code → its text
180
- .replace(/\*\*([^*]+)\*\*/g, "$1") // bold
181
- .replace(/\*([^*]+)\*/g, "$1") // italic
182
- .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // [text](url) → text
183
- .replace(/\s+/g, " ")
184
- .trim();
186
+ return (
187
+ collected
188
+ .join(" ")
189
+ .replace(/`([^`]+)`/g, "$1") // inline code → its text
190
+ .replace(/\*\*([^*]+)\*\*/g, "$1") // bold
191
+ .replace(/\*([^*]+)\*/g, "$1") // italic
192
+ // [text](url) → text. The classes also exclude the OPENING bracket/paren:
193
+ // with `[^\]]*` a body of 200k `[` rescans to the end from every `[`
194
+ // (quadratic, 17 s per call), and any one collection author can write it.
195
+ // One level of parens is kept for URLs like `/wiki/Foo_(bar)`.
196
+ .replace(/\[([^[\]]*)\]\([^()]*(?:\([^()]*\)[^()]*)*\)/g, "$1")
197
+ .replace(/\s+/g, " ")
198
+ .trim()
199
+ );
185
200
  }
186
201
 
187
202
  /**
package/src/headers.js CHANGED
@@ -147,14 +147,20 @@ export function routeCsp(reactive, embedHosts = []) {
147
147
  }
148
148
 
149
149
  /**
150
- * Baseline security headers applied to every HTML response, independent of the
150
+ * Baseline security headers applied to every response, independent of the
151
151
  * CSP. CSP is added per-route (static vs reactive) by the helpers below.
152
+ *
153
+ * HSTS is one year WITHOUT `includeSubDomains`/`preload`: those pin every
154
+ * sibling subdomain to HTTPS, which only the domain owner can safely decide.
155
+ * Browsers ignore the header over plain HTTP, so local `serve` is unaffected.
156
+ * Cloudflare custom domains send no HSTS unless the zone enables it.
152
157
  * @type {Record<string, string>}
153
158
  */
154
159
  export const BASE_SECURITY_HEADERS = {
155
160
  "X-Content-Type-Options": "nosniff",
156
161
  "Referrer-Policy": "strict-origin-when-cross-origin",
157
- "X-Frame-Options": "SAMEORIGIN"
162
+ "X-Frame-Options": "SAMEORIGIN",
163
+ "Strict-Transport-Security": "max-age=31536000"
158
164
  };
159
165
 
160
166
  /**
@@ -33,7 +33,12 @@ export async function runJobsFile(jobsFile, resultsFile) {
33
33
  /** @type {any} */
34
34
  let sharp;
35
35
  try {
36
- sharp = (await import(process.env.WD_SHARP_MODULE || "sharp")).default;
36
+ // The env override is a test seam pointed at a stub FILE; a dynamic import
37
+ // needs a file URL on Windows ("D:\..." parses as a scheme). Plain module
38
+ // specifiers (like a deliberately nonexistent package name) pass through.
39
+ const spec = process.env.WD_SHARP_MODULE;
40
+ const target = spec && /[/\\]/.test(spec) ? pathToFileURL(spec).href : spec || "sharp";
41
+ sharp = (await import(target)).default;
37
42
  } catch {
38
43
  write({ unavailable: true, failed: [] });
39
44
  return;
package/src/router.js CHANGED
@@ -41,7 +41,10 @@ export function discoverRoutes(routesRoot, options = {}) {
41
41
  /** @param {string} dir */
42
42
  function walk(dir) {
43
43
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
44
- if (isHiddenName(entry.name)) continue;
44
+ // A symlinked page would compile (and, with md sources on, publish) a
45
+ // file from anywhere on the machine: CI keys, a contributor's `.env`.
46
+ // Assets/images/fonts already skip symlinks; pages must too.
47
+ if (isHiddenName(entry.name) || entry.isSymbolicLink()) continue;
45
48
  const abs = path.join(dir, entry.name);
46
49
  if (entry.isDirectory()) {
47
50
  walk(abs);
package/src/runtime.js CHANGED
@@ -158,8 +158,8 @@ window.addEventListener("popstate", () => {
158
158
  window.addEventListener("storage", (event) => {
159
159
  const name = event.key && event.key.startsWith("wd:store:") ? event.key.slice(9) : "";
160
160
  if (!storeKeys.has(name) || event.newValue == null) return;
161
- let next;
162
- try { next = JSON.parse(event.newValue); } catch { return; }
161
+ const next = jsonOr(event.newValue);
162
+ if (next === undefined) return;
163
163
  if (JSON.stringify(next) === JSON.stringify(state[name])) return;
164
164
  state[name] = next;
165
165
  render();
@@ -225,11 +225,17 @@ function isEmpty(v) {
225
225
  * Parse JSON, or undefined when the text is not JSON. `undefined` is a safe
226
226
  * sentinel: no JSON document parses to it, so a body of literal `null` is still
227
227
  * distinguishable from "not JSON at all".
228
+ *
229
+ * Every UNTRUSTED document (a fetch body, a `from-url` param, localStorage, a
230
+ * cross-tab storage event) enters through here. The reviver drops own
231
+ * `toString`/`valueOf` keys: `{"toString":1}` is an object `String()` cannot
232
+ * convert, so it would throw on every render that paints it, and a persisted
233
+ * copy would keep the page dead after a reload of the clean URL.
228
234
  * @param {string} text
229
235
  * @returns {any}
230
236
  */
231
237
  function jsonOr(text) {
232
- try { return JSON.parse(text); } catch { return undefined; }
238
+ try { return JSON.parse(text, (k, v) => (k === "toString" || k === "valueOf" ? undefined : v)); } catch { return undefined; }
233
239
  }
234
240
 
235
241
  // The characters a value must not carry into a URL, mirroring `DEST_UNSAFE`
@@ -334,8 +340,9 @@ const FMT = {
334
340
  default: (v, [fb = ""]) => (v == null || v === "" || (Array.isArray(v) && !v.length) ? String(fb) : v),
335
341
  sum: (v, [f]) => fmtNums(fmtList(v), f).reduce((a, n) => a + n, 0),
336
342
  avg: (v, [f]) => { const l = fmtList(v); return l.length ? FMT.sum(l, [f]) / l.length : 0; },
337
- min: (v, [f]) => { const l = fmtList(v); return l.length ? Math.min(...fmtNums(l, f)) : 0; },
338
- max: (v, [f]) => { const l = fmtList(v); return l.length ? Math.max(...fmtNums(l, f)) : 0; },
343
+ // reduce, not spread: `Math.max(...list)` throws RangeError past ~100k items.
344
+ min: (v, [f]) => { const l = fmtList(v); return l.length ? fmtNums(l, f).reduce((a, n) => Math.min(a, n)) : 0; },
345
+ max: (v, [f]) => { const l = fmtList(v); return l.length ? fmtNums(l, f).reduce((a, n) => Math.max(a, n)) : 0; },
339
346
  count: (v) => fmtList(v).length,
340
347
  join: (v, [sep = ", ", f]) => fmtList(v).map((r) => fmtPick(r, f)).join(String(sep))
341
348
  };
@@ -1080,7 +1087,10 @@ function startFetch(node) {
1080
1087
  let missing = false;
1081
1088
  const url = g("url").replace(/\{\s*([\w$.]+)\s*\}/g, (_, p) => {
1082
1089
  const v = getPath(state, p);
1083
- if (v == null || v === "") missing = true;
1090
+ // `.`/`..` survive encodeURIComponent and the URL parser resolves them, so
1091
+ // `?id=..` would retarget `/api/users/{ id }/x` (headers and body included)
1092
+ // at `/api/x`. Treat them as missing, like an empty value.
1093
+ if (v == null || v === "" || v === "." || v === "..") missing = true;
1084
1094
  return encodeURIComponent(String(v ?? ""));
1085
1095
  });
1086
1096
  if (missing) return set("_loading", false);
@@ -1,2 +1,2 @@
1
- var a={},ft=new Set,nt=new Set,T=new Set,M={};function J(t,e){try{return e===void 0?localStorage.getItem(t):e===null?localStorage.removeItem(t):localStorage.setItem(t,e)}catch{return null}}var X=t=>t.replace(":",".");function mt(){for(let t of document.querySelectorAll("script[data-wd-state],script[data-wd-store]")){let e=t.getAttribute("data-wd-store"),n=JSON.parse(t.textContent||"null");for(let[r,o]of e?[[e,n]]:Object.entries(n||{})){if(r in a)continue;if(a[r]=structuredClone(M[r]=o),e?!t.hasAttribute("data-wd-store-ephemeral"):t.getAttribute("data-wd-persist")===r){(e?nt:ft).add(r);let c=(e?"wd:store:":"wd:")+r,i=J(c);i===null?e&&J(c,JSON.stringify(o??null)):D(i)===void 0?J(c,null):a[r]=D(i)}t.getAttribute("data-wd-url")===r&&(T.add(r),pt(!1,[r]))}}}function xt(){if(!T.size)return;let t=new URLSearchParams(location.search);for(let r of T){let o=a[r];JSON.stringify(o??null)===JSON.stringify(M[r]??null)?t.delete(X(r)):t.set(X(r),typeof o=="string"?o:JSON.stringify(o))}let e=t.toString(),n=location.pathname+(e?`?${e}`:"")+location.hash;if(n!==location.pathname+location.search+location.hash)try{history.replaceState(history.state,"",n)}catch{}}function pt(t,e){let n=new URLSearchParams(location.search);for(let r of e||T){let o=n.get(X(r)),s=M[r];o==null?t&&(a[r]=structuredClone(s)):a[r]=typeof s=="string"?o:D(o)??o}}function b(){for(let t of ft)J(`wd:${t}`,JSON.stringify(a[t]??null));for(let t of nt)J(`wd:store:${t}`,JSON.stringify(a[t]??null));xt()}mt();window.addEventListener("popstate",()=>{T.size&&(pt(!0),b(),g())});window.addEventListener("storage",t=>{let e=t.key&&t.key.startsWith("wd:store:")?t.key.slice(9):"";if(!nt.has(e)||t.newValue==null)return;let n;try{n=JSON.parse(t.newValue)}catch{return}JSON.stringify(n)!==JSON.stringify(a[e])&&(a[e]=n,g())});var Y=t=>t==="__proto__"||t==="constructor"||t==="prototype";function f(t,e){if(!e)return t;let n=t;for(let r of e.split(".")){if(n==null||Y(r))return;n=n[r]}return n}function Ct(t,e,n){let r=e.split("."),o=r.pop()||"",s=t;for(let c of r){if(Y(c))return;(s[c]==null||typeof s[c]!="object")&&(s[c]={}),s=s[c]}Y(o)||(s[o]=n)}function Jt(t){return t==null||typeof t=="object"&&(Array.isArray(t)?t.length:Object.keys(t).length)===0}function D(t){try{return JSON.parse(t)}catch{return}}var Tt=/[\u0000-\u0020()<>"'`\\\u007F]/g,I=(t,e)=>t.matches(e)?[t,...t.querySelectorAll(e)]:[...t.querySelectorAll(e)];function wt(t,e,n,r){if(!e)return;let o;try{o=JSON.parse(e)}catch{return}let s="",c="";for(let i=1;i<o.length;i++){let u=o[i];if(typeof u=="string"){s+=u,c+=u;continue}let l=String((u[0]==="s"?f(a[u[1]],u[2]):u[0]==="m"?r[u[1]]:f(n,u[1]))??"");c+=l,s+=l.replace(Tt,m=>`%${m.charCodeAt(0).toString(16).toUpperCase().padStart(2,"0")}`)}o[0]!=="href"&&o[0]!=="src"||t.setAttribute(o[0],/^(javascript|data|vbscript):/i.test(c.replace(/[\u0000-\u0020\u007F]+/g,""))?"":s)}var q=t=>{let e=typeof t=="number"?t:Number.parseFloat(t);return Number.isFinite(e)?e:0},_=t=>Array.isArray(t)?t:t==null?[]:[t],gt=(t,e)=>e!=null&&e!==""?t?.[e]:t,z=(t,e)=>t.map(n=>q(gt(n,e))),B=t=>{if(t instanceof Date)return t;if(typeof t=="number")return new Date(t);let e=String(t).trim();return/^-?\d+$/.test(e)?new Date(Number(e)):new Date(e)},W=(t,e,n)=>Number.isNaN(t.getTime())?String(n):new Intl.DateTimeFormat(void 0,e).format(t),H=t=>typeof t=="string"&&/^\d{4}-\d{2}-\d{2}$/.test(t.trim())?{timeZone:"UTC"}:void 0,F={money:(t,[e="USD",n])=>new Intl.NumberFormat(n||void 0,{style:"currency",currency:String(e||"USD")}).format(q(t)),number:(t,[e])=>new Intl.NumberFormat(void 0,e==null?{}:{minimumFractionDigits:+e,maximumFractionDigits:+e}).format(q(t)),percent:(t,[e=0])=>new Intl.NumberFormat(void 0,{style:"percent",minimumFractionDigits:+e,maximumFractionDigits:+e}).format(q(t)),round:(t,[e=0])=>{let n=10**+e;return Math.round(q(t)*n)/n},date:(t,[e="medium"])=>W(B(t),{dateStyle:e,...H(t)},t),time:(t,[e="short"])=>W(B(t),{timeStyle:e,...H(t)},t),datetime:(t,[e="medium",n="short"])=>W(B(t),{dateStyle:e,timeStyle:n,...H(t)},t),upper:t=>String(t??"").toUpperCase(),lower:t=>String(t??"").toLowerCase(),capitalize:t=>{let e=String(t??"");return e&&e[0].toUpperCase()+e.slice(1)},truncate:(t,[e=50,n="…"])=>{let r=String(t??"");return r.length>+e?r.slice(0,+e).trimEnd()+String(n):r},trim:t=>String(t??"").trim(),pluralize:(t,[e="",n])=>{let r=q(t);return`${new Intl.NumberFormat().format(r)} ${Math.abs(r)===1?String(e):n!=null?String(n):`${e}s`}`},default:(t,[e=""])=>t==null||t===""||Array.isArray(t)&&!t.length?String(e):t,sum:(t,[e])=>z(_(t),e).reduce((n,r)=>n+r,0),avg:(t,[e])=>{let n=_(t);return n.length?F.sum(n,[e])/n.length:0},min:(t,[e])=>{let n=_(t);return n.length?Math.min(...z(n,e)):0},max:(t,[e])=>{let n=_(t);return n.length?Math.max(...z(n,e)):0},count:t=>_(t).length,join:(t,[e=", ",n])=>_(t).map(r=>gt(r,n)).join(String(e))};function tt(t,e){let n=t.getAttribute("data-wd-fmt");if(!n)return e;let r;try{r=JSON.parse(n)}catch{return e}let o=e;for(let[s,c]of r)F[s]&&(o=F[s](o,c||[]));return o}var Ft=(t,e,n)=>F[t]?F[t](e,n==null?[]:[n]):void 0;async function rt(t,e){let n=await fetch(t,e),r=await n.text(),o=D(r);if(!n.ok){let s=o&&[o.error,o.message].find(i=>typeof i=="string"&&i),c=new Error(s||`HTTP ${n.status}`);throw c.status=n.status,c.body=o??null,c}return o===void 0?{status:n.status,body:r}:o}function x(t,e){a[t+"_error"]=e?e.message||String(e):null,a[t+"_error_body"]=(e&&e.body)??null}function yt(t){let e=t.__wdGen=(t.__wdGen||0)+1;return()=>e!==t.__wdGen}var O=!0;function P(t,e){let n=window.wd;n&&n.debug&&console.warn(`[wd] expression failed: ${t}`,e)}function Et(){for(let t of document.querySelectorAll("[data-wd-computed]")){let e=t.getAttribute("data-wd-computed-key")||"",n=t.getAttribute("data-wd-computed-expr")||"null";try{a[e]=ht(U(n),void 0)}catch(r){a[e]=void 0,P(n,r)}}}var It=(t,e)=>String(t??"").toLowerCase().includes(String(e??"").toLowerCase());function ht(t,e){let n=r=>{switch(r[0]){case"L":return r[1];case"S":return f(a[r[1]],r[2]??null);case"I":return f(e,r[1]??null);case"C":return It(n(r[1]),n(r[2]));case"A":return Ft(r[1],n(r[2]),r[3]);case"!":return!n(r[1]);case"u-":return-n(r[1]);case"&&":return n(r[1])&&n(r[2]);case"||":return n(r[1])||n(r[2]);case"==":return n(r[1])==n(r[2]);case"!=":return n(r[1])!=n(r[2]);case">":return n(r[1])>n(r[2]);case"<":return n(r[1])<n(r[2]);case">=":return n(r[1])>=n(r[2]);case"<=":return n(r[1])<=n(r[2]);case"+":return n(r[1])+n(r[2]);case"-":return n(r[1])-n(r[2]);case"*":return n(r[1])*n(r[2]);case"/":return n(r[1])/n(r[2]);case"%":return n(r[1])%n(r[2])}throw new Error(`wd: unknown op ${r[0]}`)};return n(t)}var it=new Map;function U(t){let e=it.get(t);if(e)return e;let n=JSON.parse(t);return it.set(t,n),n}function K(t,e){try{return!!ht(t,e)}catch(n){return P(t,n),!1}}function bt(t,e,n){if(!e)return;let r;try{r=JSON.parse(e)}catch{return}for(let[o,s]of r)t.classList.toggle(o,K(s,n))}function $t(t,e){let n=String(t&&typeof t=="object"?t.id??t.key??JSON.stringify(t):t).replace(/#/g,"##"),r=e.get(n)||0;return e.set(n,r+1),r?`${n}#${r}`:n}function St(t,e,n={}){let r=t.querySelectorAll("[data-wd-loop-item]"),o=s=>r.length&&s.closest("[data-wd-loop-item]");for(let s of t.querySelectorAll("[data-wd-each-if]")){if(o(s))continue;let c=s.getAttribute("data-wd-if-expr"),i=s.getAttribute("data-wd-meta"),u=c?K(U(c),e):i?n[i]:f(e,s.getAttribute("data-wd-path")),l=s.querySelector("[data-wd-each-if-out]");l&&(At(s,l,u,"wd-if"),St(l,e,n))}r=t.querySelectorAll("[data-wd-loop-item]");for(let s of I(t,"[data-wd-each]"))o(s)||(s.textContent=tt(s,f(e,s.getAttribute("data-wd-path")))??"");for(let s of t.querySelectorAll("[data-wd-each-meta]"))o(s)||(s.textContent=tt(s,n[s.getAttribute("data-wd-each-meta")||""])??"");for(let s of I(t,"[data-wd-each-class]"))o(s)||bt(s,s.getAttribute("data-wd-each-class"),e);for(let s of I(t,"[data-wd-each-attr]"))o(s)||wt(s,s.getAttribute("data-wd-each-attr"),e,n);for(let s of r)Ot(s,f(e,s.getAttribute("data-wd-loop-item")))}function at(t,e){let n=t.getAttribute(e);return n==null?null:n.startsWith("key:")?Number(a[n.slice(4)]??0):Number(n)}function Dt(t,e){let n=e.getAttribute("data-wd-loop-sort");if(n!=null){let s=n.startsWith("key:"),c=s?String(a[n.slice(4)]??""):n;if(!s||c){let i=e.getAttribute("data-wd-loop-sort-dir")||"asc";i.startsWith("key:")&&(i=String(a[i.slice(4)]??"asc"));let u=i==="desc"?-1:1;t=t.map((l,m)=>({value:l,index:m})).sort((l,m)=>{let h=f(l.value,c||null),d=f(m.value,c||null),p=typeof h=="number"&&typeof d=="number"?h-d:String(h).localeCompare(String(d));return p?p*u:l.index-m.index}).map(l=>l.value)}}e.hasAttribute("data-wd-loop-reverse")&&t.reverse();let r=at(e,"data-wd-loop-offset");r&&(t=t.slice(r));let o=at(e,"data-wd-loop-limit");return o!=null&&(t=t.slice(0,o)),t}function At(t,e,n,r){let o=String(!!n);if(t.getAttribute("data-wd-if-active")===o)return!1;t.setAttribute("data-wd-if-active",o);let s=t.querySelector(`template[data-${r}-${o}]`);return $(e),e.innerHTML=s?.innerHTML||"",O=!0}function Nt(t){let e=t.getAttribute("data-wd-if-expr"),n=e?K(U(e),void 0):f(a[t.getAttribute("data-wd-if")||""],t.getAttribute("data-wd-path")),r=t.querySelector("[data-wd-if-out]");if(!(!r||!At(t,r,n,"wd")))for(let o of r.querySelectorAll("[data-wd-if]"))Nt(o)}function Ot(t,e){let n=t.querySelector("template[data-wd-loop-template]"),r=t.querySelector("[data-wd-loop-out]");if(!n||!r)return;let o=Array.isArray(e)?e.slice():[],s=t.getAttribute("data-wd-loop-where");if(s){let d=U(s);o=o.filter(p=>K(d,p))}o=Dt(o,t);let c=t.querySelector("template[data-wd-loop-empty]");if(!o.length&&c){if(r.getAttribute("data-wd-empty")!=="1"){$(r),r.textContent="";for(let d of[...c.content.children])r.appendChild(d.cloneNode(!0));r.setAttribute("data-wd-empty","1"),O=!0}return}r.getAttribute("data-wd-empty")==="1"&&($(r),r.textContent="",r.removeAttribute("data-wd-empty"));let i=new Map;for(let d of[...r.children])i.set(d.getAttribute("data-wd-loop-key")||"",d);let u=new Map,l=new Set,m=o.length,h=!1;for(let d=0;d<m;d++){let p=o[d],S=$t(p,u),w=i.get(S);if(!w||l.has(S)){if(w=n.content.firstElementChild?.cloneNode(!0),!w)continue;w.setAttribute("data-wd-loop-key",S),O=!0}l.add(S),St(w,p,{index:d,number:d+1,first:d===0,last:d===m-1,count:m}),w.__wdItem=p,(h||r.children[d]!==w)&&(h=!0,r.appendChild(w))}for(let[d,p]of i)l.has(d)||($(p),p.remove())}var ct=0;function R(){for(let t=0;t<3;t++){O&&Pt(),O=!1,Et();for(let e of document.querySelectorAll("[data-wd-if]"))Nt(e);for(let e of document.querySelectorAll("[data-wd-class]"))bt(e,e.getAttribute("data-wd-class"),void 0);for(let e of document.querySelectorAll("[data-wd-loop]")){let n=e.getAttribute("data-wd-loop"),r=e.getAttribute("data-wd-loop-data"),o=n?n.indexOf("."):-1;Ot(e,n?o<0?a[n]:f(a[n.slice(0,o)],n.slice(o+1)):r?JSON.parse(r):[])}if(!O)break}O&&++ct<10?(window.wd?.debug&&console.warn("[wd] render did not settle; scheduling another pass"),g()):ct=0;for(let t of document.querySelectorAll("[data-wd-bind]"))t.textContent=tt(t,f(a[t.getAttribute("data-wd-bind")||""],t.getAttribute("data-wd-path")))??"";for(let t of document.querySelectorAll("[data-wd-attr]"))wt(t,t.getAttribute("data-wd-attr"),void 0,{});for(let t of document.querySelectorAll("[data-wd-bind-input]")){let e=a[t.getAttribute("data-wd-bind-input")||""]??"";t.type==="checkbox"?t.checked=!!e:t.type==="radio"?t.checked=t.value===String(e):document.activeElement!==t&&(t.value=e)}Lt(),vt(),j.size&&Kt()}function Lt(){for(let t of document.querySelectorAll("[data-wd-theme]")){let e=a[t.getAttribute("data-wd-theme")||""],n=document.documentElement;e==null||e===""||e==="auto"?n.removeAttribute("data-theme"):n.setAttribute("data-theme",String(e))}}var L=[],V=0;function vt(){if(!L.length)return;let t=!1;for(let e of L){let n=JSON.stringify(f(a,e.watch)??null);n!==e.last&&(e.last=n,kt(e.actions),t=!0)}if(!t){V=0;return}if(b(),++V>=10){V=0,console.warn("effect did not settle");return}R()}var jt=typeof requestAnimationFrame=="function"?requestAnimationFrame:queueMicrotask,Z=!1;function g(){Z||(Z=!0,jt(()=>{Z=!1,R()}))}document.addEventListener("input",t=>{let e=t.target?.closest("[data-wd-bind-input]");e&&(a[e.getAttribute("data-wd-bind-input")||""]=e.type==="range"?e.valueAsNumber:e.type==="checkbox"?e.checked:e.value,b(),g(),G())});function ut(t){if(!t)return null;let e=t.closest("[data-wd-loop-key]"),n=t.closest("[data-wd-loop]");return!e||!n?null:{srcKey:n.getAttribute("data-wd-loop"),item:e.__wdItem}}function et(t,e,n,r){let o=i=>Ct(a,n,i),s=f(a,n),c=()=>Array.isArray(s)?s:[];if(e==="inc"&&o(Number(s??0)+1),e==="dec"&&o(Number(s??0)-1),e==="add"&&o(Number(s??0)+Number(r)),e==="sub"&&o(Number(s??0)-Number(r)),e==="toggle"&&o(!s),e==="set"&&o(r),e==="append"&&o([...c(),r]),e==="prepend"&&o([r,...c()]),e==="member-toggle"&&o(c().includes(r)?c().filter(i=>i!==r):[...c(),r]),e==="remove-value"&&o(c().filter(i=>i!==r)),e==="clear"&&o(Array.isArray(s)?[]:{}),e==="merge"&&o({...s&&typeof s=="object"?s:{},...typeof r=="string"?f(a,r):r}),e==="delete"&&s&&typeof s=="object"&&(delete s[r],o(s)),e==="reset"&&o(structuredClone(f(M,n))),e==="refetch"){let i=document.querySelector(`[data-wd-fetch-key="${n}"]`);i&&v(i)}if(e==="remove"){let i=ut(t);i&&i.srcKey&&(a[i.srcKey]=(Array.isArray(a[i.srcKey])?a[i.srcKey]:[]).filter(u=>u!==i.item))}if(e==="append-row"){let i=ut(t);if(i&&i.item!==void 0){let u=i.item&&typeof i.item=="object"?structuredClone(i.item):i.item;o([...c(),u])}}}var kt=t=>{for(let e of t)et(null,e.op,e.target||"",structuredClone(e.value))};document.addEventListener("click",t=>{let e=t.target?.closest("[data-wd-action],[data-wd-actions]");if(!e)return;let n=e.getAttribute("data-wd-actions");if(n)for(let r of JSON.parse(n))et(e,r.op,r.target||"",r.value);else{let r=e.getAttribute("data-wd-value");et(e,e.getAttribute("data-wd-action")||"",e.getAttribute("data-wd-target")||"",r===null?void 0:JSON.parse(r))}b(),g(),G()});document.addEventListener("submit",t=>{let e=t.target?.closest("[data-wd-form]");if(!e)return;t.preventDefault();let n=e.getAttribute("data-wd-form")||"",r=e.getAttribute("action");if(!r){let i=new FormData(e),u=Object.fromEntries(i);for(let l of e.querySelectorAll("[data-wd-multi]")){let m=l.getAttribute("data-wd-multi");m&&(u[m]=i.getAll(m))}a[n]=u,b(),g();return}let o=yt(e);x(n),g();let s=new FormData(e);for(let i of e.querySelectorAll("[data-wd-bind-input]")){let u=i.getAttribute("data-wd-bind-input");u&&!i.getAttribute("name")&&!s.has(u)&&s.append(u,a[u]??"")}let c=e.querySelector("input[type=file]");rt(r,{method:e.getAttribute("method")||"post",headers:c?void 0:{"content-type":"application/x-www-form-urlencoded"},body:c?s:new URLSearchParams(s).toString()}).then(i=>{o()||(a[n]=i,x(n),b(),g())}).catch(i=>{o()||(x(n,i),g())})});function v(t){let e=yt(t),n=y=>t.getAttribute("data-wd-fetch-"+y)||"",r=n("key"),o=(y,A)=>{a[r+y]=A,g()},s=!1,c=n("url").replace(/\{\s*([\w$.]+)\s*\}/g,(y,A)=>{let k=f(a,A);return(k==null||k==="")&&(s=!0),encodeURIComponent(String(k??""))});if(s)return o("_loading",!1);let i=n("headers"),u=n("body"),l=n("refresh"),m=n("method")||"GET",h=Number(n("timeout")),d=Number(n("retry")),p=!1;x(r),o("_loading",!0);let S=y=>{x(r,y),o("_loading",!1)},w=()=>{let y={method:m};i&&(y.headers=a[i]||{}),u&&m!=="GET"&&(y.body=JSON.stringify(a[u]??null));let A=typeof AbortController=="function"?new AbortController:null;A&&(y.signal=A.signal);let k=h&&A?setTimeout(()=>A.abort(),h):0;rt(c,y).then(N=>{clearTimeout(k),!e()&&(a[r]=N,a[r+"_empty"]=Jt(N),x(r),o("_loading",!1))}).catch(N=>{if(clearTimeout(k),e())return;let st=N&&N.status===401;if(st&&l&&i&&!p)return p=!0,void Mt(l,i).then(qt=>{e()||(qt?w():S(N))});if(st&&p)return void S(N);if(d-- >0)return void setTimeout(w,200);S(N)})};w()}var Q=new Map;function Mt(t,e){let n=Q.get(t);if(n)return n;let r=rt(t,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(a[e]??{})}).then(o=>o&&typeof o=="object"?(a[e]=o,b(),!0):!1).catch(()=>!1).finally(()=>Q.delete(t));return Q.set(t,r),r}var ot=[],_t=t=>(t.getAttribute("data-wd-fetch-deps")||"").split(",").filter(Boolean).map(e=>JSON.stringify(f(a,e))).join("|"),dt=0,C=new Set;function G(){for(let t of ot){let e=_t(t);e&&e!==t.__wdSnap&&(t.__wdSnap=e,C.add(t))}C.size&&(clearTimeout(dt),dt=setTimeout(()=>{for(let t of C)v(t);C.clear()},150))}function $(t){for(let e of I(t,"[data-wd-fetch],[data-wd-effect],[data-wd-every]")){C.delete(e);for(let n of[ot,L,E])for(let r=n.length-1;r>=0;r--)(n[r].node||n[r])===e&&(n[r].t&&clearInterval(n[r].t),n.splice(r,1))}}function Pt(){mt();for(let t of document.querySelectorAll("[data-wd-fetch],[data-wd-effect],[data-wd-every]"))if(!t.__wdReg){if(t.__wdReg=1,t.hasAttribute("data-wd-effect")){let e=JSON.parse(t.textContent||"{}");L.push({node:t,watch:e.watch,actions:e.actions||[],last:JSON.stringify(f(a,e.watch)??null)})}if(t.hasAttribute("data-wd-every")&&(E.length||document.addEventListener("visibilitychange",()=>document.hidden?Ut():lt()),E.push({...JSON.parse(t.textContent||"{}"),node:t}),lt()),t.hasAttribute("data-wd-fetch"))if(ot.push(t),t.__wdSnap=_t(t),t.getAttribute("data-wd-fetch-when")==="visible"&&"IntersectionObserver"in window){let e=new IntersectionObserver(n=>{n.some(r=>r.isIntersecting)&&(e.disconnect(),v(t))});e.observe(t)}else v(t)}}var E=[];function lt(){if(!document.hidden)for(let t of E)t.t||!t.ms||!t.actions||(t.t=setInterval(()=>{kt(t.actions),b(),g(),G()},t.ms))}function Ut(){for(let t of E)clearInterval(t.t),t.t=0}var j=new Map;function Kt(){for(let[t,e]of j){let n=JSON.stringify(a[t]??null);if(n!==e.last){e.last=n;for(let r of e.cbs)try{r(a[t])}catch(o){P(`subscribe ${t}`,o)}}}}window.wd={state:a,debug:!1,get:t=>a[t],set:(t,e)=>{a[t]=e,b(),g(),G()},subscribe:(t,e)=>{let n=j.get(t);n||(n={cbs:new Set,last:JSON.stringify(a[t]??null)},j.set(t,n)),n.cbs.add(e);try{e(a[t])}catch(r){P(`subscribe ${t}`,r)}return()=>n.cbs.delete(e)},render:R};R();
1
+ var a={},ft=new Set,nt=new Set,T=new Set,M={};function J(t,e){try{return e===void 0?localStorage.getItem(t):e===null?localStorage.removeItem(t):localStorage.setItem(t,e)}catch{return null}}var X=t=>t.replace(":",".");function mt(){for(let t of document.querySelectorAll("script[data-wd-state],script[data-wd-store]")){let e=t.getAttribute("data-wd-store"),n=JSON.parse(t.textContent||"null");for(let[r,o]of e?[[e,n]]:Object.entries(n||{})){if(r in a)continue;if(a[r]=structuredClone(M[r]=o),e?!t.hasAttribute("data-wd-store-ephemeral"):t.getAttribute("data-wd-persist")===r){(e?nt:ft).add(r);let c=(e?"wd:store:":"wd:")+r,i=J(c);i===null?e&&J(c,JSON.stringify(o??null)):F(i)===void 0?J(c,null):a[r]=F(i)}t.getAttribute("data-wd-url")===r&&(T.add(r),pt(!1,[r]))}}}function xt(){if(!T.size)return;let t=new URLSearchParams(location.search);for(let r of T){let o=a[r];JSON.stringify(o??null)===JSON.stringify(M[r]??null)?t.delete(X(r)):t.set(X(r),typeof o=="string"?o:JSON.stringify(o))}let e=t.toString(),n=location.pathname+(e?`?${e}`:"")+location.hash;if(n!==location.pathname+location.search+location.hash)try{history.replaceState(history.state,"",n)}catch{}}function pt(t,e){let n=new URLSearchParams(location.search);for(let r of e||T){let o=n.get(X(r)),s=M[r];o==null?t&&(a[r]=structuredClone(s)):a[r]=typeof s=="string"?o:F(o)??o}}function b(){for(let t of ft)J(`wd:${t}`,JSON.stringify(a[t]??null));for(let t of nt)J(`wd:store:${t}`,JSON.stringify(a[t]??null));xt()}mt();window.addEventListener("popstate",()=>{T.size&&(pt(!0),b(),g())});window.addEventListener("storage",t=>{let e=t.key&&t.key.startsWith("wd:store:")?t.key.slice(9):"";if(!nt.has(e)||t.newValue==null)return;let n=F(t.newValue);n!==void 0&&JSON.stringify(n)!==JSON.stringify(a[e])&&(a[e]=n,g())});var Y=t=>t==="__proto__"||t==="constructor"||t==="prototype";function f(t,e){if(!e)return t;let n=t;for(let r of e.split(".")){if(n==null||Y(r))return;n=n[r]}return n}function Ct(t,e,n){let r=e.split("."),o=r.pop()||"",s=t;for(let c of r){if(Y(c))return;(s[c]==null||typeof s[c]!="object")&&(s[c]={}),s=s[c]}Y(o)||(s[o]=n)}function Jt(t){return t==null||typeof t=="object"&&(Array.isArray(t)?t.length:Object.keys(t).length)===0}function F(t){try{return JSON.parse(t,(e,n)=>e==="toString"||e==="valueOf"?void 0:n)}catch{return}}var Tt=/[\u0000-\u0020()<>"'`\\\u007F]/g,$=(t,e)=>t.matches(e)?[t,...t.querySelectorAll(e)]:[...t.querySelectorAll(e)];function wt(t,e,n,r){if(!e)return;let o;try{o=JSON.parse(e)}catch{return}let s="",c="";for(let i=1;i<o.length;i++){let u=o[i];if(typeof u=="string"){s+=u,c+=u;continue}let l=String((u[0]==="s"?f(a[u[1]],u[2]):u[0]==="m"?r[u[1]]:f(n,u[1]))??"");c+=l,s+=l.replace(Tt,m=>`%${m.charCodeAt(0).toString(16).toUpperCase().padStart(2,"0")}`)}o[0]!=="href"&&o[0]!=="src"||t.setAttribute(o[0],/^(javascript|data|vbscript):/i.test(c.replace(/[\u0000-\u0020\u007F]+/g,""))?"":s)}var q=t=>{let e=typeof t=="number"?t:Number.parseFloat(t);return Number.isFinite(e)?e:0},k=t=>Array.isArray(t)?t:t==null?[]:[t],gt=(t,e)=>e!=null&&e!==""?t?.[e]:t,z=(t,e)=>t.map(n=>q(gt(n,e))),B=t=>{if(t instanceof Date)return t;if(typeof t=="number")return new Date(t);let e=String(t).trim();return/^-?\d+$/.test(e)?new Date(Number(e)):new Date(e)},W=(t,e,n)=>Number.isNaN(t.getTime())?String(n):new Intl.DateTimeFormat(void 0,e).format(t),H=t=>typeof t=="string"&&/^\d{4}-\d{2}-\d{2}$/.test(t.trim())?{timeZone:"UTC"}:void 0,E={money:(t,[e="USD",n])=>new Intl.NumberFormat(n||void 0,{style:"currency",currency:String(e||"USD")}).format(q(t)),number:(t,[e])=>new Intl.NumberFormat(void 0,e==null?{}:{minimumFractionDigits:+e,maximumFractionDigits:+e}).format(q(t)),percent:(t,[e=0])=>new Intl.NumberFormat(void 0,{style:"percent",minimumFractionDigits:+e,maximumFractionDigits:+e}).format(q(t)),round:(t,[e=0])=>{let n=10**+e;return Math.round(q(t)*n)/n},date:(t,[e="medium"])=>W(B(t),{dateStyle:e,...H(t)},t),time:(t,[e="short"])=>W(B(t),{timeStyle:e,...H(t)},t),datetime:(t,[e="medium",n="short"])=>W(B(t),{dateStyle:e,timeStyle:n,...H(t)},t),upper:t=>String(t??"").toUpperCase(),lower:t=>String(t??"").toLowerCase(),capitalize:t=>{let e=String(t??"");return e&&e[0].toUpperCase()+e.slice(1)},truncate:(t,[e=50,n="…"])=>{let r=String(t??"");return r.length>+e?r.slice(0,+e).trimEnd()+String(n):r},trim:t=>String(t??"").trim(),pluralize:(t,[e="",n])=>{let r=q(t);return`${new Intl.NumberFormat().format(r)} ${Math.abs(r)===1?String(e):n!=null?String(n):`${e}s`}`},default:(t,[e=""])=>t==null||t===""||Array.isArray(t)&&!t.length?String(e):t,sum:(t,[e])=>z(k(t),e).reduce((n,r)=>n+r,0),avg:(t,[e])=>{let n=k(t);return n.length?E.sum(n,[e])/n.length:0},min:(t,[e])=>{let n=k(t);return n.length?z(n,e).reduce((r,o)=>Math.min(r,o)):0},max:(t,[e])=>{let n=k(t);return n.length?z(n,e).reduce((r,o)=>Math.max(r,o)):0},count:t=>k(t).length,join:(t,[e=", ",n])=>k(t).map(r=>gt(r,n)).join(String(e))};function tt(t,e){let n=t.getAttribute("data-wd-fmt");if(!n)return e;let r;try{r=JSON.parse(n)}catch{return e}let o=e;for(let[s,c]of r)E[s]&&(o=E[s](o,c||[]));return o}var Ft=(t,e,n)=>E[t]?E[t](e,n==null?[]:[n]):void 0;async function rt(t,e){let n=await fetch(t,e),r=await n.text(),o=F(r);if(!n.ok){let s=o&&[o.error,o.message].find(i=>typeof i=="string"&&i),c=new Error(s||`HTTP ${n.status}`);throw c.status=n.status,c.body=o??null,c}return o===void 0?{status:n.status,body:r}:o}function x(t,e){a[t+"_error"]=e?e.message||String(e):null,a[t+"_error_body"]=(e&&e.body)??null}function yt(t){let e=t.__wdGen=(t.__wdGen||0)+1;return()=>e!==t.__wdGen}var _=!0;function P(t,e){let n=window.wd;n&&n.debug&&console.warn(`[wd] expression failed: ${t}`,e)}function Et(){for(let t of document.querySelectorAll("[data-wd-computed]")){let e=t.getAttribute("data-wd-computed-key")||"",n=t.getAttribute("data-wd-computed-expr")||"null";try{a[e]=ht(U(n),void 0)}catch(r){a[e]=void 0,P(n,r)}}}var It=(t,e)=>String(t??"").toLowerCase().includes(String(e??"").toLowerCase());function ht(t,e){let n=r=>{switch(r[0]){case"L":return r[1];case"S":return f(a[r[1]],r[2]??null);case"I":return f(e,r[1]??null);case"C":return It(n(r[1]),n(r[2]));case"A":return Ft(r[1],n(r[2]),r[3]);case"!":return!n(r[1]);case"u-":return-n(r[1]);case"&&":return n(r[1])&&n(r[2]);case"||":return n(r[1])||n(r[2]);case"==":return n(r[1])==n(r[2]);case"!=":return n(r[1])!=n(r[2]);case">":return n(r[1])>n(r[2]);case"<":return n(r[1])<n(r[2]);case">=":return n(r[1])>=n(r[2]);case"<=":return n(r[1])<=n(r[2]);case"+":return n(r[1])+n(r[2]);case"-":return n(r[1])-n(r[2]);case"*":return n(r[1])*n(r[2]);case"/":return n(r[1])/n(r[2]);case"%":return n(r[1])%n(r[2])}throw new Error(`wd: unknown op ${r[0]}`)};return n(t)}var it=new Map;function U(t){let e=it.get(t);if(e)return e;let n=JSON.parse(t);return it.set(t,n),n}function K(t,e){try{return!!ht(t,e)}catch(n){return P(t,n),!1}}function bt(t,e,n){if(!e)return;let r;try{r=JSON.parse(e)}catch{return}for(let[o,s]of r)t.classList.toggle(o,K(s,n))}function $t(t,e){let n=String(t&&typeof t=="object"?t.id??t.key??JSON.stringify(t):t).replace(/#/g,"##"),r=e.get(n)||0;return e.set(n,r+1),r?`${n}#${r}`:n}function St(t,e,n={}){let r=t.querySelectorAll("[data-wd-loop-item]"),o=s=>r.length&&s.closest("[data-wd-loop-item]");for(let s of t.querySelectorAll("[data-wd-each-if]")){if(o(s))continue;let c=s.getAttribute("data-wd-if-expr"),i=s.getAttribute("data-wd-meta"),u=c?K(U(c),e):i?n[i]:f(e,s.getAttribute("data-wd-path")),l=s.querySelector("[data-wd-each-if-out]");l&&(At(s,l,u,"wd-if"),St(l,e,n))}r=t.querySelectorAll("[data-wd-loop-item]");for(let s of $(t,"[data-wd-each]"))o(s)||(s.textContent=tt(s,f(e,s.getAttribute("data-wd-path")))??"");for(let s of t.querySelectorAll("[data-wd-each-meta]"))o(s)||(s.textContent=tt(s,n[s.getAttribute("data-wd-each-meta")||""])??"");for(let s of $(t,"[data-wd-each-class]"))o(s)||bt(s,s.getAttribute("data-wd-each-class"),e);for(let s of $(t,"[data-wd-each-attr]"))o(s)||wt(s,s.getAttribute("data-wd-each-attr"),e,n);for(let s of r)Ot(s,f(e,s.getAttribute("data-wd-loop-item")))}function at(t,e){let n=t.getAttribute(e);return n==null?null:n.startsWith("key:")?Number(a[n.slice(4)]??0):Number(n)}function vt(t,e){let n=e.getAttribute("data-wd-loop-sort");if(n!=null){let s=n.startsWith("key:"),c=s?String(a[n.slice(4)]??""):n;if(!s||c){let i=e.getAttribute("data-wd-loop-sort-dir")||"asc";i.startsWith("key:")&&(i=String(a[i.slice(4)]??"asc"));let u=i==="desc"?-1:1;t=t.map((l,m)=>({value:l,index:m})).sort((l,m)=>{let h=f(l.value,c||null),d=f(m.value,c||null),p=typeof h=="number"&&typeof d=="number"?h-d:String(h).localeCompare(String(d));return p?p*u:l.index-m.index}).map(l=>l.value)}}e.hasAttribute("data-wd-loop-reverse")&&t.reverse();let r=at(e,"data-wd-loop-offset");r&&(t=t.slice(r));let o=at(e,"data-wd-loop-limit");return o!=null&&(t=t.slice(0,o)),t}function At(t,e,n,r){let o=String(!!n);if(t.getAttribute("data-wd-if-active")===o)return!1;t.setAttribute("data-wd-if-active",o);let s=t.querySelector(`template[data-${r}-${o}]`);return v(e),e.innerHTML=s?.innerHTML||"",_=!0}function Nt(t){let e=t.getAttribute("data-wd-if-expr"),n=e?K(U(e),void 0):f(a[t.getAttribute("data-wd-if")||""],t.getAttribute("data-wd-path")),r=t.querySelector("[data-wd-if-out]");if(!(!r||!At(t,r,n,"wd")))for(let o of r.querySelectorAll("[data-wd-if]"))Nt(o)}function Ot(t,e){let n=t.querySelector("template[data-wd-loop-template]"),r=t.querySelector("[data-wd-loop-out]");if(!n||!r)return;let o=Array.isArray(e)?e.slice():[],s=t.getAttribute("data-wd-loop-where");if(s){let d=U(s);o=o.filter(p=>K(d,p))}o=vt(o,t);let c=t.querySelector("template[data-wd-loop-empty]");if(!o.length&&c){if(r.getAttribute("data-wd-empty")!=="1"){v(r),r.textContent="";for(let d of[...c.content.children])r.appendChild(d.cloneNode(!0));r.setAttribute("data-wd-empty","1"),_=!0}return}r.getAttribute("data-wd-empty")==="1"&&(v(r),r.textContent="",r.removeAttribute("data-wd-empty"));let i=new Map;for(let d of[...r.children])i.set(d.getAttribute("data-wd-loop-key")||"",d);let u=new Map,l=new Set,m=o.length,h=!1;for(let d=0;d<m;d++){let p=o[d],S=$t(p,u),w=i.get(S);if(!w||l.has(S)){if(w=n.content.firstElementChild?.cloneNode(!0),!w)continue;w.setAttribute("data-wd-loop-key",S),_=!0}l.add(S),St(w,p,{index:d,number:d+1,first:d===0,last:d===m-1,count:m}),w.__wdItem=p,(h||r.children[d]!==w)&&(h=!0,r.appendChild(w))}for(let[d,p]of i)l.has(d)||(v(p),p.remove())}var ct=0;function R(){for(let t=0;t<3;t++){_&&Pt(),_=!1,Et();for(let e of document.querySelectorAll("[data-wd-if]"))Nt(e);for(let e of document.querySelectorAll("[data-wd-class]"))bt(e,e.getAttribute("data-wd-class"),void 0);for(let e of document.querySelectorAll("[data-wd-loop]")){let n=e.getAttribute("data-wd-loop"),r=e.getAttribute("data-wd-loop-data"),o=n?n.indexOf("."):-1;Ot(e,n?o<0?a[n]:f(a[n.slice(0,o)],n.slice(o+1)):r?JSON.parse(r):[])}if(!_)break}_&&++ct<10?(window.wd?.debug&&console.warn("[wd] render did not settle; scheduling another pass"),g()):ct=0;for(let t of document.querySelectorAll("[data-wd-bind]"))t.textContent=tt(t,f(a[t.getAttribute("data-wd-bind")||""],t.getAttribute("data-wd-path")))??"";for(let t of document.querySelectorAll("[data-wd-attr]"))wt(t,t.getAttribute("data-wd-attr"),void 0,{});for(let t of document.querySelectorAll("[data-wd-bind-input]")){let e=a[t.getAttribute("data-wd-bind-input")||""]??"";t.type==="checkbox"?t.checked=!!e:t.type==="radio"?t.checked=t.value===String(e):document.activeElement!==t&&(t.value=e)}Dt(),Lt(),j.size&&Kt()}function Dt(){for(let t of document.querySelectorAll("[data-wd-theme]")){let e=a[t.getAttribute("data-wd-theme")||""],n=document.documentElement;e==null||e===""||e==="auto"?n.removeAttribute("data-theme"):n.setAttribute("data-theme",String(e))}}var D=[],V=0;function Lt(){if(!D.length)return;let t=!1;for(let e of D){let n=JSON.stringify(f(a,e.watch)??null);n!==e.last&&(e.last=n,_t(e.actions),t=!0)}if(!t){V=0;return}if(b(),++V>=10){V=0,console.warn("effect did not settle");return}R()}var jt=typeof requestAnimationFrame=="function"?requestAnimationFrame:queueMicrotask,Z=!1;function g(){Z||(Z=!0,jt(()=>{Z=!1,R()}))}document.addEventListener("input",t=>{let e=t.target?.closest("[data-wd-bind-input]");e&&(a[e.getAttribute("data-wd-bind-input")||""]=e.type==="range"?e.valueAsNumber:e.type==="checkbox"?e.checked:e.value,b(),g(),G())});function ut(t){if(!t)return null;let e=t.closest("[data-wd-loop-key]"),n=t.closest("[data-wd-loop]");return!e||!n?null:{srcKey:n.getAttribute("data-wd-loop"),item:e.__wdItem}}function et(t,e,n,r){let o=i=>Ct(a,n,i),s=f(a,n),c=()=>Array.isArray(s)?s:[];if(e==="inc"&&o(Number(s??0)+1),e==="dec"&&o(Number(s??0)-1),e==="add"&&o(Number(s??0)+Number(r)),e==="sub"&&o(Number(s??0)-Number(r)),e==="toggle"&&o(!s),e==="set"&&o(r),e==="append"&&o([...c(),r]),e==="prepend"&&o([r,...c()]),e==="member-toggle"&&o(c().includes(r)?c().filter(i=>i!==r):[...c(),r]),e==="remove-value"&&o(c().filter(i=>i!==r)),e==="clear"&&o(Array.isArray(s)?[]:{}),e==="merge"&&o({...s&&typeof s=="object"?s:{},...typeof r=="string"?f(a,r):r}),e==="delete"&&s&&typeof s=="object"&&(delete s[r],o(s)),e==="reset"&&o(structuredClone(f(M,n))),e==="refetch"){let i=document.querySelector(`[data-wd-fetch-key="${n}"]`);i&&L(i)}if(e==="remove"){let i=ut(t);i&&i.srcKey&&(a[i.srcKey]=(Array.isArray(a[i.srcKey])?a[i.srcKey]:[]).filter(u=>u!==i.item))}if(e==="append-row"){let i=ut(t);if(i&&i.item!==void 0){let u=i.item&&typeof i.item=="object"?structuredClone(i.item):i.item;o([...c(),u])}}}var _t=t=>{for(let e of t)et(null,e.op,e.target||"",structuredClone(e.value))};document.addEventListener("click",t=>{let e=t.target?.closest("[data-wd-action],[data-wd-actions]");if(!e)return;let n=e.getAttribute("data-wd-actions");if(n)for(let r of JSON.parse(n))et(e,r.op,r.target||"",r.value);else{let r=e.getAttribute("data-wd-value");et(e,e.getAttribute("data-wd-action")||"",e.getAttribute("data-wd-target")||"",r===null?void 0:JSON.parse(r))}b(),g(),G()});document.addEventListener("submit",t=>{let e=t.target?.closest("[data-wd-form]");if(!e)return;t.preventDefault();let n=e.getAttribute("data-wd-form")||"",r=e.getAttribute("action");if(!r){let i=new FormData(e),u=Object.fromEntries(i);for(let l of e.querySelectorAll("[data-wd-multi]")){let m=l.getAttribute("data-wd-multi");m&&(u[m]=i.getAll(m))}a[n]=u,b(),g();return}let o=yt(e);x(n),g();let s=new FormData(e);for(let i of e.querySelectorAll("[data-wd-bind-input]")){let u=i.getAttribute("data-wd-bind-input");u&&!i.getAttribute("name")&&!s.has(u)&&s.append(u,a[u]??"")}let c=e.querySelector("input[type=file]");rt(r,{method:e.getAttribute("method")||"post",headers:c?void 0:{"content-type":"application/x-www-form-urlencoded"},body:c?s:new URLSearchParams(s).toString()}).then(i=>{o()||(a[n]=i,x(n),b(),g())}).catch(i=>{o()||(x(n,i),g())})});function L(t){let e=yt(t),n=y=>t.getAttribute("data-wd-fetch-"+y)||"",r=n("key"),o=(y,A)=>{a[r+y]=A,g()},s=!1,c=n("url").replace(/\{\s*([\w$.]+)\s*\}/g,(y,A)=>{let N=f(a,A);return(N==null||N===""||N==="."||N==="..")&&(s=!0),encodeURIComponent(String(N??""))});if(s)return o("_loading",!1);let i=n("headers"),u=n("body"),l=n("refresh"),m=n("method")||"GET",h=Number(n("timeout")),d=Number(n("retry")),p=!1;x(r),o("_loading",!0);let S=y=>{x(r,y),o("_loading",!1)},w=()=>{let y={method:m};i&&(y.headers=a[i]||{}),u&&m!=="GET"&&(y.body=JSON.stringify(a[u]??null));let A=typeof AbortController=="function"?new AbortController:null;A&&(y.signal=A.signal);let N=h&&A?setTimeout(()=>A.abort(),h):0;rt(c,y).then(O=>{clearTimeout(N),!e()&&(a[r]=O,a[r+"_empty"]=Jt(O),x(r),o("_loading",!1))}).catch(O=>{if(clearTimeout(N),e())return;let st=O&&O.status===401;if(st&&l&&i&&!p)return p=!0,void Mt(l,i).then(qt=>{e()||(qt?w():S(O))});if(st&&p)return void S(O);if(d-- >0)return void setTimeout(w,200);S(O)})};w()}var Q=new Map;function Mt(t,e){let n=Q.get(t);if(n)return n;let r=rt(t,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(a[e]??{})}).then(o=>o&&typeof o=="object"?(a[e]=o,b(),!0):!1).catch(()=>!1).finally(()=>Q.delete(t));return Q.set(t,r),r}var ot=[],kt=t=>(t.getAttribute("data-wd-fetch-deps")||"").split(",").filter(Boolean).map(e=>JSON.stringify(f(a,e))).join("|"),dt=0,C=new Set;function G(){for(let t of ot){let e=kt(t);e&&e!==t.__wdSnap&&(t.__wdSnap=e,C.add(t))}C.size&&(clearTimeout(dt),dt=setTimeout(()=>{for(let t of C)L(t);C.clear()},150))}function v(t){for(let e of $(t,"[data-wd-fetch],[data-wd-effect],[data-wd-every]")){C.delete(e);for(let n of[ot,D,I])for(let r=n.length-1;r>=0;r--)(n[r].node||n[r])===e&&(n[r].t&&clearInterval(n[r].t),n.splice(r,1))}}function Pt(){mt();for(let t of document.querySelectorAll("[data-wd-fetch],[data-wd-effect],[data-wd-every]"))if(!t.__wdReg){if(t.__wdReg=1,t.hasAttribute("data-wd-effect")){let e=JSON.parse(t.textContent||"{}");D.push({node:t,watch:e.watch,actions:e.actions||[],last:JSON.stringify(f(a,e.watch)??null)})}if(t.hasAttribute("data-wd-every")&&(I.length||document.addEventListener("visibilitychange",()=>document.hidden?Ut():lt()),I.push({...JSON.parse(t.textContent||"{}"),node:t}),lt()),t.hasAttribute("data-wd-fetch"))if(ot.push(t),t.__wdSnap=kt(t),t.getAttribute("data-wd-fetch-when")==="visible"&&"IntersectionObserver"in window){let e=new IntersectionObserver(n=>{n.some(r=>r.isIntersecting)&&(e.disconnect(),L(t))});e.observe(t)}else L(t)}}var I=[];function lt(){if(!document.hidden)for(let t of I)t.t||!t.ms||!t.actions||(t.t=setInterval(()=>{_t(t.actions),b(),g(),G()},t.ms))}function Ut(){for(let t of I)clearInterval(t.t),t.t=0}var j=new Map;function Kt(){for(let[t,e]of j){let n=JSON.stringify(a[t]??null);if(n!==e.last){e.last=n;for(let r of e.cbs)try{r(a[t])}catch(o){P(`subscribe ${t}`,o)}}}}window.wd={state:a,debug:!1,get:t=>a[t],set:(t,e)=>{a[t]=e,b(),g(),G()},subscribe:(t,e)=>{let n=j.get(t);n||(n={cbs:new Set,last:JSON.stringify(a[t]??null)},j.set(t,n)),n.cbs.add(e);try{e(a[t])}catch(r){P(`subscribe ${t}`,r)}return()=>n.cbs.delete(e)},render:R};R();
2
2
  //# sourceMappingURL=runtime.js.map