From dd72c8a8d6121902749cbe7b5c31d4ddddb3ca94 Mon Sep 17 00:00:00 2001
From: Yao Jianxuan <1378319314@qq.com>
Date: Thu, 7 May 2026 17:25:43 +0800
Subject: [PATCH] Fix(ui): Support Markdown Math Rendering (#381)
Add `KaTeX` extension for math formula rendering
It solves the issue #379 .
Now the interface acts like this when processing formulas:
`$\mathbb{R}$`:
`$$ \mathbb{R} $$`:
```markdown
$$
\mathbb{R}
$$
```
:
Closes #379
---------
Co-authored-by: Sisyphus
---
package-lock.json | 37 +++++++++++++++++++++++++
packages/ui/package.json | 2 ++
packages/ui/src/components/markdown.tsx | 16 +++++++++--
packages/ui/src/lib/markdown.ts | 12 ++++++++
packages/ui/src/styles/markdown.css | 26 +++++++++++++++--
5 files changed, 89 insertions(+), 4 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 4bbddcbb..91aaed6f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8306,6 +8306,31 @@
"safe-buffer": "^5.0.1"
}
},
+ "node_modules/katex": {
+ "version": "0.16.45",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz",
+ "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==",
+ "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",
"dev": true,
@@ -8514,6 +8539,16 @@
"node": ">= 18"
}
},
+ "node_modules/marked-katex-extension": {
+ "version": "5.1.8",
+ "resolved": "https://registry.npmjs.org/marked-katex-extension/-/marked-katex-extension-5.1.8.tgz",
+ "integrity": "sha512-TsV9OCHHDjVBf4IH0RSjLs4Eqsjj8HGfmVCKlimrS391EtBBxzXj2gBYdF9tY7f7oXu9tb1kHV86ExJsG3iMhw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "katex": ">=0.16 <0.17",
+ "marked": ">=4 <19"
+ }
+ },
"node_modules/matcher": {
"version": "3.0.0",
"dev": true,
@@ -13309,8 +13344,10 @@
"ansi-sequence-parser": "^1.1.3",
"debug": "^4.4.3",
"github-markdown-css": "^5.8.1",
+ "katex": "^0.16.45",
"lucide-solid": "^0.300.0",
"marked": "^12.0.0",
+ "marked-katex-extension": "^5.1.8",
"monaco-editor": "^0.52.2",
"qrcode": "^1.5.3",
"shiki": "^3.13.0",
diff --git a/packages/ui/package.json b/packages/ui/package.json
index b96c7e01..69233307 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -25,8 +25,10 @@
"ansi-sequence-parser": "^1.1.3",
"debug": "^4.4.3",
"github-markdown-css": "^5.8.1",
+ "katex": "^0.16.45",
"lucide-solid": "^0.300.0",
"marked": "^12.0.0",
+ "marked-katex-extension": "^5.1.8",
"monaco-editor": "^0.52.2",
"qrcode": "^1.5.3",
"shiki": "^3.13.0",
diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx
index 56889ca3..4ec0c5d7 100644
--- a/packages/ui/src/components/markdown.tsx
+++ b/packages/ui/src/components/markdown.tsx
@@ -9,6 +9,18 @@ const log = getLogger("session")
type MarkdownModule = typeof import("../lib/markdown")
+interface ResolvedMarkdownSnapshot {
+ part: TextPart
+ text: string
+ themeKey: string
+ highlightEnabled: boolean
+ escapeRawHtml: boolean
+ partId: string | undefined
+ cacheId: string
+ version: string
+ requestKey: string
+}
+
let markdownModulePromise: Promise | null = null
function loadMarkdownModule(): Promise {
@@ -124,7 +136,7 @@ export function Markdown(props: MarkdownProps) {
})
const commitCacheEntry = (
- snapshot: ReturnType,
+ snapshot: ResolvedMarkdownSnapshot,
renderedHtml: string,
options?: { cache?: boolean },
) => {
@@ -141,7 +153,7 @@ export function Markdown(props: MarkdownProps) {
notifyRendered()
}
- const renderSnapshot = async (snapshot: ReturnType) => {
+ const renderSnapshot = async (snapshot: ResolvedMarkdownSnapshot): Promise => {
const markdown = await loadMarkdownModule()
markdown.setMarkdownTheme(snapshot.themeKey === "dark")
const rendered = await markdown.renderMarkdown(snapshot.text, {
diff --git a/packages/ui/src/lib/markdown.ts b/packages/ui/src/lib/markdown.ts
index 86bc09e3..70bec0a5 100644
--- a/packages/ui/src/lib/markdown.ts
+++ b/packages/ui/src/lib/markdown.ts
@@ -1,4 +1,5 @@
import { marked } from "marked"
+import markedKatex from "marked-katex-extension"
import { getLogger } from "./logger"
import { tGlobal } from "./i18n"
import type { Highlighter } from "shiki/bundle/full"
@@ -16,6 +17,11 @@ let rendererSetup = false
let shikiModulePromise: Promise | null = null
let bundledLanguagesCache: typeof import("shiki/bundle/full")["bundledLanguages"] | null = null
+// Math rendering is handled by marked-katex-extension (registered in setupRenderer).
+// Delimiter rules, boundaries, CJK punctuation, and block/inline rendering are
+// all delegated to the maintained extension so we avoid ~200 lines of fragile
+// hand-rolled tokenizer code.
+
const ALLOWED_RAW_HTML_TAGS = new Set([
"a",
"blockquote",
@@ -374,6 +380,12 @@ function setupRenderer(isDark: boolean) {
gfm: true,
})
+ marked.use(markedKatex({
+ throwOnError: false,
+ nonStandard: true,
+ strict: "ignore",
+ }))
+
const renderer = new marked.Renderer()
renderer.code = (code: string, lang: string | undefined) => {
diff --git a/packages/ui/src/styles/markdown.css b/packages/ui/src/styles/markdown.css
index bbb4d70f..752182c1 100644
--- a/packages/ui/src/styles/markdown.css
+++ b/packages/ui/src/styles/markdown.css
@@ -1,4 +1,23 @@
-@import "github-markdown-css/github-markdown-light.css" layer(github-markdown-base);
+@import "github-markdown-css/github-markdown-light.css"
+ layer(github-markdown-base);
+@import "katex/dist/katex.min.css" layer(katex-base);
+
+@layer katex-overrides {
+ .markdown-body .katex-display {
+ overflow-x: auto;
+ overflow-y: hidden;
+ padding: 0.25rem 0;
+ }
+
+ .markdown-body .katex {
+ font-size: 1.05em;
+ }
+
+ .markdown-body .katex .frac-line {
+ border-bottom-width: 0.1em !important;
+ border-color: currentColor !important;
+ }
+}
@layer components {
.markdown-body {
@@ -239,7 +258,10 @@
border-radius: 4px;
cursor: pointer;
color: var(--text-secondary);
- transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
+ transition:
+ background-color 150ms ease,
+ color 150ms ease,
+ border-color 150ms ease;
margin-inline-start: auto;
font-size: var(--font-size-sm);
}