From 46556f4656db1a96df2e9717ccfb72305d067e51 Mon Sep 17 00:00:00 2001 From: xlx1212 Date: Wed, 5 Aug 2026 01:55:05 +0800 Subject: [PATCH] fix(markdown-editor): preserve LaTeX backslashes for KaTeX rendering analyzeMarkdownEditability canonicalizes markdown through a tiptap round-trip that calls escapeMarkdownPlainText, which doubles every backslash. This turns LaTeX commands like \int into \\int, \frac into \\frac, etc. KaTeX interprets \\ as a line break, breaking math rendering in the file viewer preview. Fix: use raw markdown content instead of canonicalMarkdown in both content normalization paths (toNormalizedMarkdown and initialContent loading). The tiptap editor normalizes content internally, so the canonical form is not needed for display. Add a root-cause test in tiptapMarkdown.test.ts documenting that canonicalMarkdown doubles backslashes in LaTeX math content. Closes #1952 --- .../src/tools/editor/components/MarkdownEditor.tsx | 12 +++++++----- .../editor/meditor/utils/tiptapMarkdown.test.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) 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); + }); });