diff --git a/package-lock.json b/package-lock.json index 6517d78..77ecd1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index a5ec626..1bf1944 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/renderer/src/MarkdownRenderer.test.tsx b/src/renderer/src/MarkdownRenderer.test.tsx index ce17fee..d261dc0 100644 --- a/src/renderer/src/MarkdownRenderer.test.tsx +++ b/src/renderer/src/MarkdownRenderer.test.tsx @@ -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( + {`质能方程为 $E = mc^2$。 + +$$ +\\int_0^1 x^2\\,dx = \\frac{1}{3} +$$`} + ) + + 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( + {`行内公式 \\(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 +\\] +\`\`\``} + ) + + 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( + {`段落内仍是 $E = mc^2$ 行内公式。 + +$S(A) = \\frac{\\operatorname{Area}(\\gamma_A)}{4G_N}$ + +\\(S \\leq \\frac{A}{4G_N}\\)`} + ) + + 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( + {`$\\notARealCommand{value}$ + +$\\href{javascript:alert(1)}{unsafe}$`} + ) + + 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( {`| Name | diff --git a/src/renderer/src/MarkdownRenderer.tsx b/src/renderer/src/MarkdownRenderer.tsx index 0f6c4d4..4ffe144 100644 --- a/src/renderer/src/MarkdownRenderer.tsx +++ b/src/renderer/src/MarkdownRenderer.tsx @@ -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}\$(?!\$)(.+?)(? - {unwrapMarkdownFence(children)} + {normalizeLatexDelimiters(unwrapMarkdownFence(children))} ) }) diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 69ada46..fe22214 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -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' diff --git a/src/renderer/src/markdown-math.css b/src/renderer/src/markdown-math.css new file mode 100644 index 0000000..200093e --- /dev/null +++ b/src/renderer/src/markdown-math.css @@ -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; +}