Skip to content

Commit 230a923

Browse files
committed
Handle Mermaid validation errors
1 parent f813ed8 commit 230a923

File tree

17 files changed

+205
-10
lines changed

17 files changed

+205
-10
lines changed

webview-ui/src/components/common/MermaidBlock.tsx

Lines changed: 125 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import mermaid from "mermaid"
33
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
44
import styled from "styled-components"
55
import { vscode } from "@src/utils/vscode"
6+
import { useAppTranslation } from "@src/i18n/TranslationContext"
67

78
const MERMAID_THEME = {
89
background: "#1e1e1e", // VS Code dark theme background
@@ -81,10 +82,15 @@ interface MermaidBlockProps {
8182
export default function MermaidBlock({ code }: MermaidBlockProps) {
8283
const containerRef = useRef<HTMLDivElement>(null)
8384
const [isLoading, setIsLoading] = useState(false)
85+
const [error, setError] = useState<string | null>(null)
86+
const [isErrorExpanded, setIsErrorExpanded] = useState(false)
87+
const [showCopySuccess, setShowCopySuccess] = useState(false)
88+
const { t } = useAppTranslation()
8489

8590
// 1) Whenever `code` changes, mark that we need to re-render a new chart
8691
useEffect(() => {
8792
setIsLoading(true)
93+
setError(null)
8894
}, [code])
8995

9096
// 2) Debounce the actual parse/render
@@ -93,12 +99,10 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
9399
if (containerRef.current) {
94100
containerRef.current.innerHTML = ""
95101
}
102+
96103
mermaid
97-
.parse(code, { suppressErrors: true })
98-
.then((isValid) => {
99-
if (!isValid) {
100-
throw new Error("Invalid or incomplete Mermaid code")
101-
}
104+
.parse(code)
105+
.then(() => {
102106
const id = `mermaid-${Math.random().toString(36).substring(2)}`
103107
return mermaid.render(id, code)
104108
})
@@ -109,7 +113,7 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
109113
})
110114
.catch((err) => {
111115
console.warn("Mermaid parse/render failed:", err)
112-
containerRef.current!.innerHTML = code.replace(/</g, "&lt;").replace(/>/g, "&gt;")
116+
setError(err.message || "Failed to render Mermaid diagram")
113117
})
114118
.finally(() => {
115119
setIsLoading(false)
@@ -139,12 +143,90 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
139143
}
140144
}
141145

146+
/**
147+
* Copy the mermaid code to clipboard for easier fixing
148+
*/
149+
const handleCopyCode = () => {
150+
navigator.clipboard
151+
.writeText(code)
152+
.then(() => {
153+
setShowCopySuccess(true)
154+
setTimeout(() => {
155+
setShowCopySuccess(false)
156+
}, 1000)
157+
})
158+
.catch((err) => {
159+
console.error("Failed to copy code:", err)
160+
})
161+
}
162+
142163
return (
143164
<MermaidBlockContainer>
144-
{isLoading && <LoadingMessage>Generating mermaid diagram...</LoadingMessage>}
145-
146-
{/* The container for the final <svg> or raw code. */}
147-
<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
165+
{isLoading && <LoadingMessage>{t("common:mermaid.loading")}</LoadingMessage>}
166+
167+
{error ? (
168+
<div style={{ marginTop: "0px", overflow: "hidden", marginBottom: "8px" }}>
169+
<div
170+
style={{
171+
borderBottom: isErrorExpanded ? "1px solid var(--vscode-editorGroup-border)" : "none",
172+
fontWeight: "normal",
173+
fontSize: "var(--vscode-font-size)",
174+
color: "var(--vscode-editor-foreground)",
175+
display: "flex",
176+
alignItems: "center",
177+
justifyContent: "space-between",
178+
cursor: "pointer",
179+
}}
180+
onClick={() => setIsErrorExpanded(!isErrorExpanded)}>
181+
<div
182+
style={{
183+
display: "flex",
184+
alignItems: "center",
185+
gap: "10px",
186+
flexGrow: 1,
187+
}}>
188+
<span
189+
className="codicon codicon-warning"
190+
style={{
191+
color: "var(--vscode-editorWarning-foreground)",
192+
opacity: 0.8,
193+
fontSize: 16,
194+
marginBottom: "-1.5px",
195+
}}></span>
196+
<span style={{ fontWeight: "bold" }}>{t("common:mermaid.render_error")}</span>
197+
</div>
198+
<div style={{ display: "flex", alignItems: "center" }}>
199+
<CopyButton
200+
onClick={(e) => {
201+
e.stopPropagation()
202+
handleCopyCode()
203+
}}>
204+
<span
205+
className={`codicon codicon-${showCopySuccess ? "check" : "copy"}`}
206+
title={showCopySuccess ? t("common:mermaid.copy_success") : undefined}></span>
207+
</CopyButton>
208+
<span className={`codicon codicon-chevron-${isErrorExpanded ? "up" : "down"}`}></span>
209+
</div>
210+
</div>
211+
{isErrorExpanded && (
212+
<div
213+
style={{
214+
padding: "8px",
215+
backgroundColor: "var(--vscode-editor-background)",
216+
borderTop: "none",
217+
}}>
218+
<div style={{ marginBottom: "8px", color: "var(--vscode-descriptionForeground)" }}>
219+
{error}
220+
</div>
221+
<CodeBlock>
222+
<code>{code}</code>
223+
</CodeBlock>
224+
</div>
225+
)}
226+
</div>
227+
) : (
228+
<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
229+
)}
148230
</MermaidBlockContainer>
149231
)
150232
}
@@ -212,6 +294,39 @@ const LoadingMessage = styled.div`
212294
font-size: 0.9em;
213295
`
214296

297+
const CodeBlock = styled.pre`
298+
background-color: var(--vscode-editor-background);
299+
border: 1px solid var(--vscode-editor-lineHighlightBorder);
300+
border-radius: 3px;
301+
padding: 8px;
302+
overflow: auto;
303+
max-height: 200px;
304+
305+
code {
306+
font-family: var(--vscode-editor-font-family);
307+
font-size: var(--vscode-editor-font-size);
308+
white-space: pre-wrap;
309+
word-break: break-all;
310+
}
311+
`
312+
313+
const CopyButton = styled.button`
314+
padding: 3px;
315+
height: 24px;
316+
margin-right: 4px;
317+
color: var(--vscode-editor-foreground);
318+
display: flex;
319+
align-items: center;
320+
justify-content: center;
321+
background: transparent;
322+
border: none;
323+
cursor: pointer;
324+
325+
&:hover {
326+
opacity: 0.8;
327+
}
328+
`
329+
215330
interface SvgContainerProps {
216331
$isLoading: boolean
217332
}

webview-ui/src/i18n/locales/ca/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
},
77
"ui": {
88
"search_placeholder": "Cerca..."
9+
},
10+
"mermaid": {
11+
"loading": "Generant diagrama mermaid...",
12+
"render_error": "No es pot renderitzar el diagrama",
13+
"copy_success": "Codi copiat"
914
}
1015
}

webview-ui/src/i18n/locales/de/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
},
77
"ui": {
88
"search_placeholder": "Suchen..."
9+
},
10+
"mermaid": {
11+
"loading": "Mermaid-Diagramm wird generiert...",
12+
"render_error": "Diagramm kann nicht gerendert werden",
13+
"copy_success": "Code kopiert"
914
}
1015
}

webview-ui/src/i18n/locales/en/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
},
77
"ui": {
88
"search_placeholder": "Search..."
9+
},
10+
"mermaid": {
11+
"loading": "Generating mermaid diagram...",
12+
"render_error": "Unable to Render Diagram",
13+
"copy_success": "Code copied"
914
}
1015
}

webview-ui/src/i18n/locales/es/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
},
77
"ui": {
88
"search_placeholder": "Buscar..."
9+
},
10+
"mermaid": {
11+
"loading": "Generando diagrama mermaid...",
12+
"render_error": "No se puede renderizar el diagrama",
13+
"copy_success": "Código copiado"
914
}
1015
}

webview-ui/src/i18n/locales/fr/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
},
77
"ui": {
88
"search_placeholder": "Rechercher..."
9+
},
10+
"mermaid": {
11+
"loading": "Génération du diagramme mermaid...",
12+
"render_error": "Impossible de rendre le diagramme",
13+
"copy_success": "Code copié"
914
}
1015
}

webview-ui/src/i18n/locales/hi/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
},
77
"ui": {
88
"search_placeholder": "खोजें..."
9+
},
10+
"mermaid": {
11+
"loading": "मरमेड डायग्राम जनरेट हो रहा है...",
12+
"render_error": "डायग्राम रेंडर नहीं किया जा सकता",
13+
"copy_success": "कोड कॉपी किया गया"
914
}
1015
}

webview-ui/src/i18n/locales/it/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
},
77
"ui": {
88
"search_placeholder": "Cerca..."
9+
},
10+
"mermaid": {
11+
"loading": "Generazione del diagramma mermaid...",
12+
"render_error": "Impossibile renderizzare il diagramma",
13+
"copy_success": "Codice copiato"
914
}
1015
}

webview-ui/src/i18n/locales/ja/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
},
77
"ui": {
88
"search_placeholder": "検索..."
9+
},
10+
"mermaid": {
11+
"loading": "Mermaidダイアグラムを生成中...",
12+
"render_error": "ダイアグラムをレンダリングできません",
13+
"copy_success": "コードをコピーしました"
914
}
1015
}

webview-ui/src/i18n/locales/ko/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,10 @@
66
},
77
"ui": {
88
"search_placeholder": "검색..."
9+
},
10+
"mermaid": {
11+
"loading": "머메이드 다이어그램 생성 중...",
12+
"render_error": "다이어그램을 렌더링할 수 없음",
13+
"copy_success": "코드가 복사됨"
914
}
1015
}

0 commit comments

Comments
 (0)