feat: render Markdown formulas with KaTeX

This commit is contained in:
lofyer
2026-08-12 23:48:44 +08:00
parent 2e489d5bc3
commit d33df979da
6 changed files with 550 additions and 2 deletions
+311
View File
@@ -19,6 +19,7 @@
"html-to-text": "^10.0.0",
"i18next": "^25.10.10",
"json5": "^2.2.3",
"katex": "^0.16.47",
"lucide-react": "^1.27.0",
"onnxruntime-web": "^1.23.2",
"pdfjs-dist": "^6.2.108",
@@ -29,7 +30,9 @@
"react-dom": "^19.2.8",
"react-i18next": "^16.6.6",
"react-markdown": "^10.1.0",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"sherpa-onnx": "1.13.4",
"undici": "^7.29.0",
"yaml": "^2.9.0",
@@ -3547,6 +3550,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/katex": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz",
"integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==",
"license": "MIT"
},
"node_modules/@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
@@ -7308,6 +7317,125 @@
"node": ">= 0.4"
}
},
"node_modules/hast-util-from-dom": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz",
"integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==",
"license": "ISC",
"dependencies": {
"@types/hast": "^3.0.0",
"hastscript": "^9.0.0",
"web-namespaces": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-from-html": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
"integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"devlop": "^1.1.0",
"hast-util-from-parse5": "^8.0.0",
"parse5": "^7.0.0",
"vfile": "^6.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-from-html-isomorphic": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz",
"integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"hast-util-from-dom": "^5.0.0",
"hast-util-from-html": "^2.0.0",
"unist-util-remove-position": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-from-html/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/hast-util-from-html/node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/hast-util-from-parse5": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
"integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"devlop": "^1.0.0",
"hastscript": "^9.0.0",
"property-information": "^7.0.0",
"vfile": "^6.0.0",
"vfile-location": "^5.0.0",
"web-namespaces": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-is-element": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
"integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-parse-selector": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -7335,6 +7463,22 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-text": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
"integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"hast-util-is-element": "^3.0.0",
"unist-util-find-after": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
@@ -7348,6 +7492,23 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/hastscript": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-parse-selector": "^4.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -8038,6 +8199,31 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/katex": {
"version": "0.16.47",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
"integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
"license": "MIT",
"dependencies": {
"commander": "^8.3.0"
},
"bin": {
"katex": "cli.js"
}
},
"node_modules/katex/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -8371,6 +8557,25 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-math": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz",
"integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"longest-streak": "^3.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.1.0",
"unist-util-remove-position": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-mdx-expression": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
@@ -8722,6 +8927,25 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-math": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz",
"integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==",
"license": "MIT",
"dependencies": {
"@types/katex": "^0.16.0",
"devlop": "^1.0.0",
"katex": "^0.16.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-factory-destination": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
@@ -10763,6 +10987,25 @@
"node": ">=8"
}
},
"node_modules/rehype-katex": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz",
"integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/katex": "^0.16.0",
"hast-util-from-html-isomorphic": "^2.0.0",
"hast-util-to-text": "^4.0.0",
"katex": "^0.16.0",
"unist-util-visit-parents": "^6.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-gfm": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
@@ -10781,6 +11024,22 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-math": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz",
"integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-math": "^3.0.0",
"micromark-extension-math": "^3.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@@ -11951,6 +12210,20 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-find-after": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
"integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-is": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
@@ -11977,6 +12250,20 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-remove-position": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
"integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-visit": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
@@ -12163,6 +12450,20 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-location": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
@@ -12848,6 +13149,16 @@
"node": ">=18"
}
},
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
"integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/webcrypto-core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz",
+7
View File
@@ -116,6 +116,10 @@
{
"from": "node_modules/onnxruntime-web/LICENSE",
"to": "licenses/onnxruntime-web-MIT.txt"
},
{
"from": "node_modules/katex/LICENSE",
"to": "licenses/katex-MIT.txt"
}
],
"win": {
@@ -165,6 +169,7 @@
"html-to-text": "^10.0.0",
"i18next": "^25.10.10",
"json5": "^2.2.3",
"katex": "^0.16.47",
"lucide-react": "^1.27.0",
"onnxruntime-web": "^1.23.2",
"pdfjs-dist": "^6.2.108",
@@ -175,7 +180,9 @@
"react-dom": "^19.2.8",
"react-i18next": "^16.6.6",
"react-markdown": "^10.1.0",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"sherpa-onnx": "1.13.4",
"undici": "^7.29.0",
"yaml": "^2.9.0",
@@ -73,6 +73,98 @@ const ready = true
expect(container.querySelector('pre')).not.toBeInTheDocument()
})
it('renders inline and display LaTeX formulas with KaTeX', () => {
const { container } = render(
<MarkdownRenderer>{`质能方程为 $E = mc^2$
$$
\\int_0^1 x^2\\,dx = \\frac{1}{3}
$$`}</MarkdownRenderer>
)
expect(container.querySelectorAll('.katex')).toHaveLength(2)
expect(container.querySelector('.katex-display')).toBeInTheDocument()
expect(
container.querySelector(
'annotation[encoding="application/x-tex"]'
)
).toHaveTextContent('E = mc^2')
expect(
[...container.querySelectorAll(
'annotation[encoding="application/x-tex"]'
)].at(-1)
).toHaveTextContent('\\int_0^1 x^2\\,dx = \\frac{1}{3}')
})
it('supports common model-style LaTeX delimiters outside code', () => {
const { container } = render(
<MarkdownRenderer>{`行内公式 \\(a^2 + b^2 = c^2\\)。
\\[
\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}
\\]
\\[ \\sqrt{x^2} = |x| \\]
\`\\(not rendered\\)\`
\`\`\`tex
\\[
also not rendered
\\]
\`\`\``}</MarkdownRenderer>
)
expect(container.querySelectorAll('.katex')).toHaveLength(3)
expect(container.querySelectorAll('.katex-display')).toHaveLength(2)
expect(screen.getByText('\\(not rendered\\)')).toBeInTheDocument()
expect(screen.getByText(/also not rendered/u)).toBeInTheDocument()
expect(container.querySelectorAll('code .katex')).toHaveLength(0)
})
it('promotes formulas that occupy a whole line to centered display math', () => {
const { container } = render(
<MarkdownRenderer>{`段落内仍是 $E = mc^2$ 行内公式。
$S(A) = \\frac{\\operatorname{Area}(\\gamma_A)}{4G_N}$
\\(S \\leq \\frac{A}{4G_N}\\)`}</MarkdownRenderer>
)
expect(container.querySelectorAll('.katex')).toHaveLength(3)
expect(container.querySelectorAll('.katex-display')).toHaveLength(2)
expect(
container.querySelector(
'p:first-child .katex-display'
)
).not.toBeInTheDocument()
for (const display of container.querySelectorAll('.katex-display')) {
expect(display.querySelector(':scope > .katex')).toBeInTheDocument()
}
})
it('shows invalid formulas without crashing or rendering raw HTML', () => {
const { container } = render(
<MarkdownRenderer>{`$\\notARealCommand{value}$
$\\href{javascript:alert(1)}{unsafe}$`}</MarkdownRenderer>
)
expect(
container.querySelector(
'annotation[encoding="application/x-tex"]'
)
).toHaveTextContent('\\notARealCommand{value}')
expect(container.querySelectorAll('.katex')).toHaveLength(2)
expect(container).toHaveTextContent(
'\\notARealCommand'
)
expect(container.querySelector('script')).not.toBeInTheDocument()
expect(
container.querySelector('a[href^="javascript:"]')
).not.toBeInTheDocument()
})
it('updates table accessibility copy when the locale changes', async () => {
render(
<MarkdownRenderer>{`| Name |
+111 -2
View File
@@ -1,6 +1,8 @@
import { memo, useMemo } from 'react'
import rehypeKatex from 'rehype-katex'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import type { Components } from 'react-markdown'
import { useTranslation } from 'react-i18next'
@@ -43,6 +45,100 @@ type MarkdownRendererProps = {
const wholeMarkdownFence =
/^```(?:markdown|md)\s*\r?\n([\s\S]*?)\r?\n```$/iu
function replaceLatexDelimiters(line: string): string {
let output = ''
let index = 0
let codeDelimiterLength = 0
while (index < line.length) {
if (line[index] === '`') {
let runLength = 1
while (line[index + runLength] === '`') {
runLength += 1
}
if (
codeDelimiterLength === 0 ||
codeDelimiterLength === runLength
) {
codeDelimiterLength =
codeDelimiterLength === 0 ? runLength : 0
}
output += line.slice(index, index + runLength)
index += runLength
continue
}
if (
codeDelimiterLength === 0 &&
line[index] === '\\' &&
line[index - 1] !== '\\' &&
(line[index + 1] === '(' || line[index + 1] === ')')
) {
output += '$'
index += 2
continue
}
output += line[index]
index += 1
}
return output
}
function normalizeLatexDelimiters(content: string): string {
let fence:
| {
character: '`' | '~'
length: number
}
| undefined
return content
.split(/\r?\n/u)
.map((line) => {
if (fence) {
const closingFence = new RegExp(
`^ {0,3}${fence.character}{${fence.length},}\\s*$`,
'u'
)
if (closingFence.test(line)) {
fence = undefined
}
return line
}
const openingFence = /^ {0,3}(`{3,}|~{3,})/u.exec(line)
if (openingFence) {
const marker = openingFence[1]!
fence = {
character: marker[0] as '`' | '~',
length: marker.length
}
return line
}
const standaloneDollar =
/^ {0,3}\$(?!\$)(.+?)(?<!\\)\$\s*$/u.exec(line)
if (standaloneDollar) {
return `$$\n${standaloneDollar[1]}\n$$`
}
const standaloneParentheses =
/^ {0,3}\\\(\s*(.*?)\s*\\\)\s*$/u.exec(line)
if (standaloneParentheses) {
return `$$\n${standaloneParentheses[1]}\n$$`
}
const singleLineDisplay =
/^ {0,3}\\\[\s*(.*?)\s*\\\]\s*$/u.exec(line)
if (singleLineDisplay) {
return `$$\n${singleLineDisplay[1]}\n$$`
}
if (/^ {0,3}\\\[\s*$/u.test(line)) {
return '$$'
}
if (/^ {0,3}\\\]\s*$/u.test(line)) {
return '$$'
}
return replaceLatexDelimiters(line)
})
.join('\n')
}
function unwrapMarkdownFence(content: string): string {
const fencedMarkdown = wholeMarkdownFence.exec(content.trim())
return fencedMarkdown?.[1] ?? content
@@ -60,10 +156,23 @@ export const MarkdownRenderer = memo(function MarkdownRenderer({
return (
<ReactMarkdown
components={components}
remarkPlugins={[remarkGfm]}
rehypePlugins={[
[
rehypeKatex,
{
output: 'htmlAndMathml',
strict: 'warn',
trust: false
}
]
]}
remarkPlugins={[
remarkGfm,
[remarkMath, { singleDollarTextMath: true }]
]}
skipHtml
>
{unwrapMarkdownFence(children)}
{normalizeLatexDelimiters(unwrapMarkdownFence(children))}
</ReactMarkdown>
)
})
+2
View File
@@ -1,6 +1,8 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import '@fontsource-variable/noto-sans-sc/wght.css'
import 'katex/dist/katex.min.css'
import './markdown-math.css'
import App from './App'
import { installBundledUiFonts } from './fonts'
import { changeUiLocale } from './i18n'
+27
View File
@@ -0,0 +1,27 @@
.markdown-content .katex {
color: inherit;
font-size: 1.05em;
}
.markdown-content .katex-display {
max-width: 100%;
margin: 0.85em 0;
padding: var(--space-1) 0;
overflow-x: auto;
overflow-y: hidden;
text-align: center;
}
.markdown-content .katex-display > .katex {
width: max-content;
min-width: max-content;
margin-inline: auto;
text-align: center;
}
.markdown-content .katex-error {
color: var(--danger);
font-family: var(--font-family-mono);
font-size: 0.9em;
overflow-wrap: anywhere;
}