// ── Markdown rendering ── import { esc } from './util.js'; import { state } from './state.js'; marked.use({ breaks: true, gfm: true }); const purifyConfig = { ALLOWED_TAGS: ['h1','h2','h3','h4','h5','h6','p','br','hr','ul','ol','li','a','strong','em','del','code','pre','blockquote','table','thead','tbody','tr','th','td','img','input','span','div','audio','source'], ALLOWED_ATTR: ['href','src','alt','width','height','type','checked','disabled','class','style','data-card-id','title','controls','preload'], ALLOW_DATA_ATTR: true, }; function normalizeListBlock(block) { const lines = String(block || '').replace(/\r\n?/g, '\n').split('\n'); while (lines.length && !lines[0].trim()) lines.shift(); while (lines.length && !lines[lines.length - 1].trim()) lines.pop(); if (!lines.length) return ''; const minIndent = lines .filter(l => l.trim()) .map(l => (l.match(/^[ \t]*/) || [''])[0].replace(/\t/g, ' ').length) .reduce((min, n) => Math.min(min, n), Infinity); return lines .map(l => { const expanded = l.replace(/\t/g, ' '); if (!expanded.trim()) return ''; return expanded.slice(Math.min(minIndent, expanded.length)); }) .join('\n'); } export function renderMd(src) { let processed = String(src || '').replace(/\r\n?/g, '\n') // Custom fenced list blocks: ```tree / ```bullets with or without closing ``` .replace(/```(tree|bullets)\s*\n([\s\S]*?)(?:```|$)/g, (_, kind, content) => { const normalized = normalizeListBlock(content); return `\n\n${normalized}\n`; }) // Help parser: ensure a list can start after text without requiring manual blank line .replace(/([^\n])\n(?=(?: {0,3}[-*+]\s| {0,3}\d+\.\s))/g, '$1\n\n') // Preserve extra blank lines as
spacers (3+ newlines = 1+ extra blank lines) .replace(/\n{3,}/g, m => '\n\n' + '
\n'.repeat(m.length - 2)) // Ensure hr rules have one clean blank line before and after, keep extra
beyond that .replace(/
\n(---+|___+|\*\*\*+)\n/gm, '\n\n$1\n') .replace(/(---+|___+|\*\*\*+)\n
/gm, '$1\n\n'); const linked = processed.replace(/\[\[([^\]]+)\]\]/g, (_, title) => { const card = state.cards.find(c => c.title.toLowerCase() === title.toLowerCase()); if (card) return `${esc(title)}`; return `${esc(title)}`; }); let html = marked.parse(linked); // Convert list hints before sanitize (DOMPurify strips comments) html = html.replace(/\s*