From e1124d170d10cd7e8f5d9ec091fc3888f2c8567b Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:29:16 +0530 Subject: [PATCH] feat: update LaTeX delimiter conversion for Streamdown compatibility --- surfsense_web/components/markdown-viewer.tsx | 64 ++++++++++++-------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/surfsense_web/components/markdown-viewer.tsx b/surfsense_web/components/markdown-viewer.tsx index bf5310303..81310e751 100644 --- a/surfsense_web/components/markdown-viewer.tsx +++ b/surfsense_web/components/markdown-viewer.tsx @@ -29,45 +29,59 @@ function stripOuterMarkdownFence(content: string): string { } /** - * Convert all LaTeX delimiter styles to the dollar-sign syntax - * that remark-math understands, and normalise edge-cases that - * commonly appear in LLM-generated markdown. + * Convert all LaTeX delimiter styles to the double-dollar syntax + * that Streamdown's @streamdown/math plugin understands. * - * \[...\] → $$ ... $$ (block / display math) - * \(...\) → $ ... $ (inline math) - * \begin{equation}...\end{equation} → $$ ... $$ (block math) - * \begin{displaymath}...\end{displaymath} → $$ ... $$ (block math) - * \begin{math}...\end{math} → $ ... $ (inline math) - * same-line $$…$$ → $ ... $ (inline math — display math - * can't live inside table cells) - * `$$ … $$` → $$ … $$ (strip wrapping backtick code) - * `$ … $` → $ … $ (strip wrapping backtick code) + * Streamdown math conventions (different from remark-math!): + * $$...$$ on the SAME line → inline math + * $$\n...\n$$ on SEPARATE lines → block (display) math + * + * Conversions performed: + * \[...\] → $$\n ... \n$$ (block math) + * \(...\) → $$...$$ (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) + * $...$ → $$...$$ (normalise single-$ to double-$$) */ function convertLatexDelimiters(content: string): string { - // 1. Block math: \[...\] → $$...$$ - content = content.replace(/\\\[([\s\S]*?)\\\]/g, (_, inner) => `$$${inner}$$`); - // 2. Inline math: \(...\) → $...$ - content = content.replace(/\\\(([\s\S]*?)\\\)/g, (_, inner) => `$${inner}$`); - // 3. Block: \begin{equation}...\end{equation} → $$...$$ + // 1. Block math: \[...\] → $$\n...\n$$ (display math on separate lines) + content = content.replace( + /\\\[([\s\S]*?)\\\]/g, + (_, inner) => `\n$$\n${inner.trim()}\n$$\n`, + ); + // 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( /\\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( /\\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( /\\begin\{math\}([\s\S]*?)\\end\{math\}/g, - (_, inner) => `$${inner}$`, + (_, inner) => `$$${inner.trim()}$$`, ); // 6. Strip backtick wrapping around math: `$$...$$` → $$...$$ and `$...$` → $...$ content = content.replace(/`(\${1,2})((?:(?!\1).)+)\1`/g, "$1$2$1"); - // 7. Same-line $$...$$ → $...$ (inline math) so it works inside table cells. - // True display math has $$ on its own line, so this only affects inline usage. - content = content.replace(/\$\$([^\n]+?)\$\$/g, (_, inner) => `$${inner}$`); + // 7. Normalise single-dollar $...$ to double-dollar $$...$$ so they render + // reliably in Streamdown (single-$ has strict no-space rules that often fail). + // We match $…$ where the content starts with a backslash (LaTeX command) + // to avoid converting currency like $50. + content = content.replace( + /(? `$$${inner.trim()}$$`, + ); return content; }