feat: update LaTeX delimiter conversion for Streamdown compatibility

This commit is contained in:
Anish Sarkar 2026-02-14 14:29:16 +05:30
parent 8746051670
commit e1124d170d

View file

@ -29,45 +29,59 @@ function stripOuterMarkdownFence(content: string): string {
} }
/** /**
* Convert all LaTeX delimiter styles to the dollar-sign syntax * Convert all LaTeX delimiter styles to the double-dollar syntax
* that remark-math understands, and normalise edge-cases that * that Streamdown's @streamdown/math plugin understands.
* commonly appear in LLM-generated markdown.
* *
* \[...\] $$ ... $$ (block / display math) * Streamdown math conventions (different from remark-math!):
* \(...\) $ ... $ (inline math) * $$...$$ on the SAME line inline math
* \begin{equation}...\end{equation} $$ ... $$ (block math) * $$\n...\n$$ on SEPARATE lines block (display) math
* \begin{displaymath}...\end{displaymath} $$ ... $$ (block math) *
* \begin{math}...\end{math} $ ... $ (inline math) * Conversions performed:
* same-line $$$$ $ ... $ (inline math display math * \[...\] $$\n ... \n$$ (block math)
* can't live inside table cells) * \(...\) $$...$$ (inline math, same line)
* \begin{equation}...\end{equation} $$\n ... \n$$ (block math)
* \begin{displaymath}...\end{displaymath} $$\n ... \n$$ (block math)
* \begin{math}...\end{math} $$...$$ (inline math, same line)
* `$$$$` $$ $$ (strip wrapping backtick code) * `$$$$` $$ $$ (strip wrapping backtick code)
* `$$` $ $ (strip wrapping backtick code) * `$$` $ $ (strip wrapping backtick code)
* $...$ $$...$$ (normalise single-$ to double-$$)
*/ */
function convertLatexDelimiters(content: string): string { function convertLatexDelimiters(content: string): string {
// 1. Block math: \[...\] → $$...$$ // 1. Block math: \[...\] → $$\n...\n$$ (display math on separate lines)
content = content.replace(/\\\[([\s\S]*?)\\\]/g, (_, inner) => `$$${inner}$$`); content = content.replace(
// 2. Inline math: \(...\) → $...$ /\\\[([\s\S]*?)\\\]/g,
content = content.replace(/\\\(([\s\S]*?)\\\)/g, (_, inner) => `$${inner}$`); (_, inner) => `\n$$\n${inner.trim()}\n$$\n`,
// 3. Block: \begin{equation}...\end{equation} → $$...$$ );
// 2. Inline math: \(...\) → $$...$$ (inline math on same line)
content = content.replace(
/\\\(([\s\S]*?)\\\)/g,
(_, inner) => `$$${inner.trim()}$$`,
);
// 3. Block: \begin{equation}...\end{equation} → $$\n...\n$$
content = content.replace( content = content.replace(
/\\begin\{equation\}([\s\S]*?)\\end\{equation\}/g, /\\begin\{equation\}([\s\S]*?)\\end\{equation\}/g,
(_, inner) => `$$${inner}$$`, (_, inner) => `\n$$\n${inner.trim()}\n$$\n`,
); );
// 4. Block: \begin{displaymath}...\end{displaymath} → $$...$$ // 4. Block: \begin{displaymath}...\end{displaymath} → $$\n...\n$$
content = content.replace( content = content.replace(
/\\begin\{displaymath\}([\s\S]*?)\\end\{displaymath\}/g, /\\begin\{displaymath\}([\s\S]*?)\\end\{displaymath\}/g,
(_, inner) => `$$${inner}$$`, (_, inner) => `\n$$\n${inner.trim()}\n$$\n`,
); );
// 5. Inline: \begin{math}...\end{math} → $...$ // 5. Inline: \begin{math}...\end{math} → $$...$$
content = content.replace( content = content.replace(
/\\begin\{math\}([\s\S]*?)\\end\{math\}/g, /\\begin\{math\}([\s\S]*?)\\end\{math\}/g,
(_, inner) => `$${inner}$`, (_, inner) => `$$${inner.trim()}$$`,
); );
// 6. Strip backtick wrapping around math: `$$...$$` → $$...$$ and `$...$` → $...$ // 6. Strip backtick wrapping around math: `$$...$$` → $$...$$ and `$...$` → $...$
content = content.replace(/`(\${1,2})((?:(?!\1).)+)\1`/g, "$1$2$1"); content = content.replace(/`(\${1,2})((?:(?!\1).)+)\1`/g, "$1$2$1");
// 7. Same-line $$...$$ → $...$ (inline math) so it works inside table cells. // 7. Normalise single-dollar $...$ to double-dollar $$...$$ so they render
// True display math has $$ on its own line, so this only affects inline usage. // reliably in Streamdown (single-$ has strict no-space rules that often fail).
content = content.replace(/\$\$([^\n]+?)\$\$/g, (_, inner) => `$${inner}$`); // We match $…$ where the content starts with a backslash (LaTeX command)
// to avoid converting currency like $50.
content = content.replace(
/(?<!\$)\$(?!\$)(\\[a-zA-Z][\s\S]*?)(?<!\$)\$(?!\$)/g,
(_, inner) => `$$${inner.trim()}$$`,
);
return content; return content;
} }