zcbot/web/static/js/markdown.js

91 lines
3.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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) {}
});
}