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
26 changes: 26 additions & 0 deletions src/components/ai-elements/math-delimiters.parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,32 @@ describe("remark-math with singleDollarTextMath: false", () => {
expect(parseMath(normalizeMathDelimiters(text))).toEqual([])
}
})

it("does not rewrite \\(...\\) inside a fence nested in a longer fence", () => {
// ````md wraps a ```js block; the inner ``` must not close the outer
// ```` fence, so the code stays verbatim.
const text = "````md\n```js\nconst x = 1 // \\(x\\)\n```\n````"
expect(normalizeMathDelimiters(text)).toBe(text)
expect(parseMath(normalizeMathDelimiters(text))).toEqual([])
})

it("does not rewrite \\(...\\) inside a still-open fence (streaming mid-state)", () => {
// A code block whose closing fence has not streamed in yet runs to EOF;
// its contents must not be touched while it is transiently unclosed.
const text = "Here is the file:\n```tex\n\\(a+b\\)\n"
expect(normalizeMathDelimiters(text)).toBe(text)
})

it("still rewrites \\(...\\) in prose between two fenced blocks", () => {
const text =
"```\ncode1 \\(keep\\)\n```\nprose \\(x\\) here\n```\ncode2\n```"
const out = normalizeMathDelimiters(text)
expect(out).toContain("$$x$$")
expect(out).toContain("\\(keep\\)")
expect(parseMath(out)).toEqual([
{ type: "inlineMath", value: "x", meta: null },
])
})
})

describe("block structure around a normalized formula", () => {
Expand Down
10 changes: 9 additions & 1 deletion src/components/ai-elements/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,16 @@ export function normalizeMathDelimiters(text: string): string {
saved.push(m)
return `\0CBLK${saved.length - 1}\0`
}
// A fenced block closes only on a fence of the SAME char run at least as
// long as its opener (CommonMark 4.5), and an unclosed fence runs to EOF.
// Pairing any 3+ run with the next one (the old `` `{3,}[\s\S]*?`{3,} ``)
// mis-split a ```` ````md ```` block at its inner ``` ``` ```` fence and left
// the code inside unmasked, so `\(...\)` there got rewritten — and every
// streaming code block hit the same rewrite while its closing fence was
// still pending. Capture the opener and require the closer to be `\1`+ (or
// end of string).
const masked = text.replace(
/`{3,}[\s\S]*?`{3,}|~{3,}[\s\S]*?~{3,}|`[^`\n]+`/g,
/(`{3,})[\s\S]*?(?:\1`*|$)|(~{3,})[\s\S]*?(?:\2~*|$)|`[^`\n]+`/g,
placeholder
)
// Fold line endings only after masking, so the offsets and line scans below
Expand Down
Loading