From e9d63185d18517276ac835fcb10a99441b880a6e 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> |",