Lumi/plugins/okf/backend/markdown.js
2026-06-18 21:41:23 +02:00

115 lines
3.1 KiB
JavaScript

function escapeHtml(value) {
const map = {
"&": "&",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;"
};
return String(value || "").replace(/[&<>"']/g, (char) => map[char]);
}
function safeUrl(value) {
const raw = String(value || "").trim();
if (!raw) return "";
if (/^(https?:\/\/|\/(?!\/)|#)/i.test(raw)) return escapeHtml(raw);
return "";
}
function renderMarkdownInline(value) {
let output = escapeHtml(value);
output = output.replace(/`([^`]+)`/g, "<code>$1</code>");
output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
const href = safeUrl(url);
return href ? `<a href="${href}" target="_blank" rel="noopener noreferrer">${escapeHtml(text)}</a>` : escapeHtml(text);
});
output = output.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
output = output.replace(/\*([^*]+)\*/g, "<em>$1</em>");
return output;
}
function renderMarkdown(value) {
const lines = String(value || "").replace(/\r\n?/g, "\n").split("\n");
let html = "";
let paragraph = [];
let listType = null;
let inCode = false;
let codeLang = "";
let codeLines = [];
const flushParagraph = () => {
if (!paragraph.length) return;
html += `<p>${renderMarkdownInline(paragraph.join(" "))}</p>`;
paragraph = [];
};
const closeList = () => {
if (!listType) return;
html += `</${listType}>`;
listType = null;
};
for (const line of lines) {
const trimmed = line.trim();
if (inCode) {
if (trimmed.startsWith("```")) {
const langClass = codeLang ? ` class="language-${escapeHtml(codeLang)}"` : "";
html += `<pre><code${langClass}>${escapeHtml(codeLines.join("\n"))}</code></pre>`;
inCode = false;
codeLang = "";
codeLines = [];
continue;
}
codeLines.push(line);
continue;
}
if (trimmed.startsWith("```")) {
flushParagraph();
closeList();
inCode = true;
codeLang = trimmed.slice(3).trim().replace(/[^a-z0-9_-]/gi, "").slice(0, 32);
continue;
}
if (!trimmed) {
flushParagraph();
closeList();
continue;
}
const heading = trimmed.match(/^(#{1,6})\s+(.*)$/);
if (heading) {
flushParagraph();
closeList();
const level = heading[1].length;
html += `<h${level}>${renderMarkdownInline(heading[2])}</h${level}>`;
continue;
}
const list = trimmed.match(/^([*-]|\d+\.)\s+(.*)$/);
if (list) {
flushParagraph();
const nextType = list[1].endsWith(".") ? "ol" : "ul";
if (listType && listType !== nextType) closeList();
if (!listType) {
listType = nextType;
html += `<${listType}>`;
}
html += `<li>${renderMarkdownInline(list[2])}</li>`;
continue;
}
paragraph.push(trimmed);
}
if (inCode) {
const langClass = codeLang ? ` class="language-${escapeHtml(codeLang)}"` : "";
html += `<pre><code${langClass}>${escapeHtml(codeLines.join("\n"))}</code></pre>`;
}
flushParagraph();
closeList();
return html || "<p></p>";
}
module.exports = {
escapeHtml,
renderMarkdown,
renderMarkdownInline,
safeUrl
};