| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970 |
- function escapeHtml(text) {
- return String(text || "")
- .replace(/&/g, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
- }
- function sanitizeMarkdownUrl(url) {
- const s = String(url || "").trim();
- if (!s) return "";
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s)) {
- const scheme = s.split(":", 1)[0].toLowerCase();
- if (!["http", "https", "mailto"].includes(scheme)) return "";
- }
- return s;
- }
- function renderMarkdown(md) {
- const raw = String(md || "");
- const escaped = escapeHtml(raw);
- const blocks = [];
- const placeholder = (i) => `@@BLOCK_${i}@@`;
- const fenced = escaped.replace(/```([\s\S]*?)```/g, (_m, code) => {
- const html = `<pre class="code"><code>${code.replace(/^\n+|\n+$/g, "")}</code></pre>`;
- blocks.push(html);
- return placeholder(blocks.length - 1);
- });
- let html = fenced
- .replace(/^### (.*)$/gm, "<h3>$1</h3>")
- .replace(/^## (.*)$/gm, "<h2>$1</h2>")
- .replace(/^# (.*)$/gm, "<h1>$1</h1>")
- .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_m, alt, url) => {
- const safeUrl = sanitizeMarkdownUrl(url);
- if (!safeUrl) return `<span class="muted">[图片已拦截]</span>`;
- return `<img class="md-img" alt="${alt}" src="${escapeHtml(safeUrl)}" />`;
- })
- .replace(/@\[(video)\]\(([^)]+)\)/g, (_m, _t, url) => {
- const safeUrl = sanitizeMarkdownUrl(url);
- if (!safeUrl) return `<span class="muted">[视频已拦截]</span>`;
- return `<video class="md-video" controls src="${escapeHtml(safeUrl)}"></video>`;
- })
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text, url) => {
- const safeUrl = sanitizeMarkdownUrl(url);
- if (!safeUrl) return `<span>${text}</span>`;
- return `<a href="${escapeHtml(safeUrl)}" target="_blank" rel="noopener">${text}</a>`;
- })
- .replace(/`([^`]+)`/g, "<code>$1</code>")
- .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
- .replace(/\*([^*\n]+)\*/g, "<em>$1</em>");
- html = html.replace(/\n{2,}/g, "\n\n");
- html = html
- .split("\n\n")
- .map((p) => {
- if (p.startsWith("@@BLOCK_")) return p;
- if (/^<h[1-3]>/.test(p.trim()) || /^<pre /.test(p.trim())) return p;
- const lines = p.split("\n").join("<br>");
- return `<p>${lines}</p>`;
- })
- .join("\n");
- blocks.forEach((b, i) => {
- html = html.replaceAll(placeholder(i), b);
- });
- return html;
- }
|