@ai-setting/roy-plugin-task-show 2.4.2 → 2.5.1

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.
@@ -0,0 +1,565 @@
1
+ /**
2
+ * @fileoverview Chat panel — client controller.
3
+ *
4
+ * v2.5.0+ REQ-2 (home) + REQ-3 (task-aware) + REQ-4 (markdown).
5
+ *
6
+ * The module exposes a single `attachChatPanel(root, opts)` factory so
7
+ * the same JS file can drive both the home chat panel and the
8
+ * task-detail chat panel. The legacy `init()` entry-point is preserved
9
+ * for backward compat with any page that wires the home chat via
10
+ * `<script src="chat-panel.js">` alone (no opts needed).
11
+ *
12
+ * DOM contract (pinned by `test/home-chat-v25.test.ts` and
13
+ * `test/task-chat-v25.test.ts`):
14
+ *
15
+ * [data-chat-shell] <section> wrapper
16
+ * [data-chat-scope] "home" | "task" (read by factory)
17
+ * [data-task-id] task id (when scope=task) (read by factory)
18
+ * [data-chat-messages] <div> message log (role="log")
19
+ * [data-chat-form] <form> submit handler
20
+ * [data-chat-input] <input> text input
21
+ * [data-chat-send] <button> send button (type="submit")
22
+ *
23
+ * sessionId is persisted in localStorage under a scope-aware key:
24
+ * - scope=home: `taskShow.chat.sessionId` (one-off uuid)
25
+ * - scope=task: `taskShow.chat.task.${taskId}.sid` (long-lived, per task)
26
+ * The task-detail key intentionally differs from the home key so the
27
+ * two scopes don't clobber each other.
28
+ *
29
+ * The server speaks SSE for streaming chunks. We use `fetch` + an
30
+ * `ReadableStream` reader instead of `EventSource` because the chat
31
+ * endpoint is POST (EventSource can't POST) and we need to send the
32
+ * `message` in the request body.
33
+ *
34
+ * If the chat feature is disabled server-side (503), the panel hides
35
+ * itself with a banner instead of throwing.
36
+ *
37
+ * v2.5.x REQ-4: assistant messages are rendered through
38
+ * `markdownRenderer.renderMarkdown()` (vendored `marked` +
39
+ * `DOMPurify`). User messages deliberately stay `textContent` —
40
+ * users must not be able to inject markdown / HTML into their own
41
+ * bubbles.
42
+ */
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Module dual-mode
46
+ //
47
+ // - In the browser this file is loaded as a classic <script>. The
48
+ // trailing bootstrap calls `init()` and the panel becomes live.
49
+ // - In tests (bun:test) it is imported as an ES module; the bootstrap
50
+ // branch is skipped (no `document`/`window` globals), and the named
51
+ // exports below are what the harness drives.
52
+ // ---------------------------------------------------------------------------
53
+
54
+ const HOME_STORAGE_KEY = "taskShow.chat.sessionId";
55
+ const SHELL_SEL = "[data-chat-shell]";
56
+ const MESSAGES_SEL = "[data-chat-messages]";
57
+ const FORM_SEL = "[data-chat-form]";
58
+ const INPUT_SEL = "[data-chat-input]";
59
+ const SEND_SEL = "[data-chat-send]";
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Portable DOM access. happy-dom tests give us a Window via
63
+ // `container.ownerDocument`; the browser gives us a global `document`.
64
+ // ---------------------------------------------------------------------------
65
+
66
+ function getDoc(container) {
67
+ if (container && container.ownerDocument) return container.ownerDocument;
68
+ if (typeof document !== "undefined") return document;
69
+ return null;
70
+ }
71
+
72
+ function getWindow(doc) {
73
+ if (doc && doc.defaultView) return doc.defaultView;
74
+ if (typeof window !== "undefined") return window;
75
+ return null;
76
+ }
77
+
78
+ function $(container, sel) {
79
+ const doc = getDoc(container);
80
+ if (doc) return doc.querySelector(sel);
81
+ // Fallback — should never happen for the production code path.
82
+ return typeof document !== "undefined" ? document.querySelector(sel) : null;
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Markdown renderer resolution. In production the browser script tag
87
+ // order is:
88
+ // marked.min.js → dompurify.min.js → markdown-renderer.js → chat-panel.js
89
+ // so by the time chat-panel.js runs, `window.markdownRenderer` is set.
90
+ //
91
+ // If a test harness imported chat-panel.js without loading
92
+ // markdown-renderer.js first, we lazily require it (Node-only path).
93
+ // ---------------------------------------------------------------------------
94
+
95
+ function resolveRenderer() {
96
+ if (typeof window !== "undefined" && window.markdownRenderer) {
97
+ return window.markdownRenderer;
98
+ }
99
+ if (typeof globalThis !== "undefined" && globalThis.markdownRenderer) {
100
+ return globalThis.markdownRenderer;
101
+ }
102
+ try {
103
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
104
+ const mod = require("./markdown-renderer.js");
105
+ return { renderMarkdown: mod.renderMarkdown, attachStreamingRenderer: mod.attachStreamingRenderer };
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Scope-aware storage key helpers (REQ-3). The home key is the legacy
113
+ // single global; the task key is per-task-id so different tasks don't
114
+ // clobber each other across reloads.
115
+ // ---------------------------------------------------------------------------
116
+
117
+ function storageKey(scope, taskId) {
118
+ if (scope === "task" && Number.isInteger(taskId)) {
119
+ return `taskShow.chat.task.${taskId}.sid`;
120
+ }
121
+ return HOME_STORAGE_KEY;
122
+ }
123
+
124
+ function readScopedSessionId(scope, taskId) {
125
+ try {
126
+ const w = typeof window !== "undefined" ? window : null;
127
+ if (!w || !w.localStorage) return "";
128
+ return w.localStorage.getItem(storageKey(scope, taskId)) || "";
129
+ } catch {
130
+ return "";
131
+ }
132
+ }
133
+
134
+ function writeScopedSessionId(scope, taskId, id) {
135
+ try {
136
+ const w = typeof window !== "undefined" ? window : null;
137
+ if (!w || !w.localStorage) return;
138
+ w.localStorage.setItem(storageKey(scope, taskId), id);
139
+ } catch {
140
+ /* ignore */
141
+ }
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Persistence helpers (legacy single-key API; REQ-2 home path uses
146
+ // these directly). REQ-3 task scope goes through readScopedSessionId /
147
+ // writeScopedSessionId above. Guarded try/catch so headless test envs
148
+ // without localStorage don't blow up.
149
+ // ---------------------------------------------------------------------------
150
+
151
+ export function readSessionId(win) {
152
+ try {
153
+ const w = win || (typeof window !== "undefined" ? window : null);
154
+ if (!w || !w.localStorage) return "";
155
+ return w.localStorage.getItem(HOME_STORAGE_KEY) || "";
156
+ } catch {
157
+ return "";
158
+ }
159
+ }
160
+
161
+ export function writeSessionId(id, win) {
162
+ try {
163
+ const w = win || (typeof window !== "undefined" ? window : null);
164
+ if (!w || !w.localStorage) return;
165
+ w.localStorage.setItem(HOME_STORAGE_KEY, id);
166
+ } catch {
167
+ /* ignore */
168
+ }
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // DOM mutation helpers
173
+ // ---------------------------------------------------------------------------
174
+
175
+ export function clearMessages(container) {
176
+ while (container.firstChild) container.removeChild(container.firstChild);
177
+ }
178
+
179
+ /**
180
+ * Append a message bubble to the chat log.
181
+ *
182
+ * - `role === "assistant"` → text is rendered via
183
+ * `markdownRenderer.renderMarkdown()` (marked + DOMPurify).
184
+ * - `role === "user"` → text stays `textContent`. Users must not
185
+ * be able to inject HTML / markdown into their own bubble.
186
+ * - `role === "error"` → also `textContent` — error text should
187
+ * be readable as-is, no formatting.
188
+ *
189
+ * Returns the created element so the test harness can inspect it.
190
+ */
191
+ export function appendMessage(container, role, text) {
192
+ const doc = getDoc(container);
193
+ if (!doc) throw new Error("chat-panel.appendMessage: no document available");
194
+ const empty = container.querySelector(".chat-empty");
195
+ if (empty) empty.remove();
196
+ const el = doc.createElement("div");
197
+ el.className = "chat-message chat-message--" + role;
198
+ el.dataset.role = role;
199
+ const label = role === "user" ? "You" : role === "assistant" ? "Assistant" : "Error";
200
+ el.innerHTML =
201
+ `<div class="chat-message-meta">${label}</div>` +
202
+ `<div class="chat-message-text"></div>`;
203
+ const textEl = el.querySelector(".chat-message-text");
204
+ if (role === "assistant") {
205
+ const renderer = resolveRenderer();
206
+ if (renderer) {
207
+ textEl.innerHTML = renderer.renderMarkdown(text);
208
+ } else {
209
+ // Defensive fallback — should never hit in production.
210
+ textEl.textContent = text;
211
+ }
212
+ } else {
213
+ // user / error — textContent literal.
214
+ textEl.textContent = text;
215
+ }
216
+ container.appendChild(el);
217
+ if (container.scrollHeight != null) {
218
+ container.scrollTop = container.scrollHeight;
219
+ }
220
+ return el;
221
+ }
222
+
223
+ /**
224
+ * Streaming chunk → debounced markdown render. The first chunk of a
225
+ * turn creates the assistant bubble lazily; subsequent chunks feed
226
+ * the bubble's streaming renderer.
227
+ *
228
+ * Returns the controller (with `flush` / `dispose`) so the caller
229
+ * can force a final render on `event: done`.
230
+ */
231
+ export function appendChunk(container, text) {
232
+ const win = getWindow(getDoc(container));
233
+ let last = container.querySelector(".chat-message--assistant:last-of-type");
234
+ if (!last) last = appendMessage(container, "assistant", "");
235
+ const textEl = last.querySelector(".chat-message-text");
236
+
237
+ // Cache the streaming controller on the element so successive
238
+ // chunks reuse the same buffer + timer.
239
+ let ctrl = last.__markdownStreamCtrl;
240
+ const renderer = resolveRenderer();
241
+ if (!ctrl && renderer) {
242
+ ctrl = renderer.attachStreamingRenderer(textEl, { debounceMs: 50 });
243
+ last.__markdownStreamCtrl = ctrl;
244
+ }
245
+
246
+ if (ctrl) {
247
+ ctrl.append(text);
248
+ } else {
249
+ // No renderer available — fall back to textContent (textContent
250
+ // of a chunk is the safest path when DOMPurify isn't loaded).
251
+ textEl.textContent += text;
252
+ }
253
+ if (win && container.scrollHeight != null) {
254
+ container.scrollTop = container.scrollHeight;
255
+ }
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Form / network helpers (kept outside any IIFE so they can be reached
260
+ // in the browser; tests don't exercise these).
261
+ // ---------------------------------------------------------------------------
262
+
263
+ function setBusy(form, busy) {
264
+ const input = form.querySelector(INPUT_SEL);
265
+ const send = form.querySelector(SEND_SEL);
266
+ if (input) input.disabled = busy;
267
+ if (send) {
268
+ send.disabled = busy;
269
+ send.textContent = busy ? "Sending…" : "Send";
270
+ }
271
+ }
272
+
273
+ function showDisabledBanner(shell, msg) {
274
+ const doc = getDoc(shell);
275
+ if (!doc) return;
276
+ const banner = doc.createElement("p");
277
+ banner.className = "chat-banner";
278
+ banner.dataset.role = "banner";
279
+ banner.textContent = msg;
280
+ shell.appendChild(banner);
281
+ const form = shell.querySelector(FORM_SEL);
282
+ if (form) form.remove();
283
+ }
284
+
285
+ /**
286
+ * Resolve a fresh session id for the current scope.
287
+ * - scope=home: POST /api/chat/start → { sessionId }
288
+ * - scope=task: deterministic `task-${taskId}`; we don't need to
289
+ * round-trip the server because the task-detail endpoint keys
290
+ * off the taskId directly. This guarantees multi-turn
291
+ * conversations survive reloads even when the CLI never saw a
292
+ * /start call.
293
+ */
294
+ async function ensureSessionId(scope, taskId) {
295
+ if (scope === "task" && Number.isInteger(taskId)) {
296
+ return `task-${taskId}`;
297
+ }
298
+ const cached = readScopedSessionId(scope, taskId);
299
+ if (cached) return cached;
300
+ const res = await fetch("/api/chat/start", {
301
+ method: "POST",
302
+ headers: { "Content-Type": "application/json" },
303
+ body: "{}",
304
+ });
305
+ if (!res.ok) throw new Error("chat_start_failed: " + res.status);
306
+ const j = await res.json();
307
+ writeScopedSessionId(scope, taskId, j.sessionId);
308
+ return j.sessionId;
309
+ }
310
+
311
+ /**
312
+ * Compute the message-endpoint URL for the current scope.
313
+ * - scope=home: /api/chat/<sessionId>/message
314
+ * - scope=task: /api/task-chat/<taskId>/message (server derives
315
+ * sessionId from taskId, so we never need it on the client side)
316
+ *
317
+ * If `opts.endpoint` is supplied, it's used as the base prefix
318
+ * (mostly for tests / proxies); the resource sub-path is still
319
+ * computed from scope + (taskId|sessionId).
320
+ */
321
+ function endpointFor(opts, sessionId) {
322
+ const base = (opts && opts.endpoint) || "";
323
+ const trimBase = base.replace(/\/+$/, "");
324
+ if (opts.scope === "task" && Number.isInteger(opts.taskId)) {
325
+ const url = `/api/task-chat/${opts.taskId}/message`;
326
+ return trimBase ? `${trimBase}${url}` : url;
327
+ }
328
+ const url = `/api/chat/${encodeURIComponent(sessionId)}/message`;
329
+ return trimBase ? `${trimBase}${url}` : url;
330
+ }
331
+
332
+ /**
333
+ * Public hook for the test harness — only used by the browser init
334
+ * flow in tests that simulate a full submit. Kept as a named export
335
+ * so home-chat-v25's fake ACT test can call into the same code path.
336
+ *
337
+ * Legacy signature: sendMessage(form, container, message) — used by
338
+ * `init()` for the home scope. REQ-3 goes through attachChatPanel()
339
+ * instead, which calls into streamSession() below.
340
+ */
341
+ export async function sendMessage(form, container, message) {
342
+ return streamSession({ scope: "home", endpoint: "" }, form, container, message);
343
+ }
344
+
345
+ /**
346
+ * Stream one user message to the server, render chunks back into the
347
+ * container. Shared by both legacy `sendMessage` (home) and the
348
+ * REQ-3 `attachChatPanel` (home + task).
349
+ */
350
+ async function streamSession(opts, form, container, message) {
351
+ let sessionId;
352
+ try {
353
+ sessionId = await ensureSessionId(opts.scope, opts.taskId);
354
+ } catch (e) {
355
+ appendMessage(container, "error", String(e && e.message || e));
356
+ return;
357
+ }
358
+ appendMessage(container, "user", message);
359
+ setBusy(form, true);
360
+
361
+ const url = endpointFor(opts, sessionId);
362
+ let res;
363
+ try {
364
+ res = await fetch(url, {
365
+ method: "POST",
366
+ headers: {
367
+ "Content-Type": "application/json",
368
+ Accept: "text/event-stream",
369
+ },
370
+ body: JSON.stringify({ message }),
371
+ });
372
+ } catch (e) {
373
+ appendMessage(container, "error", "network_error: " + (e && e.message || e));
374
+ setBusy(form, false);
375
+ return;
376
+ }
377
+
378
+ if (!res.ok || !res.body) {
379
+ let detail = "";
380
+ try { detail = await res.text(); } catch { /* ignore */ }
381
+ appendMessage(container, "error", `server_error: ${res.status} ${detail.slice(0, 200)}`);
382
+ setBusy(form, false);
383
+ return;
384
+ }
385
+
386
+ const reader = res.body.getReader();
387
+ const decoder = new TextDecoder("utf-8");
388
+ let buf = "";
389
+ let sawError = false;
390
+
391
+ try {
392
+ while (true) {
393
+ const { value, done } = await reader.read();
394
+ if (done) break;
395
+ buf += decoder.decode(value, { stream: true });
396
+ let nl;
397
+ while ((nl = buf.indexOf("\n\n")) >= 0) {
398
+ const frame = buf.slice(0, nl);
399
+ buf = buf.slice(nl + 2);
400
+ handleFrame(frame, container, (err) => { sawError = sawError || !!err; });
401
+ }
402
+ }
403
+ // Flush trailing frame if any.
404
+ if (buf.trim().length > 0) handleFrame(buf, container, () => {});
405
+ } catch (e) {
406
+ appendMessage(container, "error", "stream_error: " + (e && e.message || e));
407
+ } finally {
408
+ setBusy(form, false);
409
+ // Final markdown flush so the last debounced chunk renders before
410
+ // the busy spinner goes away.
411
+ const last = container.querySelector(".chat-message--assistant:last-of-type");
412
+ if (last && last.__markdownStreamCtrl && last.__markdownStreamCtrl.flush) {
413
+ last.__markdownStreamCtrl.flush();
414
+ }
415
+ // Suppress the final empty-message that may have been left if the
416
+ // assistant produced no text. We intentionally leave error messages
417
+ // alone so the user can see what went wrong.
418
+ if (!sawError) {
419
+ const last2 = container.querySelector(".chat-message--assistant:last-of-type");
420
+ if (last2 && !last2.querySelector(".chat-message-text").textContent.trim()) {
421
+ last2.remove();
422
+ }
423
+ }
424
+ }
425
+ }
426
+
427
+ /** Parse one SSE frame (event:/data:/id: lines). Returns the parsed frame or null. */
428
+ function handleFrame(raw, container, onError) {
429
+ let event = "";
430
+ let data = "";
431
+ let id = "";
432
+ for (const line of raw.split(/\r?\n/)) {
433
+ if (line.startsWith(":")) continue;
434
+ if (line.startsWith("event:")) event = line.slice(6).trim();
435
+ else if (line.startsWith("data:")) data += (data ? "\n" : "") + line.slice(5).trim();
436
+ else if (line.startsWith("id:")) id = line.slice(3).trim();
437
+ }
438
+ if (!event && !data) return null;
439
+ let parsed = null;
440
+ if (data) {
441
+ try { parsed = JSON.parse(data); } catch { parsed = data; }
442
+ }
443
+ if (event === "chunk" && parsed && typeof parsed === "object") {
444
+ if (parsed.type === "text" && typeof parsed.text === "string") {
445
+ appendChunk(container, parsed.text);
446
+ } else if (parsed.type === "reasoning" && typeof parsed.text === "string") {
447
+ // Reasoning is hidden in the chat UI by design; we still log it
448
+ // so a developer can inspect via DevTools.
449
+ if (typeof console !== "undefined" && console.debug) {
450
+ console.debug("[chat] reasoning:", parsed.text);
451
+ }
452
+ } else if (parsed.type === "start") {
453
+ // No-op: the assistant bubble is created lazily on first text chunk.
454
+ } else if (parsed.type === "error" && parsed.error) {
455
+ onError(parsed);
456
+ appendMessage(container, "error", parsed.error.message || "unknown error");
457
+ }
458
+ } else if (event === "done") {
459
+ // Terminal marker — final flush is handled by the caller's finally.
460
+ } else if (event === "error") {
461
+ onError(parsed);
462
+ const msg = parsed && parsed.error && parsed.error.message
463
+ ? parsed.error.message
464
+ : "stream error";
465
+ appendMessage(container, "error", msg);
466
+ } else if (event === "ready") {
467
+ // SSE handshake — nothing to render.
468
+ }
469
+ return { event, data, id, parsed };
470
+ }
471
+
472
+ /**
473
+ * Resolve the chat options from DOM data-attrs when the caller
474
+ * didn't pass them explicitly. Lets the server emit a single
475
+ * `<section data-chat-scope="task" data-task-id="4242">` and have
476
+ * the JS pick up the right endpoint / storage key automatically.
477
+ */
478
+ function readOptsFromRoot(root, opts) {
479
+ const merged = Object.assign({}, opts || {});
480
+ if (!merged.scope) {
481
+ const scopeAttr = root.getAttribute && root.getAttribute("data-chat-scope");
482
+ merged.scope = scopeAttr === "task" ? "task" : "home";
483
+ }
484
+ if (merged.scope === "task" && !Number.isInteger(merged.taskId)) {
485
+ const idAttr = root.getAttribute && root.getAttribute("data-task-id");
486
+ const n = Number(idAttr);
487
+ if (Number.isInteger(n)) merged.taskId = n;
488
+ }
489
+ return merged;
490
+ }
491
+
492
+ /**
493
+ * Factory — wire a chat panel inside `root` using the supplied
494
+ * options. Returns the controller handle (for tests / programmatic
495
+ * sends). Backward-compatible with the legacy `init()` entry point
496
+ * which auto-discovers the home shell.
497
+ *
498
+ * @param {HTMLElement} root the [data-chat-shell] element
499
+ * @param {{
500
+ * endpoint?: string; // optional base prefix (e.g. "" or "https://proxy")
501
+ * scope?: "home"|"task"; // which API endpoint to use (default: from DOM data-chat-scope)
502
+ * taskId?: number; // required when scope === "task"
503
+ * }} [opts]
504
+ */
505
+ export function attachChatPanel(root, opts) {
506
+ if (!root) return null;
507
+ const finalOpts = readOptsFromRoot(root, opts);
508
+ const form = root.querySelector(FORM_SEL);
509
+ const container = root.querySelector(MESSAGES_SEL);
510
+ const input = root.querySelector(INPUT_SEL);
511
+ if (!form || !container || !input) return null;
512
+
513
+ // Probe feature availability with a HEAD request. If 503, hide UI.
514
+ // For task scope we don't have a generic /api/chat/start — but
515
+ // the same server returns 503 on /api/task-chat/0/message when
516
+ // the chat is disabled (any numeric id triggers the chatManager
517
+ // check), so we use that as a feature probe.
518
+ const probeUrl = finalOpts.scope === "task"
519
+ ? `/api/task-chat/0/message`
520
+ : "/api/chat/start";
521
+ fetch(probeUrl, { method: "HEAD" }).then((res) => {
522
+ if (res.status === 503) {
523
+ showDisabledBanner(root, "Chat disabled (server returned 503 — chatOptions not configured).");
524
+ }
525
+ }).catch(() => { /* offline — leave UI alone */ });
526
+
527
+ form.addEventListener("submit", (e) => {
528
+ e.preventDefault();
529
+ const message = (input.value || "").trim();
530
+ if (!message) return;
531
+ input.value = "";
532
+ streamSession(finalOpts, form, container, message);
533
+ });
534
+
535
+ return { root, opts: finalOpts, send: (msg) => streamSession(finalOpts, form, container, msg) };
536
+ }
537
+
538
+ export function init() {
539
+ const shell = $(null, SHELL_SEL);
540
+ if (!shell) return null;
541
+ return attachChatPanel(shell, {});
542
+ }
543
+
544
+ // ---------------------------------------------------------------------------
545
+ // Browser-only bootstrap. Tests run this file under bun:test where
546
+ // `document`/`window` are not globals, so the if-branch is skipped and
547
+ // only the named exports are available.
548
+ // ---------------------------------------------------------------------------
549
+
550
+ if (typeof document !== "undefined" && typeof window !== "undefined") {
551
+ if (document.readyState === "loading") {
552
+ document.addEventListener("DOMContentLoaded", init);
553
+ } else {
554
+ init();
555
+ }
556
+ // Expose a tiny test hook so headless harness can drive the panel
557
+ // without firing real events.
558
+ window.taskShowChatPanel = {
559
+ init,
560
+ attachChatPanel,
561
+ sendMessage,
562
+ storageKey,
563
+ HOME_STORAGE_KEY,
564
+ };
565
+ }
@@ -0,0 +1,3 @@
1
+ /*! @license DOMPurify 3.4.12 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.12/LICENSE */
2
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,function(){"use strict";function e(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=Array(t);n<t;n++)o[n]=e[n];return o}function t(t,n){return function(e){if(Array.isArray(e))return e}(t)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var o,r,i,a,l=[],c=!0,s=!1;try{if(i=(n=n.call(e)).next,0===t);else for(;!(c=(o=i.call(n)).done)&&(l.push(o.value),l.length!==t);c=!0);}catch(e){s=!0,r=e}finally{try{if(!c&&null!=n.return&&(a=n.return(),Object(a)!==a))return}finally{if(s)throw r}}return l}}(t,n)||function(t,n){if(t){if("string"==typeof t)return e(t,n);var o={}.toString.call(t).slice(8,-1);return"Object"===o&&t.constructor&&(o=t.constructor.name),"Map"===o||"Set"===o?Array.from(t):"Arguments"===o||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(o)?e(t,n):void 0}}(t,n)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}const n=Object.entries,o=Object.setPrototypeOf,r=Object.isFrozen,i=Object.getPrototypeOf,a=Object.getOwnPropertyDescriptor;let l=Object.freeze,c=Object.seal,s=Object.create,u="undefined"!=typeof Reflect&&Reflect,f=u.apply,p=u.construct;l||(l=function(e){return e}),c||(c=function(e){return e}),f||(f=function(e,t){for(var n=arguments.length,o=new Array(n>2?n-2:0),r=2;r<n;r++)o[r-2]=arguments[r];return e.apply(t,o)}),p||(p=function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),o=1;o<t;o++)n[o-1]=arguments[o];return new e(...n)});const m=L(Array.prototype.forEach),d=L(Array.prototype.lastIndexOf),h=L(Array.prototype.pop),g=L(Array.prototype.push),y=L(Array.prototype.splice),b=Array.isArray,T=L(String.prototype.toLowerCase),S=L(String.prototype.toString),E=L(String.prototype.match),A=L(String.prototype.replace),N=L(String.prototype.indexOf),_=L(String.prototype.trim),w=L(Number.prototype.toString),O=L(Boolean.prototype.toString),v="undefined"==typeof BigInt?null:L(BigInt.prototype.toString),D="undefined"==typeof Symbol?null:L(Symbol.prototype.toString),R=L(Object.prototype.hasOwnProperty),C=L(Object.prototype.toString),I=L(RegExp.prototype.test),x=(k=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return p(k,t)});var k;function L(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return f(e,t,o)}}function M(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:T;if(o&&o(e,null),!b(t))return e;let i=t.length;for(;i--;){let o=t[i];if("string"==typeof o){const e=n(o);e!==o&&(r(t)||(t[i]=e),o=e)}e[o]=!0}return e}function z(e){for(let t=0;t<e.length;t++){R(e,t)||(e[t]=null)}return e}function P(e){const o=s(null);for(const i of n(e)){var r=t(i,2);const n=r[0],a=r[1];R(e,n)&&(b(a)?o[n]=z(a):a&&"object"==typeof a&&a.constructor===Object?o[n]=P(a):o[n]=a)}return o}function U(e,t){for(;null!==e;){const n=a(e,t);if(n){if(n.get)return L(n.get);if("function"==typeof n.value)return L(n.value)}e=i(e)}return function(){return null}}const F=l(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),H=l(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),j=l(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),B=l(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),G=l(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),W=l(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),Y=l(["#text"]),q=l(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","command","commandfor","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns"]),X=l(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dominant-baseline","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-orientation","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),$=l(["accent","accentunder","align","bevelled","close","columnalign","columnlines","columnspacing","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lquote","lspace","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),K=l(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),V=c(/{{[\w\W]*|^[\w\W]*}}/g),Z=c(/<%[\w\W]*|^[\w\W]*%>/g),J=c(/\${[\w\W]*/g),Q=c(/^data-[\-\w.\u00B7-\uFFFF]+$/),ee=c(/^aria-[\-\w]+$/),te=c(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),ne=c(/^(?:\w+script|data):/i),oe=c(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),re=c(/^html$/i),ie=c(/^[a-z][.\w]*(-[.\w]+)+$/i),ae=c(/<[/\w!]/g),le=c(/<[/\w]/g),ce=c(/<\/no(script|embed|frames)/i),se=c(/\/>/i),ue=1,fe=3,pe=7,me=8,de=9,he=11,ge=function(){return"undefined"==typeof window?null:window},ye=function(e,t,n,o){return R(e,t)&&b(e[t])?M(o.base?P(o.base):{},e[t],o.transform):n};var be=function e(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:ge();const o=t=>e(t);if(o.version="3.4.12",o.removed=[],!t||!t.document||t.document.nodeType!==de||!t.Element)return o.isSupported=!1,o;let r=t.document;const i=r,a=i.currentScript;t.DocumentFragment;const u=t.HTMLTemplateElement,f=t.Node,p=t.Element,k=t.NodeFilter,L=t.NamedNodeMap;void 0===L&&(t.NamedNodeMap||t.MozNamedAttrMap),t.HTMLFormElement;const z=t.DOMParser,be=t.trustedTypes,Te=p.prototype,Se=U(Te,"cloneNode"),Ee=U(Te,"remove"),Ae=U(Te,"nextSibling"),Ne=U(Te,"childNodes"),_e=U(Te,"parentNode"),we=U(Te,"shadowRoot"),Oe=U(Te,"attributes"),ve=f&&f.prototype?U(f.prototype,"nodeType"):null,De=f&&f.prototype?U(f.prototype,"nodeName"):null;if("function"==typeof u){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let Re,Ce,Ie="",xe=!1,ke=0;const Le=function(){if(ke>0)throw x('A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.')},Me=function(e){Le(),ke++;try{return Re.createHTML(e)}finally{ke--}},ze=function(){return xe||(Ce=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(be,a),xe=!0),Ce},Pe=r,Ue=Pe.implementation,Fe=Pe.createNodeIterator,He=Pe.createDocumentFragment,je=Pe.getElementsByTagName,Be=i.importNode;let Ge={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};o.isSupported="function"==typeof n&&"function"==typeof _e&&Ue&&void 0!==Ue.createHTMLDocument;const We=V,Ye=Z,qe=J,Xe=Q,$e=ee,Ke=ne,Ve=oe,Ze=ie;let Je=te,Qe=null;const et=M({},[...F,...H,...j,...G,...Y]);let tt=null;const nt=M({},[...q,...X,...$,...K]);let ot=Object.seal(s(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),rt=null,it=null;const at=Object.seal(s(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let lt=!0,ct=!0,st=!1,ut=!0,ft=!1,pt=!0,mt=!1,dt=!1,ht=null,gt=null,yt=!1,bt=!1,Tt=!1,St=!1,Et=!0,At=!1;const Nt="user-content-";let _t=!0,wt=!1,Ot={},vt=null;const Dt=M({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","selectedcontent","style","svg","template","thead","title","video","xmp"]);let Rt=null;const Ct=M({},["audio","video","img","source","image","track"]);let It=null;const xt=M({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),kt="http://www.w3.org/1998/Math/MathML",Lt="http://www.w3.org/2000/svg",Mt="http://www.w3.org/1999/xhtml";let zt=Mt,Pt=!1,Ut=null;const Ft=M({},[kt,Lt,Mt],S),Ht=l(["mi","mo","mn","ms","mtext"]);let jt=M({},Ht);const Bt=l(["annotation-xml"]);let Gt=M({},Bt);const Wt=M({},["title","style","font","a","script"]);let Yt=null;const qt=["application/xhtml+xml","text/html"];let Xt=null,$t=null;const Kt=r.createElement("form"),Vt=function(e){return e instanceof RegExp||e instanceof Function},Zt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if($t&&$t===e)return;e&&"object"==typeof e||(e={}),e=P(e),Yt=-1===qt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,Xt="application/xhtml+xml"===Yt?S:T,Qe=ye(e,"ALLOWED_TAGS",et,{transform:Xt}),tt=ye(e,"ALLOWED_ATTR",nt,{transform:Xt}),Ut=ye(e,"ALLOWED_NAMESPACES",Ft,{transform:S}),It=ye(e,"ADD_URI_SAFE_ATTR",xt,{transform:Xt,base:xt}),Rt=ye(e,"ADD_DATA_URI_TAGS",Ct,{transform:Xt,base:Ct}),vt=ye(e,"FORBID_CONTENTS",Dt,{transform:Xt}),rt=ye(e,"FORBID_TAGS",P({}),{transform:Xt}),it=ye(e,"FORBID_ATTR",P({}),{transform:Xt}),Ot=!!R(e,"USE_PROFILES")&&(e.USE_PROFILES&&"object"==typeof e.USE_PROFILES?P(e.USE_PROFILES):e.USE_PROFILES),lt=!1!==e.ALLOW_ARIA_ATTR,ct=!1!==e.ALLOW_DATA_ATTR,st=e.ALLOW_UNKNOWN_PROTOCOLS||!1,ut=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ft=e.SAFE_FOR_TEMPLATES||!1,pt=!1!==e.SAFE_FOR_XML,mt=e.WHOLE_DOCUMENT||!1,bt=e.RETURN_DOM||!1,Tt=e.RETURN_DOM_FRAGMENT||!1,St=e.RETURN_TRUSTED_TYPE||!1,yt=e.FORCE_BODY||!1,Et=!1!==e.SANITIZE_DOM,At=e.SANITIZE_NAMED_PROPS||!1,_t=!1!==e.KEEP_CONTENT,wt=e.IN_PLACE||!1,Je=function(e){try{return I(e,""),!0}catch(e){return!1}}(e.ALLOWED_URI_REGEXP)?e.ALLOWED_URI_REGEXP:te,zt="string"==typeof e.NAMESPACE?e.NAMESPACE:Mt,jt=R(e,"MATHML_TEXT_INTEGRATION_POINTS")&&e.MATHML_TEXT_INTEGRATION_POINTS&&"object"==typeof e.MATHML_TEXT_INTEGRATION_POINTS?P(e.MATHML_TEXT_INTEGRATION_POINTS):M({},Ht),Gt=R(e,"HTML_INTEGRATION_POINTS")&&e.HTML_INTEGRATION_POINTS&&"object"==typeof e.HTML_INTEGRATION_POINTS?P(e.HTML_INTEGRATION_POINTS):M({},Bt);const t=R(e,"CUSTOM_ELEMENT_HANDLING")&&e.CUSTOM_ELEMENT_HANDLING&&"object"==typeof e.CUSTOM_ELEMENT_HANDLING?P(e.CUSTOM_ELEMENT_HANDLING):s(null);if(ot=s(null),R(t,"tagNameCheck")&&Vt(t.tagNameCheck)&&(ot.tagNameCheck=t.tagNameCheck),R(t,"attributeNameCheck")&&Vt(t.attributeNameCheck)&&(ot.attributeNameCheck=t.attributeNameCheck),R(t,"allowCustomizedBuiltInElements")&&"boolean"==typeof t.allowCustomizedBuiltInElements&&(ot.allowCustomizedBuiltInElements=t.allowCustomizedBuiltInElements),c(ot),ft&&(ct=!1),Tt&&(bt=!0),Ot&&(Qe=M({},Y),tt=s(null),!0===Ot.html&&(M(Qe,F),M(tt,q)),!0===Ot.svg&&(M(Qe,H),M(tt,X),M(tt,K)),!0===Ot.svgFilters&&(M(Qe,j),M(tt,X),M(tt,K)),!0===Ot.mathMl&&(M(Qe,G),M(tt,$),M(tt,K))),at.tagCheck=null,at.attributeCheck=null,R(e,"ADD_TAGS")&&("function"==typeof e.ADD_TAGS?at.tagCheck=e.ADD_TAGS:b(e.ADD_TAGS)&&(Qe===et&&(Qe=P(Qe)),M(Qe,e.ADD_TAGS,Xt))),R(e,"ADD_ATTR")&&("function"==typeof e.ADD_ATTR?at.attributeCheck=e.ADD_ATTR:b(e.ADD_ATTR)&&(tt===nt&&(tt=P(tt)),M(tt,e.ADD_ATTR,Xt))),R(e,"ADD_URI_SAFE_ATTR")&&b(e.ADD_URI_SAFE_ATTR)&&M(It,e.ADD_URI_SAFE_ATTR,Xt),R(e,"FORBID_CONTENTS")&&b(e.FORBID_CONTENTS)&&(vt===Dt&&(vt=P(vt)),M(vt,e.FORBID_CONTENTS,Xt)),R(e,"ADD_FORBID_CONTENTS")&&b(e.ADD_FORBID_CONTENTS)&&(vt===Dt&&(vt=P(vt)),M(vt,e.ADD_FORBID_CONTENTS,Xt)),_t&&(Qe["#text"]=!0),mt&&M(Qe,["html","head","body"]),Qe.table&&(M(Qe,["tbody"]),delete rt.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw x('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw x('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');const t=Re;Re=e.TRUSTED_TYPES_POLICY;try{Ie=Me("")}catch(e){throw Re=t,e}}else null===e.TRUSTED_TYPES_POLICY?(Re=void 0,Ie=""):(void 0===Re&&(Re=ze()),Re&&"string"==typeof Ie&&(Ie=Me("")));l&&l(e),$t=e},Jt=M({},[...H,...j,...B]),Qt=M({},[...G,...W]),en=function(e){let t=_e(e);t&&t.tagName||(t={namespaceURI:zt,tagName:"template"});const n=T(e.tagName),o=T(t.tagName);return!!Ut[e.namespaceURI]&&(e.namespaceURI===Lt?function(e,t,n){return t.namespaceURI===Mt?"svg"===e:t.namespaceURI===kt?"svg"===e&&("annotation-xml"===n||jt[n]):Boolean(Jt[e])}(n,t,o):e.namespaceURI===kt?function(e,t,n){return t.namespaceURI===Mt?"math"===e:t.namespaceURI===Lt?"math"===e&&Gt[n]:Boolean(Qt[e])}(n,t,o):e.namespaceURI===Mt?function(e,t,n){return!(t.namespaceURI===Lt&&!Gt[n])&&!(t.namespaceURI===kt&&!jt[n])&&!Qt[e]&&(Wt[e]||!Jt[e])}(n,t,o):!("application/xhtml+xml"!==Yt||!Ut[e.namespaceURI]))},tn=function(e){g(o.removed,{element:e});try{_e(e).removeChild(e)}catch(t){if(Ee(e),!_e(e))throw x("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place")}},nn=function(e){an(e);const t=Ne(e);if(t){const e=[];m(t,t=>{g(e,t)}),m(e,e=>{try{Ee(e)}catch(e){}})}const n=Oe(e);if(n)for(let t=n.length-1;t>=0;--t){const o=n[t],r=o&&o.name;if("string"==typeof r)try{e.removeAttribute(r)}catch(e){}}},on=function(e,t){try{g(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){g(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(bt||Tt)try{tn(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},rn=function(e){const t=Oe(e);if(t)for(let n=t.length-1;n>=0;--n){const o=t[n],r=o&&o.name;if("string"==typeof r&&!tt[Xt(r)])try{e.removeAttribute(r)}catch(e){}}},an=function(e){const t=[e];for(;t.length>0;){const e=t.pop();(ve?ve(e):e.nodeType)===ue&&rn(e);const n=Ne(e);if(n)for(let e=n.length-1;e>=0;--e)t.push(n[e])}},ln=function(e){let t=null,n=null;if(yt)e="<remove></remove>"+e;else{const t=E(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===Yt&&zt===Mt&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const o=Re?Me(e):e;if(zt===Mt)try{t=(new z).parseFromString(o,Yt)}catch(e){}if(!t||!t.documentElement){t=Ue.createDocument(zt,"template",null);try{t.documentElement.innerHTML=Pt?Ie:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),zt===Mt?je.call(t,mt?"html":"body")[0]:mt?t.documentElement:i},cn=function(e){return Fe.call(e.ownerDocument||e,e,k.SHOW_ELEMENT|k.SHOW_COMMENT|k.SHOW_TEXT|k.SHOW_PROCESSING_INSTRUCTION|k.SHOW_CDATA_SECTION,null)},sn=function(e){return e=A(e,We," "),e=A(e,Ye," "),e=A(e,qe," ")},un=function(e){var t;e.normalize();const n=Fe.call(e.ownerDocument||e,e,k.SHOW_TEXT|k.SHOW_COMMENT|k.SHOW_CDATA_SECTION|k.SHOW_PROCESSING_INSTRUCTION,null);let o=n.nextNode();for(;o;)o.data=sn(o.data),o=n.nextNode();const r=null===(t=e.querySelectorAll)||void 0===t?void 0:t.call(e,"template");r&&m(r,e=>{pn(e.content)&&un(e.content)})},fn=function(e){const t=De?De(e):null;return"string"==typeof t&&("form"===Xt(t)&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||e.attributes!==Oe(e)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes||e.nodeType!==ve(e)||e.childNodes!==Ne(e)))},pn=function(e){if(!ve||"object"!=typeof e||null===e)return!1;try{return ve(e)===he}catch(e){return!1}},mn=function(e){if(!ve||"object"!=typeof e||null===e)return!1;try{return"number"==typeof ve(e)}catch(e){return!1}};function dn(e,t,n){0!==e.length&&m(e,e=>{e.call(o,t,n,$t)})}const hn=function(e,t){if(dn(Ge.beforeSanitizeElements,e,null),e!==t&&null===_e(e))return!0;if(fn(e))return tn(e),!0;const n=Xt(De?De(e):e.nodeName);if(dn(Ge.uponSanitizeElement,e,{tagName:n,allowedTags:Qe}),e!==t&&null===_e(e))return!0;if(function(e,t){return!!(pt&&e.hasChildNodes()&&!mn(e.firstElementChild)&&I(ae,e.textContent)&&I(ae,e.innerHTML))||!(!pt||e.namespaceURI!==Mt||"style"!==t||!mn(e.firstElementChild))||e.nodeType===pe||!(!pt||e.nodeType!==me||!I(le,e.data))}(e,n))return tn(e),!0;if(rt[n]||!(at.tagCheck instanceof Function&&at.tagCheck(n))&&!Qe[n]){const t=function(e,t){if(!rt[t]&&bn(t)){if(ot.tagNameCheck instanceof RegExp&&I(ot.tagNameCheck,t))return!1;if(ot.tagNameCheck instanceof Function&&ot.tagNameCheck(t))return!1}if(_t&&!vt[t]){const t=_e(e),n=Ne(e);if(n&&t)for(let o=n.length-1;o>=0;--o){const r=wt?n[o]:Se(n[o],!0);t.insertBefore(r,Ae(e))}}return tn(e),!0}(e,n);return!1===t&&dn(Ge.afterSanitizeElements,e,null),t}if((ve?ve(e):e.nodeType)===ue&&!en(e))return tn(e),!0;if(("noscript"===n||"noembed"===n||"noframes"===n)&&I(ce,e.innerHTML))return tn(e),!0;if(ft&&e.nodeType===fe){const t=sn(e.textContent);e.textContent!==t&&(g(o.removed,{element:e.cloneNode()}),e.textContent=t)}return dn(Ge.afterSanitizeElements,e,null),!1},gn=function(e,t,n){if(it[t])return!1;if(pt&&"patchsrc"===t)return!1;if(pt&&"for"===t&&"label"!==e&&"output"!==e)return!1;if(Et&&("id"===t||"name"===t)&&(n in r||n in Kt))return!1;const o=tt[t]||at.attributeCheck instanceof Function&&at.attributeCheck(t,e);if(ct&&I(Xe,t));else if(lt&&I($e,t));else if(o)if(It[t]);else if(I(Je,A(n,Ve,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==N(n,"data:")||!Rt[e]){if(st&&!I(Ke,A(n,Ve,"")));else if(n)return!1}else;else if(!(bn(e)&&(ot.tagNameCheck instanceof RegExp&&I(ot.tagNameCheck,e)||ot.tagNameCheck instanceof Function&&ot.tagNameCheck(e))&&(ot.attributeNameCheck instanceof RegExp&&I(ot.attributeNameCheck,t)||ot.attributeNameCheck instanceof Function&&ot.attributeNameCheck(t,e))||"is"===t&&ot.allowCustomizedBuiltInElements&&(ot.tagNameCheck instanceof RegExp&&I(ot.tagNameCheck,n)||ot.tagNameCheck instanceof Function&&ot.tagNameCheck(n))))return!1;return!0},yn=M({},["annotation-xml","color-profile","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","missing-glyph"]),bn=function(e){return!yn[T(e)]&&I(Ze,e)},Tn=function(e,t,n,o){if(Re&&"object"==typeof be&&"function"==typeof be.getAttributeType&&!n)switch(be.getAttributeType(e,t)){case"TrustedHTML":return Me(o);case"TrustedScriptURL":return function(e){Le(),ke++;try{return Re.createScriptURL(e)}finally{ke--}}(o)}return o},Sn=function(e,t,n,r){try{n?e.setAttributeNS(n,t,r):e.setAttribute(t,r),fn(e)?tn(e):h(o.removed)}catch(n){on(t,e)}},En=function(e){dn(Ge.beforeSanitizeAttributes,e,null);const t=e.attributes;if(!t||fn(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:tt,forceKeepAttr:void 0};let o=t.length;const r=Xt(e.nodeName);for(;o--;){const i=t[o],a=i.name,l=i.namespaceURI,c=i.value,s=Xt(a),u=c;let f="value"===a?u:_(u);n.attrName=s,n.attrValue=f,n.keepAttr=!0,n.forceKeepAttr=void 0,dn(Ge.uponSanitizeAttribute,e,n),f=n.attrValue,!At||"id"!==s&&"name"!==s||0===N(f,Nt)||(on(a,e),f=Nt+f),pt&&I(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,f)?on(a,e):"attributename"===s&&E(f,"href")?on(a,e):n.forceKeepAttr||(n.keepAttr&&(ut||!I(se,f))?(ft&&(f=sn(f)),gn(r,s,f)?(f=Tn(r,s,l,f),f!==u&&Sn(e,a,l,f)):on(a,e)):on(a,e))}dn(Ge.afterSanitizeAttributes,e,null)},An=function(e){let t=null;const n=cn(e);for(dn(Ge.beforeSanitizeShadowDOM,e,null);t=n.nextNode();){dn(Ge.uponSanitizeShadowNode,t,null),hn(t,e),En(t),pn(t.content)&&An(t.content);if((ve?ve(t):t.nodeType)===ue){const e=we(t);pn(e)&&(Nn(e),An(e))}}dn(Ge.afterSanitizeShadowDOM,e,null)},Nn=function(e){const t=[{node:e,shadow:null}];for(;t.length>0;){const e=t.pop();if(e.shadow){An(e.shadow);continue}const n=e.node,o=(ve?ve(n):n.nodeType)===ue,r=Ne(n);if(r)for(let e=r.length-1;e>=0;--e)t.push({node:r[e],shadow:null});if(o){const e=De?De(n):null;if("string"==typeof e&&"template"===Xt(e)){const e=n.content;pn(e)&&t.push({node:e,shadow:null})}}if(o){const e=we(n);pn(e)&&t.push({node:null,shadow:e},{node:e,shadow:null})}}};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,a=null,l=null;if(Pt=!e,Pt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!mn(e)&&"string"!=typeof(e=function(e){switch(typeof e){case"string":return e;case"number":return w(e);case"boolean":return O(e);case"bigint":return v?v(e):"0";case"symbol":return D?D(e):"Symbol()";case"undefined":default:return C(e);case"function":case"object":{if(null===e)return C(e);const t=e,n=U(t,"toString");if("function"==typeof n){const e=n(t);return"string"==typeof e?e:C(e)}return C(e)}}}(e)))throw x("dirty is not a string, aborting");if(!o.isSupported)return e;dt?(Qe=ht,tt=gt):Zt(t),(Ge.uponSanitizeElement.length>0||Ge.uponSanitizeAttribute.length>0)&&(Qe=P(Qe)),Ge.uponSanitizeAttribute.length>0&&(tt=P(tt)),o.removed=[];const c=wt&&"string"!=typeof e&&mn(e);if(c){!function(e){if(!pt)return;const t=[e];for(;t.length>0;){const e=t.pop(),n=ve?ve(e):e.nodeType;if(n===pe||n===me&&I(le,e.data)){try{Ee(e)}catch(e){}continue}if(n===ue){const t=e,n=Xt(De?De(e):e.nodeName);try{t.hasAttribute&&t.hasAttribute("patchsrc")&&t.removeAttribute("patchsrc"),t.hasAttribute&&t.hasAttribute("for")&&"label"!==n&&"output"!==n&&t.removeAttribute("for")}catch(e){}}const o=Ne(e);if(o)for(let e=o.length-1;e>=0;--e)t.push(o[e])}}(e);const t=De?De(e):e.nodeName;if("string"==typeof t){const n=Xt(t);if(!Qe[n]||rt[n])throw nn(e),x("root node is forbidden and cannot be sanitized in-place")}if(fn(e))throw nn(e),x("root node is clobbered and cannot be sanitized in-place");try{Nn(e)}catch(t){throw nn(e),t}}else if(mn(e))n=ln("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===ue&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r),Nn(r);else{if(!bt&&!ft&&!mt&&-1===e.indexOf("<"))return Re&&St?Me(e):e;if(n=ln(e),!n)return bt?null:St?Ie:""}n&&yt&&tn(n.firstChild);const s=c?e:n,u=cn(s);try{for(;a=u.nextNode();)hn(a,s),En(a),pn(a.content)&&An(a.content)}catch(t){throw c&&(nn(e),m(o.removed,e=>{e.element&&an(e.element)})),t}if(c)return m(o.removed,e=>{e.element&&an(e.element)}),ft&&un(e),e;if(bt){if(ft&&un(n),Tt)for(l=He.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(tt.shadowroot||tt.shadowrootmode)&&(l=Be.call(i,l,!0)),l}let f=mt?n.outerHTML:n.innerHTML;return mt&&Qe["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&I(re,n.ownerDocument.doctype.name)&&(f="<!DOCTYPE "+n.ownerDocument.doctype.name+">\n"+f),ft&&(f=sn(f)),Re&&St?Me(f):f},o.setConfig=function(){Zt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),dt=!0,ht=Qe,gt=tt},o.clearConfig=function(){$t=null,dt=!1,ht=null,gt=null,Re=Ce,Ie=""},o.isValidAttribute=function(e,t,n){$t||Zt({});const o=Xt(e),r=Xt(t);return gn(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&R(Ge,e)&&g(Ge[e],t)},o.removeHook=function(e,t){if(R(Ge,e)){if(void 0!==t){const n=d(Ge[e],t);return-1===n?void 0:y(Ge[e],n,1)[0]}return h(Ge[e])}},o.removeHooks=function(e){R(Ge,e)&&(Ge[e]=[])},o.removeAllHooks=function(){Ge={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},o}();return be});
3
+ //# sourceMappingURL=purify.min.js.map
package/public/index.html CHANGED
@@ -107,5 +107,16 @@
107
107
  <script src="/static/tasks-tree.js"></script>
108
108
  <script src="/static/task-operations.js"></script>
109
109
  <script src="/static/session-forest.js"></script>
110
+ <!--
111
+ v2.5.x REQ-4: vendor markdown rendering for the home chat panel.
112
+ Load order matters - marked must register globals before DOMPurify
113
+ is constructed, and both must register before markdown-renderer.js
114
+ resolves them at import time. chat-panel.js only depends on
115
+ window.markdownRenderer, which is set by markdown-renderer.js.
116
+ -->
117
+ <script src="/static/marked.min.js" defer></script>
118
+ <script src="/static/dompurify.min.js" defer></script>
119
+ <script src="/static/markdown-renderer.js" defer></script>
120
+ <script src="/static/chat-panel.js" defer></script>
110
121
  </body>
111
122
  </html>