syntax checking and preview
This commit is contained in:
@@ -31,6 +31,7 @@ import { ConditionalInline } from "./extensions/ConditionalInline";
|
|||||||
import TextAlign from "@tiptap/extension-text-align";
|
import TextAlign from "@tiptap/extension-text-align";
|
||||||
import Superscript from "@tiptap/extension-superscript";
|
import Superscript from "@tiptap/extension-superscript";
|
||||||
import Subscript from "@tiptap/extension-subscript";
|
import Subscript from "@tiptap/extension-subscript";
|
||||||
|
import mermaid from "mermaid";
|
||||||
import css from "highlight.js/lib/languages/css";
|
import css from "highlight.js/lib/languages/css";
|
||||||
import js from "highlight.js/lib/languages/javascript";
|
import js from "highlight.js/lib/languages/javascript";
|
||||||
import ts from "highlight.js/lib/languages/typescript";
|
import ts from "highlight.js/lib/languages/typescript";
|
||||||
@@ -714,6 +715,25 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
let bubbleMenuRef!: HTMLDivElement;
|
let bubbleMenuRef!: HTMLDivElement;
|
||||||
let containerRef!: 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 [showBubbleMenu, setShowBubbleMenu] = createSignal(false);
|
||||||
const [bubbleMenuPosition, setBubbleMenuPosition] = createSignal({
|
const [bubbleMenuPosition, setBubbleMenuPosition] = createSignal({
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -746,6 +766,12 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
const [mermaidEditorPos, setMermaidEditorPos] = createSignal<number | null>(
|
const [mermaidEditorPos, setMermaidEditorPos] = createSignal<number | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const [mermaidValidation, setMermaidValidation] = createSignal<{
|
||||||
|
valid: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}>({ valid: true, error: null });
|
||||||
|
const [mermaidPreviewSvg, setMermaidPreviewSvg] = createSignal<string>("");
|
||||||
|
let mermaidValidationTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
// References section heading customization
|
// References section heading customization
|
||||||
const [referencesHeading, setReferencesHeading] = createSignal(
|
const [referencesHeading, setReferencesHeading] = createSignal(
|
||||||
@@ -1031,6 +1057,54 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
setShowMermaidTemplates(false);
|
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
|
// Capture history snapshot
|
||||||
const captureHistory = async (editorInstance: any) => {
|
const captureHistory = async (editorInstance: any) => {
|
||||||
// Skip if initial load
|
// Skip if initial load
|
||||||
@@ -4340,27 +4414,67 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
{/* Editor */}
|
{/* Editor */}
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-text mb-2 block text-sm font-semibold">
|
<div class="mb-2 flex items-center justify-between">
|
||||||
|
<label class="text-text text-sm font-semibold">
|
||||||
Diagram Code
|
Diagram Code
|
||||||
</label>
|
</label>
|
||||||
|
{/* Validation Status */}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Show when={mermaidValidation().valid}>
|
||||||
|
<span class="text-green flex items-center gap-1 text-xs font-semibold">
|
||||||
|
<span>✓</span> Valid syntax
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
!mermaidValidation().valid && mermaidValidation().error
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="text-red flex items-center gap-1 text-xs font-semibold">
|
||||||
|
<span>✗</span> Invalid syntax
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
class="bg-surface0 border-surface2 text-text focus:border-blue w-full rounded border p-3 font-mono text-sm focus:outline-none"
|
class={`bg-surface0 text-text w-full rounded border p-3 font-mono text-sm transition-colors focus:outline-none ${
|
||||||
rows={15}
|
mermaidValidation().valid
|
||||||
|
? "border-surface2 focus:border-blue"
|
||||||
|
: "border-red focus:border-red"
|
||||||
|
}`}
|
||||||
|
rows={12}
|
||||||
value={mermaidEditorContent()}
|
value={mermaidEditorContent()}
|
||||||
onInput={(e) =>
|
onInput={(e) =>
|
||||||
setMermaidEditorContent(e.currentTarget.value)
|
setMermaidEditorContent(e.currentTarget.value)
|
||||||
}
|
}
|
||||||
placeholder="Enter your mermaid diagram code..."
|
placeholder="Enter your mermaid diagram code..."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
<Show
|
||||||
|
when={!mermaidValidation().valid && mermaidValidation().error}
|
||||||
|
>
|
||||||
|
<div class="text-red border-red bg-red/10 mt-2 rounded-lg border p-2 text-xs">
|
||||||
|
<strong>Error:</strong> {mermaidValidation().error}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Preview Info */}
|
{/* Live Preview */}
|
||||||
|
<Show when={mermaidPreviewSvg()}>
|
||||||
|
<div>
|
||||||
|
<label class="text-text mb-2 block text-sm font-semibold">
|
||||||
|
Live Preview
|
||||||
|
</label>
|
||||||
|
<div class="bg-surface0 border-surface2 max-h-96 overflow-auto rounded border p-4">
|
||||||
|
<div innerHTML={mermaidPreviewSvg()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
<div class="text-subtext0 border-yellow bg-yellow/10 rounded-lg border p-3 text-sm">
|
<div class="text-subtext0 border-yellow bg-yellow/10 rounded-lg border p-3 text-sm">
|
||||||
<strong>Note:</strong> The diagram will render when you view the
|
<strong>Tip:</strong> Use ASCII hyphens for arrows (
|
||||||
post. Make sure your syntax is correct to avoid rendering
|
|
||||||
errors.
|
|
||||||
<br />
|
|
||||||
<strong>Common issue:</strong> Use ASCII hyphens for arrows (
|
|
||||||
<code>--></code> not <code>—></code>). Smart punctuation
|
<code>--></code> not <code>—></code>). Smart punctuation
|
||||||
can break diagrams.
|
can break diagrams.
|
||||||
</div>
|
</div>
|
||||||
@@ -4377,7 +4491,13 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={saveMermaidEdit}
|
onClick={saveMermaidEdit}
|
||||||
class="bg-blue rounded px-4 py-2 text-sm text-white transition-all hover:brightness-110 active:scale-95"
|
disabled={!mermaidValidation().valid}
|
||||||
|
class="bg-blue rounded px-4 py-2 text-sm text-white transition-all hover:brightness-110 active:scale-95 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
title={
|
||||||
|
!mermaidValidation().valid
|
||||||
|
? "Fix syntax errors before saving"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -154,6 +154,41 @@ export const Mermaid = Node.create({
|
|||||||
code.textContent = node.attrs.content || "";
|
code.textContent = node.attrs.content || "";
|
||||||
pre.appendChild(code);
|
pre.appendChild(code);
|
||||||
|
|
||||||
|
// Validation status indicator
|
||||||
|
const statusIndicator = document.createElement("div");
|
||||||
|
statusIndicator.className =
|
||||||
|
"absolute top-2 left-2 w-3 h-3 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-200";
|
||||||
|
|
||||||
|
// Validate syntax asynchronously
|
||||||
|
const validateSyntax = async () => {
|
||||||
|
const content = node.attrs.content || "";
|
||||||
|
if (!content.trim()) {
|
||||||
|
statusIndicator.className =
|
||||||
|
"absolute top-2 left-2 w-3 h-3 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-200";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Dynamic import to avoid bundling issues
|
||||||
|
const mermaid = (await import("mermaid")).default;
|
||||||
|
await mermaid.parse(content);
|
||||||
|
// Valid - green indicator
|
||||||
|
statusIndicator.className =
|
||||||
|
"absolute top-2 left-2 w-3 h-3 rounded-full bg-green opacity-0 group-hover:opacity-100 transition-opacity duration-200";
|
||||||
|
statusIndicator.title = "Valid mermaid syntax";
|
||||||
|
} catch (err) {
|
||||||
|
// Invalid - red indicator
|
||||||
|
statusIndicator.className =
|
||||||
|
"absolute top-2 left-2 w-3 h-3 rounded-full bg-red opacity-0 group-hover:opacity-100 transition-opacity duration-200";
|
||||||
|
statusIndicator.title = `Invalid syntax: ${
|
||||||
|
(err as any).message || "Parse error"
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run validation
|
||||||
|
validateSyntax();
|
||||||
|
|
||||||
// Edit button overlay
|
// Edit button overlay
|
||||||
const editBtn = document.createElement("button");
|
const editBtn = document.createElement("button");
|
||||||
editBtn.className =
|
editBtn.className =
|
||||||
@@ -174,6 +209,7 @@ export const Mermaid = Node.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
dom.appendChild(pre);
|
dom.appendChild(pre);
|
||||||
|
dom.appendChild(statusIndicator);
|
||||||
dom.appendChild(editBtn);
|
dom.appendChild(editBtn);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -184,6 +220,8 @@ export const Mermaid = Node.create({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
code.textContent = updatedNode.attrs.content || "";
|
code.textContent = updatedNode.attrs.content || "";
|
||||||
|
// Re-validate on update
|
||||||
|
validateSyntax();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user