Handle Mermaid validation errors (#3112)

* Handle Mermaid validation errors

* PR feedback

* More PR feedback
This commit is contained in:
Matt Rubens 2025-05-02 01:23:44 -04:00 committed by GitHub
parent f813ed850b
commit da9b857376
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 155 additions and 9 deletions

View file

@ -3,6 +3,9 @@ import mermaid from "mermaid"
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
import styled from "styled-components"
import { vscode } from "@src/utils/vscode"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useCopyToClipboard } from "@src/utils/clipboard"
import CodeBlock from "./CodeBlock"
const MERMAID_THEME = {
background: "#1e1e1e", // VS Code dark theme background
@ -81,10 +84,15 @@ interface MermaidBlockProps {
export default function MermaidBlock({ code }: MermaidBlockProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [isErrorExpanded, setIsErrorExpanded] = useState(false)
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
const { t } = useAppTranslation()
// 1) Whenever `code` changes, mark that we need to re-render a new chart
useEffect(() => {
setIsLoading(true)
setError(null)
}, [code])
// 2) Debounce the actual parse/render
@ -93,12 +101,10 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
if (containerRef.current) {
containerRef.current.innerHTML = ""
}
mermaid
.parse(code, { suppressErrors: true })
.then((isValid) => {
if (!isValid) {
throw new Error("Invalid or incomplete Mermaid code")
}
.parse(code)
.then(() => {
const id = `mermaid-${Math.random().toString(36).substring(2)}`
return mermaid.render(id, code)
})
@ -109,7 +115,7 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
})
.catch((err) => {
console.warn("Mermaid parse/render failed:", err)
containerRef.current!.innerHTML = code.replace(/</g, "&lt;").replace(/>/g, "&gt;")
setError(err.message || "Failed to render Mermaid diagram")
})
.finally(() => {
setIsLoading(false)
@ -139,12 +145,71 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
}
}
// Copy functionality handled directly through the copyWithFeedback utility
return (
<MermaidBlockContainer>
{isLoading && <LoadingMessage>Generating mermaid diagram...</LoadingMessage>}
{isLoading && <LoadingMessage>{t("common:mermaid.loading")}</LoadingMessage>}
{/* The container for the final <svg> or raw code. */}
<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
{error ? (
<div style={{ marginTop: "0px", overflow: "hidden", marginBottom: "8px" }}>
<div
style={{
borderBottom: isErrorExpanded ? "1px solid var(--vscode-editorGroup-border)" : "none",
fontWeight: "normal",
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-editor-foreground)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
cursor: "pointer",
}}
onClick={() => setIsErrorExpanded(!isErrorExpanded)}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "10px",
flexGrow: 1,
}}>
<span
className="codicon codicon-warning"
style={{
color: "var(--vscode-editorWarning-foreground)",
opacity: 0.8,
fontSize: 16,
marginBottom: "-1.5px",
}}></span>
<span style={{ fontWeight: "bold" }}>{t("common:mermaid.render_error")}</span>
</div>
<div style={{ display: "flex", alignItems: "center" }}>
<CopyButton
onClick={(e) => {
e.stopPropagation()
copyWithFeedback(code, e)
}}>
<span className={`codicon codicon-${showCopyFeedback ? "check" : "copy"}`}></span>
</CopyButton>
<span className={`codicon codicon-chevron-${isErrorExpanded ? "up" : "down"}`}></span>
</div>
</div>
{isErrorExpanded && (
<div
style={{
padding: "8px",
backgroundColor: "var(--vscode-editor-background)",
borderTop: "none",
}}>
<div style={{ marginBottom: "8px", color: "var(--vscode-descriptionForeground)" }}>
{error}
</div>
<CodeBlock language="mermaid" source={code} />
</div>
)}
</div>
) : (
<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
)}
</MermaidBlockContainer>
)
}
@ -212,6 +277,23 @@ const LoadingMessage = styled.div`
font-size: 0.9em;
`
const CopyButton = styled.button`
padding: 3px;
height: 24px;
margin-right: 4px;
color: var(--vscode-editor-foreground);
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
cursor: pointer;
&:hover {
opacity: 0.8;
}
`
interface SvgContainerProps {
$isLoading: boolean
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "Cerca..."
},
"mermaid": {
"loading": "Generant diagrama mermaid...",
"render_error": "No es pot renderitzar el diagrama"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "Suchen..."
},
"mermaid": {
"loading": "Mermaid-Diagramm wird generiert...",
"render_error": "Diagramm kann nicht gerendert werden"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "Search..."
},
"mermaid": {
"loading": "Generating mermaid diagram...",
"render_error": "Unable to Render Diagram"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "Buscar..."
},
"mermaid": {
"loading": "Generando diagrama mermaid...",
"render_error": "No se puede renderizar el diagrama"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "Rechercher..."
},
"mermaid": {
"loading": "Génération du diagramme mermaid...",
"render_error": "Impossible de rendre le diagramme"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "खोजें..."
},
"mermaid": {
"loading": "मरमेड डायग्राम जनरेट हो रहा है...",
"render_error": "डायग्राम रेंडर नहीं किया जा सकता"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "Cerca..."
},
"mermaid": {
"loading": "Generazione del diagramma mermaid...",
"render_error": "Impossibile renderizzare il diagramma"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "検索..."
},
"mermaid": {
"loading": "Mermaidダイアグラムを生成中...",
"render_error": "ダイアグラムをレンダリングできません"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "검색..."
},
"mermaid": {
"loading": "머메이드 다이어그램 생성 중...",
"render_error": "다이어그램을 렌더링할 수 없음"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "Szukaj..."
},
"mermaid": {
"loading": "Generowanie diagramu mermaid...",
"render_error": "Nie można renderować diagramu"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "Pesquisar..."
},
"mermaid": {
"loading": "Gerando diagrama mermaid...",
"render_error": "Não foi possível renderizar o diagrama"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "Поиск..."
},
"mermaid": {
"loading": "Создание диаграммы mermaid...",
"render_error": "Не удалось отобразить диаграмму"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "Ara..."
},
"mermaid": {
"loading": "Mermaid diyagramı oluşturuluyor...",
"render_error": "Diyagram render edilemiyor"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "Tìm kiếm..."
},
"mermaid": {
"loading": "Đang tạo biểu đồ mermaid...",
"render_error": "Không thể hiển thị biểu đồ"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "搜索..."
},
"mermaid": {
"loading": "生成 Mermaid 图表中...",
"render_error": "无法渲染图表"
}
}

View file

@ -6,5 +6,9 @@
},
"ui": {
"search_placeholder": "搜尋..."
},
"mermaid": {
"loading": "產生 Mermaid 圖表中...",
"render_error": "無法渲染圖表"
}
}