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
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import mermaid from "mermaid";
|
|
||||||
import { useEffect, useId, useState } from "react";
|
import { useEffect, useId, useState } from "react";
|
||||||
|
|
||||||
import { mermaidRenderConfig } from "../../mermaidConfig";
|
import { mermaidRenderErrorToast, renderMermaidDiagram } from "../../lib/mermaid";
|
||||||
|
import { useToast } from "../Toasts/Toasts";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
chart: string;
|
chart: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MermaidRenderState = {
|
||||||
|
source: string;
|
||||||
|
svg: string | null;
|
||||||
|
hasError: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function MermaidDiagram({ chart }: Props) {
|
export function MermaidDiagram({ chart }: Props) {
|
||||||
const id = useId().replace(/:/g, "");
|
const id = useId().replace(/:/g, "");
|
||||||
const [svg, setSvg] = useState<string | null>(null);
|
const [renderState, setRenderState] = useState<MermaidRenderState>({
|
||||||
const [error, setError] = useState<string | null>(null);
|
source: "",
|
||||||
|
svg: null,
|
||||||
|
hasError: false,
|
||||||
|
});
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const source = (chart ?? "").trim();
|
const source = (chart ?? "").trim();
|
||||||
|
const svg = renderState.source === source ? renderState.svg : null;
|
||||||
|
const hasError = renderState.source === source && renderState.hasError;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!source) {
|
if (!source) {
|
||||||
@@ -41,28 +53,33 @@ export function MermaidDiagram({ chart }: Props) {
|
|||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
mermaid.initialize(mermaidRenderConfig);
|
renderMermaidDiagram(`mermaid-${id}`, source)
|
||||||
|
|
||||||
mermaid
|
|
||||||
.render(`mermaid-${id}`, source)
|
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setSvg(result.svg);
|
setRenderState({
|
||||||
setError(null);
|
source,
|
||||||
|
svg: result.svg,
|
||||||
|
hasError: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
.catch(() => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setRenderState({
|
||||||
|
source,
|
||||||
|
svg: null,
|
||||||
|
hasError: true,
|
||||||
|
});
|
||||||
|
toast(mermaidRenderErrorToast);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [source, id]);
|
}, [source, id, toast]);
|
||||||
|
|
||||||
if (error) {
|
if (hasError) {
|
||||||
return (
|
return (
|
||||||
<pre className="border border-border-solid rounded p-4 bg-transparent font-mono text-sm overflow-x-auto text-inherit">
|
<pre className="border border-border-solid rounded p-4 bg-transparent font-mono text-sm overflow-x-auto text-inherit">
|
||||||
<code>{chart}</code>
|
<code>{chart}</code>
|
||||||
|
|||||||
@@ -5,45 +5,64 @@
|
|||||||
import { CodeIcon, EyeIcon } from "@phosphor-icons/react";
|
import { CodeIcon, EyeIcon } from "@phosphor-icons/react";
|
||||||
import type { ReactNodeViewProps } from "@tiptap/react";
|
import type { ReactNodeViewProps } from "@tiptap/react";
|
||||||
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
||||||
import mermaid from "mermaid";
|
|
||||||
import { useEffect, useId, useState } from "react";
|
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 MermaidMode = "code" | "preview";
|
||||||
|
|
||||||
|
type MermaidRenderState = {
|
||||||
|
source: string;
|
||||||
|
svg: string | null;
|
||||||
|
hasError: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
function MermaidPreview({ chart }: { chart: string }) {
|
function MermaidPreview({ chart }: { chart: string }) {
|
||||||
const id = useId().replace(/:/g, "");
|
const id = useId().replace(/:/g, "");
|
||||||
const [svg, setSvg] = useState<string | null>(null);
|
const [renderState, setRenderState] = useState<MermaidRenderState>({
|
||||||
const [error, setError] = useState<string | null>(null);
|
source: "",
|
||||||
|
svg: null,
|
||||||
|
hasError: false,
|
||||||
|
});
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const source = chart.trim();
|
const source = chart.trim();
|
||||||
|
const svg = renderState.source === source ? renderState.svg : null;
|
||||||
|
const hasError = renderState.source === source && renderState.hasError;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (source.length === 0) return;
|
if (source.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
mermaid.initialize(mermaidRenderConfig);
|
renderMermaidDiagram(`mermaid-editor-${id}`, source)
|
||||||
|
|
||||||
mermaid
|
|
||||||
.render(`mermaid-editor-${id}`, source)
|
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setSvg(result.svg);
|
setRenderState({
|
||||||
setError(null);
|
source,
|
||||||
|
svg: result.svg,
|
||||||
|
hasError: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
.catch(() => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setRenderState({
|
||||||
|
source,
|
||||||
|
svg: null,
|
||||||
|
hasError: true,
|
||||||
|
});
|
||||||
|
toast(mermaidRenderErrorToast);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [source, id]);
|
}, [source, id, toast]);
|
||||||
|
|
||||||
if (source.length === 0) {
|
if (source.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -53,10 +72,10 @@ function MermaidPreview({ chart }: { chart: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="mermaid-error">
|
<div className="mermaid-error">
|
||||||
{error}
|
Unable to render diagram. Check the syntax and try again.
|
||||||
</div>
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -374,7 +374,7 @@
|
|||||||
<script>{{.MermaidJS}}</script>
|
<script>{{.MermaidJS}}</script>
|
||||||
<script>
|
<script>
|
||||||
window.__mermaidReady = false;
|
window.__mermaidReady = false;
|
||||||
mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
|
mermaid.initialize({ startOnLoad: false, theme: 'neutral', suppressErrorRendering: true });
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
mermaid.run().then(function () {
|
mermaid.run().then(function () {
|
||||||
window.__mermaidReady = true;
|
window.__mermaidReady = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user