From ea03f30e5174825049ed4dfedebf8e43fbe751a4 Mon Sep 17 00:00:00 2001 From: qer Date: Thu, 25 Jun 2026 12:47:40 +0800 Subject: [PATCH] feat(web): render LaTeX math in chat via KaTeX (#1035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(web): render LaTeX math in chat via KaTeX * fix(web): keep literal prose dollars out of KaTeX inline math Enabling KaTeX turned plain prose with two dollar-prefixed tokens (`Check $PATH before $HOME`, `costs $5 and $10`) into a single inline formula, since markstream's $…$ tokenizer has no "no whitespace inside the delimiters" rule. Add a postTransformTokens guard that turns a single-$ inline span back into literal text when its content starts or ends with whitespace. Real inline math is written tight (`$E=mc^2$`, `$\frac{1}{2}$`), while the prose false-positives always have whitespace inside the delimiters, so this keeps inline/block math working while leaving prices, env vars, and ranges as readable text. Code spans are already excluded by the tokenizer, and running on the flat token stream also covers dollars nested inside lists and blockquotes. Addresses the Codex review comment on PR #1035. * fix(web): reject compact currency ranges before rendering math The literal-dollar guard only caught prose whose content had whitespace inside the delimiters, so a compact range like `costs $5/$10` still rendered `5/` as a formula and dropped the second dollar. (markstream's own currency check rejects `-`/`~` ranges but not `/`.) Extend the guard to also reject a single-$ span whose content is a numeric amount with a trailing range connector (`/`, `-`, `~`, en/em dash) -- a complete formula never ends in a dangling operator. Scoped to digit-led content so symbolic math is left alone, and numeric math that is not a range (`$5/2$`, `$5-2$`, `$0.5$`) still renders. Added tests for the range cases and the non-range math. Addresses the follow-up Codex review comment on PR #1035. * fix(web): treat shell/path dollar pairs as literal text Adjacent shell variables and PATH-like values (`Use \$HOME/bin:\$PATH`, `\$PATH:\$HOME`) were still rendered as math, because the prose-dollar guard only looked at the span's own content (whitespace inside the delimiters, or a trailing numeric range connector) and never at what touches the delimiters from the outside. Replace the two bespoke heuristics with the two industry-standard rules, now driven by the surrounding text tokens: - Pandoc (tex_math_dollars): no whitespace immediately inside the delimiters. - GitHub: each \$ must be bounded on its outer side by whitespace, a line boundary, or structural punctuation. A letter or digit there means a second prose token, so the span is literal text. The GitHub outer-boundary rule subsumes the old numeric-range check (a closing \$ in \$5/\$10 is followed by a digit) and also catches shell/path cases Pandoc's inner rule misses. Normal math -- including bare \$x\$, \$x^2\$., and (\$x^2\$) -- still renders. Added tests for shell/path values and punctuation-wrapped math. Addresses the third Codex review comment on PR #1035. * fix(web): render math next to CJK punctuation and quotes The outer-boundary guard only accepted ASCII punctuation, so a formula followed by full-width punctuation or wrapped in typographic quotes was misclassified as prose: `公式为 \$E=mc^2\$,其中` and `“\$x\$”` showed raw dollars instead of rendering. Invert the boundary check from an allow-list of ASCII punctuation to a deny-list of ASCII letters/digits. A \$ glued to an ASCII letter/digit still means a second prose token (\$PATH:\$HOME, \$5/\$10), but whitespace, line boundaries, and every other character -- full-width punctuation, CJK ideographs, curly quotes -- is now a valid math boundary, which is the correct behavior for localized prose. Addresses the fourth Codex review comment on PR #1035. * fix(web): preserve later math after literal-dollar spans A prose dollar in front of a real formula in the same inline run (`costs $5 and formula $x$`, `Use \$HOME before $E=mc^2$`) exposed the core limit of the token-level guard: markstream's tokenizer greedily pairs the first literal \$ with the formula's opening \$ before any hook runs, so converting that span back to text could only blank it -- the later formula's opening \$ was already consumed and the formula rendered as raw text. Move the guard from postTransformTokens to a source-level preprocessor that runs before tokenization. escapeProseDollars protects code spans, fenced code blocks, and \$\$…\$\$ display math, then pairs single \$ delimiters using the Pandoc (tight delimiters) and GitHub-style outer-boundary rules: any \$ without a valid partner is escaped as \\\$, so the tokenizer leaves it literal while real formulas -- including ones that come after a prose dollar -- still parse as math. The component now preprocesses each markdown segment's text and the postTransformTokens hook is gone. Rewrote the tests around the string-in/string-out helper, including the prose-before-formula case, code spans, fenced code, and block math. Addresses the fifth Codex review comment on PR #1035. * fix(web): protect indented code blocks before escaping dollars The dollar-escaping preprocessor stashed fenced code blocks, inline code, and display math, but not 4-space / tab indented code blocks. So a snippet like ` echo \$HOME` had its dollar rewritten to `\\$HOME`, and because Markdown renders backslashes literally inside code, the web chat corrupted the code to show a stray backslash. Add an indented-code regex and protect those lines too. Also make the placeholder restore iterative, so nested protected regions (e.g. inline code that looks like display math) restore correctly instead of leaving a placeholder behind. Addresses the sixth Codex review comment on PR #1035. * fix(web): do not treat list-continuation lines as indented code The indented-code regex protected every 4-space line, but inside a list item a 4-space indent is a normal continuation paragraph, not a code block (code under a list marker needs deeper indentation). So a message like `- total\n costs \$5 and \$10` had that nested line stashed as "code", leaving its dollars un-escaped -- and the KaTeX parser then rendered the price range as math. Narrow the indented-code rule to a run of 4-space / tab lines that is preceded by a blank line (or the start of the text). That still protects real top-level indented code blocks and deeper-indented code inside lists, while letting 4-space list-continuation lines get their dollars escaped. Addresses the seventh Codex review comment on PR #1035. * refactor(web): render only $$…$$ display math, drop single-$ inline Enable KaTeX for display math only: disable markstream's inline math rule (`md.inline.ruler.disable('math')`) via customMarkdownIt, leaving the `math_block` rule for $$…$$. Single $ now stays literal everywhere, so prices, env vars, shell paths, and code are never mis-rendered as math -- with no escaping, no code detection, and no preprocessor. This removes the escapeProseDollars normalization layer and all of its code-protection machinery (the 8 review comments it attracted were symptoms of trying to make a lax single-$ tokenizer behave). Display $$…$$ math continues to render via KaTeX. Changeset updated to describe display-math-only support. --- .changeset/web-katex-math.md | 5 +++ apps/kimi-web/package.json | 1 + .../kimi-web/src/components/chat/Markdown.vue | 34 ++++++++++++++++++- pnpm-lock.yaml | 3 ++ 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 .changeset/web-katex-math.md diff --git a/.changeset/web-katex-math.md b/.changeset/web-katex-math.md new file mode 100644 index 000000000..b09dfe676 --- /dev/null +++ b/.changeset/web-katex-math.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Render LaTeX display math (`$$…$$`) in the web chat via KaTeX. Single `$` is intentionally left as literal text, so prices, env vars, and shell paths (e.g. `$PATH`, `$5/$10`, `$HOME/bin`) are never swallowed as a formula. diff --git a/apps/kimi-web/package.json b/apps/kimi-web/package.json index c3a262ada..0f3c60f43 100644 --- a/apps/kimi-web/package.json +++ b/apps/kimi-web/package.json @@ -16,6 +16,7 @@ "@fontsource-variable/jetbrains-mono": "^5.2.8", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", + "katex": "^0.16.22", "markstream-vue": "1.0.3", "shiki": "^4.2.0", "stream-markdown": "^0.0.16", diff --git a/apps/kimi-web/src/components/chat/Markdown.vue b/apps/kimi-web/src/components/chat/Markdown.vue index b90f2afba..dcafb4790 100644 --- a/apps/kimi-web/src/components/chat/Markdown.vue +++ b/apps/kimi-web/src/components/chat/Markdown.vue @@ -2,7 +2,8 @@