mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
feat(web): render LaTeX math in chat via KaTeX (#1035)
* 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.
This commit is contained in:
parent
884b65a040
commit
ea03f30e51
4 changed files with 42 additions and 1 deletions
5
.changeset/web-katex-math.md
Normal file
5
.changeset/web-katex-math.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, inject, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { MarkdownRender } from 'markstream-vue';
|
||||
import { MarkdownRender, enableKatex } from 'markstream-vue';
|
||||
import type { MarkdownIt } from 'markstream-vue';
|
||||
import { useIsDark } from '../../composables/useIsDark';
|
||||
import type { FilePreviewRequest } from '../../types';
|
||||
import { collectFilePathAliases, findFilePathLinks } from '../../lib/filePathLinks';
|
||||
|
|
@ -13,6 +14,23 @@ import { copyTextToClipboard } from '../../lib/clipboard';
|
|||
// Terminal Pro. Importing the same file from multiple components is a no-op
|
||||
// after the first (Vite dedups the CSS import).
|
||||
import 'markstream-vue/index.px.css';
|
||||
// KaTeX math: markstream renders `$$…$$` display math only after the optional
|
||||
// katex peer is enabled, and its stylesheet (+ bundled fonts) is what gives
|
||||
// formulas their layout. enableKatex() registers the default `import('katex')`
|
||||
// loader; it runs once on first import of this module and is safe at module
|
||||
// scope. Without the CSS the math renders unstyled, so both must travel
|
||||
// together.
|
||||
import 'katex/dist/katex.min.css';
|
||||
enableKatex();
|
||||
|
||||
// Only `$$…$$` display math is rendered; single `$` inline math is disabled so
|
||||
// prices, env vars, and shell paths (`$5`, `$PATH`, `$HOME/bin`) stay literal
|
||||
// without any escaping or code-detection gymnastics. `math_block` (the $$ rule)
|
||||
// is left enabled.
|
||||
function disableInlineMath(md: MarkdownIt): MarkdownIt {
|
||||
md.inline.ruler.disable('math');
|
||||
return md;
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
|
@ -353,6 +371,7 @@ function copyDiff(code: string, idx: number) {
|
|||
<MarkdownRender
|
||||
v-if="seg.kind === 'md'"
|
||||
:content="seg.text"
|
||||
:custom-markdown-it="disableInlineMath"
|
||||
mode="chat"
|
||||
:code-renderer="renderPlan.codeRenderer"
|
||||
:is-dark="isDark"
|
||||
|
|
@ -575,6 +594,19 @@ function copyDiff(code: string, idx: number) {
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* KaTeX math. Colour already inherits (--text) since KaTeX draws with
|
||||
currentColor, so the only skinning needed is layout: let a wide display
|
||||
formula scroll inside its own box instead of overflowing the chat column and
|
||||
breaking the mobile layout. Inline math stays in the text flow. */
|
||||
.md :deep(.katex-display) {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
/* room for the horizontal scrollbar so it doesn't clip the bottom of the
|
||||
formula (e.g. integral/sum subscripts) */
|
||||
padding: 2px 0 6px;
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
/* Blockquote */
|
||||
.md :deep(blockquote) {
|
||||
margin: 0.5em 0;
|
||||
|
|
|
|||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -163,6 +163,9 @@ importers:
|
|||
'@xterm/xterm':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
katex:
|
||||
specifier: ^0.16.22
|
||||
version: 0.16.47
|
||||
markstream-vue:
|
||||
specifier: 1.0.3
|
||||
version: 1.0.3(katex@0.16.47)(mermaid@11.15.0)(stream-markdown@0.0.16(react@19.2.5)(shiki@4.2.0)(vue@3.5.35(typescript@6.0.2)))(vue-i18n@11.4.5(vue@3.5.35(typescript@6.0.2)))(vue@3.5.35(typescript@6.0.2))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue