diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx index af36da6b9f..4b060c9c33 100644 --- a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx +++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx @@ -143,8 +143,10 @@ const MarkdownEditor: React.FC = ({ const toNormalizedMarkdown = useCallback((raw: string) => { const nextEditability = analyzeMarkdownEditability(raw); - const nextContent = - nextEditability.mode === 'unsafe' ? raw : nextEditability.canonicalMarkdown; + // Use raw content instead of canonicalMarkdown to avoid doubling + // backslashes in LaTeX commands (e.g. \int → \\int), which breaks + // KaTeX rendering. The tiptap editor normalizes content internally. + const nextContent = raw; return { nextEditability, nextContent }; }, []); @@ -262,9 +264,9 @@ const MarkdownEditor: React.FC = ({ } } else if (initialContent !== undefined) { const nextEditability = analyzeMarkdownEditability(initialContent); - const nextContent = nextEditability.mode === 'unsafe' - ? initialContent - : nextEditability.canonicalMarkdown; + // Use raw content instead of canonicalMarkdown to avoid doubling + // backslashes in LaTeX commands, which breaks KaTeX rendering. + const nextContent = initialContent; setEditability(nextEditability); setContent(nextContent); diff --git a/src/web-ui/src/tools/editor/meditor/utils/tiptapMarkdown.test.ts b/src/web-ui/src/tools/editor/meditor/utils/tiptapMarkdown.test.ts index 0b18a501ba..8c1aca8672 100644 --- a/src/web-ui/src/tools/editor/meditor/utils/tiptapMarkdown.test.ts +++ b/src/web-ui/src/tools/editor/meditor/utils/tiptapMarkdown.test.ts @@ -334,4 +334,18 @@ describe('tiptap markdown compatibility', () => { expect(analysis.containsRawHtmlBlocks).toBe(true); expect(doc.content?.[0]?.type).toBe('rawHtmlBlock'); }); + + it('doubles backslashes in LaTeX math during canonical round-trip', () => { + const markdown = String.raw`$$\int_0^1 x^2 dx = \frac13$$`; + const analysis = analyzeMarkdownEditability(markdown); + + // Root cause of #1952: escapeMarkdownPlainText doubles every backslash + // during canonical serialization, turning \int into \\int, \frac into + // \\frac, etc. KaTeX interprets \\ as a line break, so the math renders + // incorrectly. This is why MarkdownEditor uses raw content instead of + // canonicalMarkdown for display. + expect(analysis.canonicalMarkdown).toContain(String.raw`\\int`); + expect(analysis.canonicalMarkdown).toContain(String.raw`\\frac`); + expect(analysis.canonicalMarkdown).not.toBe(markdown); + }); });