123 lines
4.9 KiB
JavaScript
123 lines
4.9 KiB
JavaScript
/* 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 });
|
|
})();
|