91 lines
3.1 KiB
JavaScript
91 lines
3.1 KiB
JavaScript
// markdown 渲染 + 代码高亮。依赖 vendor 全局(window.marked / DOMPurify / hljs)。
|
||
// 三个库任一缺失 → 优雅降级回 <pre>escapeHtml</pre>(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 `<pre style="white-space:pre-wrap;word-break:break-word;font-family:inherit;margin:0;">${escapeHtml(raw)}</pre>`;
|
||
}
|
||
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) {}
|
||
});
|
||
}
|