138 lines
4.1 KiB
JavaScript
138 lines
4.1 KiB
JavaScript
(function () {
|
|
const tokenPattern = /%%MDTOKEN(\d+)%%/g;
|
|
|
|
function escapeHtml(value) {
|
|
return String(value || "").replace(/[&<>"']/g, (char) => ({
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
})[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;
|
|
})();
|