Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 163 lines 5.1 kB view raw
1/** 2 * Markdown Toggle (Source View) 3 * 4 * Manages toggling between WYSIWYG (TipTap) and raw markdown editing modes. 5 * Pure state management — no DOM manipulation. The caller (main.js) handles 6 * the actual UI show/hide of editor vs textarea. 7 * 8 * Custom TipTap blocks (mermaid, math, page breaks, toggle blocks, footnotes, 9 * suggestion marks) cannot survive a markdown roundtrip, so they are extracted 10 * before conversion and restored afterward using placeholder markers. 11 */ 12import type { MarkdownToggleOptions, MarkdownToggleApi } from './types.js'; 13 14export const TOGGLE_MODE = Object.freeze({ 15 WYSIWYG: 'wysiwyg' as const, 16 MARKDOWN: 'markdown' as const, 17}); 18 19export type ToggleMode = typeof TOGGLE_MODE[keyof typeof TOGGLE_MODE]; 20 21// --- Custom block preservation --- 22 23/** Regex patterns for custom TipTap blocks that don't survive markdown roundtrip */ 24const CUSTOM_BLOCK_PATTERNS = [ 25 // Mermaid blocks 26 /<div[^>]*data-mermaid-block[^>]*>[\s\S]*?<\/div>/gi, 27 // Math blocks 28 /<div[^>]*data-math-block[^>]*>[\s\S]*?<\/div>/gi, 29 // Page breaks 30 /<div[^>]*data-type="page-break"[^>]*>[\s\S]*?<\/div>/gi, 31 // Toggle blocks (details/summary) 32 /<details[^>]*>[\s\S]*?<\/details>/gi, 33 // Footnote markers (inline) 34 /<sup[^>]*data-footnote-id[^>]*>[^<]*<\/sup>/gi, 35 // Suggestion insert marks 36 /<span[^>]*data-suggestion-type="insert"[^>]*>[\s\S]*?<\/span>/gi, 37 /<span[^>]*class="suggestion-insert"[^>]*>[\s\S]*?<\/span>/gi, 38 // Suggestion delete marks 39 /<span[^>]*data-suggestion-type="delete"[^>]*>[\s\S]*?<\/span>/gi, 40 /<span[^>]*class="suggestion-delete"[^>]*>[\s\S]*?<\/span>/gi, 41]; 42 43interface PreservedBlock { 44 placeholder: string; 45 html: string; 46} 47 48/** 49 * Extract custom blocks from HTML, replacing them with unique placeholders. 50 */ 51export function extractCustomBlocks(html: string): { cleanHtml: string; blocks: PreservedBlock[] } { 52 const blocks: PreservedBlock[] = []; 53 let cleanHtml = html; 54 55 for (const pattern of CUSTOM_BLOCK_PATTERNS) { 56 cleanHtml = cleanHtml.replace(pattern, (match) => { 57 const placeholder = `\u00AB\u00ABBLOCK_${blocks.length}\u00BB\u00BB`; 58 blocks.push({ placeholder, html: match }); 59 return placeholder; 60 }); 61 } 62 63 return { cleanHtml, blocks }; 64} 65 66/** 67 * Restore custom blocks from markdown text by replacing placeholders with original HTML. 68 */ 69export function restoreCustomBlocks(html: string, blocks: PreservedBlock[]): string { 70 let result = html; 71 for (const block of blocks) { 72 // The placeholder may be wrapped in <p> tags by markdown-it 73 const wrappedPattern = new RegExp( 74 `<p>${escapeRegex(block.placeholder)}<\\/p>`, 75 'g' 76 ); 77 result = result.replace(wrappedPattern, block.html); 78 // Also replace bare placeholders (not wrapped) 79 result = result.replace(block.placeholder, block.html); 80 } 81 return result; 82} 83 84function escapeRegex(s: string): string { 85 return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 86} 87 88/** 89 * Create a markdown toggle state manager. 90 */ 91export function createMarkdownToggle(opts: MarkdownToggleOptions): MarkdownToggleApi { 92 const { getEditorHtml, setEditorHtml, htmlToMarkdown, markdownToHtml, onModeChange } = opts; 93 94 let mode: ToggleMode = TOGGLE_MODE.WYSIWYG; 95 let markdownContent = ''; 96 let preservedBlocks: PreservedBlock[] = []; 97 98 function toggle(): void { 99 if (mode === TOGGLE_MODE.WYSIWYG) { 100 // Switching TO markdown mode: extract custom blocks, then convert 101 const editorHtml = getEditorHtml(); 102 const { cleanHtml, blocks } = extractCustomBlocks(editorHtml); 103 preservedBlocks = blocks; 104 markdownContent = htmlToMarkdown(cleanHtml); 105 // Append preserved block placeholders as visible comments in markdown 106 // so the user can see them and knows not to delete them 107 for (const block of blocks) { 108 markdownContent = markdownContent.replace( 109 block.placeholder, 110 `<!-- preserved: ${block.placeholder} -->` 111 ); 112 } 113 mode = TOGGLE_MODE.MARKDOWN; 114 } else { 115 // Switching BACK to WYSIWYG: restore placeholders, convert markdown, re-inject blocks 116 let md = markdownContent; 117 // Restore placeholder syntax from HTML comments 118 for (const block of preservedBlocks) { 119 md = md.replace( 120 `<!-- preserved: ${block.placeholder} -->`, 121 block.placeholder 122 ); 123 } 124 let html = markdownToHtml(md); 125 html = restoreCustomBlocks(html, preservedBlocks); 126 setEditorHtml(html); 127 markdownContent = ''; 128 preservedBlocks = []; 129 mode = TOGGLE_MODE.WYSIWYG; 130 } 131 132 if (onModeChange) { 133 onModeChange(mode); 134 } 135 } 136 137 function getMode(): string { 138 return mode; 139 } 140 141 function isMarkdownMode(): boolean { 142 return mode === TOGGLE_MODE.MARKDOWN; 143 } 144 145 function getMarkdownContent(): string { 146 return markdownContent; 147 } 148 149 function setMarkdownContent(content: string): void { 150 if (mode === TOGGLE_MODE.MARKDOWN) { 151 markdownContent = content; 152 } 153 // Ignored in WYSIWYG mode 154 } 155 156 return { 157 toggle, 158 getMode, 159 isMarkdownMode, 160 getMarkdownContent, 161 setMarkdownContent, 162 }; 163}