Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,14 @@
"@tailwindcss/postcss": "^4.1.18",
"@vitejs/plugin-react": "^5.1.2",
"autoprefixer": "^10.4.23",
"hast-util-to-html": "^9.0.5",
"jsdom": "^26.1.0",
"postcss": "^8.5.6",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"unified": "^11.0.5",
"vite": "^7.3.0",
"vitest": "^4.0.16"
}
Expand Down
54 changes: 53 additions & 1 deletion web/src/components/assistant-ui/markdown-text.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { describe, expect, it } from 'vitest'
import { unified } from 'unified'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] This test now imports unified, remark-parse, remark-rehype, and hast-util-to-html directly, but the web package does not declare any of them. That makes bun run test:web depend on transitive dependencies from other markdown packages, so a valid dependency update can break this test without touching it.

Suggested fix:

{
    "devDependencies": {
        "hast-util-to-html": "^9.0.5",
        "remark-parse": "^11.0.0",
        "remark-rehype": "^11.1.2",
        "unified": "^11.0.5"
    }
}

import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import { toHtml } from 'hast-util-to-html'
import remarkBreaks from 'remark-breaks'
import remarkNonHttpsAutolink from '@/lib/remark-non-https-autolink'
import remarkStripCjkAutolink from '@/lib/remark-strip-cjk-autolink'
import { MARKDOWN_PLUGINS, MARKDOWN_PLUGINS_WITH_BREAKS } from '@/components/assistant-ui/markdown-text'
import {
MARKDOWN_PLUGINS,
MARKDOWN_PLUGINS_WITH_BREAKS,
MARKDOWN_REHYPE_PLUGINS,
} from '@/components/assistant-ui/markdown-text'

describe('MARKDOWN_PLUGINS integration', () => {
it('includes remarkNonHttpsAutolink', () => {
Expand All @@ -21,3 +29,47 @@ describe('MARKDOWN_PLUGINS integration', () => {
expect(MARKDOWN_PLUGINS_WITH_BREAKS).toContain(remarkBreaks)
})
})

function render(markdown: string): string {
const processor = unified()
.use(remarkParse)
.use(MARKDOWN_PLUGINS)
.use(remarkRehype)
.use(MARKDOWN_REHYPE_PLUGINS)
const tree = processor.runSync(processor.parse(markdown))
return toHtml(tree as never)
}

describe('MARKDOWN_PLUGINS — currency prose vs KaTeX', () => {
// Regression: prose with multiple "$N" amounts must NOT be eaten by KaTeX.
// remarkMath is configured with `singleDollarTextMath: false` so single
// dollar signs are treated as literal text, matching GitHub markdown.

it('does not render single-$ currency amounts as KaTeX math', () => {
const md = "The plan is $200/mo and the bill is $80 — total $400 saved."
const html = render(md)
expect(html).not.toContain('class="katex"')
expect(html).not.toContain('<math')
expect(html).toContain('$200')
expect(html).toContain('$80')
expect(html).toContain('$400')
})

it('does not render the reported real-world prose as KaTeX', () => {
// Lifted (paraphrased) from the bug report: paragraph with multiple
// "$N" amounts and apostrophes that previously collapsed into a single
// KaTeX block and stripped whitespace from the running text.
const md = "Cursor's UI quotes the ratio: at least $400 of API usage on a $200 plan. That's 2:1."
const html = render(md)
expect(html).not.toContain('class="katex"')
expect(html).not.toContain('<math')
expect(html).toContain('$400')
expect(html).toContain('$200')
})

it('still renders block math with $$...$$ on its own lines', () => {
const md = "Before\n\n$$\nE = mc^2\n$$\n\nAfter"
const html = render(md)
expect(html).toContain('class="katex"')
})
})
9 changes: 8 additions & 1 deletion web/src/components/assistant-ui/markdown-text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,17 @@ import type { MarkdownTextPrimitiveProps } from '@assistant-ui/react-markdown'
// from them. Both must come before remarkMath (to avoid treating TeX as URI).
// remarkFilePathLinks runs last to convert file paths → links after all other
// transforms have settled.
//
// remarkMath is configured with `singleDollarTextMath: false` so that prose
// containing currency amounts (e.g. "$200/mo ... $80 bill") is not garbled
// into KaTeX output. Block math `$$...$$` (on its own line) still works.
// This matches GitHub-flavored markdown behavior. The option lives on the
// shared TAIL so both MARKDOWN_PLUGINS (default) and MARKDOWN_PLUGINS_WITH_BREAKS
// (user-prompt rendering with hard breaks) inherit the fix.
const MARKDOWN_PLUGIN_TAIL = [
remarkNonHttpsAutolink,
remarkStripCjkAutolink,
remarkMath,
[remarkMath, { singleDollarTextMath: false }],
remarkDisableIndentedCode,
remarkFilePathLinks, // upstream — file path → link conversion, runs last
] satisfies NonNullable<MarkdownTextPrimitiveProps['remarkPlugins']>
Expand Down
Loading