';
})
// 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)
// Insert BETWEEN paragraphs but not before block-level elements
.replace(/\n{3,}/g, m => {
const extra = m.length - 2;
return '\n\n' + ' \n\n'.repeat(extra > 3 ? 3 : extra);
})
// Ensure hr rules have clean blank lines
.replace(/ \n\n(---+|___+|\*\*\*+)/gm, '\n\n$1')
.replace(/(---+|___+|\*\*\*+)\n\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*
/g, '
');
html = html.replace(/\s*
/g, '
');
html = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(html, purifyConfig) : html;
return html;
}
export function addCodeCopyButtons(container) {
if (!container) return;
container.querySelectorAll('pre > code').forEach(code => {
const pre = code.parentElement;
if (pre.querySelector('.code-copy')) return;
const btn = document.createElement('button');
btn.className = 'code-copy';
btn.type = 'button';
btn.innerHTML = '';
btn.addEventListener('click', async () => {
await navigator.clipboard.writeText(code.textContent);
btn.classList.add('copied');
btn.innerHTML = '';
setTimeout(() => { btn.classList.remove('copied'); btn.innerHTML = ''; }, 1500);
});
pre.appendChild(btn);
});
}
export function formatTodoItems(container) {
if (!container) return;
container.querySelectorAll('li').forEach(li => {
const cb = li.querySelector('input[type="checkbox"]');
if (!cb) return;
if (li.classList.contains('todo-item')) return;
const checked = cb.checked;
li.classList.add('todo-item');
if (checked) li.classList.add('todo-done');
// Replace with a styled button
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'todo-cb' + (checked ? ' checked' : '');
btn.innerHTML = '';
btn.dataset.idx = cb.dataset.idx || '';
cb.replaceWith(btn);
// Wrap remaining content in a span
const span = document.createElement('span');
span.className = 'todo-text';
while (btn.nextSibling) span.appendChild(btn.nextSibling);
li.appendChild(span);
});
}
export function stripMd(src) {
return (src || '')
.replace(/]*>/gi, '')
.replace(/<[^>]+>/g, '')
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
.replace(/\[\[([^\]]+)\]\]/g, '$1')
.replace(/^#{1,6}\s+/gm, '')
.replace(/(\*{1,3}|_{1,3}|~~)(.*?)\1/g, '$2')
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/`{1,3}[^`]*`{1,3}/g, '')
.replace(/^[-*>]\s+/gm, '')
.replace(/^\d+\.\s+/gm, '')
.replace(/\n{2,}/g, '\n')
.trim();
}
export function getTodos(src) {
const s = src || '';
const total = (s.match(/- \[([ x])\]/g) || []).length;
if (!total) return null;
return { total, done: (s.match(/- \[x\]/g) || []).length };
}
export function firstImage(src) {
const s = src || '';
const md = s.match(/!\[[^\]]*\]\(([^)]+)\)/);
if (md) return md[1];
const html = s.match(/]+src="([^"]+)"/i);
return html ? html[1] : null;
}
export function leadingImage(src) {
const s = (src || '').trimStart();
if (!s) return null;
const md = s.match(/^!\[[^\]]*\]\(([^)]+)\)/);
if (md) return md[1].split(/\s+/)[0].replace(/^<(.+)>$/, '$1') || null;
const html = s.match(/^]+src=(['"])(.*?)\1/i);
return html ? html[2] : null;
}