/** * Markdown 富文本渲染工具。 * *

统一为 chat / 工作流运行结果 / 运行记录页 三处提供一致的渲染: * 支持 GFM(表格 / 删除线 / 任务列表)、代码块(highlight.js 语法高亮)、 * 行内与块级公式(KaTeX)。 * *

使用: * ```js * import { renderMarkdown } from '@/utils/markdown' * // 在 Vue 模板中 *

* ``` * *

样式:本工具只产出 HTML 字符串;视觉表现(字号、配色、表格边框)由调用方 * 通过 {@code .md-body :deep(...)} CSS 控制,参考 ChatMessageList.vue 的 {@code .assistant-md}。 * 语法高亮的 token 配色由 {@code styles/hljs-theme.css} 统一定义。 */ import { Marked } from 'marked' import { markedHighlight } from 'marked-highlight' import markedKatex from 'marked-katex-extension' import DOMPurify from 'dompurify' import hljs from 'highlight.js/lib/common' // KaTeX 样式:渲染数学公式必需,挂到全局一次即可 import 'katex/dist/katex.min.css' // 语法高亮深色主题:仅 token 前景色,背景由调用方控制 import '../styles/hljs-theme.css' // 模块级单例:避免每次调用都重新装配 marked // 语法高亮规则(使用 highlight.js/lib/common,含 30+ 常用语言,体积约 30KB): // 1) 显式指定语言(如 ```js)且已注册:用 hljs.highlight 精确高亮 // 2) 未指定语言或语言未注册:用 hljs.highlightAuto 自动检测(限 50KB 以内避免性能问题) // 3) 异常或超长:降级为纯文本(hljs 主题的 .hljs 默认色) const marked = new Marked( markedHighlight({ langPrefix: 'hljs language-', highlight(code, lang) { try { const language = lang && hljs.getLanguage(lang) ? lang : null if (language) { return hljs.highlight(code, { language }).value } if (code.length < 50000) { return hljs.highlightAuto(code).value } return code } catch (e) { return code } } }) ) marked.use({ gfm: true, breaks: true }) marked.use(markedKatex({ throwOnError: false, output: 'html' })) /** * 把 markdown 字符串渲染为已 sanitize 的 HTML。 * *

流程:marked.parse → DOMPurify.sanitize(防 XSS)→ HTML 字符串。 * 异常时降级为对纯文本做 sanitize,保证不抛错。 * * @param {string} text 原始 markdown 文本,可为空 * @returns {string} 可安全 v-html 注入的 HTML */ export function renderMarkdown(text) { if (!text) return '' try { const raw = marked.parse(text) // marked.parse 同步返回 string(未启用 async);保险起见 toString return DOMPurify.sanitize(String(raw), { ADD_TAGS: ['span', 'math', 'semantics', 'annotation'], ADD_ATTR: ['class'] }) } catch (e) { console.warn('[utils/markdown] 渲染失败', e?.message || e) return DOMPurify.sanitize(String(text)) } }