138 lines
4.1 KiB
JavaScript

(function () {
const tokenPattern = /%%MDTOKEN(\d+)%%/g;
function escapeHtml(value) {
return String(value || "").replace(/[&<>"']/g, (char) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
})[char]);
}
function sanitizeUrl(value) {
const url = String(value || "").trim().replace(/[\u0000-\u001f\u007f\s]+/g, "");
if (/^(https?:\/\/|\/|\.\/|\.\.\/|uploads\/|mailto:|#)/i.test(url)) {
return escapeHtml(url);
}
return "#";
}
function storeToken(tokens, html) {
const token = `%%MDTOKEN${tokens.length}%%`;
tokens.push(html);
return token;
}
function renderInline(markdown) {
const tokens = [];
let text = String(markdown || "");
text = text.replace(/`([^`]+)`/g, (_, code) =>
storeToken(tokens, `<code>${escapeHtml(code)}</code>`),
);
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, url) =>
storeToken(
tokens,
`<img src="${sanitizeUrl(url)}" alt="${escapeHtml(alt)}" loading="lazy">`,
),
);
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) =>
storeToken(
tokens,
`<a href="${sanitizeUrl(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(label)}</a>`,
),
);
text = escapeHtml(text)
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
.replace(/__([^_]+)__/g, "<strong>$1</strong>")
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
.replace(/_([^_]+)_/g, "<em>$1</em>");
return text.replace(tokenPattern, (_, index) => tokens[Number(index)] || "");
}
function renderList(lines) {
const items = lines
.map((line) => line.replace(/^\s*[-*]\s+/, ""))
.map((line) => `<li>${renderInline(line)}</li>`)
.join("");
return `<ul>${items}</ul>`;
}
function renderMarkdown(markdown) {
const lines = String(markdown || "").replace(/\r\n/g, "\n").split("\n");
const blocks = [];
let paragraph = [];
let list = [];
let codeBlock = null;
function flushParagraph() {
if (!paragraph.length) return;
blocks.push(`<p>${renderInline(paragraph.join(" "))}</p>`);
paragraph = [];
}
function flushList() {
if (!list.length) return;
blocks.push(renderList(list));
list = [];
}
lines.forEach((line) => {
if (line.trim().startsWith("```")) {
if (codeBlock) {
blocks.push(`<pre><code>${escapeHtml(codeBlock.join("\n"))}</code></pre>`);
codeBlock = null;
} else {
flushParagraph();
flushList();
codeBlock = [];
}
return;
}
if (codeBlock) {
codeBlock.push(line);
return;
}
if (!line.trim()) {
flushParagraph();
flushList();
return;
}
const heading = line.match(/^(#{1,3})\s+(.+)$/);
if (heading) {
flushParagraph();
flushList();
const level = heading[1].length + 2;
blocks.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
return;
}
if (/^\s*[-*]\s+/.test(line)) {
flushParagraph();
list.push(line);
return;
}
flushList();
paragraph.push(line.trim());
});
flushParagraph();
flushList();
if (codeBlock) {
blocks.push(`<pre><code>${escapeHtml(codeBlock.join("\n"))}</code></pre>`);
}
return blocks.join("");
}
window.renderMarkdown = renderMarkdown;
})();