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}$`:
<img width="1016" height="83" alt="image"
src="https://github.com/user-attachments/assets/7a7534c0-3128-4a98-a01f-336db42e0f26"
/>

`$$ \mathbb{R} $$`:
<img width="1016" height="89" alt="image"
src="https://github.com/user-attachments/assets/9da7d2e4-06cc-41e8-b598-eab8b3be7677"
/>

```markdown
$$
\mathbb{R}
$$
```
:
<img width="1016" height="88" alt="image"
src="https://github.com/user-attachments/assets/a1be91e3-7ff1-4cf9-8982-c2657348317d"
/>


Closes #379

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Yao Jianxuan 2026-05-07 17:25:43 +08:00 committed by GitHub
parent 0f7267743e
commit dd72c8a8d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 89 additions and 4 deletions

37
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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<MarkdownModule> | null = null
function loadMarkdownModule(): Promise<MarkdownModule> {
@ -124,7 +136,7 @@ export function Markdown(props: MarkdownProps) {
})
const commitCacheEntry = (
snapshot: ReturnType<typeof resolved>,
snapshot: ResolvedMarkdownSnapshot,
renderedHtml: string,
options?: { cache?: boolean },
) => {
@ -141,7 +153,7 @@ export function Markdown(props: MarkdownProps) {
notifyRendered()
}
const renderSnapshot = async (snapshot: ReturnType<typeof resolved>) => {
const renderSnapshot = async (snapshot: ResolvedMarkdownSnapshot): Promise<void> => {
const markdown = await loadMarkdownModule()
markdown.setMarkdownTheme(snapshot.themeKey === "dark")
const rendered = await markdown.renderMarkdown(snapshot.text, {

View file

@ -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<typeof import("shiki/bundle/full")> | 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) => {

View file

@ -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);
}