$1')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/(^|[^*])\*([^*]+)\*/g, '$1$2')
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (m, label, url) => {
const u = String(url).trim();
if (u && !/^(https?:|mailto:|\/|#)/i.test(u)) return m;
return park(link(u, label));
});
// Nackte Quellen-URLs klickbar machen — bei Web-Recherchen nennt der
// Assistent Belege oft als reine URL, nicht als Markdown-Link. Nur http(s),
// und Satzzeichen am Ende gehören zum Satz, nicht zur Adresse.
s = s.replace(/https?:\/\/[^\s<>"'`\u0000]+/g, (u) => {
const m = u.match(/[.,;:!?)\]]+$/);
const rein = m ? u.slice(0, -m[0].length) : u;
return park(link(rein, rein)) + (m ? m[0] : '');
});
return s.replace(/\u0000(\d+)\u0000/g, (m, i) => parked[Number(i)]);
}
// Trennzeile einer Markdown-Tabelle: |---|:---:|---:| (mind. zwei Striche je
// Spalte, Doppelpunkte für die Ausrichtung).
function isTableSeparator(line) {
return /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(line);
}
function tableCells(line) {
return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
}
function alignOf(sep) {
const links = sep.startsWith(':');
const rechts = sep.endsWith(':');
if (links && rechts) return 'center';
if (rechts) return 'right';
return null; // linksbündig ist der Standard, kein style nötig
}
function renderMarkdown(src) {
const esc = escapeHtml(src == null ? '' : String(src)).replace(/\r\n/g, '\n');
const lines = esc.split('\n');
let html = '';
let i = 0;
let para = [];
const flushPara = () => {
if (para.length) html += '' + inlineFmt(para.join(' ')) + '
'; para = []; }; while (i < lines.length) { const line = lines[i]; if (/^```/.test(line)) { // fenced code block flushPara(); const code = []; i++; while (i < lines.length && !/^```/.test(lines[i])) { code.push(lines[i]); i++; } i++; // closing fence html += '' + code.join('\n') + '';
continue;
}
const hm = line.match(/^(#{1,6})\s+(.*)$/);
if (hm) {
flushPara();
const lvl = Math.min(hm[1].length, 3);
html += '