This commit is contained in:
th committed 2026-04-07 17:47:09 +02:00
1 parent 03f57cbaee
commit a9603aa0e8
11 files changed
+1010 -93

No files matched your search

+63 -3
View File
@@ -11,14 +11,74 @@ const purifyConfig = {
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) {
const linked = (src || '').replace(/\[\[([^\]]+)\]\]/g, (_, title) => {
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<!-- list:${kind} -->\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 <br> spacers (3+ newlines = 1+ extra blank lines)
.replace(/\n{3,}/g, m => '\n\n' + '<br>\n'.repeat(m.length - 2))
// Ensure hr rules have one clean blank line before and after, keep extra <br> beyond that
.replace(/<br>\n(---+|___+|\*\*\*+)\n/gm, '\n\n$1\n')
.replace(/(---+|___+|\*\*\*+)\n<br>/gm, '$1\n\n');
const linked = processed.replace(/\[\[([^\]]+)\]\]/g, (_, title) => {
const card = state.cards.find(c => c.title.toLowerCase() === title.toLowerCase());
if (card) return `<span class="card-link" data-card-id="${card.id}">${esc(title)}</span>`;
return `<span class="card-link broken" title="card not found">${esc(title)}</span>`;
});
const raw = marked.parse(linked);
return typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(raw, purifyConfig) : raw;
let html = marked.parse(linked);
// Convert list hints before sanitize (DOMPurify strips comments)
html = html.replace(/<!--\s*list:tree\s*-->\s*<ul>/g, '<ul class="tree-list">');
html = html.replace(/<!--\s*list:bullets\s*-->\s*<ul>/g, '<ul class="bullet-list">');
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 = '<i class="ph ph-copy"></i>';
btn.addEventListener('click', async () => {
await navigator.clipboard.writeText(code.textContent);
btn.classList.add('copied');
btn.innerHTML = '<i class="ph ph-check"></i>';
setTimeout(() => { btn.classList.remove('copied'); btn.innerHTML = '<i class="ph ph-copy"></i>'; }, 1500);
});
pre.appendChild(btn);
});
}
export function stripMd(src) {