@linxin666/dsh-client-ui-diagrams 0.4.15 → 0.4.16

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/lib/client.js CHANGED
@@ -133,9 +133,9 @@ window.__ModuleLoader__.load({
133
133
  * Mermaid's strict security level already escapes label content, but the SVG is
134
134
  * third-party output injected into the page, so it is parsed in a detached
135
135
  * document first and stripped of anything executable: script-ish elements,
136
- * `on*` handlers and `javascript:` URLs. Cleaned markup is returned; input that
137
- * still carries a dangerous token after cleaning throws, and the caller falls
138
- * back to the untouched code block.
136
+ * `on*` handlers and URL attributes with an executable scheme. Cleaned markup is
137
+ * returned; input that still carries a dangerous token after cleaning throws, and
138
+ * the caller falls back to the untouched code block.
139
139
  *
140
140
  * One walk does both removals: dropping a disallowed element drops everything
141
141
  * beneath it, so a second pass would only re-examine nodes that are already
@@ -158,9 +158,37 @@ window.__ModuleLoader__.load({
158
158
  function isEventHandler(name) {
159
159
  return /^on/i.test(name);
160
160
  }
161
- /** A href-like value that executes script when followed. */
162
- function isScriptUrl(value) {
163
- return /^\s*javascript:/i.test(value);
161
+ /**
162
+ * URL schemes that can carry or turn into executable content.
163
+ *
164
+ * `javascript:` and `vbscript:` run script when the link is followed; `data:` can
165
+ * navigate to attacker-authored markup (`data:text/html,...`), and the mermaid
166
+ * runtime's own `@braintree/sanitize-url` already blocks all three, so nothing it
167
+ * renders depends on a `data:` target.
168
+ */
169
+ const EXECUTABLE_SCHEMES = /* @__PURE__ */ new Set([
170
+ "javascript:",
171
+ "vbscript:",
172
+ "data:"
173
+ ]);
174
+ /**
175
+ * A URL attribute value whose scheme is executable once the browser parses it.
176
+ *
177
+ * The value is parsed instead of prefix-matched: a browser removes ASCII tab, line
178
+ * feed and carriage return characters anywhere in a URL and leading C0 controls and
179
+ * spaces before it reads the scheme, so `java<TAB>script:` and ` javascript:` both
180
+ * execute while a `^\s*javascript:` test sees neither. Parsing also sees the
181
+ * entity-decoded form, because the attribute is already decoded by the time the
182
+ * template parses the markup. A value without a colon carries no scheme, so it skips
183
+ * the parse; the base only makes a relative value absolute and is never fetched.
184
+ */
185
+ function isExecutableUrl(value) {
186
+ if (!value.includes(":")) return false;
187
+ try {
188
+ return EXECUTABLE_SCHEMES.has(new URL(value, "https://diagram.invalid/").protocol);
189
+ } catch {
190
+ return false;
191
+ }
164
192
  }
165
193
  /**
166
194
  * Strip executable content from one rendered diagram.
@@ -177,7 +205,7 @@ window.__ModuleLoader__.load({
177
205
  element.remove();
178
206
  continue;
179
207
  }
180
- for (const attribute of Array.from(element.attributes)) if (isEventHandler(attribute.name) || isScriptUrl(attribute.value)) element.removeAttribute(attribute.name);
208
+ for (const attribute of Array.from(element.attributes)) if (isEventHandler(attribute.name) || isExecutableUrl(attribute.value)) element.removeAttribute(attribute.name);
181
209
  }
182
210
  const cleaned = template.innerHTML;
183
211
  if (SUSPICIOUS_CONTENT.test(cleaned)) throw new Error("diagrams: rendered SVG still carries executable content after sanitizing");
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../../packages/dsh-diagrams/src/core/contract.ts","../../../packages/dsh-diagrams/src/client/blocks.ts","../../../packages/dsh-diagrams/src/client/runtime.ts","../../../packages/dsh-diagrams/src/client/sanitize.ts","../../../packages/dsh-diagrams/src/client/theme.ts","../../../packages/dsh-diagrams/src/client/enhance.ts","../../../packages/dsh-diagrams/src/client/index.ts"],"sourcesContent":["/**\n * The one place the diagram feature's external names live: the host route the\n * browser half loads the vendored runtime from, and the DOM hooks the official\n * markdown renderer exposes. Both halves and the tests read these constants, so\n * a host upgrade that renames a hook fails one contract test instead of\n * silently disabling the feature.\n *\n * The DOM hooks are NOT a published contract: the renderer has no code-block\n * extension seam (see the Agent Note), so they are read off its elements.\n * `md-code-block` is a literal class name in the renderer's JSX; the fence\n * language only appears as the banner's infostring text.\n * @module @linxin666/dsh-client-ui-diagrams/core/contract\n */\n\n/** Host route serving the vendored mermaid runtime (same origin, no CDN). */\nexport const RUNTIME_ROUTE = '/api/dsh-diagrams/vendor/mermaid.js'\n\n/** Class the official markdown renderer puts on every fenced code block. */\nexport const CODE_BLOCK_CLASS = 'md-code-block'\n\n/** Class-name fragment of the banner element carrying the fence language. */\nexport const INFOSTRING_CLASS_FRAGMENT = '_infostring_'\n\n/** Fence language this package renders. Everything else is left untouched. */\nexport const RENDERED_LANGUAGE = 'mermaid'\n\n/** Class the rendered diagram container carries (styled by the theme). */\nexport const DIAGRAM_CLASS = 'dsh-diagrams-figure'\n\n/** `pending` | `rendering` | `done` — the container's render life cycle. */\nexport const DATA_STATE = 'data-diagram-state'\n\n/** The verbatim fence source, kept for theme re-renders. */\nexport const DATA_SOURCE = 'data-diagram-source'\n\n/** Marks the code block this package already claimed (one render per block). */\nexport const DATA_CLAIMED = 'data-diagram-claimed'\n\n/** Semantic-attribute values from the family contract (`data-dsh-plugin` / `data-dsh-part`). */\nexport const PLUGIN_NAME = 'diagrams'\n\n/** Semantic part value for the rendered figure. */\nexport const PART_NAME = 'diagram'\n","/**\n * Finding, claiming and restoring mermaid code blocks in the official markdown\n * DOM, and the container that replaces them.\n *\n * The renderer's fenced code block is a `div.md-code-block` whose banner\n * carries the fence language as plain text; its `<pre>` has no language class.\n * The original `<pre>` is therefore HIDDEN, never removed: the block's own copy\n * button keeps reading the source from it.\n * @module @linxin666/dsh-client-ui-diagrams/client/blocks\n */\n\nimport {\n CODE_BLOCK_CLASS,\n DATA_CLAIMED,\n DATA_SOURCE,\n DATA_STATE,\n DIAGRAM_CLASS,\n INFOSTRING_CLASS_FRAGMENT,\n PART_NAME,\n PLUGIN_NAME,\n RENDERED_LANGUAGE,\n} from '../core/contract.ts'\n\n/** The mermaid blocks under one scope that this package has not claimed yet. */\nexport function findMermaidBlocks(scope: ParentNode): HTMLPreElement[] {\n const blocks: Element[] = []\n if (scope instanceof Element && scope.classList.contains(CODE_BLOCK_CLASS)) blocks.push(scope)\n blocks.push(...Array.from(scope.querySelectorAll(`.${CODE_BLOCK_CLASS}`)))\n\n const found: HTMLPreElement[] = []\n const seen = new Set<Element>()\n for (const block of blocks) {\n const infostring = block.querySelector(`[class*=\"${INFOSTRING_CLASS_FRAGMENT}\"]`)\n if ((infostring?.textContent ?? '').trim() !== RENDERED_LANGUAGE) continue\n const pre = block.querySelector('pre')\n if (!(pre instanceof HTMLPreElement) || seen.has(pre)) continue\n seen.add(pre)\n if (pre.hasAttribute(DATA_CLAIMED)) continue\n if ((pre.textContent ?? '').trim() === '') continue\n found.push(pre)\n }\n return found\n}\n\n/**\n * Create the diagram container for one block and mark the block claimed.\n * @param pre - The block's `<pre>` (kept in the tree, hidden once rendered).\n * @returns The container to fill with the sanitized SVG.\n */\nexport function claimBlock(pre: HTMLPreElement): HTMLElement {\n pre.setAttribute(DATA_CLAIMED, '1')\n const container = document.createElement('div')\n container.className = DIAGRAM_CLASS\n container.setAttribute(DATA_STATE, 'pending')\n container.setAttribute(DATA_SOURCE, pre.textContent ?? '')\n container.setAttribute('data-dsh-plugin', PLUGIN_NAME)\n container.setAttribute('data-dsh-part', PART_NAME)\n pre.insertAdjacentElement('afterend', container)\n return container\n}\n\n/**\n * Undo one claim: drop the container and free the block for a later attempt\n * (a streaming fence becomes renderable once it closes).\n * @param pre - The block's `<pre>`.\n * @param container - The container created for it.\n */\nexport function restoreBlock(pre: HTMLPreElement, container: HTMLElement): void {\n container.remove()\n pre.removeAttribute(DATA_CLAIMED)\n pre.style.removeProperty('display')\n}\n\n/** Hide the source block once its diagram landed (the copy button still reads it). */\nexport function hideSourceBlock(pre: HTMLPreElement): void {\n pre.style.display = 'none'\n}\n\n/** Every finished container under one scope (retheme input). */\nexport function finishedContainers(scope: ParentNode): HTMLElement[] {\n return Array.from(scope.querySelectorAll<HTMLElement>(`.${DIAGRAM_CLASS}[data-diagram-state=\"done\"]`))\n}\n","/**\n * On-demand loader for the vendored mermaid runtime.\n *\n * The bundle is a third-party IIFE that leaves a `window.mermaid` global, so\n * the handle is validated structurally (never trusted) and one `<script>` is\n * injected per page no matter how many blocks ask for it. A failed load clears\n * the memo so a later block can retry after a transient failure.\n * @module @linxin666/dsh-client-ui-diagrams/client/runtime\n */\n\n/** The slice of the mermaid API this package consumes. */\nexport interface MermaidRuntime {\n /** Apply global render options (called once per render batch). */\n initialize: (config: Record<string, unknown>) => void\n /** Render one diagram source to SVG markup. */\n render: (id: string, text: string) => Promise<{ svg: string }>\n}\n\n/** Resolve the runtime global, or null while it is absent or malformed. */\nexport function runtimeIfLoaded(): MermaidRuntime | null {\n const candidate = (globalThis as Record<string, unknown>).mermaid\n if (typeof candidate !== 'object' || candidate === null) return null\n const checked = candidate as Record<string, unknown>\n if (typeof checked.initialize !== 'function' || typeof checked.render !== 'function') return null\n return checked as unknown as MermaidRuntime\n}\n\n/** Shared in-flight load, so concurrent batches inject one script. */\nlet pending: Promise<MermaidRuntime> | undefined\n\n/**\n * Load the runtime once per page from the host route.\n * @param url - Same-origin vendor route serving the IIFE bundle.\n * @returns The validated runtime; rejects when the script cannot load.\n */\nexport function loadRuntime(url: string): Promise<MermaidRuntime> {\n const loaded = runtimeIfLoaded()\n if (loaded !== null) return Promise.resolve(loaded)\n if (pending !== undefined) return pending\n pending = new Promise<MermaidRuntime>((resolve, reject) => {\n const script = document.createElement('script')\n script.src = url\n script.async = true\n script.addEventListener('load', () => {\n const runtime = runtimeIfLoaded()\n if (runtime === null) {\n pending = undefined\n reject(new Error('diagrams: the vendor script loaded but left no mermaid global'))\n return\n }\n resolve(runtime)\n })\n script.addEventListener('error', () => {\n pending = undefined\n reject(new Error(`diagrams: failed to load ${url}`))\n })\n document.head.appendChild(script)\n })\n return pending\n}\n","/**\n * Defence in depth for the rendered SVG.\n *\n * Mermaid's strict security level already escapes label content, but the SVG is\n * third-party output injected into the page, so it is parsed in a detached\n * document first and stripped of anything executable: script-ish elements,\n * `on*` handlers and `javascript:` URLs. Cleaned markup is returned; input that\n * still carries a dangerous token after cleaning throws, and the caller falls\n * back to the untouched code block.\n *\n * One walk does both removals: dropping a disallowed element drops everything\n * beneath it, so a second pass would only re-examine nodes that are already\n * detached. The suspicious-token check runs as a case-insensitive pattern over\n * the markup rather than over a lowercased copy of it, because a large diagram\n * is hundreds of kilobytes of markup and that copy is pure garbage.\n * @module @linxin666/dsh-client-ui-diagrams/client/sanitize\n */\n\n/** Elements that must never reach the page from a diagram's markup. */\nconst DISALLOWED_ELEMENTS = new Set(['script', 'foreignobject', 'iframe', 'object', 'embed'])\n\n/** A token that may not survive sanitizing: a script open tag or a javascript: URL. */\nconst SUSPICIOUS_CONTENT = /<script|javascript:/i\n\n/** An attribute name that is an event handler (`onclick`, `onload`, ...). */\nfunction isEventHandler(name: string): boolean {\n return /^on/i.test(name)\n}\n\n/** A href-like value that executes script when followed. */\nfunction isScriptUrl(value: string): boolean {\n return /^\\s*javascript:/i.test(value)\n}\n\n/**\n * Strip executable content from one rendered diagram.\n * @param svg - Markup returned by the mermaid runtime.\n * @returns The cleaned markup, safe to assign as `innerHTML`.\n * @throws When the markup cannot be parsed or still carries a dangerous token.\n */\nexport function sanitizeSvg(svg: string): string {\n const template = document.createElement('template')\n template.innerHTML = svg\n const root = template.content\n\n for (const element of Array.from(root.querySelectorAll('*'))) {\n if (DISALLOWED_ELEMENTS.has(element.tagName.toLowerCase())) {\n element.remove()\n continue\n }\n for (const attribute of Array.from(element.attributes)) {\n if (isEventHandler(attribute.name) || isScriptUrl(attribute.value)) {\n element.removeAttribute(attribute.name)\n }\n }\n }\n\n const cleaned = template.innerHTML\n if (SUSPICIOUS_CONTENT.test(cleaned)) {\n throw new Error('diagrams: rendered SVG still carries executable content after sanitizing')\n }\n return cleaned\n}\n","/**\n * Shell theme signal for diagram rendering.\n *\n * The shell marks a dark palette with `data-ds-dark-theme` on `<body>`; mermaid\n * renders colors into the SVG itself, so a theme flip must re-render the\n * diagrams rather than recolor them with CSS.\n * @module @linxin666/dsh-client-ui-diagrams/client/theme\n */\n\n/** Mermaid theme name for the shell palette. */\nexport function mermaidTheme(isDark: boolean): 'default' | 'dark' {\n return isDark ? 'dark' : 'default'\n}\n\n/** Whether the shell currently carries the dark-palette marker. */\nexport function shellIsDark(): boolean {\n return document.body.hasAttribute('data-ds-dark-theme')\n}\n\n/**\n * Watch the dark-palette marker.\n * @param onChange - Called with the new state on every flip.\n * @returns The disposer.\n */\nexport function watchTheme(onChange: (isDark: boolean) => void): () => void {\n const observer = new MutationObserver(() => { onChange(shellIsDark()) })\n observer.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })\n return () => { observer.disconnect() }\n}\n","/**\n * The DOM enhancement: watch the page for markdown code blocks whose fence\n * language is `mermaid`, render each one with the vendored runtime, and keep\n * the diagrams in step with the shell theme.\n *\n * Why an observer instead of a render hook: the official markdown renderer has\n * no code-block extension seam (see the Agent Note), so the only integration\n * point is its output DOM. The observer is scoped to mutated subtrees (mapped to\n * the owning code block) and coalesced to one animation frame per batch, so a\n * long streaming transcript never re-scans the whole document.\n *\n * Cost control: mermaid parses and lays out synchronously on the page's main\n * thread, so the work here is scheduled rather than fired at once. Diagrams are\n * painted one per frame (a document with twelve diagrams stays scrollable while\n * they land), the sanitized SVG is memoized per theme and source (a palette\n * flip back, or a host re-render that re-claims a block, costs nothing), the\n * runtime is initialized only when the palette actually changes, and a fence the\n * parser rejects is never retried with the same source — its own rollback\n * mutates the DOM, so retrying on every mutation would spin the page forever.\n *\n * Failure policy: every failure restores the untouched code block — a missing\n * runtime, a half-streamed fence, or a diagram the parser rejects never leaves a\n * broken node behind and never throws into the caller.\n * @module @linxin666/dsh-client-ui-diagrams/client/enhance\n */\n\nimport { CODE_BLOCK_CLASS, DIAGRAM_CLASS, DATA_STATE, RUNTIME_ROUTE } from '../core/contract.ts'\nimport { claimBlock, findMermaidBlocks, finishedContainers, hideSourceBlock, restoreBlock } from './blocks.ts'\nimport { loadRuntime, type MermaidRuntime } from './runtime.ts'\nimport { sanitizeSvg } from './sanitize.ts'\nimport { mermaidTheme, shellIsDark, watchTheme } from './theme.ts'\n\n/** Options for {@link startEnhancement} (tests override the loader and root). */\nexport interface EnhancementOptions {\n /** Same-origin route serving the runtime bundle. */\n runtimeUrl?: string\n /** Observation root; defaults to `document.body`. */\n root?: HTMLElement\n /** Runtime loader override; defaults to the script-injecting loader. */\n load?: (url: string) => Promise<MermaidRuntime>\n}\n\n/** Monotonic id source: mermaid keys its `<svg>` by the id it is handed. */\nlet renderSeq = 0\n\n/** How many rendered diagrams one page keeps for reuse (oldest is evicted first). */\nconst CACHE_LIMIT = 32\n\n/** Quiet period before a fence whose source changed since its last rejection retries. */\nconst RETRY_DELAY_MS = 400\n\n/**\n * Map one mutation batch to the minimal DOM scopes that can hold a new fence.\n * Removal-only records contribute nothing (removing a node never introduces a\n * block), and records inside an already-rendered figure are ignored so writing\n * the sanitized SVG cannot re-trigger the pass.\n * @param records - The batch the observer delivered.\n * @returns Connected elements to scan, deduped by identity.\n */\nexport function scopesForRecords(records: MutationRecord[]): Element[] {\n const scopes = new Set<Element>()\n const consider = (element: Element | null): void => {\n if (element === null || !element.isConnected) return\n if (element.closest(`.${DIAGRAM_CLASS}`) !== null) return\n scopes.add(element.closest(`.${CODE_BLOCK_CLASS}`) ?? element)\n }\n for (const record of records) {\n if (record.addedNodes.length === 0) continue\n if (record.target instanceof Element) consider(record.target)\n for (const node of Array.from(record.addedNodes)) {\n consider(node instanceof Element ? node : node.parentElement)\n }\n }\n return [...scopes]\n}\n\n/** The mermaid palette the shell currently asks for. */\nfunction currentTheme(): string {\n return mermaidTheme(shellIsDark())\n}\n\n/** Upper bound on one scheduling hop, so a throttled tab cannot stall the queue. */\nconst FRAME_BACKSTOP_MS = 48\n\n/**\n * Run one callback on the next frame, or after a short timeout when frames do\n * not arrive (a hidden tab throttles `requestAnimationFrame` to nothing).\n * @param run - The callback to run once.\n * @returns The cancelling callback: runs (or marks) the callback once only.\n */\nfunction afterNextFrame(run: () => void): () => void {\n let frame = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n let done = false\n const once = (): void => {\n if (done) return\n done = true\n if (timer !== undefined) clearTimeout(timer)\n if (frame !== 0 && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(frame)\n run()\n }\n if (typeof requestAnimationFrame === 'function') frame = requestAnimationFrame(once)\n timer = setTimeout(once, FRAME_BACKSTOP_MS)\n return once\n}\n\n/** Let the page paint between two diagrams so a long document stays responsive. */\nfunction nextPaint(): Promise<void> {\n return new Promise<void>((resolve) => { afterNextFrame(() => { resolve() }) })\n}\n\n/**\n * Start the enhancement and return its disposer.\n *\n * The first pass scans the whole observation root (the transcript usually\n * mounted before this plugin), then only mutated subtrees are scanned. A theme\n * flip repaints every finished diagram from its stored source.\n * @param options - Root, route and loader overrides.\n * @returns Disposer: disconnect the observer, stop the theme watch, drop timers.\n */\nexport function startEnhancement(options: EnhancementOptions = {}): () => void {\n if (typeof document === 'undefined') return () => {}\n const root = options.root ?? document.body\n const runtimeUrl = options.runtimeUrl ?? RUNTIME_ROUTE\n const load = options.load ?? loadRuntime\n\n let disposed = false\n /** Cancels the queued flush, while one is scheduled. */\n let cancelFlush: (() => void) | undefined\n const queued = new Set<Element>()\n\n /** Sanitized SVG per `theme + source`, oldest entry evicted past the limit. */\n const painted = new Map<string, string>()\n /** The source that last made a block fail; the same source is never retried. */\n const rejected = new WeakMap<HTMLPreElement, string>()\n /** Pending quiet-period retries, cleared on dispose. */\n const retries = new Set<ReturnType<typeof setTimeout>>()\n /** The runtime and palette the shared runtime was last initialized for. */\n let initialized: { runtime: MermaidRuntime; theme: string } | undefined\n\n /** Remember one rendered diagram, evicting the oldest entry past the limit. */\n const remember = (key: string, svg: string): void => {\n if (painted.has(key)) painted.delete(key)\n painted.set(key, svg)\n for (const oldest of painted.keys()) {\n if (painted.size <= CACHE_LIMIT) break\n painted.delete(oldest)\n }\n }\n\n /** Apply the runtime options once per runtime and palette (mermaid re-inits are expensive). */\n const ensureInitialized = (runtime: MermaidRuntime, theme: string): void => {\n if (initialized?.runtime === runtime && initialized.theme === theme) return\n runtime.initialize({\n startOnLoad: false,\n theme,\n securityLevel: 'strict',\n // Labels must be SVG text: mermaid's HTML labels live in <foreignObject>,\n // which the sanitizer strips, so an HTML label renders as an empty box.\n htmlLabels: false,\n fontFamily: 'inherit',\n })\n initialized = { runtime, theme }\n }\n\n /**\n * Paint one claimed container, reusing a memoized diagram when possible.\n * @param container - The container claimed for this fence.\n * @param runtime - Loaded mermaid runtime.\n * @param theme - Palette the diagram must be painted for.\n * @returns The sanitized SVG for that container.\n */\n const paint = async (container: HTMLElement, runtime: MermaidRuntime, theme: string): Promise<string> => {\n const source = container.getAttribute('data-diagram-source') ?? ''\n const key = `${theme}\\u0000${source}`\n const cached = painted.get(key)\n if (cached !== undefined) return cached\n const { svg } = await runtime.render(`dsh-diagrams-${(renderSeq += 1)}`, source)\n const cleaned = sanitizeSvg(svg)\n remember(key, cleaned)\n return cleaned\n }\n\n /**\n * Retry one fence after the quiet period, but only when its source changed:\n * the source that already failed is final until the fence grows.\n * @param pre - The source block whose render was rejected.\n */\n const scheduleRetry = (pre: HTMLPreElement): void => {\n const timer = setTimeout(() => {\n retries.delete(timer)\n if (disposed || !pre.isConnected || pre.hasAttribute('data-diagram-claimed')) return\n if (rejected.get(pre) === (pre.textContent ?? '')) return\n void (async () => {\n let runtime: MermaidRuntime\n try {\n runtime = await load(runtimeUrl)\n } catch {\n return // A vendor route that cannot answer leaves the fence as an ordinary block.\n }\n if (disposed || !pre.isConnected || pre.hasAttribute('data-diagram-claimed')) return\n const theme = currentTheme()\n ensureInitialized(runtime, theme)\n await renderBlock(pre, runtime, theme)\n })()\n }, RETRY_DELAY_MS)\n retries.add(timer)\n }\n\n /**\n * Render one source block in place, restoring it on any failure.\n * @param pre - The fence's source block.\n * @param runtime - Loaded mermaid runtime.\n * @param theme - Palette to paint for.\n */\n const renderBlock = async (pre: HTMLPreElement, runtime: MermaidRuntime, theme: string): Promise<void> => {\n const container = claimBlock(pre)\n try {\n container.setAttribute(DATA_STATE, 'rendering')\n const cleaned = await paint(container, runtime, theme)\n if (disposed || !container.isConnected) return\n container.innerHTML = cleaned\n container.setAttribute(DATA_STATE, 'done')\n hideSourceBlock(pre)\n } catch {\n // Syntax error, missing runtime support or a half-streamed fence: put the\n // code block back untouched and never retry this exact source again.\n const source = container.getAttribute('data-diagram-source') ?? ''\n restoreBlock(pre, container)\n rejected.set(pre, source)\n scheduleRetry(pre)\n }\n }\n\n /** Render every unclaimed mermaid block under one scope, one diagram per frame. */\n const enhanceScope = async (scope: ParentNode, runtime: MermaidRuntime, theme: string): Promise<void> => {\n const blocks = findMermaidBlocks(scope).filter(pre => rejected.get(pre) !== (pre.textContent ?? ''))\n if (blocks.length === 0) return\n ensureInitialized(runtime, theme)\n for (const pre of blocks) {\n if (disposed) return\n if (!pre.isConnected || pre.hasAttribute('data-diagram-claimed')) continue\n // oxlint-disable-next-line eslint/no-await-in-loop -- one diagram per frame is the point of this loop.\n await renderBlock(pre, runtime, theme)\n if (disposed) return\n // oxlint-disable-next-line eslint/no-await-in-loop -- the painted frame between diagrams is the point of this loop.\n await nextPaint()\n }\n }\n\n /** Repaint finished diagrams for the current shell theme, reusing what is cached. */\n const retheme = async (): Promise<void> => {\n const containers = finishedContainers(root)\n if (containers.length === 0) return\n let runtime: MermaidRuntime\n try {\n runtime = await load(runtimeUrl)\n } catch {\n return\n }\n if (disposed) return\n const theme = currentTheme()\n ensureInitialized(runtime, theme)\n for (const container of containers) {\n if (disposed) return\n if (!container.isConnected) continue\n try {\n // oxlint-disable-next-line eslint/no-await-in-loop -- one repaint per frame is the point of this loop.\n const cleaned = await paint(container, runtime, theme)\n if (disposed || !container.isConnected) return\n container.innerHTML = cleaned\n container.setAttribute(DATA_STATE, 'done')\n } catch {\n // A theme flip must never blank a diagram that already rendered.\n }\n // oxlint-disable-next-line eslint/no-await-in-loop -- the painted frame between repaints is the point of this loop.\n await nextPaint()\n }\n }\n\n /** One coalesced pass over the queued scopes. */\n const flush = async (): Promise<void> => {\n cancelFlush = undefined\n const scopes = [...queued]\n queued.clear()\n if (disposed || scopes.length === 0) return\n let runtime: MermaidRuntime\n try {\n runtime = await load(runtimeUrl)\n } catch {\n return // no vendor route: leave every fence as an ordinary code block\n }\n const theme = currentTheme()\n for (const scope of scopes) {\n if (disposed) return\n // oxlint-disable-next-line eslint/no-await-in-loop -- scopes are scanned one at a time so each can yield.\n await enhanceScope(scope, runtime, theme)\n }\n }\n\n const schedule = (scopes: readonly Element[]): void => {\n for (const scope of scopes) queued.add(scope)\n if (queued.size === 0 || cancelFlush !== undefined) return\n cancelFlush = afterNextFrame(() => {\n cancelFlush = undefined\n void flush()\n })\n }\n\n if (typeof MutationObserver === 'function') {\n const observer = new MutationObserver((records) => { schedule(scopesForRecords(records)) })\n observer.observe(root, { childList: true, subtree: true })\n }\n const stopThemeWatch = watchTheme(() => { void retheme() })\n schedule([root])\n\n return () => {\n disposed = true\n cancelFlush?.()\n cancelFlush = undefined\n for (const timer of retries) clearTimeout(timer)\n retries.clear()\n stopThemeWatch()\n }\n}\n","/**\n * Browser entry of the diagrams plugin.\n *\n * The enhancement needs no slot and no service: it observes the page for the\n * official markdown renderer's mermaid fences (chat transcript and document\n * preview alike) and renders them itself. `apply` therefore only owns the\n * effect's lifetime.\n * @module @linxin666/dsh-client-ui-diagrams/client\n */\n\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport { startEnhancement } from './enhance.ts'\n\n/**\n * Start the diagram enhancement for the lifetime of this client plugin.\n * @param ctx - Client context; the effect disposes the observer on unload.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => startEnhancement(), 'diagrams: render mermaid fences')\n}\n\nexport { startEnhancement } from './enhance.ts'\nexport { scopesForRecords } from './enhance.ts'\nexport { findMermaidBlocks } from './blocks.ts'\nexport { sanitizeSvg } from './sanitize.ts'\nexport { mermaidTheme, shellIsDark, watchTheme } from './theme.ts'\nexport { loadRuntime, runtimeIfLoaded, type MermaidRuntime } from './runtime.ts'\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../../packages/dsh-diagrams/src/core/contract.ts","../../../packages/dsh-diagrams/src/client/blocks.ts","../../../packages/dsh-diagrams/src/client/runtime.ts","../../../packages/dsh-diagrams/src/client/sanitize.ts","../../../packages/dsh-diagrams/src/client/theme.ts","../../../packages/dsh-diagrams/src/client/enhance.ts","../../../packages/dsh-diagrams/src/client/index.ts"],"sourcesContent":["/**\n * The one place the diagram feature's external names live: the host route the\n * browser half loads the vendored runtime from, and the DOM hooks the official\n * markdown renderer exposes. Both halves and the tests read these constants, so\n * a host upgrade that renames a hook fails one contract test instead of\n * silently disabling the feature.\n *\n * The DOM hooks are NOT a published contract: the renderer has no code-block\n * extension seam (see the Agent Note), so they are read off its elements.\n * `md-code-block` is a literal class name in the renderer's JSX; the fence\n * language only appears as the banner's infostring text.\n * @module @linxin666/dsh-client-ui-diagrams/core/contract\n */\n\n/** Host route serving the vendored mermaid runtime (same origin, no CDN). */\nexport const RUNTIME_ROUTE = '/api/dsh-diagrams/vendor/mermaid.js'\n\n/** Class the official markdown renderer puts on every fenced code block. */\nexport const CODE_BLOCK_CLASS = 'md-code-block'\n\n/** Class-name fragment of the banner element carrying the fence language. */\nexport const INFOSTRING_CLASS_FRAGMENT = '_infostring_'\n\n/** Fence language this package renders. Everything else is left untouched. */\nexport const RENDERED_LANGUAGE = 'mermaid'\n\n/** Class the rendered diagram container carries (styled by the theme). */\nexport const DIAGRAM_CLASS = 'dsh-diagrams-figure'\n\n/** `pending` | `rendering` | `done` — the container's render life cycle. */\nexport const DATA_STATE = 'data-diagram-state'\n\n/** The verbatim fence source, kept for theme re-renders. */\nexport const DATA_SOURCE = 'data-diagram-source'\n\n/** Marks the code block this package already claimed (one render per block). */\nexport const DATA_CLAIMED = 'data-diagram-claimed'\n\n/** Semantic-attribute values from the family contract (`data-dsh-plugin` / `data-dsh-part`). */\nexport const PLUGIN_NAME = 'diagrams'\n\n/** Semantic part value for the rendered figure. */\nexport const PART_NAME = 'diagram'\n","/**\n * Finding, claiming and restoring mermaid code blocks in the official markdown\n * DOM, and the container that replaces them.\n *\n * The renderer's fenced code block is a `div.md-code-block` whose banner\n * carries the fence language as plain text; its `<pre>` has no language class.\n * The original `<pre>` is therefore HIDDEN, never removed: the block's own copy\n * button keeps reading the source from it.\n * @module @linxin666/dsh-client-ui-diagrams/client/blocks\n */\n\nimport {\n CODE_BLOCK_CLASS,\n DATA_CLAIMED,\n DATA_SOURCE,\n DATA_STATE,\n DIAGRAM_CLASS,\n INFOSTRING_CLASS_FRAGMENT,\n PART_NAME,\n PLUGIN_NAME,\n RENDERED_LANGUAGE,\n} from '../core/contract.ts'\n\n/** The mermaid blocks under one scope that this package has not claimed yet. */\nexport function findMermaidBlocks(scope: ParentNode): HTMLPreElement[] {\n const blocks: Element[] = []\n if (scope instanceof Element && scope.classList.contains(CODE_BLOCK_CLASS)) blocks.push(scope)\n blocks.push(...Array.from(scope.querySelectorAll(`.${CODE_BLOCK_CLASS}`)))\n\n const found: HTMLPreElement[] = []\n const seen = new Set<Element>()\n for (const block of blocks) {\n const infostring = block.querySelector(`[class*=\"${INFOSTRING_CLASS_FRAGMENT}\"]`)\n if ((infostring?.textContent ?? '').trim() !== RENDERED_LANGUAGE) continue\n const pre = block.querySelector('pre')\n if (!(pre instanceof HTMLPreElement) || seen.has(pre)) continue\n seen.add(pre)\n if (pre.hasAttribute(DATA_CLAIMED)) continue\n if ((pre.textContent ?? '').trim() === '') continue\n found.push(pre)\n }\n return found\n}\n\n/**\n * Create the diagram container for one block and mark the block claimed.\n * @param pre - The block's `<pre>` (kept in the tree, hidden once rendered).\n * @returns The container to fill with the sanitized SVG.\n */\nexport function claimBlock(pre: HTMLPreElement): HTMLElement {\n pre.setAttribute(DATA_CLAIMED, '1')\n const container = document.createElement('div')\n container.className = DIAGRAM_CLASS\n container.setAttribute(DATA_STATE, 'pending')\n container.setAttribute(DATA_SOURCE, pre.textContent ?? '')\n container.setAttribute('data-dsh-plugin', PLUGIN_NAME)\n container.setAttribute('data-dsh-part', PART_NAME)\n pre.insertAdjacentElement('afterend', container)\n return container\n}\n\n/**\n * Undo one claim: drop the container and free the block for a later attempt\n * (a streaming fence becomes renderable once it closes).\n * @param pre - The block's `<pre>`.\n * @param container - The container created for it.\n */\nexport function restoreBlock(pre: HTMLPreElement, container: HTMLElement): void {\n container.remove()\n pre.removeAttribute(DATA_CLAIMED)\n pre.style.removeProperty('display')\n}\n\n/** Hide the source block once its diagram landed (the copy button still reads it). */\nexport function hideSourceBlock(pre: HTMLPreElement): void {\n pre.style.display = 'none'\n}\n\n/** Every finished container under one scope (retheme input). */\nexport function finishedContainers(scope: ParentNode): HTMLElement[] {\n return Array.from(scope.querySelectorAll<HTMLElement>(`.${DIAGRAM_CLASS}[data-diagram-state=\"done\"]`))\n}\n","/**\n * On-demand loader for the vendored mermaid runtime.\n *\n * The bundle is a third-party IIFE that leaves a `window.mermaid` global, so\n * the handle is validated structurally (never trusted) and one `<script>` is\n * injected per page no matter how many blocks ask for it. A failed load clears\n * the memo so a later block can retry after a transient failure.\n * @module @linxin666/dsh-client-ui-diagrams/client/runtime\n */\n\n/** The slice of the mermaid API this package consumes. */\nexport interface MermaidRuntime {\n /** Apply global render options (called once per render batch). */\n initialize: (config: Record<string, unknown>) => void\n /** Render one diagram source to SVG markup. */\n render: (id: string, text: string) => Promise<{ svg: string }>\n}\n\n/** Resolve the runtime global, or null while it is absent or malformed. */\nexport function runtimeIfLoaded(): MermaidRuntime | null {\n const candidate = (globalThis as Record<string, unknown>).mermaid\n if (typeof candidate !== 'object' || candidate === null) return null\n const checked = candidate as Record<string, unknown>\n if (typeof checked.initialize !== 'function' || typeof checked.render !== 'function') return null\n return checked as unknown as MermaidRuntime\n}\n\n/** Shared in-flight load, so concurrent batches inject one script. */\nlet pending: Promise<MermaidRuntime> | undefined\n\n/**\n * Load the runtime once per page from the host route.\n * @param url - Same-origin vendor route serving the IIFE bundle.\n * @returns The validated runtime; rejects when the script cannot load.\n */\nexport function loadRuntime(url: string): Promise<MermaidRuntime> {\n const loaded = runtimeIfLoaded()\n if (loaded !== null) return Promise.resolve(loaded)\n if (pending !== undefined) return pending\n pending = new Promise<MermaidRuntime>((resolve, reject) => {\n const script = document.createElement('script')\n script.src = url\n script.async = true\n script.addEventListener('load', () => {\n const runtime = runtimeIfLoaded()\n if (runtime === null) {\n pending = undefined\n reject(new Error('diagrams: the vendor script loaded but left no mermaid global'))\n return\n }\n resolve(runtime)\n })\n script.addEventListener('error', () => {\n pending = undefined\n reject(new Error(`diagrams: failed to load ${url}`))\n })\n document.head.appendChild(script)\n })\n return pending\n}\n","/**\n * Defence in depth for the rendered SVG.\n *\n * Mermaid's strict security level already escapes label content, but the SVG is\n * third-party output injected into the page, so it is parsed in a detached\n * document first and stripped of anything executable: script-ish elements,\n * `on*` handlers and URL attributes with an executable scheme. Cleaned markup is\n * returned; input that still carries a dangerous token after cleaning throws, and\n * the caller falls back to the untouched code block.\n *\n * One walk does both removals: dropping a disallowed element drops everything\n * beneath it, so a second pass would only re-examine nodes that are already\n * detached. The suspicious-token check runs as a case-insensitive pattern over\n * the markup rather than over a lowercased copy of it, because a large diagram\n * is hundreds of kilobytes of markup and that copy is pure garbage.\n * @module @linxin666/dsh-client-ui-diagrams/client/sanitize\n */\n\n/** Elements that must never reach the page from a diagram's markup. */\nconst DISALLOWED_ELEMENTS = new Set(['script', 'foreignobject', 'iframe', 'object', 'embed'])\n\n/** A token that may not survive sanitizing: a script open tag or a javascript: URL. */\nconst SUSPICIOUS_CONTENT = /<script|javascript:/i\n\n/** An attribute name that is an event handler (`onclick`, `onload`, ...). */\nfunction isEventHandler(name: string): boolean {\n return /^on/i.test(name)\n}\n\n/**\n * URL schemes that can carry or turn into executable content.\n *\n * `javascript:` and `vbscript:` run script when the link is followed; `data:` can\n * navigate to attacker-authored markup (`data:text/html,...`), and the mermaid\n * runtime's own `@braintree/sanitize-url` already blocks all three, so nothing it\n * renders depends on a `data:` target.\n */\nconst EXECUTABLE_SCHEMES = new Set(['javascript:', 'vbscript:', 'data:'])\n\n/**\n * A URL attribute value whose scheme is executable once the browser parses it.\n *\n * The value is parsed instead of prefix-matched: a browser removes ASCII tab, line\n * feed and carriage return characters anywhere in a URL and leading C0 controls and\n * spaces before it reads the scheme, so `java<TAB>script:` and ` javascript:` both\n * execute while a `^\\s*javascript:` test sees neither. Parsing also sees the\n * entity-decoded form, because the attribute is already decoded by the time the\n * template parses the markup. A value without a colon carries no scheme, so it skips\n * the parse; the base only makes a relative value absolute and is never fetched.\n */\nfunction isExecutableUrl(value: string): boolean {\n if (!value.includes(':')) {\n return false\n }\n try {\n return EXECUTABLE_SCHEMES.has(new URL(value, 'https://diagram.invalid/').protocol)\n } catch {\n return false\n }\n}\n\n/**\n * Strip executable content from one rendered diagram.\n * @param svg - Markup returned by the mermaid runtime.\n * @returns The cleaned markup, safe to assign as `innerHTML`.\n * @throws When the markup cannot be parsed or still carries a dangerous token.\n */\nexport function sanitizeSvg(svg: string): string {\n const template = document.createElement('template')\n template.innerHTML = svg\n const root = template.content\n\n for (const element of Array.from(root.querySelectorAll('*'))) {\n if (DISALLOWED_ELEMENTS.has(element.tagName.toLowerCase())) {\n element.remove()\n continue\n }\n for (const attribute of Array.from(element.attributes)) {\n if (isEventHandler(attribute.name) || isExecutableUrl(attribute.value)) {\n element.removeAttribute(attribute.name)\n }\n }\n }\n\n const cleaned = template.innerHTML\n if (SUSPICIOUS_CONTENT.test(cleaned)) {\n throw new Error('diagrams: rendered SVG still carries executable content after sanitizing')\n }\n return cleaned\n}\n","/**\n * Shell theme signal for diagram rendering.\n *\n * The shell marks a dark palette with `data-ds-dark-theme` on `<body>`; mermaid\n * renders colors into the SVG itself, so a theme flip must re-render the\n * diagrams rather than recolor them with CSS.\n * @module @linxin666/dsh-client-ui-diagrams/client/theme\n */\n\n/** Mermaid theme name for the shell palette. */\nexport function mermaidTheme(isDark: boolean): 'default' | 'dark' {\n return isDark ? 'dark' : 'default'\n}\n\n/** Whether the shell currently carries the dark-palette marker. */\nexport function shellIsDark(): boolean {\n return document.body.hasAttribute('data-ds-dark-theme')\n}\n\n/**\n * Watch the dark-palette marker.\n * @param onChange - Called with the new state on every flip.\n * @returns The disposer.\n */\nexport function watchTheme(onChange: (isDark: boolean) => void): () => void {\n const observer = new MutationObserver(() => { onChange(shellIsDark()) })\n observer.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })\n return () => { observer.disconnect() }\n}\n","/**\n * The DOM enhancement: watch the page for markdown code blocks whose fence\n * language is `mermaid`, render each one with the vendored runtime, and keep\n * the diagrams in step with the shell theme.\n *\n * Why an observer instead of a render hook: the official markdown renderer has\n * no code-block extension seam (see the Agent Note), so the only integration\n * point is its output DOM. The observer is scoped to mutated subtrees (mapped to\n * the owning code block) and coalesced to one animation frame per batch, so a\n * long streaming transcript never re-scans the whole document.\n *\n * Cost control: mermaid parses and lays out synchronously on the page's main\n * thread, so the work here is scheduled rather than fired at once. Diagrams are\n * painted one per frame (a document with twelve diagrams stays scrollable while\n * they land), the sanitized SVG is memoized per theme and source (a palette\n * flip back, or a host re-render that re-claims a block, costs nothing), the\n * runtime is initialized only when the palette actually changes, and a fence the\n * parser rejects is never retried with the same source — its own rollback\n * mutates the DOM, so retrying on every mutation would spin the page forever.\n *\n * Failure policy: every failure restores the untouched code block — a missing\n * runtime, a half-streamed fence, or a diagram the parser rejects never leaves a\n * broken node behind and never throws into the caller.\n * @module @linxin666/dsh-client-ui-diagrams/client/enhance\n */\n\nimport { CODE_BLOCK_CLASS, DIAGRAM_CLASS, DATA_STATE, RUNTIME_ROUTE } from '../core/contract.ts'\nimport { claimBlock, findMermaidBlocks, finishedContainers, hideSourceBlock, restoreBlock } from './blocks.ts'\nimport { loadRuntime, type MermaidRuntime } from './runtime.ts'\nimport { sanitizeSvg } from './sanitize.ts'\nimport { mermaidTheme, shellIsDark, watchTheme } from './theme.ts'\n\n/** Options for {@link startEnhancement} (tests override the loader and root). */\nexport interface EnhancementOptions {\n /** Same-origin route serving the runtime bundle. */\n runtimeUrl?: string\n /** Observation root; defaults to `document.body`. */\n root?: HTMLElement\n /** Runtime loader override; defaults to the script-injecting loader. */\n load?: (url: string) => Promise<MermaidRuntime>\n}\n\n/** Monotonic id source: mermaid keys its `<svg>` by the id it is handed. */\nlet renderSeq = 0\n\n/** How many rendered diagrams one page keeps for reuse (oldest is evicted first). */\nconst CACHE_LIMIT = 32\n\n/** Quiet period before a fence whose source changed since its last rejection retries. */\nconst RETRY_DELAY_MS = 400\n\n/**\n * Map one mutation batch to the minimal DOM scopes that can hold a new fence.\n * Removal-only records contribute nothing (removing a node never introduces a\n * block), and records inside an already-rendered figure are ignored so writing\n * the sanitized SVG cannot re-trigger the pass.\n * @param records - The batch the observer delivered.\n * @returns Connected elements to scan, deduped by identity.\n */\nexport function scopesForRecords(records: MutationRecord[]): Element[] {\n const scopes = new Set<Element>()\n const consider = (element: Element | null): void => {\n if (element === null || !element.isConnected) return\n if (element.closest(`.${DIAGRAM_CLASS}`) !== null) return\n scopes.add(element.closest(`.${CODE_BLOCK_CLASS}`) ?? element)\n }\n for (const record of records) {\n if (record.addedNodes.length === 0) continue\n if (record.target instanceof Element) consider(record.target)\n for (const node of Array.from(record.addedNodes)) {\n consider(node instanceof Element ? node : node.parentElement)\n }\n }\n return [...scopes]\n}\n\n/** The mermaid palette the shell currently asks for. */\nfunction currentTheme(): string {\n return mermaidTheme(shellIsDark())\n}\n\n/** Upper bound on one scheduling hop, so a throttled tab cannot stall the queue. */\nconst FRAME_BACKSTOP_MS = 48\n\n/**\n * Run one callback on the next frame, or after a short timeout when frames do\n * not arrive (a hidden tab throttles `requestAnimationFrame` to nothing).\n * @param run - The callback to run once.\n * @returns The cancelling callback: runs (or marks) the callback once only.\n */\nfunction afterNextFrame(run: () => void): () => void {\n let frame = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n let done = false\n const once = (): void => {\n if (done) return\n done = true\n if (timer !== undefined) clearTimeout(timer)\n if (frame !== 0 && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(frame)\n run()\n }\n if (typeof requestAnimationFrame === 'function') frame = requestAnimationFrame(once)\n timer = setTimeout(once, FRAME_BACKSTOP_MS)\n return once\n}\n\n/** Let the page paint between two diagrams so a long document stays responsive. */\nfunction nextPaint(): Promise<void> {\n return new Promise<void>((resolve) => { afterNextFrame(() => { resolve() }) })\n}\n\n/**\n * Start the enhancement and return its disposer.\n *\n * The first pass scans the whole observation root (the transcript usually\n * mounted before this plugin), then only mutated subtrees are scanned. A theme\n * flip repaints every finished diagram from its stored source.\n * @param options - Root, route and loader overrides.\n * @returns Disposer: disconnect the observer, stop the theme watch, drop timers.\n */\nexport function startEnhancement(options: EnhancementOptions = {}): () => void {\n if (typeof document === 'undefined') return () => {}\n const root = options.root ?? document.body\n const runtimeUrl = options.runtimeUrl ?? RUNTIME_ROUTE\n const load = options.load ?? loadRuntime\n\n let disposed = false\n /** Cancels the queued flush, while one is scheduled. */\n let cancelFlush: (() => void) | undefined\n const queued = new Set<Element>()\n\n /** Sanitized SVG per `theme + source`, oldest entry evicted past the limit. */\n const painted = new Map<string, string>()\n /** The source that last made a block fail; the same source is never retried. */\n const rejected = new WeakMap<HTMLPreElement, string>()\n /** Pending quiet-period retries, cleared on dispose. */\n const retries = new Set<ReturnType<typeof setTimeout>>()\n /** The runtime and palette the shared runtime was last initialized for. */\n let initialized: { runtime: MermaidRuntime; theme: string } | undefined\n\n /** Remember one rendered diagram, evicting the oldest entry past the limit. */\n const remember = (key: string, svg: string): void => {\n if (painted.has(key)) painted.delete(key)\n painted.set(key, svg)\n for (const oldest of painted.keys()) {\n if (painted.size <= CACHE_LIMIT) break\n painted.delete(oldest)\n }\n }\n\n /** Apply the runtime options once per runtime and palette (mermaid re-inits are expensive). */\n const ensureInitialized = (runtime: MermaidRuntime, theme: string): void => {\n if (initialized?.runtime === runtime && initialized.theme === theme) return\n runtime.initialize({\n startOnLoad: false,\n theme,\n securityLevel: 'strict',\n // Labels must be SVG text: mermaid's HTML labels live in <foreignObject>,\n // which the sanitizer strips, so an HTML label renders as an empty box.\n htmlLabels: false,\n fontFamily: 'inherit',\n })\n initialized = { runtime, theme }\n }\n\n /**\n * Paint one claimed container, reusing a memoized diagram when possible.\n * @param container - The container claimed for this fence.\n * @param runtime - Loaded mermaid runtime.\n * @param theme - Palette the diagram must be painted for.\n * @returns The sanitized SVG for that container.\n */\n const paint = async (container: HTMLElement, runtime: MermaidRuntime, theme: string): Promise<string> => {\n const source = container.getAttribute('data-diagram-source') ?? ''\n const key = `${theme}\\u0000${source}`\n const cached = painted.get(key)\n if (cached !== undefined) return cached\n const { svg } = await runtime.render(`dsh-diagrams-${(renderSeq += 1)}`, source)\n const cleaned = sanitizeSvg(svg)\n remember(key, cleaned)\n return cleaned\n }\n\n /**\n * Retry one fence after the quiet period, but only when its source changed:\n * the source that already failed is final until the fence grows.\n * @param pre - The source block whose render was rejected.\n */\n const scheduleRetry = (pre: HTMLPreElement): void => {\n const timer = setTimeout(() => {\n retries.delete(timer)\n if (disposed || !pre.isConnected || pre.hasAttribute('data-diagram-claimed')) return\n if (rejected.get(pre) === (pre.textContent ?? '')) return\n void (async () => {\n let runtime: MermaidRuntime\n try {\n runtime = await load(runtimeUrl)\n } catch {\n return // A vendor route that cannot answer leaves the fence as an ordinary block.\n }\n if (disposed || !pre.isConnected || pre.hasAttribute('data-diagram-claimed')) return\n const theme = currentTheme()\n ensureInitialized(runtime, theme)\n await renderBlock(pre, runtime, theme)\n })()\n }, RETRY_DELAY_MS)\n retries.add(timer)\n }\n\n /**\n * Render one source block in place, restoring it on any failure.\n * @param pre - The fence's source block.\n * @param runtime - Loaded mermaid runtime.\n * @param theme - Palette to paint for.\n */\n const renderBlock = async (pre: HTMLPreElement, runtime: MermaidRuntime, theme: string): Promise<void> => {\n const container = claimBlock(pre)\n try {\n container.setAttribute(DATA_STATE, 'rendering')\n const cleaned = await paint(container, runtime, theme)\n if (disposed || !container.isConnected) return\n container.innerHTML = cleaned\n container.setAttribute(DATA_STATE, 'done')\n hideSourceBlock(pre)\n } catch {\n // Syntax error, missing runtime support or a half-streamed fence: put the\n // code block back untouched and never retry this exact source again.\n const source = container.getAttribute('data-diagram-source') ?? ''\n restoreBlock(pre, container)\n rejected.set(pre, source)\n scheduleRetry(pre)\n }\n }\n\n /** Render every unclaimed mermaid block under one scope, one diagram per frame. */\n const enhanceScope = async (scope: ParentNode, runtime: MermaidRuntime, theme: string): Promise<void> => {\n const blocks = findMermaidBlocks(scope).filter(pre => rejected.get(pre) !== (pre.textContent ?? ''))\n if (blocks.length === 0) return\n ensureInitialized(runtime, theme)\n for (const pre of blocks) {\n if (disposed) return\n if (!pre.isConnected || pre.hasAttribute('data-diagram-claimed')) continue\n // oxlint-disable-next-line eslint/no-await-in-loop -- one diagram per frame is the point of this loop.\n await renderBlock(pre, runtime, theme)\n if (disposed) return\n // oxlint-disable-next-line eslint/no-await-in-loop -- the painted frame between diagrams is the point of this loop.\n await nextPaint()\n }\n }\n\n /** Repaint finished diagrams for the current shell theme, reusing what is cached. */\n const retheme = async (): Promise<void> => {\n const containers = finishedContainers(root)\n if (containers.length === 0) return\n let runtime: MermaidRuntime\n try {\n runtime = await load(runtimeUrl)\n } catch {\n return\n }\n if (disposed) return\n const theme = currentTheme()\n ensureInitialized(runtime, theme)\n for (const container of containers) {\n if (disposed) return\n if (!container.isConnected) continue\n try {\n // oxlint-disable-next-line eslint/no-await-in-loop -- one repaint per frame is the point of this loop.\n const cleaned = await paint(container, runtime, theme)\n if (disposed || !container.isConnected) return\n container.innerHTML = cleaned\n container.setAttribute(DATA_STATE, 'done')\n } catch {\n // A theme flip must never blank a diagram that already rendered.\n }\n // oxlint-disable-next-line eslint/no-await-in-loop -- the painted frame between repaints is the point of this loop.\n await nextPaint()\n }\n }\n\n /** One coalesced pass over the queued scopes. */\n const flush = async (): Promise<void> => {\n cancelFlush = undefined\n const scopes = [...queued]\n queued.clear()\n if (disposed || scopes.length === 0) return\n let runtime: MermaidRuntime\n try {\n runtime = await load(runtimeUrl)\n } catch {\n return // no vendor route: leave every fence as an ordinary code block\n }\n const theme = currentTheme()\n for (const scope of scopes) {\n if (disposed) return\n // oxlint-disable-next-line eslint/no-await-in-loop -- scopes are scanned one at a time so each can yield.\n await enhanceScope(scope, runtime, theme)\n }\n }\n\n const schedule = (scopes: readonly Element[]): void => {\n for (const scope of scopes) queued.add(scope)\n if (queued.size === 0 || cancelFlush !== undefined) return\n cancelFlush = afterNextFrame(() => {\n cancelFlush = undefined\n void flush()\n })\n }\n\n if (typeof MutationObserver === 'function') {\n const observer = new MutationObserver((records) => { schedule(scopesForRecords(records)) })\n observer.observe(root, { childList: true, subtree: true })\n }\n const stopThemeWatch = watchTheme(() => { void retheme() })\n schedule([root])\n\n return () => {\n disposed = true\n cancelFlush?.()\n cancelFlush = undefined\n for (const timer of retries) clearTimeout(timer)\n retries.clear()\n stopThemeWatch()\n }\n}\n","/**\n * Browser entry of the diagrams plugin.\n *\n * The enhancement needs no slot and no service: it observes the page for the\n * official markdown renderer's mermaid fences (chat transcript and document\n * preview alike) and renders them itself. `apply` therefore only owns the\n * effect's lifetime.\n * @module @linxin666/dsh-client-ui-diagrams/client\n */\n\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport { startEnhancement } from './enhance.ts'\n\n/**\n * Start the diagram enhancement for the lifetime of this client plugin.\n * @param ctx - Client context; the effect disposes the observer on unload.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => startEnhancement(), 'diagrams: render mermaid fences')\n}\n\nexport { startEnhancement } from './enhance.ts'\nexport { scopesForRecords } from './enhance.ts'\nexport { findMermaidBlocks } from './blocks.ts'\nexport { sanitizeSvg } from './sanitize.ts'\nexport { mermaidTheme, shellIsDark, watchTheme } from './theme.ts'\nexport { loadRuntime, runtimeIfLoaded, type MermaidRuntime } from './runtime.ts'\n"],"mappings":"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"}
@@ -4,9 +4,9 @@
4
4
  * Mermaid's strict security level already escapes label content, but the SVG is
5
5
  * third-party output injected into the page, so it is parsed in a detached
6
6
  * document first and stripped of anything executable: script-ish elements,
7
- * `on*` handlers and `javascript:` URLs. Cleaned markup is returned; input that
8
- * still carries a dangerous token after cleaning throws, and the caller falls
9
- * back to the untouched code block.
7
+ * `on*` handlers and URL attributes with an executable scheme. Cleaned markup is
8
+ * returned; input that still carries a dangerous token after cleaning throws, and
9
+ * the caller falls back to the untouched code block.
10
10
  *
11
11
  * One walk does both removals: dropping a disallowed element drops everything
12
12
  * beneath it, so a second pass would only re-examine nodes that are already
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-client-ui-diagrams",
3
3
  "description": "Mermaid diagram rendering for the DSH Web GUI: fenced mermaid blocks in the chat transcript and the document preview are rendered in place as SVG, from a runtime the host serves on demand.",
4
- "version": "0.4.15",
4
+ "version": "0.4.16",
5
5
  "icon": "icon.svg",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",