52 lines
2.7 KiB
JavaScript
52 lines
2.7 KiB
JavaScript
function escapeHtml(value) {
|
|
return String(value).replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
|
|
}
|
|
|
|
function safeUrl(value) {
|
|
const url = String(value).trim();
|
|
if (/^(https?:\/\/|\/|\.\/|\.\.\/|#)/i.test(url)) return escapeHtml(url);
|
|
return "#";
|
|
}
|
|
|
|
function inline(value) {
|
|
const tokens = [];
|
|
let html = escapeHtml(value);
|
|
html = html.replace(/!\[([^\]]*)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, alt, url, title) => {
|
|
const token = `\u0000IMG${tokens.length}\u0000`;
|
|
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
|
tokens.push(`<img src="${safeUrl(url)}" alt="${alt}" loading="lazy" decoding="async"${titleAttr}>`);
|
|
return token;
|
|
});
|
|
html = html
|
|
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
.replace(/~~([^~]+)~~/g, "<s>$1</s>")
|
|
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
|
|
.replace(/\[([^\]]+)\]\(([^\s)]+)(?:\s+["']([^"']*)["'])?\)/g, (_, label, url, title) => {
|
|
const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
|
|
return `<a href="${safeUrl(url)}" target="_blank" rel="noopener noreferrer"${titleAttr}>${label}</a>`;
|
|
});
|
|
return html.replace(/\u0000IMG(\d+)\u0000/g, (_, index) => tokens[Number(index)] || "");
|
|
}
|
|
|
|
export function renderMarkdown(source) {
|
|
let html = "", inCode = false, language = "", code = [], list = null;
|
|
const closeList = () => { if (list) { html += `</${list}>`; list = null; } };
|
|
const closeCode = () => {
|
|
const body = escapeHtml(code.join("\n"));
|
|
html += language.toLowerCase() === "mermaid"
|
|
? `<div class="mermaid">${body}</div>`
|
|
: `<pre><code class="language-${escapeHtml(language)}">${body}</code></pre>`;
|
|
code = []; language = "";
|
|
};
|
|
for (const line of String(source).split("\n")) {
|
|
if (line.startsWith("```")) { closeList(); if (inCode) closeCode(); else language = line.slice(3).trim(); inCode = !inCode; continue; }
|
|
if (inCode) { code.push(line); continue; }
|
|
const heading = line.match(/^(#{1,6})\s+(.+)$/), ul = line.match(/^\s*[-*+]\s+(.+)$/), ol = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
if (heading) { closeList(); const n = heading[1].length; html += `<h${n}>${inline(heading[2])}</h${n}>`; }
|
|
else if (ul || ol) { const type = ul ? "ul" : "ol"; if (list !== type) { closeList(); html += `<${type}>`; list = type; } html += `<li>${inline((ul || ol)[1])}</li>`; }
|
|
else { closeList(); if (/^---+$/.test(line)) html += "<hr>"; else if (line.startsWith("> ")) html += `<blockquote>${inline(line.slice(2))}</blockquote>`; else if (line.trim()) html += `<p>${inline(line)}</p>`; else html += "<br>"; }
|
|
}
|
|
closeList(); if (inCode) closeCode(); return html;
|
|
}
|