From 61f668f3940ac588beaa5491bf105acda557add8 Mon Sep 17 00:00:00 2001 From: Okan YILDIRIM <63511519+ragokan@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:37:55 +0000 Subject: [PATCH] fix(app): preserve table headers during source adaptation Replace the preceding line ending when inserting a table separator. This keeps the separator before the header's source range, so entity and local image edits cannot consume the paragraph break. --- .../src/components/markdown/renderer.test.ts | 48 +++++++++++++++++++ .../app/src/components/markdown/source.ts | 3 +- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/markdown/renderer.test.ts b/packages/app/src/components/markdown/renderer.test.ts index 85b31eed46..c1c19f627d 100644 --- a/packages/app/src/components/markdown/renderer.test.ts +++ b/packages/app/src/components/markdown/renderer.test.ts @@ -146,6 +146,18 @@ it.each([ "Notes\n\nItem | Detail\n:--- | ---:\nA | one", ["Document", "Paragraph", "Table"], ], + [ + "an entity at the start of a header without outer pipes", + "Notes\nAT&T | Detail\n--- | ---\nA | one", + "Notes\n\nAT\\&T | Detail\n--- | ---\nA | one", + ["Document", "Paragraph", "Table"], + ], + [ + "a local image at the start of a header without outer pipes", + "Notes\n![Local](assets/a.png) | Detail\n--- | ---\nA | one", + "Notes\n\nLocal | Detail\n--- | ---\nA | one", + ["Document", "Paragraph", "Table"], + ], [ "CRLF lines", "Notes\r\n| Item |\r\n| --- |\r\n| A |", @@ -205,6 +217,42 @@ it.each([ expect(blocks(await parseMarkdown(rendered, flags))).toEqual(expected); }); +it.each([ + [ + "a literal backslash", + "Notes\\\n| Item |\n| --- |\n| A |", + "Notes\\\n\n| Item |\n| --- |\n| A |", + "Notes\\", + ], + [ + "an entity and literal backslash with LF lines", + "AT&T\\\n| Item |\n| --- |\n| A |", + "AT\\&T\\\n\n| Item |\n| --- |\n| A |", + "AT&T\\", + ], + [ + "an entity and literal backslash with CRLF lines", + "AT&T\\\r\n| Item |\r\n| --- |\r\n| A |", + "AT\\&T\\\r\n\r\n| Item |\r\n| --- |\r\n| A |", + "AT&T\\", + ], + [ + "an entity and literal backslash with CR lines", + "AT&T\\\r| Item |\r| --- |\r| A |", + "AT\\&T\\\r\r| Item |\r| --- |\r| A |", + "AT&T\\", + ], +])( + "preserves paragraph text before an interrupting table in %s", + async (_name, source, separated, text) => { + const rendered = prepareMarkdownSource(source).render(); + expect(rendered).toEqual(separated); + const tree = await parseMarkdown(rendered, { hardSoftBreaks: true, latexMath: false }); + expect(tree.children?.map((node) => node.type)).toEqual(["Paragraph", "Table"]); + expect(tree.children?.[0]?.children?.map((node) => node.content).join("")).toEqual(text); + }, +); + it.each([ "```yaml\n---\nkey: value\n---\n```\n", "~~~md\n| Item |\n| --- |\n| A |\n~~~\n", diff --git a/packages/app/src/components/markdown/source.ts b/packages/app/src/components/markdown/source.ts index 177d571000..c1b13633c6 100644 --- a/packages/app/src/components/markdown/source.ts +++ b/packages/app/src/components/markdown/source.ts @@ -216,7 +216,8 @@ export function prepareMarkdownSource( Math.max(source.lastIndexOf("\n", start - 1), source.lastIndexOf("\r", start - 1)) + 1; const newline = source.slice(lineStart - 2, lineStart).replace(/[^\r\n]/g, ""); const blank = source.slice(lineStart, start).replace(/[^>]/g, " ").trimEnd(); - edits.push({ start: lineStart, end: lineStart, text: `${blank}${newline}` }); + const boundary = lineStart - newline.length; + edits.push({ start: boundary, end: lineStart, text: `${newline}${blank}${newline}` }); } if (node.type === "link") { const title = workspaceHoverTitle(node, source.slice(start, end), workspaceRoot);