Suppress Mermaid render error artifacts

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 <cursoragent@cursor.com>

Co-authored-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Cursor Agent
2026-06-02 12:12:48 +00:00
committed by Émile Ré
parent 6e11886365
commit dcb7ffa945
5 changed files with 101 additions and 31 deletions

View File

@@ -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<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [renderState, setRenderState] = useState<MermaidRenderState>({
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 (
<pre className="border border-border-solid rounded p-4 bg-transparent font-mono text-sm overflow-x-auto text-inherit">
<code>{chart}</code>

View File

@@ -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<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [renderState, setRenderState] = useState<MermaidRenderState>({
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 (
<div className="mermaid-error">
{error}
Unable to render diagram. Check the syntax and try again.
</div>
);
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// 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;
}
}