From 4a9c50b9c926c86473ee2481c3457db7875ef744 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 08:41:29 +0000 Subject: [PATCH] fix(markdown): end bare URL links at table cell breaks Keep a whitespace boundary before inline line separators so GFM autolinking cannot absorb the next line. Cover URL-to-file and URL-to-URL breaks through the actual parser and link recognizer. --- packages/app/src/components/markdown/html.ts | 4 ++-- .../src/components/markdown/renderer.test.ts | 24 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/app/src/components/markdown/html.ts b/packages/app/src/components/markdown/html.ts index bdc054e5bc..b3bb121e54 100644 --- a/packages/app/src/components/markdown/html.ts +++ b/packages/app/src/components/markdown/html.ts @@ -98,9 +98,9 @@ export function normalizeMarkdownHtml(source: string): string { if (EMPHASIS[name]) return EMPHASIS[name]; if (name === "br") { if (closing) return ""; - // A Unicode line separator renders a break without ending the GFM row. + // A Unicode line separator keeps the row intact; its space ends GFM autolinks. return tableRanges.some(({ start, end }) => offset >= start && offset < end) - ? "\u2028" + ? " \u2028" : " \n"; } const boundary = layoutBoundary(name, source, raw, offset, closing, startsAfterBoundary); diff --git a/packages/app/src/components/markdown/renderer.test.ts b/packages/app/src/components/markdown/renderer.test.ts index 1594399c07..852b274203 100644 --- a/packages/app/src/components/markdown/renderer.test.ts +++ b/packages/app/src/components/markdown/renderer.test.ts @@ -57,9 +57,9 @@ describe("shared Markdown source adaptation", () => { (node.content ?? "") + (node.children?.map(text).join("") ?? ""); expect(rows.map((row) => row.children?.map(text))).toEqual([ ["A", "B"], - ["1", "first\u2028second"], - ["2", "first\u2028second"], - ["3", "first\u2028second"], + ["1", "first \u2028second"], + ["2", "first \u2028second"], + ["3", "first \u2028second"], ]); }); it("keeps table breaks inside a details wrapper without a blank line", async () => { @@ -78,12 +78,16 @@ describe("shared Markdown source adaptation", () => { expect(tables).toHaveLength(1); expect(tables[0].attributes?.bodyRowCount).toBe("1"); expect(tables[0].children?.[1]?.children?.[0]?.children?.[1]?.children).toEqual([ - { type: "Text", content: "first\u2028second" }, + { type: "Text", content: "first \u2028second" }, ]); }); - it("recognizes file links separately on either side of a table break", async () => { + it.each([ + ["src/first.ts", "src/second.ts"], + ["https://example.com/first", "src/second.ts"], + ["https://example.com/first", "https://example.com/second"], + ])("keeps %s and %s separate across a table break", async (first, second) => { const prepared = prepareMarkdownSource( - "| A | B |\n|---|---|\n| 1 | src/first.ts
src/second.ts |", + `| A | B |\n|---|---|\n| 1 | ${first}
${second} |`, "/repo", ); const tree = recognizeTextLinks( @@ -97,17 +101,17 @@ describe("shared Markdown source adaptation", () => { node.children?.forEach(collect); }; collect(tree); - expect(links).toEqual(["src/first.ts", "src/second.ts"]); + expect(links).toEqual([first, second]); }); it.each([ - ["| A
B | C |\n|---|---|\n| x | y |", "| A\u2028B | C |\n|---|---|\n| x | y |"], + ["| A
B | C |\n|---|---|\n| x | y |", "| A \u2028B | C |\n|---|---|\n| x | y |"], [ "> | A | B |\n> |---|---|\n> | x | first
second |", - "> | A | B |\n> |---|---|\n> | x | first\u2028second |", + "> | A | B |\n> |---|---|\n> | x | first \u2028second |", ], [ "| A | B |\n|---|---|\n| `
` | **first

second** |", - "| A | B |\n|---|---|\n| `
` | **first\u2028\u2028second** |", + "| A | B |\n|---|---|\n| `
` | **first \u2028 \u2028second** |", ], [ "| A | B |\n|---|---|\n| x | \\
<br> |",