/** * Markdown Toggle (Source View) * * Manages toggling between WYSIWYG (TipTap) and raw markdown editing modes. * Pure state management — no DOM manipulation. The caller (main.js) handles * the actual UI show/hide of editor vs textarea. * * Custom TipTap blocks (mermaid, math, page breaks, toggle blocks, footnotes, * suggestion marks) cannot survive a markdown roundtrip, so they are extracted * before conversion and restored afterward using placeholder markers. */ import type { MarkdownToggleOptions, MarkdownToggleApi } from './types.js'; export const TOGGLE_MODE = Object.freeze({ WYSIWYG: 'wysiwyg' as const, MARKDOWN: 'markdown' as const, }); export type ToggleMode = typeof TOGGLE_MODE[keyof typeof TOGGLE_MODE]; // --- Custom block preservation --- /** Regex patterns for custom TipTap blocks that don't survive markdown roundtrip */ const CUSTOM_BLOCK_PATTERNS = [ // Mermaid blocks /
tags by markdown-it const wrappedPattern = new RegExp( `
${escapeRegex(block.placeholder)}<\\/p>`, 'g' ); result = result.replace(wrappedPattern, block.html); // Also replace bare placeholders (not wrapped) result = result.replace(block.placeholder, block.html); } return result; } function escapeRegex(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Create a markdown toggle state manager. */ export function createMarkdownToggle(opts: MarkdownToggleOptions): MarkdownToggleApi { const { getEditorHtml, setEditorHtml, htmlToMarkdown, markdownToHtml, onModeChange } = opts; let mode: ToggleMode = TOGGLE_MODE.WYSIWYG; let markdownContent = ''; let preservedBlocks: PreservedBlock[] = []; function toggle(): void { if (mode === TOGGLE_MODE.WYSIWYG) { // Switching TO markdown mode: extract custom blocks, then convert const editorHtml = getEditorHtml(); const { cleanHtml, blocks } = extractCustomBlocks(editorHtml); preservedBlocks = blocks; markdownContent = htmlToMarkdown(cleanHtml); // Append preserved block placeholders as visible comments in markdown // so the user can see them and knows not to delete them for (const block of blocks) { markdownContent = markdownContent.replace( block.placeholder, `` ); } mode = TOGGLE_MODE.MARKDOWN; } else { // Switching BACK to WYSIWYG: restore placeholders, convert markdown, re-inject blocks let md = markdownContent; // Restore placeholder syntax from HTML comments for (const block of preservedBlocks) { md = md.replace( ``, block.placeholder ); } let html = markdownToHtml(md); html = restoreCustomBlocks(html, preservedBlocks); setEditorHtml(html); markdownContent = ''; preservedBlocks = []; mode = TOGGLE_MODE.WYSIWYG; } if (onModeChange) { onModeChange(mode); } } function getMode(): string { return mode; } function isMarkdownMode(): boolean { return mode === TOGGLE_MODE.MARKDOWN; } function getMarkdownContent(): string { return markdownContent; } function setMarkdownContent(content: string): void { if (mode === TOGGLE_MODE.MARKDOWN) { markdownContent = content; } // Ignored in WYSIWYG mode } return { toggle, getMode, isMarkdownMode, getMarkdownContent, setMarkdownContent, }; }