From 326ebd2f8a085cf8b47e3d34dd9f9464505afdde Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Mon, 29 Dec 2025 15:28:05 -0500 Subject: [PATCH] fix mermaid hydration --- src/components/blog/TextEditor.tsx | 211 +++++++++++++++++++++- src/components/blog/extensions/Mermaid.ts | 150 ++++++++++++++- 2 files changed, 351 insertions(+), 10 deletions(-) diff --git a/src/components/blog/TextEditor.tsx b/src/components/blog/TextEditor.tsx index 745ee26..2e811d8 100644 --- a/src/components/blog/TextEditor.tsx +++ b/src/components/blog/TextEditor.tsx @@ -1,4 +1,12 @@ -import { Show, untrack, createEffect, on, createSignal, For } from "solid-js"; +import { + Show, + untrack, + createEffect, + on, + createSignal, + For, + onMount +} from "solid-js"; import { useSearchParams, useNavigate } from "@solidjs/router"; import { api } from "~/lib/api"; import { createTiptapEditor } from "solid-tiptap"; @@ -732,6 +740,13 @@ export default function TextEditor(props: TextEditorProps) { const [showKeyboardHelp, setShowKeyboardHelp] = createSignal(false); + // Mermaid editor modal state + const [showMermaidEditor, setShowMermaidEditor] = createSignal(false); + const [mermaidEditorContent, setMermaidEditorContent] = createSignal(""); + const [mermaidEditorPos, setMermaidEditorPos] = createSignal( + null + ); + // References section heading customization const [referencesHeading, setReferencesHeading] = createSignal( typeof window !== "undefined" @@ -764,14 +779,20 @@ export default function TextEditor(props: TextEditorProps) { inline: false }); - // Search params and navigation for fullscreen persistence const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); - // Initialize fullscreen from URL search param const [isFullscreen, setIsFullscreen] = createSignal( searchParams.fullscreen === "true" ); + onMount(() => { + if (isFullscreen()) { + const navigationElement = document.getElementById("navigation"); + if (navigationElement) { + navigationElement.classList.add("hidden"); + } + } + }); const [keyboardVisible, setKeyboardVisible] = createSignal(false); const [keyboardHeight, setKeyboardHeight] = createSignal(0); @@ -977,6 +998,39 @@ export default function TextEditor(props: TextEditorProps) { setCurrentSuggestion(""); }; + // Mermaid editor helpers + const saveMermaidEdit = () => { + const instance = editor(); + const pos = mermaidEditorPos(); + const content = mermaidEditorContent(); + + if (!instance || pos === null) return; + + // Update the node at the stored position + const tr = instance.state.tr; + const node = instance.state.doc.nodeAt(pos); + + if (node && node.type.name === "mermaid") { + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + content + }); + instance.view.dispatch(tr); + } + + setShowMermaidEditor(false); + setMermaidEditorContent(""); + setMermaidEditorPos(null); + }; + + const insertMermaidFromTemplate = (templateCode: string) => { + const instance = editor(); + if (!instance) return; + + instance.commands.setMermaid(templateCode); + setShowMermaidTemplates(false); + }; + // Capture history snapshot const captureHistory = async (editorInstance: any) => { // Skip if initial load @@ -1445,6 +1499,13 @@ export default function TextEditor(props: TextEditorProps) { setTimeout(() => { isInitialLoad = false; }, 1000); + + // Listen for mermaid edit events + editor.view.dom.addEventListener("edit-mermaid", ((e: CustomEvent) => { + setMermaidEditorContent(e.detail.content); + setMermaidEditorPos(e.detail.pos); + setShowMermaidEditor(true); + }) as EventListener); }, editorProps: { attributes: { @@ -1676,7 +1737,10 @@ export default function TextEditor(props: TextEditorProps) { } // Migrate legacy superscript references to Reference marks - setTimeout(() => migrateLegacyReferences(instance), 50); + setTimeout(() => { + migrateLegacyReferences(instance); + migrateLegacyMermaidBlocks(instance); + }, 50); // Capture initial state in history only if no history was loaded setTimeout(() => { @@ -1724,6 +1788,73 @@ export default function TextEditor(props: TextEditorProps) { } }); + const migrateLegacyMermaidBlocks = (editorInstance: any) => { + if (!editorInstance) return; + + const doc = editorInstance.state.doc; + const blocksToMigrate: Array<{ pos: number; content: string }> = []; + + // Mermaid diagram keywords to detect + const mermaidKeywords = [ + "graph ", + "sequenceDiagram", + "classDiagram", + "stateDiagram", + "erDiagram", + "gantt", + "pie ", + "journey", + "gitGraph", + "flowchart ", + "mindmap", + "timeline", + "quadrantChart", + "requirementDiagram", + "C4Context" + ]; + + // Find code blocks that look like mermaid + doc.descendants((node: any, pos: number) => { + if (node.type.name === "codeBlock") { + const content = node.textContent || ""; + const trimmedContent = content.trim(); + + // Check if this looks like a mermaid diagram + const isMermaid = mermaidKeywords.some((keyword) => + trimmedContent.startsWith(keyword) + ); + + if (isMermaid) { + blocksToMigrate.push({ pos, content: trimmedContent }); + } + } + }); + + if (blocksToMigrate.length === 0) { + return; + } + + // Migrate from end to start to avoid position shifts + blocksToMigrate.sort((a, b) => b.pos - a.pos); + + const tr = editorInstance.state.tr; + + blocksToMigrate.forEach(({ pos, content }) => { + const node = editorInstance.state.doc.nodeAt(pos); + if (node) { + // Create new mermaid node + const mermaidNode = editorInstance.schema.nodes.mermaid.create({ + content + }); + + // Replace the code block with mermaid node + tr.replaceWith(pos, pos + node.nodeSize, mermaidNode); + } + }); + + editorInstance.view.dispatch(tr); + }; + const migrateLegacyReferences = (editorInstance: any) => { if (!editorInstance) return; @@ -4183,6 +4314,78 @@ export default function TextEditor(props: TextEditorProps) { + + {/* Mermaid Editor Modal */} + +
setShowMermaidEditor(false)} + > +
e.stopPropagation()} + > + {/* Header */} +
+

Edit Mermaid Diagram

+ +
+ + {/* Editor */} +
+
+ +