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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
34
packages/ui/src/lib/mermaid.ts
Normal file
34
packages/ui/src/lib/mermaid.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user