From dcb7ffa945506df6f80101233b8190e487d4c1ae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 2 Jun 2026 12:12:48 +0000 Subject: [PATCH] Suppress Mermaid render error artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configure Mermaid to suppress its built-in error renderer in both interactive UI previews and generated document HTML. Render failures now surface through the product toast flow with a safe inline fallback instead of leaving raw Mermaid diagnostics in the document body. Signed-off-by: Cursor Agent Co-authored-by: Émile Ré --- .../ui/src/Atoms/Markdown/MermaidDiagram.tsx | 45 +++++++++++----- .../ui/src/RichEditor/MermaidNodeView.tsx | 51 +++++++++++++------ packages/ui/src/lib/mermaid.ts | 34 +++++++++++++ packages/ui/src/{ => lib}/mermaidConfig.ts | 0 pkg/docgen/template.html | 2 +- 5 files changed, 101 insertions(+), 31 deletions(-) create mode 100644 packages/ui/src/lib/mermaid.ts rename packages/ui/src/{ => lib}/mermaidConfig.ts (100%) diff --git a/packages/ui/src/Atoms/Markdown/MermaidDiagram.tsx b/packages/ui/src/Atoms/Markdown/MermaidDiagram.tsx index 47a64e6b0..c229de85d 100644 --- a/packages/ui/src/Atoms/Markdown/MermaidDiagram.tsx +++ b/packages/ui/src/Atoms/Markdown/MermaidDiagram.tsx @@ -18,21 +18,33 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import mermaid from "mermaid"; import { useEffect, useId, useState } from "react"; -import { mermaidRenderConfig } from "../../mermaidConfig"; +import { mermaidRenderErrorToast, renderMermaidDiagram } from "../../lib/mermaid"; +import { useToast } from "../Toasts/Toasts"; type Props = { chart: string; }; +type MermaidRenderState = { + source: string; + svg: string | null; + hasError: boolean; +}; + export function MermaidDiagram({ chart }: Props) { const id = useId().replace(/:/g, ""); - const [svg, setSvg] = useState(null); - const [error, setError] = useState(null); + const [renderState, setRenderState] = useState({ + source: "", + svg: null, + hasError: false, + }); + const { toast } = useToast(); const source = (chart ?? "").trim(); + const svg = renderState.source === source ? renderState.svg : null; + const hasError = renderState.source === source && renderState.hasError; useEffect(() => { if (!source) { @@ -41,28 +53,33 @@ export function MermaidDiagram({ chart }: Props) { let cancelled = false; - mermaid.initialize(mermaidRenderConfig); - - mermaid - .render(`mermaid-${id}`, source) + renderMermaidDiagram(`mermaid-${id}`, source) .then((result) => { if (!cancelled) { - setSvg(result.svg); - setError(null); + setRenderState({ + source, + svg: result.svg, + hasError: false, + }); } }) - .catch((err: unknown) => { + .catch(() => { if (!cancelled) { - setError(err instanceof Error ? err.message : String(err)); + setRenderState({ + source, + svg: null, + hasError: true, + }); + toast(mermaidRenderErrorToast); } }); return () => { cancelled = true; }; - }, [source, id]); + }, [source, id, toast]); - if (error) { + if (hasError) { return (
         {chart}
diff --git a/packages/ui/src/RichEditor/MermaidNodeView.tsx b/packages/ui/src/RichEditor/MermaidNodeView.tsx
index ecb5f38c9..3c74debc0 100644
--- a/packages/ui/src/RichEditor/MermaidNodeView.tsx
+++ b/packages/ui/src/RichEditor/MermaidNodeView.tsx
@@ -5,45 +5,64 @@
 import { CodeIcon, EyeIcon } from "@phosphor-icons/react";
 import type { ReactNodeViewProps } from "@tiptap/react";
 import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
-import mermaid from "mermaid";
 import { useEffect, useId, useState } from "react";
 
-import { mermaidRenderConfig } from "../mermaidConfig";
+import { useToast } from "../Atoms/Toasts/Toasts";
+import { mermaidRenderErrorToast, renderMermaidDiagram } from "../lib/mermaid";
 
 type MermaidMode = "code" | "preview";
 
+type MermaidRenderState = {
+  source: string;
+  svg: string | null;
+  hasError: boolean;
+};
+
 function MermaidPreview({ chart }: { chart: string }) {
   const id = useId().replace(/:/g, "");
-  const [svg, setSvg] = useState(null);
-  const [error, setError] = useState(null);
+  const [renderState, setRenderState] = useState({
+    source: "",
+    svg: null,
+    hasError: false,
+  });
+  const { toast } = useToast();
 
   const source = chart.trim();
+  const svg = renderState.source === source ? renderState.svg : null;
+  const hasError = renderState.source === source && renderState.hasError;
 
   useEffect(() => {
-    if (source.length === 0) return;
+    if (source.length === 0) {
+      return;
+    }
 
     let cancelled = false;
 
-    mermaid.initialize(mermaidRenderConfig);
-
-    mermaid
-      .render(`mermaid-editor-${id}`, source)
+    renderMermaidDiagram(`mermaid-editor-${id}`, source)
       .then((result) => {
         if (!cancelled) {
-          setSvg(result.svg);
-          setError(null);
+          setRenderState({
+            source,
+            svg: result.svg,
+            hasError: false,
+          });
         }
       })
-      .catch((err: unknown) => {
+      .catch(() => {
         if (!cancelled) {
-          setError(err instanceof Error ? err.message : String(err));
+          setRenderState({
+            source,
+            svg: null,
+            hasError: true,
+          });
+          toast(mermaidRenderErrorToast);
         }
       });
 
     return () => {
       cancelled = true;
     };
-  }, [source, id]);
+  }, [source, id, toast]);
 
   if (source.length === 0) {
     return (
@@ -53,10 +72,10 @@ function MermaidPreview({ chart }: { chart: string }) {
     );
   }
 
-  if (error) {
+  if (hasError) {
     return (
       
- {error} + Unable to render diagram. Check the syntax and try again.
); } diff --git a/packages/ui/src/lib/mermaid.ts b/packages/ui/src/lib/mermaid.ts new file mode 100644 index 000000000..a3732d405 --- /dev/null +++ b/packages/ui/src/lib/mermaid.ts @@ -0,0 +1,34 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import mermaid from "mermaid"; + +import { mermaidRenderConfig } from "./mermaidConfig"; + +export const mermaidRenderErrorToast = { + title: "Unable to render Mermaid diagram", + description: "Check the diagram syntax and try again.", + variant: "error", +} as const; + +export async function renderMermaidDiagram(id: string, source: string) { + mermaid.initialize(mermaidRenderConfig); + + try { + return await mermaid.render(id, source); + } catch (error) { + document.getElementById(`d${id}`)?.remove(); + throw error; + } +} diff --git a/packages/ui/src/mermaidConfig.ts b/packages/ui/src/lib/mermaidConfig.ts similarity index 100% rename from packages/ui/src/mermaidConfig.ts rename to packages/ui/src/lib/mermaidConfig.ts diff --git a/pkg/docgen/template.html b/pkg/docgen/template.html index a8a95062b..625e181a0 100644 --- a/pkg/docgen/template.html +++ b/pkg/docgen/template.html @@ -374,7 +374,7 @@