From c3d2f680c0331643f4e0ed04e2dac2cffdc5c1c4 Mon Sep 17 00:00:00 2001 From: Hanserwei <2628273921@qq.com> Date: Thu, 23 Oct 2025 21:39:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=20markdown-it-mathj?= =?UTF-8?q?ax3-pro=20=E7=9A=84=E6=94=AF=E6=8C=81=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E6=95=B0=E5=AD=A6=E5=85=AC=E5=BC=8F=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 68 +++++++++++++++++++++++++ src/components/StreamMarkdownRender.vue | 52 ++++++++++++++++++- 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 694cfcb..f3269a8 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "highlight.js": "^11.11.1", "markdown-it": "^14.1.0", "markdown-it-highlightjs": "^4.2.0", + "markdown-it-mathjax3-pro": "^1.0.0", "pinia": "^3.0.3", "tailwindcss": "^4.1.15", "vue": "^3.5.22", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e9be45..3ee6b06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: markdown-it-highlightjs: specifier: ^4.2.0 version: 4.2.0 + markdown-it-mathjax3-pro: + specifier: ^1.0.0 + version: 1.0.0(markdown-it@14.1.0) pinia: specifier: ^3.0.3 version: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) @@ -916,6 +919,10 @@ packages: vue: optional: true + '@xmldom/xmldom@0.9.8': + resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==} + engines: {node: '>=14.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1108,6 +1115,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -1400,6 +1411,10 @@ packages: jiti: optional: true + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2038,6 +2053,12 @@ packages: markdown-it-highlightjs@4.2.0: resolution: {integrity: sha512-NC7pXE8KkOl6xWJVRNt8p6wgJVznXKsE0HgYGdk6DD2tn1l4L9f0ALf3VIoGVkotNU1uGQatSxfBF1zZPUMmuQ==} + markdown-it-mathjax3-pro@1.0.0: + resolution: {integrity: sha512-irEoz78IQ+iHbveeTeOJJ4hM/e29/PT+jFjF6y9PvTFBo96q5rv0F7bJ0/+8ZXhrzor/Njws+QaKZC8p501EAQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + markdown-it: '>=12.0.0' + markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true @@ -2046,6 +2067,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mathjax-full@3.2.2: + resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} + deprecated: Version 4 replaces this package with the scoped package @mathjax/src + mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} @@ -2064,6 +2089,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + mhchemparser@4.2.1: + resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} + micromatch@3.1.0: resolution: {integrity: sha512-3StSelAE+hnRvMs8IdVW7Uhk8CVed5tp+kLLGlBP6WiRAXS21GPGu/Nat4WNPXj2Eoc24B02SaeoyozPMfj0/g==} engines: {node: '>=0.10.0'} @@ -2089,6 +2117,9 @@ packages: resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} engines: {node: '>=0.10.0'} + mj-context-menu@0.6.1: + resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -2509,6 +2540,10 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + speech-rule-engine@4.1.2: + resolution: {integrity: sha512-S6ji+flMEga+1QU79NDbwZ8Ivf0S/MpupQQiIC0rTpU/ZTKgcajijJJb1OcByBQDjrXCN1/DJtGz4ZJeBMPGJw==} + hasBin: true + split-string@3.1.0: resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} engines: {node: '>=0.10.0'} @@ -2876,6 +2911,9 @@ packages: engines: {node: ^18.17.0 || >=20.5.0} hasBin: true + wicked-good-xpath@1.3.0: + resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -3723,6 +3761,8 @@ snapshots: typescript: 5.9.3 vue: 3.5.22(typescript@5.9.3) + '@xmldom/xmldom@0.9.8': {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -3947,6 +3987,8 @@ snapshots: color-name@1.1.4: {} + commander@13.1.0: {} + commander@7.2.0: {} component-emitter@1.3.1: {} @@ -4321,6 +4363,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm@3.2.25: {} + espree@10.4.0: dependencies: acorn: 8.15.0 @@ -4918,6 +4962,11 @@ snapshots: dependencies: highlight.js: 11.11.1 + markdown-it-mathjax3-pro@1.0.0(markdown-it@14.1.0): + dependencies: + markdown-it: 14.1.0 + mathjax-full: 3.2.2 + markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -4929,6 +4978,13 @@ snapshots: math-intrinsics@1.1.0: {} + mathjax-full@3.2.2: + dependencies: + esm: 3.2.25 + mhchemparser: 4.2.1 + mj-context-menu: 0.6.1 + speech-rule-engine: 4.1.2 + mdn-data@2.0.14: {} mdurl@2.0.0: {} @@ -4941,6 +4997,8 @@ snapshots: merge2@1.4.1: {} + mhchemparser@4.2.1: {} + micromatch@3.1.0: dependencies: arr-diff: 4.0.0 @@ -4981,6 +5039,8 @@ snapshots: for-in: 1.0.2 is-extendable: 1.0.1 + mj-context-menu@0.6.1: {} + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -5452,6 +5512,12 @@ snapshots: speakingurl@14.0.1: {} + speech-rule-engine@4.1.2: + dependencies: + '@xmldom/xmldom': 0.9.8 + commander: 13.1.0 + wicked-good-xpath: 1.3.0 + split-string@3.1.0: dependencies: extend-shallow: 3.0.2 @@ -5905,6 +5971,8 @@ snapshots: dependencies: isexe: 3.1.1 + wicked-good-xpath@1.3.0: {} + word-wrap@1.2.5: {} wsl-utils@0.1.0: diff --git a/src/components/StreamMarkdownRender.vue b/src/components/StreamMarkdownRender.vue index 60fd761..be9658e 100644 --- a/src/components/StreamMarkdownRender.vue +++ b/src/components/StreamMarkdownRender.vue @@ -9,6 +9,7 @@ import { ref, watch, nextTick } from 'vue' import { message } from 'ant-design-vue' import MarkdownIt from 'markdown-it' import markdownItHighlightJs from 'markdown-it-highlightjs' +import MarkdownItMathJaX3PRO from 'markdown-it-mathjax3-pro' import hljs from 'highlight.js' import 'highlight.js/styles/github.css' // 引入代码高亮样式 @@ -82,6 +83,36 @@ const props = withDefaults( // 解析后的 HTML const renderedContent = ref('') +const MATHJAX_STYLE_ATTR = 'data-stream-markdown-mathjax-style' +let mathJaxStyleElement: HTMLStyleElement | null = null + +type InjectContentItem = { + type: string + content?: string +} + +const applyMathJaxStyles = (injectContent?: InjectContentItem[]) => { + if (typeof document === 'undefined' || !injectContent?.length) return + + const styleItem = injectContent.find((item) => item.type === 'style' && item.content) + if (!styleItem?.content) return + + if (!mathJaxStyleElement || !document.head.contains(mathJaxStyleElement)) { + mathJaxStyleElement = + document.head.querySelector(`style[${MATHJAX_STYLE_ATTR}]`) ?? + document.createElement('style') + + if (!mathJaxStyleElement.hasAttribute(MATHJAX_STYLE_ATTR)) { + mathJaxStyleElement.setAttribute(MATHJAX_STYLE_ATTR, 'true') + mathJaxStyleElement.type = 'text/css' + document.head.appendChild(mathJaxStyleElement) + } + } + + if (mathJaxStyleElement.textContent !== styleItem.content) { + mathJaxStyleElement.textContent = styleItem.content + } +} // 初始化 MarkdownIt const md = new MarkdownIt({ @@ -100,6 +131,23 @@ md.use(markdownItHighlightJs, { code: true, // 高亮内联代码 }) +md.use(MarkdownItMathJaX3PRO, { + tex: { + inlineMath: [['$', '$'], ['\\(', '\\)']], + displayMath: [['$$', '$$'], ['\\[', '\\]']], + tags: 'ams', + packages: ['base', 'ams', 'newcommand', 'configmacros'] + }, + chtml: { + fontURL: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/output/chtml/fonts/woff-v2' + }, + // 使用 SVG 输出代替 CHTML + svg: { + fontCache: 'local', + displayAlign: 'center' + } +}); + type FenceRenderRule = NonNullable['renderer']['rules']['fence']> // 保存默认的代码块渲染规则 @@ -166,7 +214,9 @@ watch( (newVal) => { if (newVal) { // 渲染为 HTML - const html = md.render(newVal) + const env: { frontmatter?: { inject_content?: InjectContentItem[] } } = {} + const html = md.render(newVal, env) + applyMathJaxStyles(env.frontmatter?.inject_content) renderedContent.value = html // 确保复制功能在 DOM 更新后可用