From 37444eb7f16111f2efd2ffa5d3bbc4b5cc5bec5c Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:03:12 +0800 Subject: [PATCH] fix editor image markdown roundtrip Co-authored-by: multica-agent --- .../extensions/file-card-markdown.test.ts | 4 +- .../image-markdown-roundtrip.test.ts | 88 +++++++++++++++++++ packages/views/editor/extensions/index.ts | 4 +- 3 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 packages/views/editor/extensions/image-markdown-roundtrip.test.ts diff --git a/packages/views/editor/extensions/file-card-markdown.test.ts b/packages/views/editor/extensions/file-card-markdown.test.ts index 1689c192c..348b6250b 100644 --- a/packages/views/editor/extensions/file-card-markdown.test.ts +++ b/packages/views/editor/extensions/file-card-markdown.test.ts @@ -27,14 +27,14 @@ describe("ImageExtension.renderMarkdown", () => { expect(md).toContain("\\\\"); expect(md).toContain("\\["); expect(md).toContain("\\("); - expect(md).toMatch(/^!\[.*\]\(https:\/\/cdn\.example\.com\/img\.png\)\n\n$/); + expect(md).toMatch(/^!\[.*\]\(https:\/\/cdn\.example\.com\/img\.png\)$/); }); it("leaves normal alt text unchanged", () => { const md = imageRenderMarkdown({ attrs: { src: "https://cdn.example.com/img.png", alt: "screenshot" }, }); - expect(md).toBe("![screenshot](https://cdn.example.com/img.png)\n\n"); + expect(md).toBe("![screenshot](https://cdn.example.com/img.png)"); }); }); diff --git a/packages/views/editor/extensions/image-markdown-roundtrip.test.ts b/packages/views/editor/extensions/image-markdown-roundtrip.test.ts new file mode 100644 index 000000000..63e527829 --- /dev/null +++ b/packages/views/editor/extensions/image-markdown-roundtrip.test.ts @@ -0,0 +1,88 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { Editor } from "@tiptap/core"; +import StarterKit from "@tiptap/starter-kit"; +import { Markdown } from "@tiptap/markdown"; +import { ImageExtension } from "./index"; + +const IMAGE_URL = "https://cdn.example.com/screen.png"; +const IMAGE_MD = `![screen](${IMAGE_URL})`; + +let editors: Editor[] = []; + +function makeEditor() { + const element = document.createElement("div"); + document.body.appendChild(element); + const editor = new Editor({ + element, + extensions: [ + StarterKit, + ImageExtension, + Markdown.configure({ indentation: { style: "space", size: 3 } }), + ], + }); + editors.push(editor); + return editor; +} + +function roundTripMany(input: string, rounds: number) { + const editor = makeEditor(); + const outputs: string[] = []; + let markdown = input; + + for (let i = 0; i < rounds; i++) { + editor.commands.setContent(markdown, { contentType: "markdown" }); + markdown = editor.getMarkdown().trimEnd(); + outputs.push(markdown); + } + + return outputs; +} + +function findParagraphTexts(editor: Editor) { + return Array.from(editor.view.dom.querySelectorAll("p")).map( + (p) => p.textContent ?? "", + ); +} + +afterEach(() => { + for (const editor of editors) editor.destroy(); + editors = []; + document.body.innerHTML = ""; +}); + +describe("ImageExtension markdown round-trip", () => { + it("does not accumulate blank paragraphs around an internal image", () => { + const input = ["before", "", IMAGE_MD, "", "after"].join("\n"); + const outputs = roundTripMany(input, 5); + + expect(outputs).toEqual([input, input, input, input, input]); + }); + + it("does not reparse a live image followed by text into an empty paragraph", () => { + const editor = makeEditor(); + editor.commands.setContent({ + type: "doc", + content: [ + { + type: "image", + attrs: { src: IMAGE_URL, alt: "screen" }, + }, + { + type: "paragraph", + content: [{ type: "text", text: "after" }], + }, + ], + }); + + const emitted = editor.getMarkdown().trimEnd(); + expect(emitted).toBe([IMAGE_MD, "", "after"].join("\n")); + + const reparsed = makeEditor(); + reparsed.commands.setContent(emitted, { contentType: "markdown" }); + + expect(reparsed.getHTML()).toBe( + `screen

after

`, + ); + expect(findParagraphTexts(reparsed)).toEqual(["after"]); + }); +}); diff --git a/packages/views/editor/extensions/index.ts b/packages/views/editor/extensions/index.ts index 71f3df583..82a5a9ea8 100644 --- a/packages/views/editor/extensions/index.ts +++ b/packages/views/editor/extensions/index.ts @@ -82,9 +82,9 @@ export const ImageExtension = Image.extend({ const alt = escapeMarkdownLabel(node.attrs?.alt || ""); const title = node.attrs?.title; if (title) { - return `![${alt}](${src} "${title}")\n\n`; + return `![${alt}](${src} "${title}")`; } - return `![${alt}](${src})\n\n`; + return `![${alt}](${src})`; }, }).configure({ inline: false,