This commit is contained in:
pporcheret
2026-10-30 22:10:03 +01:00
parent 7f26f9b015
commit 90ffa635a0
161 changed files with 19042 additions and 111 deletions
@@ -0,0 +1,122 @@
/* 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 });
})();