/* Dona Markdown V1 : rendu DOM sécurisé, sans injection de HTML depuis Hermes. * Nécessite uniquement les API standard du navigateur (aucun CDN). */ (() => { 'use strict'; function normalize(value) { return String(value ?? '') .replace(/\r\n?/g, '\n') .replace(/\u00a0/g, ' ') .replace(/&#(?:x20|32);| /gi, ' ') .replace(/\\\n/g, '\n') // Certains formats rééchappent les délimiteurs Markdown en \\* ou \\`. .replace(/\\([*`_])/g, '$1'); } function element(tag, parent, cls) { const node = document.createElement(tag); if (cls) node.className = cls; parent.appendChild(node); return node; } function appendInline(parent, input, depth = 0) { const str = String(input ?? ''); if (!str || depth > 5) { parent.appendChild(document.createTextNode(str)); return; } const tokens = /`([^`\n]+)`|\*\*([^*\n]+?)\*\*|__([^_\n]+?)__|~~([^~\n]+?)~~|\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)|\*([^*\n]+?)\*|_([^_\n]+?)_/g; let last = 0, m; while ((m = tokens.exec(str)) !== null) { if (m.index > last) parent.appendChild(document.createTextNode(str.slice(last, m.index))); if (m[1] !== undefined) element('code', parent).textContent = m[1]; else if (m[2] !== undefined || m[3] !== undefined) appendInline(element('strong', parent), m[2] ?? m[3], depth + 1); else if (m[4] !== undefined) appendInline(element('del', parent), m[4], depth + 1); else if (m[5] !== undefined) { const a = element('a', parent); a.textContent = m[5]; a.href = m[6]; // Seulement http(s), filtré par la regexp. a.rel = 'noopener noreferrer'; a.target = '_blank'; } else if (m[7] !== undefined || m[8] !== undefined) appendInline(element('em', parent), m[7] ?? m[8], depth + 1); last = tokens.lastIndex; } if (last < str.length) parent.appendChild(document.createTextNode(str.slice(last))); } function render(target, value, options = {}) { if (!target || typeof target.replaceChildren !== 'function') return; const source = normalize(value); const fragment = document.createDocumentFragment(); if (options.inline) { appendInline(fragment, source); target.replaceChildren(fragment); return; } const lines = source.split('\n'); let paragraph = []; let codeLines = null; let lists = []; function flushParagraph() { if (!paragraph.length) return; const p = element('p', fragment); paragraph.forEach((line, i) => { if (i) p.appendChild(document.createElement('br')); appendInline(p, line); }); paragraph = []; } for (let index = 0; index < lines.length; index++) { const line = lines[index]; const trimmed = line.trim(); if (codeLines !== null) { if (/^\s*```\s*$/.test(line)) { const pre = element('pre', fragment); element('code', pre).textContent = codeLines.join('\n'); codeLines = null; } else codeLines.push(line); continue; } if (/^\s*```(?:[\w+-]+)?\s*$/.test(line)) { flushParagraph(); lists = []; codeLines = []; continue; } if (!trimmed) { flushParagraph(); lists = []; continue; } const list = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.+)$/); if (list) { flushParagraph(); const indent = list[1].replace(/\t/g, ' ').length; const type = /\d/.test(list[2][0]) ? 'ol' : 'ul'; while (lists.length && (indent < lists[lists.length - 1].indent || (indent === lists[lists.length - 1].indent && type !== lists[lists.length - 1].type))) lists.pop(); if (!lists.length || indent > lists[lists.length - 1].indent) { const parent = lists.length && lists[lists.length - 1].lastItem ? lists[lists.length - 1].lastItem : fragment; const node = element(type, parent); lists.push({node, type, indent, lastItem: null}); } const current = lists[lists.length - 1]; current.lastItem = element('li', current.node); appendInline(current.lastItem, list[3]); continue; } lists = []; const heading = line.match(/^\s*(#{1,6})\s+(.+)$/); if (heading) { flushParagraph(); appendInline(element('h' + Math.min(heading[1].length + 2, 6), fragment), heading[2]); continue; } const quote = line.match(/^\s*>\s?(.*)$/); if (quote) { flushParagraph(); appendInline(element('blockquote', fragment), quote[1]); continue; } if (/^\s*(?:-{3,}|\*{3,})\s*$/.test(line)) { flushParagraph(); element('hr', fragment); continue; } paragraph.push(line.trim()); } if (codeLines !== null) { const pre = element('pre', fragment); element('code', pre).textContent = codeLines.join('\n'); } flushParagraph(); target.replaceChildren(fragment); } window.DonaMarkdown = Object.freeze({ render, normalize }); })();