diff --git a/apps/web/package.json b/apps/web/package.json index 16579e730f1b..6f87f55a825c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,6 +42,7 @@ "jszip": "3.10.1", "lexical": "^0.41.0", "lucide-react": "^0.564.0", + "mermaid": "^11.16.1", "react": "19.2.6", "react-dom": "19.2.6", "react-markdown": "^10.1.0", diff --git a/apps/web/src/components/ChatMarkdown.test.tsx b/apps/web/src/components/ChatMarkdown.test.tsx index 4e22e68ca902..901037a08197 100644 --- a/apps/web/src/components/ChatMarkdown.test.tsx +++ b/apps/web/src/components/ChatMarkdown.test.tsx @@ -860,3 +860,39 @@ describe("ChatMarkdown Windows file links", () => { expect(html).not.toContain("chat-markdown-file-link"); }); }); + +describe("ChatMarkdown mermaid fences", () => { + it("renders mermaid fences as diagrams with a source toggle", () => { + const html = renderToStaticMarkup( + B\n```"} />, + ); + + expect(html).toContain('data-language="mermaid"'); + expect(html).toContain('data-mermaid="diagram"'); + expect(html).toContain("Show source"); + }); + + it("recognizes the mmd alias", () => { + const html = renderToStaticMarkup( + B\n```"} />, + ); + + expect(html).toContain('data-language="mmd"'); + expect(html).toContain('data-mermaid="diagram"'); + expect(html).toContain("Show source"); + }); + + it("keeps mermaid source while the message is streaming", () => { + const html = renderToStaticMarkup( + B\n```"} + />, + ); + + expect(html).toContain('data-language="mermaid"'); + expect(html).not.toContain("data-mermaid"); + expect(html).not.toContain("Show source"); + }); +}); diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index b0209754b0d0..cee58bddb413 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -7,7 +7,9 @@ import { import { CheckIcon, ChevronRightIcon, + Code2Icon, CopyIcon, + EyeIcon, FileSpreadsheetIcon, FileTextIcon, GlobeIcon, @@ -193,6 +195,7 @@ import { BrowserSettingsReadError, } from "../browser/openFileInPreview"; import { resolveLinkTarget } from "../browser/browserLinkTarget"; +import { MermaidDiagram } from "./MermaidDiagram"; import { PullRequestLinkPreview } from "./pullRequest/PullRequestLinkPreview"; interface ChatMarkdownProps { @@ -549,6 +552,12 @@ function extractFenceLanguage(className: string | undefined): string { return raw === "gitignore" ? "ini" : raw; } +/** Mermaid fences render as diagrams; `mmd` is the same language under its common alias. */ +export function isMermaidFenceLanguage(language: string): boolean { + const normalized = language.trim().toLowerCase(); + return normalized === "mermaid" || normalized === "mmd"; +} + const FENCE_TITLE_ATTR_REGEX = /(?:^|\s)(?:title|file(?:name)?)=(?:"([^"]+)"|'([^']+)'|(\S+))/i; const FENCE_FILENAME_TOKEN_REGEX = /^[\w@][\w@./-]*\.[A-Za-z0-9]+$/; @@ -912,19 +921,41 @@ function MarkdownCodeBlock({ language, fenceTitle, theme, + renderDiagram = false, children, }: { code: string; language: string; fenceTitle: string | null; theme: "light" | "dark"; + renderDiagram?: boolean; children: ReactNode; }) { const [copied, setCopied] = useState(false); const [wrapped, setWrapped] = useState(readInitialWordWrapSetting); + const [showSource, setShowSource] = useState(false); + // Failed diagrams fall back to source silently; parse errors echo the + // raw source and read as app breakage. + const [diagramFailed, setDiagramFailed] = useState(false); const copiedTimerRef = useRef | null>(null); const wrapLabel = wrapped ? "Disable line wrap" : "Wrap lines"; const copyLabel = copied ? "Copied" : "Copy code"; + const showDiagram = renderDiagram && !showSource && !diagramFailed; + const sourceToggleLabel = showSource ? "Show diagram" : "Show source"; + + // Reset the toggle on fence edits, gated so streaming blocks (re-rendered + // per token) pay no state updates here. + const [lastCode, setLastCode] = useState(code); + if (renderDiagram && lastCode !== code) { + setLastCode(code); + setShowSource(false); + setDiagramFailed(false); + } + + const handleDiagramError = useCallback(() => { + setDiagramFailed(true); + setShowSource(true); + }, []); const handleCopy = useCallback(() => { if (typeof navigator === "undefined" || navigator.clipboard == null) { @@ -968,6 +999,7 @@ function MarkdownCodeBlock({
@@ -979,24 +1011,53 @@ function MarkdownCodeBlock({ /> - - setWrapped((value) => !value)} - aria-label={wrapLabel} - /> - } - > - - - {wrapLabel} - + {renderDiagram ? ( + + { + if (showSource) { + setDiagramFailed(false); + setShowSource(false); + } else { + setShowSource(true); + } + }} + aria-label={sourceToggleLabel} + /> + } + > + {showSource ? : } + + {sourceToggleLabel} + + ) : null} + {showDiagram ? null : ( + + setWrapped((value) => !value)} + aria-label={wrapLabel} + /> + } + > + + + {wrapLabel} + + )}
- {children} + {showDiagram ? ( + + ) : ( + children + )}
); } @@ -3185,34 +3257,39 @@ const CHAT_MARKDOWN_COMPONENTS = { const language = extractFenceLanguage(codeBlock.className); const fenceTitle = extractFenceTitle(extractPreCodeMeta(node)); + const codeFallback = ( + {children}} + > + {/* Reserve the block's height but stay hidden until Shiki has colored + it, so plain text never flashes before the highlighted version. */} + + {children} + + } + > + + + + ); + const renderMermaid = !isStreaming && isMermaidFenceLanguage(language); return ( - {children}} - > - {/* Reserve the block's height but stay hidden until Shiki has colored - it, so plain text never flashes before the highlighted version. */} - - {children} - - } - > - - - + {codeFallback} ); }, diff --git a/apps/web/src/components/MermaidDiagram.test.tsx b/apps/web/src/components/MermaidDiagram.test.tsx new file mode 100644 index 000000000000..95d504c888d5 --- /dev/null +++ b/apps/web/src/components/MermaidDiagram.test.tsx @@ -0,0 +1,191 @@ +import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; +import { renderToStaticMarkup } from "react-dom/server"; + +const mermaid = vi.hoisted(() => ({ + initialize: vi.fn(), + render: vi.fn(), +})); + +vi.mock("mermaid", () => ({ default: mermaid })); + +import { + MermaidDiagram, + MermaidDiagramDialog, + cacheRenderedDiagram, + mermaidSvgNaturalSize, + renderMermaidDiagram, +} from "./MermaidDiagram"; +import { serializeMarkdownCodeFence } from "../markdown-clipboard"; + +describe("renderMermaidDiagram", () => { + beforeEach(() => { + mermaid.initialize.mockReset(); + mermaid.render.mockReset(); + }); + + it("renders with strict security and the selected theme", async () => { + mermaid.render.mockResolvedValue({ svg: "" }); + + await renderMermaidDiagram("diagram-1", "flowchart LR\nA-->B", "dark"); + + expect(mermaid.initialize).toHaveBeenCalledWith({ + startOnLoad: false, + securityLevel: "strict", + suppressErrorRendering: true, + secure: [ + "secure", + "securityLevel", + "startOnLoad", + "maxTextSize", + "suppressErrorRendering", + "maxEdges", + "themeCSS", + "fontFamily", + "altFontFamily", + ], + theme: "dark", + }); + expect(mermaid.render).toHaveBeenCalledWith("diagram-1", "flowchart LR\nA-->B"); + }); + + it("continues rendering after an invalid diagram", async () => { + mermaid.render + .mockRejectedValueOnce(new Error("Invalid diagram")) + .mockResolvedValueOnce({ svg: "" }); + + await expect(renderMermaidDiagram("diagram-1", "invalid", "light")).rejects.toThrow(); + await expect( + renderMermaidDiagram("diagram-2", "sequenceDiagram\nA->>B: Hi", "light"), + ).resolves.toEqual({ svg: "" }); + }); + + it("skips queued work after its diagram unmounts", async () => { + let finishFirstRender!: (result: { svg: string }) => void; + mermaid.render.mockImplementationOnce( + () => + new Promise((resolve) => { + finishFirstRender = resolve; + }), + ); + + const first = renderMermaidDiagram("diagram-1", "flowchart LR\nA-->B", "light"); + await vi.waitFor(() => expect(mermaid.render).toHaveBeenCalledTimes(1)); + const second = renderMermaidDiagram("diagram-2", "flowchart LR\nB-->C", "light", () => false); + finishFirstRender({ svg: "" }); + + await first; + await expect(second).resolves.toBeNull(); + expect(mermaid.render).toHaveBeenCalledTimes(1); + }); + + it("chooses a fence longer than backtick runs in copied source", () => { + expect(serializeMarkdownCodeFence("flowchart LR\n%% ``` in a comment", "mermaid")).toBe( + "````mermaid\nflowchart LR\n%% ``` in a comment\n````\n\n", + ); + }); +}); + +describe("MermaidDiagram expand", () => { + const code = "flowchart LR\nExpandA-->ExpandB"; + + beforeEach(() => { + mermaid.initialize.mockReset(); + mermaid.render.mockReset(); + // Seeds the module-level SVG cache so SSR reads the rendered diagram + // without running effects, exactly like a remount after scrolling. + cacheRenderedDiagram("dark", code, "expanded-diagram"); + }); + + it("renders the cached diagram as an expandable button carrying source for copy", () => { + mermaid.render.mockClear(); + const html = renderToStaticMarkup( + fallback} />, + ); + + expect(mermaid.render).not.toHaveBeenCalled(); + expect(html).toContain("expanded-diagram"); + expect(html).toContain('role="button"'); + expect(html).toContain('aria-label="Expand diagram"'); + expect(html).toContain("cursor-zoom-in"); + expect(html).toContain("data-markdown-copy"); + expect(html).toContain("flowchart LR"); + expect(html).not.toContain("fallback"); + }); + + it("renders the expanded dialog scrollable with the same SVG and a source copy", () => { + const html = renderToStaticMarkup( + undefined} + />, + ); + + expect(html).toContain('role="dialog"'); + expect(html).toContain("expanded-diagram"); + expect(html).toContain("overflow-auto"); + expect(html).toContain("chat-markdown-mermaid-dialog"); + expect(html).toContain('aria-label="Close diagram preview"'); + expect(html).toContain('aria-label="Copy diagram source"'); + expect(html).toContain("data-markdown-copy"); + }); +}); + +describe("mermaidSvgNaturalSize", () => { + it("reads the natural size from the viewBox", () => { + expect( + mermaidSvgNaturalSize( + '', + ), + ).toEqual({ width: 444.890625, height: 174 }); + }); + + it("rejects missing or degenerate viewBoxes", () => { + expect(mermaidSvgNaturalSize("no viewBox")).toBeNull(); + expect(mermaidSvgNaturalSize('')).toBeNull(); + expect(mermaidSvgNaturalSize('')).toBeNull(); + }); +}); + +describe("MermaidDiagramDialog sizing", () => { + it("fixes the scroll container to the diagram natural width", () => { + const html = renderToStaticMarkup( + undefined} + />, + ); + + expect(html).toContain("width:573.2px"); + expect(html).toContain("chat-markdown-mermaid-dialog"); + }); +}); + +describe("MermaidDiagram visibility gating", () => { + const code = "flowchart LR\nGatedA-->GatedB"; + + it("defers rendering until the diagram nears the viewport", () => { + vi.stubGlobal( + "IntersectionObserver", + class { + observe() {} + disconnect() {} + }, + ); + try { + const html = renderToStaticMarkup( + gated-fallback} />, + ); + + expect(html).toContain("gated-fallback"); + expect(html).not.toContain('role="button"'); + expect(html).not.toContain("Expand diagram"); + // The fallback is wrapped in the observation host that triggers the + // render on near-viewport entry. Bare fallback would be a single div. + expect(html).toBe("
gated-fallback
"); + } finally { + vi.unstubAllGlobals(); + } + }); +}); diff --git a/apps/web/src/components/MermaidDiagram.tsx b/apps/web/src/components/MermaidDiagram.tsx new file mode 100644 index 000000000000..fdf4e2a42050 --- /dev/null +++ b/apps/web/src/components/MermaidDiagram.tsx @@ -0,0 +1,364 @@ +import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react"; +import { createPortal } from "react-dom"; +import { CheckIcon, CopyIcon, XIcon } from "lucide-react"; +import type { RenderResult } from "mermaid"; + +import { serializeMarkdownCodeFence } from "../markdown-clipboard"; +import { Button } from "./ui/button"; +import { composerFloatingLayerProps } from "./chat/composerEventScope"; +import { isContextMenuOpen } from "../contextMenuFallback"; + +export type MermaidTheme = "light" | "dark"; + +// Mermaid configuration is global, so initialization and rendering must stay paired. +let mermaidRenderQueue = Promise.resolve(); + +// The chat list unmounts off-screen rows. Without this, every scroll back past a +// diagram flashed the code fallback, re-ran Mermaid, and jumped the row height. +const MAX_CACHED_DIAGRAMS = 50; +const renderedDiagrams = new Map(); + +function diagramCacheKey(theme: MermaidTheme, code: string) { + return `${theme}\n${code}`; +} + +function rememberRenderedDiagram(key: string, svg: string) { + renderedDiagrams.delete(key); + renderedDiagrams.set(key, svg); + if (renderedDiagrams.size > MAX_CACHED_DIAGRAMS) { + const oldest = renderedDiagrams.keys().next().value; + if (oldest !== undefined) renderedDiagrams.delete(oldest); + } +} + +/** Caches a rendered diagram for scroll remounts. Exported for tests. */ +export function cacheRenderedDiagram(theme: MermaidTheme, code: string, svg: string) { + rememberRenderedDiagram(diagramCacheKey(theme, code), svg); +} + +export function renderMermaidDiagram( + id: string, + code: string, + theme: MermaidTheme, + isActive: () => boolean = () => true, +) { + const render = async () => { + if (!isActive()) return null; + const { default: mermaid } = await import("mermaid"); + if (!isActive()) return null; + mermaid.initialize({ + startOnLoad: false, + securityLevel: "strict", + suppressErrorRendering: true, + secure: [ + "secure", + "securityLevel", + "startOnLoad", + "maxTextSize", + "suppressErrorRendering", + "maxEdges", + "themeCSS", + "fontFamily", + "altFontFamily", + ], + theme: theme === "dark" ? "dark" : "default", + }); + return mermaid.render(id, code); + }; + + const result = mermaidRenderQueue.then(render, render); + mermaidRenderQueue = result.then( + () => undefined, + () => undefined, + ); + return result; +} + +function mermaidErrorMessage(cause: unknown): string { + if (cause instanceof Error && cause.message.trim().length > 0) { + return cause.message; + } + return "Couldn't render diagram."; +} + +const MERMAID_VIEWBOX_PATTERN = /]*\bviewBox\s*=\s*"([^"]+)"/; + +/** Natural pixel size from the SVG viewBox. Pure string parsing, SSR-safe. */ +export function mermaidSvgNaturalSize(svg: string): { width: number; height: number } | null { + const viewBox = MERMAID_VIEWBOX_PATTERN.exec(svg)?.[1]; + const dimensions = viewBox?.trim().split(/\s+/).map(Number); + const width = dimensions?.[2]; + const height = dimensions?.[3]; + if ( + dimensions?.length !== 4 || + !Number.isFinite(width) || + !Number.isFinite(height) || + (width ?? 0) <= 0 || + (height ?? 0) <= 0 + ) { + return null; + } + return { width: width as number, height: height as number }; +} + +export function MermaidDiagramDialog({ + svg, + copyMarkdown, + onClose, +}: { + svg: string; + copyMarkdown: string; + onClose: () => void; +}) { + const [copied, setCopied] = useState(false); + const copiedTimerRef = useRef | null>(null); + + const handleCopy = useCallback(() => { + if (typeof navigator === "undefined" || navigator.clipboard == null) { + return; + } + void navigator.clipboard + .writeText(copyMarkdown) + .then(() => { + if (copiedTimerRef.current != null) { + clearTimeout(copiedTimerRef.current); + } + setCopied(true); + copiedTimerRef.current = setTimeout(() => { + setCopied(false); + copiedTimerRef.current = null; + }, 1200); + }) + .catch(() => undefined); + }, [copyMarkdown]); + + useEffect( + () => () => { + if (copiedTimerRef.current != null) { + clearTimeout(copiedTimerRef.current); + copiedTimerRef.current = null; + } + }, + [], + ); + + // The element that opened the preview gets focus back on close. + const openerRef = useRef(null); + const dialogRef = useRef(null); + const closeButtonRef = useRef(null); + useEffect(() => { + openerRef.current = document.activeElement; + closeButtonRef.current?.focus({ preventScroll: true }); + return () => { + const opener = openerRef.current; + if (opener instanceof HTMLElement && opener.isConnected) { + opener.focus({ preventScroll: true }); + } + }; + }, []); + + useEffect(() => { + const onKeyDown = (event: globalThis.KeyboardEvent) => { + if (event.defaultPrevented || isContextMenuOpen()) { + return; + } + if (event.key === "Escape") { + event.preventDefault(); + event.stopPropagation(); + onClose(); + return; + } + if (event.key !== "Tab") return; + const root = dialogRef.current; + if (!root) return; + const controls = Array.from( + root.querySelectorAll( + 'button:not([disabled]):not([tabindex="-1"]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + ), + ); + if (controls.length === 0) return; + const first = controls[0] as HTMLElement; + const last = controls[controls.length - 1] as HTMLElement; + const active = document.activeElement; + if (event.shiftKey ? active === first || !root.contains(active) : active === last) { + event.preventDefault(); + (event.shiftKey ? last : first).focus(); + } + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [onClose]); + + // Definite container width so the SVG's `width="100%"` resolves to its + // natural size instead of collapsing. Capped by max-w-[92vw]. + const naturalSize = useMemo(() => mermaidSvgNaturalSize(svg), [svg]); + + const dialog = ( +
+ + +
+
+
+ + ); + // Portals escape clipped chat rows; inline fallback covers SSR/tests. + return typeof document === "undefined" ? dialog : createPortal(dialog, document.body); +} + +export function MermaidDiagram({ + code, + language = "mermaid", + theme, + fallback, + onError, +}: { + code: string; + language?: string; + theme: MermaidTheme; + fallback: ReactNode; + onError?: (message: string) => void; +}) { + const reactId = useId(); + const diagramId = `t3-mermaid-${reactId.replace(/[^a-zA-Z0-9_-]/g, "")}`; + const cacheKey = diagramCacheKey(theme, code); + // Remounted per code and theme via key, so the stored result always matches. + const [svg, setSvg] = useState(() => renderedDiagrams.get(cacheKey) ?? null); + const [expanded, setExpanded] = useState(false); + const copyMarkdown = serializeMarkdownCodeFence(code, language); + // Mermaid runs only for diagrams at or near the viewport. Chat unmounts + // far rows, but file previews and PR bodies mount whole documents, so + // without this every diagram below the fold would render on open. + const [inView, setInView] = useState( + () => renderedDiagrams.has(cacheKey) || typeof IntersectionObserver === "undefined", + ); + const hostRef = useRef(null); + + // Adopt a diagram cached after this mount's initializers ran (sibling won + // the race). Render-phase adjustment avoids an extra commit cycle. + const cachedSvg = renderedDiagrams.get(cacheKey); + if (cachedSvg !== undefined && cachedSvg !== svg) { + setSvg(cachedSvg); + } + if (!inView && cachedSvg !== undefined) { + setInView(true); + } + + useEffect(() => { + if (inView) return undefined; + const host = hostRef.current; + if (host === null) return undefined; + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setInView(true); + observer.disconnect(); + } + }, + // Start rendering just before the diagram scrolls into view. + { rootMargin: "400px" }, + ); + observer.observe(host); + return () => observer.disconnect(); + // cacheKey omitted: the parent remounts per code and theme, so it never + // changes within a mount and would only refire the observer for nothing. + }, [inView]); + + useEffect(() => { + if (!inView || svg !== null) return undefined; + let active = true; + void renderMermaidDiagram(diagramId, code, theme, () => active).then( + (result: RenderResult | null) => { + if (!result) return; + rememberRenderedDiagram(cacheKey, result.svg); + if (active) setSvg(result.svg); + }, + (cause: unknown) => { + if (!active) return; + onError?.(mermaidErrorMessage(cause)); + }, + ); + return () => { + active = false; + }; + }, [cacheKey, code, diagramId, inView, onError, svg, theme]); + + if (!svg) { + // Code fallback doubles as the visibility-observation host. + return
{fallback}
; + } + + return ( + <> +
{ + // Links inside the SVG (e.g. `click node href`) keep working, and a + // drag that selects diagram text must not pop the overlay open. + if (event.target instanceof Element && event.target.closest("a") !== null) return; + const selection = window.getSelection(); + if (selection !== null && !selection.isCollapsed) return; + setExpanded(true); + }} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + setExpanded(true); + } + }} + dangerouslySetInnerHTML={{ __html: svg }} + /> + {expanded ? ( + setExpanded(false)} + /> + ) : null} + + ); +} diff --git a/apps/web/src/index.css b/apps/web/src/index.css index e7c5d919a211..28034f792260 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1891,6 +1891,38 @@ code { background: transparent !important; } +.chat-markdown .chat-markdown-mermaid { + max-width: 100%; + overflow-x: auto; + padding: 1rem; +} + +.chat-markdown .chat-markdown-codeblock:has(.chat-markdown-mermaid) .chat-markdown-wrap-action { + display: none; +} + +/* Mermaid emits width="100%" plus an inline max-width equal to the diagram's + native width, so the SVG shrinks to fit the chat column and never grows past + its natural size. Only the height needs help: it must follow the viewBox. */ +.chat-markdown .chat-markdown-mermaid svg { + display: block; + height: auto; + margin: 0 auto; +} + +/* Expanded overlay lives outside `.chat-markdown`: natural width inline, + capped by max-w-[92vw], container scrolls for larger diagrams. */ +.chat-markdown-mermaid-dialog { + max-width: 100%; + overflow: auto; +} + +.chat-markdown-mermaid-dialog svg { + display: block; + height: auto; + margin: 0 auto; +} + /* Paragraphs and code blocks arrive in chunks while a response streams. Fade each new block in so the chunk does not pop. @starting-style only applies when an element is first inserted, and the rule is gated on data-streaming, diff --git a/apps/web/src/markdown-clipboard.test.ts b/apps/web/src/markdown-clipboard.test.ts index 9296967e4b82..653b2a97938e 100644 --- a/apps/web/src/markdown-clipboard.test.ts +++ b/apps/web/src/markdown-clipboard.test.ts @@ -1,6 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; -import { serializeRenderedMarkdownFragment } from "./markdown-clipboard"; +import { + chatMarkdownClipboardPayload, + serializeRenderedMarkdownFragment, +} from "./markdown-clipboard"; import { EnvironmentId, MessageId, ThreadId } from "@t3tools/contracts"; import { collectAssistantCitations, @@ -13,6 +16,7 @@ const ELEMENT_NODE = 1; class FakeText { readonly nodeType = TEXT_NODE; readonly childNodes: ReadonlyArray = []; + parentElement: FakeElement | null = null; constructor(readonly textContent: string) {} } @@ -20,6 +24,7 @@ class FakeText { class FakeElement { readonly nodeType = ELEMENT_NODE; readonly childNodes: Array = []; + parentElement: FakeElement | null = null; readonly classList = { contains: (name: string) => this.classNames.includes(name), }; @@ -43,10 +48,16 @@ class FakeElement { } append(...children: Array): this { + for (const child of children) child.parentElement = this; this.childNodes.push(...children); return this; } + appendChild(child: T): T { + this.append(child); + return child; + } + getAttribute(name: string): string | null { return this.attributes[name] ?? null; } @@ -55,8 +66,20 @@ class FakeElement { return Object.hasOwn(this.attributes, name); } - closest(): FakeElement | null { - return null; + closest(selector: string): FakeElement | null { + if (selector === "[data-markdown-copy]" && this.hasAttribute("data-markdown-copy")) { + return this; + } + if (this.tagName === selector.toUpperCase()) return this; + return this.parentElement?.closest(selector) ?? null; + } + + querySelectorAll(): ReadonlyArray { + return []; + } + + get innerHTML(): string { + return this.textContent; } /** Supports only the selectors markdown-clipboard actually asks for. */ @@ -277,4 +300,26 @@ describe("serializeRenderedMarkdownFragment", () => { "Hello World (Document template)", ); }); + + it("uses an ancestor's explicit Markdown when a selection only clones its children", () => { + const label = new FakeElement("TEXT").append(new FakeText("A")); + new FakeElement("DIV", [], { + "data-markdown-copy": "```mermaid\nflowchart LR\nA-->B\n```\n\n", + }).append(new FakeElement("SVG").append(label)); + vi.stubGlobal("document", { + createElement: () => new FakeElement("DIV"), + }); + + const payload = chatMarkdownClipboardPayload({ + rangeCount: 1, + getRangeAt: () => ({ + collapsed: false, + cloneContents: () => new FakeElement("SVG").append(new FakeText("A")), + commonAncestorContainer: label, + toString: () => "A", + }), + } as unknown as Selection); + + expect(payload?.text).toBe("```mermaid\nflowchart LR\nA-->B\n```\n\n"); + }); }); diff --git a/apps/web/src/markdown-clipboard.ts b/apps/web/src/markdown-clipboard.ts index 4a96c8b31d13..c279921d9136 100644 --- a/apps/web/src/markdown-clipboard.ts +++ b/apps/web/src/markdown-clipboard.ts @@ -71,6 +71,12 @@ function codeFenceFor(code: string): string { return "`".repeat(Math.max(3, longestRun + 1)); } +export function serializeMarkdownCodeFence(code: string, infoString: string): string { + const normalizedCode = code.replace(/\n$/, ""); + const fence = codeFenceFor(normalizedCode); + return `${fence}${infoString}\n${normalizedCode}\n${fence}\n\n`; +} + function resolveCodeBlockLanguage(pre: Element): string | null { const declared = pre.closest("[data-language]")?.getAttribute("data-language") ?? @@ -80,9 +86,7 @@ function resolveCodeBlockLanguage(pre: Element): string | null { } function serializeCodeBlock(pre: Element): string { - const code = (pre.textContent ?? "").replace(/\n$/, ""); - const fence = codeFenceFor(code); - return `${fence}${resolveCodeBlockLanguage(pre) ?? ""}\n${code}\n${fence}\n\n`; + return serializeMarkdownCodeFence(pre.textContent ?? "", resolveCodeBlockLanguage(pre) ?? ""); } function serializeTableCell(cell: Element): string { @@ -391,6 +395,14 @@ export function chatMarkdownClipboardPayload( const ancestor = range.commonAncestorContainer; const ancestorElement = ancestor.nodeType === Node.ELEMENT_NODE ? (ancestor as Element) : ancestor.parentElement; + const markdownCopy = ancestorElement + ?.closest("[data-markdown-copy]") + ?.getAttribute("data-markdown-copy"); + if (markdownCopy != null) { + texts.push(markdownCopy); + htmls.push(sanitizedHtmlFrom(container)); + continue; + } if (ancestorElement?.closest("pre")) { const text = range.toString(); if (text) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a37d9bf6fa4..36ccf4a5fb99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -657,6 +657,9 @@ importers: lucide-react: specifier: ^0.564.0 version: 0.564.0(react@19.2.6) + mermaid: + specifier: ^11.16.1 + version: 11.17.2 react: specifier: 19.2.6 version: 19.2.6 @@ -1081,6 +1084,9 @@ packages: '@alchemy.run/node-utils@2.0.0-beta.76': resolution: {integrity: sha512-KUAlGdoeWeMEToUP1V3I+mEf6FVzUjU+6Tc8savNvMsXN9t3uwZVqD1FeMzTMzMTJJijjxVljQTCtyoeE9W43w==} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@anthropic-ai/claude-agent-sdk@0.3.260': resolution: {integrity: sha512-PmABtP4Rwd6l95itQrqzguv6rS9uACqikPB9g8BPeWRKZOpy3xpEOjJLYauof3BFk2wNZnfhr0Ttx8ttcZzq0w==} engines: {node: '>=18.0.0'} @@ -1717,6 +1723,9 @@ packages: '@blazediff/core@1.9.1': resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + '@bruits/satteri-darwin-arm64@0.9.3': resolution: {integrity: sha512-dRUZZrdwh1asfTOyM1nDNmzolhnHtlIFpqYrl1Tdd3YVcaebKmrfJgGL7NAoGPjbEwYmZxaugrxA0uzw83c0dw==} cpu: [arm64] @@ -1779,6 +1788,9 @@ packages: '@chevrotain/types@10.5.0': resolution: {integrity: sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==} + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + '@chevrotain/utils@10.5.0': resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} @@ -2888,6 +2900,12 @@ packages: '@iarna/toml@2.2.5': resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.4': + resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==} + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -3430,6 +3448,9 @@ packages: '@material/material-color-utilities@0.3.0': resolution: {integrity: sha512-ztmtTd6xwnuh2/xu+Vb01btgV8SQWYCaK56CkRK8gEkWe5TuDyBcYJ0wgkMRn+2VcE9KUmhvkz+N9GHrqw/C0g==} + '@mermaid-js/parser@1.2.1': + resolution: {integrity: sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==} + '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -5357,6 +5378,99 @@ packages: '@types/culori@4.0.1': resolution: {integrity: sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.1': + resolution: {integrity: sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.4': + resolution: {integrity: sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.2.0': + resolution: {integrity: sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -5381,6 +5495,9 @@ packages: '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hammerjs@2.0.46': resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} @@ -5452,6 +5569,9 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -5599,6 +5719,9 @@ packages: '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + '@vercel/config@0.3.0': resolution: {integrity: sha512-Tf5k5y2F478oTiQcU5R8Ntix1UejE6NdduZnI7aa1XXxjCtifX1XdRS/D2uTjiQAwIL3pLa1LSAN80ABbba+TQ==} hasBin: true @@ -6662,6 +6785,10 @@ packages: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -6743,6 +6870,12 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cross-dirname@0.1.0: resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} @@ -6792,6 +6925,165 @@ packages: resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.34.2: + resolution: {integrity: sha512-Cm2jaj1X/PBNlzV9yH8zcfGOxO7U+CJ/+mxSBVPSchLaugdp4jtlGx5qaHtPRZ6tgiZ5P+o1XoRfJA+ba6KM3g==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + + dayjs@1.11.23: + resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==} + dbus-next@0.10.2: resolution: {integrity: sha512-kLNQoadPstLgKKGIXKrnRsMgtAK/o+ix3ZmcfTfvBHzghiO9yHXpoKImGnB50EXwnfSFaSAullW/7UrSkAISSQ==} @@ -6857,6 +7149,9 @@ packages: defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -6930,6 +7225,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.4.14: + resolution: {integrity: sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -7676,6 +7974,9 @@ packages: resolution: {integrity: sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==} hasBin: true + fastdom@1.0.12: + resolution: {integrity: sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg==} + fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} engines: {node: '>= 4.9.1'} @@ -7904,6 +8205,9 @@ packages: h3@1.15.11: resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -8074,6 +8378,9 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + indent-string@5.0.0: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} @@ -8101,6 +8408,13 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -8352,9 +8666,16 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -8372,6 +8693,12 @@ packages: resolution: {integrity: sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A==} hasBin: true + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lazy-val@1.0.5: resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} @@ -8486,6 +8813,9 @@ packages: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -8568,6 +8898,11 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} @@ -8663,6 +8998,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + mermaid@11.17.2: + resolution: {integrity: sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg==} + metro-babel-transformer@0.84.4: resolution: {integrity: sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} @@ -9336,6 +9674,9 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -9479,6 +9820,12 @@ packages: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + postcss@8.5.15: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} @@ -10040,6 +10387,9 @@ packages: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rolldown@1.0.0-rc.17: resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -10055,6 +10405,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -10062,6 +10415,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -10373,6 +10729,9 @@ packages: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} + strictdom@1.0.1: + resolution: {integrity: sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -10419,6 +10778,9 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + sumchecker@3.0.1: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} @@ -10590,6 +10952,10 @@ packages: ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -10884,6 +11250,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + uuid@7.0.3: resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). @@ -11394,6 +11764,11 @@ snapshots: '@alchemy.run/node-utils@2.0.0-beta.76': {} + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.2.4 + '@anthropic-ai/claude-agent-sdk@0.3.260(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.93.0(zod@4.4.3) @@ -12289,6 +12664,8 @@ snapshots: '@blazediff/core@1.9.1': {} + '@braintree/sanitize-url@7.1.2': {} + '@bruits/satteri-darwin-arm64@0.9.3': optional: true @@ -12337,6 +12714,8 @@ snapshots: '@chevrotain/types@10.5.0': {} + '@chevrotain/types@11.1.2': {} + '@chevrotain/utils@10.5.0': {} '@clack/core@1.4.2': @@ -13702,6 +14081,14 @@ snapshots: '@iarna/toml@2.2.5': {} + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.4': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -14232,6 +14619,10 @@ snapshots: '@material/material-color-utilities@0.3.0': {} + '@mermaid-js/parser@1.2.1': + dependencies: + '@chevrotain/types': 11.1.2 + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.27) @@ -15859,6 +16250,123 @@ snapshots: '@types/culori@4.0.1': {} + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.1': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.4': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.2.0': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.1 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.2.0 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -15890,6 +16398,8 @@ snapshots: dependencies: '@types/node': 24.12.4 + '@types/geojson@7946.0.16': {} + '@types/hammerjs@2.0.46': {} '@types/hast@3.0.4': @@ -15966,6 +16476,9 @@ snapshots: '@types/statuses@2.0.6': optional: true + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -16054,6 +16567,11 @@ snapshots: '@ungap/structured-clone@1.3.1': {} + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + '@vercel/config@0.3.0': dependencies: '@vercel/routing-utils': 6.2.0 @@ -16097,7 +16615,7 @@ snapshots: sirv: 3.0.2 tinyrainbow: 3.1.0 vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@7.0.2)) - ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - msw @@ -17205,6 +17723,8 @@ snapshots: commander@7.2.0: {} + commander@8.3.0: {} + commander@9.5.0: optional: true @@ -17285,6 +17805,14 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + cross-dirname@0.1.0: optional: true @@ -17338,6 +17866,192 @@ snapshots: culori@4.0.2: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.2): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.34.2 + + cytoscape-fcose@2.2.0(cytoscape@3.34.2): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.34.2 + + cytoscape@3.34.2: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + + dayjs@1.11.23: {} + dbus-next@0.10.2(patch_hash=cfff57561b0ee59b5addb3b2e6c6f20906e967507a530ab67e8db8108e520ba4): dependencies: '@nornagon/put': 0.0.8 @@ -17398,6 +18112,10 @@ snapshots: defu@6.1.7: {} + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + delayed-stream@1.0.0: {} denque@2.1.0: @@ -17461,6 +18179,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.4.14: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -18359,6 +19081,10 @@ snapshots: strnum: 2.3.0 xml-naming: 0.1.0 + fastdom@1.0.12: + dependencies: + strictdom: 1.0.1 + fastest-levenshtein@1.0.16: {} fastq@1.20.1: @@ -18638,6 +19364,8 @@ snapshots: ufo: 1.6.4 uncrypto: 0.1.3 + hachure-fill@0.5.2: {} + has-flag@3.0.0: {} has-flag@4.0.0: {} @@ -18857,7 +19585,6 @@ snapshots: iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 - optional: true iconv-lite@0.7.2: dependencies: @@ -18876,6 +19603,8 @@ snapshots: immediate@3.0.6: {} + import-meta-resolve@4.2.0: {} + indent-string@5.0.0: {} inflight@1.0.6: @@ -18922,6 +19651,10 @@ snapshots: inline-style-parser@0.2.7: {} + internmap@1.0.1: {} + + internmap@2.0.3: {} + invariant@2.2.4: dependencies: loose-envify: 1.4.0 @@ -19130,10 +19863,16 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + katex@0.16.47: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 + khroma@2.1.0: {} + kleur@3.0.3: {} kleur@4.1.5: {} @@ -19156,6 +19895,10 @@ snapshots: lan-network@0.2.1: {} + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + lazy-val@1.0.5: {} leven@3.1.0: {} @@ -19254,6 +19997,8 @@ snapshots: p-locate: 3.0.0 path-exists: 3.0.0 + lodash-es@4.18.1: {} + lodash.debounce@4.0.8: {} lodash.escaperegexp@4.1.2: {} @@ -19323,6 +20068,8 @@ snapshots: markdown-table@3.0.4: {} + marked@16.4.2: {} + marky@1.3.0: {} matcher@3.0.0: @@ -19530,6 +20277,31 @@ snapshots: merge2@1.4.1: {} + mermaid@11.17.2: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.4 + '@mermaid-js/parser': 1.2.1 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.34.2 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.2) + cytoscape-fcose: 2.2.0(cytoscape@3.34.2) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.23 + dompurify: 3.4.14 + es-toolkit: 1.47.0 + fastdom: 1.0.12 + katex: 0.16.47 + khroma: 2.1.0 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.4.0 + ts-dedent: 2.3.0 + uuid: 14.0.2 + metro-babel-transformer@0.84.4: dependencies: '@babel/core': 7.29.7 @@ -20599,6 +21371,8 @@ snapshots: path-browserify@1.0.1: {} + path-data-parser@0.1.0: {} + path-exists@3.0.0: {} path-expression-matcher@1.5.0: {} @@ -20724,6 +21498,13 @@ snapshots: pngjs@7.0.0: {} + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + postcss@8.5.15: dependencies: nanoid: 3.3.12 @@ -21452,6 +22233,8 @@ snapshots: sprintf-js: 1.1.3 optional: true + robust-predicates@3.0.3: {} + rolldown@1.0.0-rc.17: dependencies: '@oxc-project/types': 0.127.0 @@ -21527,6 +22310,13 @@ snapshots: fsevents: 2.3.3 optional: true + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + router@2.2.0: dependencies: debug: 4.4.3 @@ -21541,6 +22331,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rw@1.3.3: {} + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -21928,6 +22720,8 @@ snapshots: strict-uri-encode@2.0.0: {} + strictdom@1.0.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -21980,6 +22774,8 @@ snapshots: dependencies: inline-style-parser: 0.2.7 + stylis@4.4.0: {} + sumchecker@3.0.1: dependencies: debug: 4.4.3 @@ -22150,6 +22946,8 @@ snapshots: ts-algebra@2.0.0: {} + ts-dedent@2.3.0: {} + tslib@2.8.1: {} type-fest@0.13.1: @@ -22406,6 +23204,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@14.0.2: {} + uuid@7.0.3: {} valibot@1.2.0(typescript@6.0.3): @@ -22521,7 +23321,7 @@ snapshots: magic-string: 0.30.21 obug: 2.1.3 pathe: 2.0.3 - picomatch: 4.0.4 + picomatch: 4.0.7 std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.2.4