From ec3b36948ed2551fff6900affc8f4ff045af05a3 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Mon, 29 Dec 2025 16:14:03 -0500 Subject: [PATCH] syntax checking and preview --- src/components/blog/TextEditor.tsx | 144 ++++++++++++++++++++-- src/components/blog/extensions/Mermaid.ts | 38 ++++++ 2 files changed, 170 insertions(+), 12 deletions(-) diff --git a/src/components/blog/TextEditor.tsx b/src/components/blog/TextEditor.tsx index 2e811d8..ede4029 100644 --- a/src/components/blog/TextEditor.tsx +++ b/src/components/blog/TextEditor.tsx @@ -31,6 +31,7 @@ import { ConditionalInline } from "./extensions/ConditionalInline"; import TextAlign from "@tiptap/extension-text-align"; import Superscript from "@tiptap/extension-superscript"; import Subscript from "@tiptap/extension-subscript"; +import mermaid from "mermaid"; import css from "highlight.js/lib/languages/css"; import js from "highlight.js/lib/languages/javascript"; import ts from "highlight.js/lib/languages/typescript"; @@ -714,6 +715,25 @@ export default function TextEditor(props: TextEditorProps) { let bubbleMenuRef!: HTMLDivElement; let containerRef!: HTMLDivElement; + // Initialize mermaid for validation and preview + onMount(() => { + mermaid.initialize({ + startOnLoad: false, + theme: "dark", + securityLevel: "loose", + fontFamily: "monospace", + themeVariables: { + darkMode: true, + primaryColor: "#2c2f40", + primaryTextColor: "#b5c1f1", + primaryBorderColor: "#739df2", + lineColor: "#739df2", + secondaryColor: "#3e4255", + tertiaryColor: "#505469" + } + }); + }); + const [showBubbleMenu, setShowBubbleMenu] = createSignal(false); const [bubbleMenuPosition, setBubbleMenuPosition] = createSignal({ top: 0, @@ -746,6 +766,12 @@ export default function TextEditor(props: TextEditorProps) { const [mermaidEditorPos, setMermaidEditorPos] = createSignal( null ); + const [mermaidValidation, setMermaidValidation] = createSignal<{ + valid: boolean; + error: string | null; + }>({ valid: true, error: null }); + const [mermaidPreviewSvg, setMermaidPreviewSvg] = createSignal(""); + let mermaidValidationTimer: ReturnType | null = null; // References section heading customization const [referencesHeading, setReferencesHeading] = createSignal( @@ -1031,6 +1057,54 @@ export default function TextEditor(props: TextEditorProps) { setShowMermaidTemplates(false); }; + // Validate and preview mermaid syntax + const validateAndPreviewMermaid = async (code: string) => { + if (!code.trim()) { + setMermaidValidation({ valid: true, error: null }); + setMermaidPreviewSvg(""); + return; + } + + try { + // Validate syntax using mermaid's parse function + await mermaid.parse(code); + + // If valid, render preview + const id = `mermaid-preview-${Date.now()}`; + const { svg } = await mermaid.render(id, code); + + setMermaidValidation({ valid: true, error: null }); + setMermaidPreviewSvg(svg); + } catch (err: any) { + // Extract useful error message + let errorMsg = err.message || "Invalid syntax"; + + // Clean up mermaid error messages for better readability + if (errorMsg.includes("Parse error")) { + errorMsg = errorMsg.replace( + /^.*Parse error on line \d+:\s*/i, + "Parse error: " + ); + } + + setMermaidValidation({ valid: false, error: errorMsg }); + setMermaidPreviewSvg(""); + } + }; + + // Debounced validation when content changes + createEffect(() => { + const content = mermaidEditorContent(); + + if (mermaidValidationTimer) { + clearTimeout(mermaidValidationTimer); + } + + mermaidValidationTimer = setTimeout(() => { + validateAndPreviewMermaid(content); + }, 500); + }); + // Capture history snapshot const captureHistory = async (editorInstance: any) => { // Skip if initial load @@ -4340,27 +4414,67 @@ export default function TextEditor(props: TextEditorProps) { {/* Editor */}
- +
+ + {/* Validation Status */} +
+ + + Valid syntax + + + + + Invalid syntax + + +
+