// markdown 渲染 + 代码高亮。依赖 vendor 全局(window.marked / DOMPurify / hljs)。 // 三个库任一缺失 → 优雅降级回
escapeHtml(plain text wrap)。 import { escapeHtml } from "./format.js"; if (window.marked && window.marked.setOptions) { window.marked.setOptions({ gfm: true, breaks: true, headerIds: false, mangle: false }); } const FENCE_RE = /^( {0,3})(`{3,}|~{3,})([^\r\n]*)(\r?\n)?$/; function parseFence(line) { const m = String(line || "").match(FENCE_RE); if (!m) return null; return { indent: m[1], char: m[2][0], len: m[2].length, info: m[3].trim() }; } function nextNonblank(lines, start) { for (let i = start; i < lines.length; i++) { if (lines[i].trim()) return i; } return -1; } function replaceFence(line, length) { const m = String(line || "").match(FENCE_RE); if (!m) return line; return m[1] + m[2][0].repeat(length) + m[3] + (m[4] || ""); } // 只修复明确的「markdown 外层与内层语言块使用同长围栏」形态。 // 其他残缺 Markdown 原样交给 marked,避免猜测作者意图。 export function normalizeMarkdownFences(text) { const lines = String(text || "").match(/.*(?:\r\n|\n|$)/g).filter(Boolean); for (let i = 0; i < lines.length;) { const outer = parseFence(lines[i]); if (!outer || !["markdown", "md"].includes(outer.info.toLowerCase())) { i++; continue; } const innerIdx = nextNonblank(lines, i + 1); const inner = innerIdx >= 0 ? parseFence(lines[innerIdx]) : null; if (!inner || !inner.info || inner.char !== outer.char || inner.len < outer.len) { i++; continue; } let innerCloseIdx = -1; for (let j = innerIdx + 1; j < lines.length; j++) { const close = parseFence(lines[j]); if (close && !close.info && close.char === inner.char && close.len >= inner.len) { innerCloseIdx = j; break; } } if (innerCloseIdx < 0) { i++; continue; } const outerCloseIdx = nextNonblank(lines, innerCloseIdx + 1); const outerClose = outerCloseIdx >= 0 ? parseFence(lines[outerCloseIdx]) : null; if (!outerClose || outerClose.info || outerClose.char !== outer.char || outerClose.len < outer.len) { i++; continue; } const repairedLen = Math.max(outer.len, inner.len) + 1; lines[i] = replaceFence(lines[i], repairedLen); lines[outerCloseIdx] = replaceFence(lines[outerCloseIdx], repairedLen); i = outerCloseIdx + 1; } return lines.join(""); } export function renderMd(text) { const raw = normalizeMarkdownFences(text); if (!window.marked || !window.marked.parse) { return `
${escapeHtml(raw)}`;
}
let html = window.marked.parse(raw);
if (window.DOMPurify) {
html = window.DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
}
return html;
}
export function highlightIn(container) {
if (!window.hljs || !container) return;
container.querySelectorAll("pre code").forEach((b) => {
if (b.dataset.hl === "1") return;
try { window.hljs.highlightElement(b); b.dataset.hl = "1"; } catch (e) {}
});
}