authorgravatar for adam.bogucki2018@gmail.comAdamulek123 <adam.bogucki2018@gmail.com> 2026-09-03 07:30:10+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 01:30:10-04:00
log1575ada305b9686b9771df2dca304f391e85a76e
tree97e39b8195b458d378fc273378d116bcf5b28b72
parentf6c04c552c203350705f9ab1e47773ea736af245
signature Signed by PGP key B5690EEEBB952194

fix(mobile): stop indented code overflowing Android chat bubbles (#9347)


2 files changed, 43 insertions(+), 6 deletions(-)

apps/mobile/src/lib/wideMarkdownBlocks.test.ts+16-4
...@@ -17,6 +17,20 @@ describe("hasWideMarkdownBlock", () => {...@@ -17,6 +17,20 @@ describe("hasWideMarkdownBlock", () => {
17 expect(hasWideMarkdownBlock(" ```\ncode\n```")).toBe(true);17 expect(hasWideMarkdownBlock(" ```\ncode\n```")).toBe(true);
18 });18 });
1919
20 it("detects indented code blocks", () => {
21 const prompt = 'before\n\n def search(x):\n return x\n\n"""\n\nafter';
22 expect(hasWideMarkdownBlock(prompt)).toBe(true);
23 expect(hasWideMarkdownBlock("before\n\n\treturn x\n\nafter")).toBe(true);
24 expect(hasWideMarkdownBlock("before\n\n \treturn x\n\nafter")).toBe(true);
25 expect(hasWideMarkdownBlock("> return x")).toBe(true);
26 expect(hasWideMarkdownBlock("> > \treturn x")).toBe(true);
27 expect(hasWideMarkdownBlock(" 1. indented code")).toBe(true);
28 expect(hasWideMarkdownBlock(" - code-like bullet")).toBe(true);
29 expect(hasWideMarkdownBlock("before\n not code\nafter")).toBe(false);
30 expect(hasWideMarkdownBlock("before\n \nafter")).toBe(false);
31 expect(hasWideMarkdownBlock("before\n \t\nafter")).toBe(false);
32 });
33
20 it("detects top-level and blockquoted ordered-list markers", () => {34 it("detects top-level and blockquoted ordered-list markers", () => {
21 expect(hasWideMarkdownBlock("1. One\n2. Two\n3. Three\n4. Four\n5. Five")).toBe(true);35 expect(hasWideMarkdownBlock("1. One\n2. Two\n3. Three\n4. Four\n5. Five")).toBe(true);
22 expect(hasWideMarkdownBlock("before\n3) Three")).toBe(true);36 expect(hasWideMarkdownBlock("before\n3) Three")).toBe(true);
...@@ -24,11 +38,9 @@ describe("hasWideMarkdownBlock", () => {...@@ -24,11 +38,9 @@ describe("hasWideMarkdownBlock", () => {
24 expect(hasWideMarkdownBlock("> > 3) Three")).toBe(true);38 expect(hasWideMarkdownBlock("> > 3) Three")).toBe(true);
25 });39 });
2640
27 it("detects nested ordered lists without treating indented code as a list", () => {41 it("detects nested ordered lists", () => {
28 expect(hasWideMarkdownBlock("- Parent\n 1. Child\n 2. Child")).toBe(true);42 expect(hasWideMarkdownBlock("- Parent\n 1. Child\n 2. Child")).toBe(true);
29 expect(hasWideMarkdownBlock("> - Parent\n> 1. Child")).toBe(true);43 expect(hasWideMarkdownBlock("> - Parent\n> 1. Child")).toBe(true);
30 expect(hasWideMarkdownBlock(" 1. indented code")).toBe(false);
31 expect(hasWideMarkdownBlock(" - code-like bullet\n 1. indented code")).toBe(false);
32 });44 });
3345
34 it("can limit ordered-list width pinning to Android", () => {46 it("can limit ordered-list width pinning to Android", () => {
...@@ -46,7 +58,7 @@ describe("hasWideMarkdownBlock", () => {...@@ -46,7 +58,7 @@ describe("hasWideMarkdownBlock", () => {
46 expect(hasWideMarkdownBlock(" > quoted", { includeBlockquotes: true })).toBe(true);58 expect(hasWideMarkdownBlock(" > quoted", { includeBlockquotes: true })).toBe(true);
47 expect(hasWideMarkdownBlock("> quoted")).toBe(false);59 expect(hasWideMarkdownBlock("> quoted")).toBe(false);
48 expect(hasWideMarkdownBlock("prose > quoted", { includeBlockquotes: true })).toBe(false);60 expect(hasWideMarkdownBlock("prose > quoted", { includeBlockquotes: true })).toBe(false);
49 expect(hasWideMarkdownBlock(" > indented code", { includeBlockquotes: true })).toBe(false);61 expect(hasWideMarkdownBlock(" > indented code", { includeBlockquotes: true })).toBe(true);
50 });62 });
5163
52 it("detects GFM tables", () => {64 it("detects GFM tables", () => {
apps/mobile/src/lib/wideMarkdownBlocks.ts+27-2
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1/**1/**
2 * Detects markdown that the renderer draws as a block requiring a definite2 * Detects markdown that the renderer draws as a block requiring a definite
3 * user-bubble width: fenced code blocks, GFM tables, ordered lists, and3 * user-bubble width: fenced and indented code blocks, GFM tables, ordered
4 * blockquotes when requested by the caller.4 * lists, and blockquotes when requested by the caller.
5 *5 *
6 * Fenced code blocks and tables report an intrinsic width equal to their6 * Fenced code blocks and tables report an intrinsic width equal to their
7 * widest line, which is effectively unbounded. A user bubble sizes itself7 * widest line, which is effectively unbounded. A user bubble sizes itself
...@@ -43,6 +43,28 @@ function stripBlockquotePrefixes(line: string): string {...@@ -43,6 +43,28 @@ function stripBlockquotePrefixes(line: string): string {
43 return content;43 return content;
44}44}
4545
46function hasIndentedCodeBlock(text: string): boolean {
47 return text.split("\n").some((rawLine) => {
48 const line = stripBlockquotePrefixes(rawLine);
49 let column = 0;
50 let index = 0;
51
52 // Markdown tabs advance to the next four-column stop.
53 while (index < line.length) {
54 if (line[index] === " ") {
55 column += 1;
56 } else if (line[index] === "\t") {
57 column += 4 - (column % 4);
58 } else {
59 break;
60 }
61 index += 1;
62 }
63
64 return column >= 4 && index < line.length && line[index] !== "\r";
65 });
66}
67
46function hasBlockquote(text: string): boolean {68function hasBlockquote(text: string): boolean {
47 return text.split("\n").some((line) => BLOCKQUOTE_PREFIX.test(line));69 return text.split("\n").some((line) => BLOCKQUOTE_PREFIX.test(line));
48}70}
...@@ -89,6 +111,9 @@ export function hasWideMarkdownBlock(...@@ -89,6 +111,9 @@ export function hasWideMarkdownBlock(
89 if (options.includeBlockquotes === true && hasBlockquote(text)) {111 if (options.includeBlockquotes === true && hasBlockquote(text)) {
90 return true;112 return true;
91 }113 }
114 if (hasIndentedCodeBlock(text)) {
115 return true;
116 }
92 if (options.includeOrderedLists !== false && hasOrderedListItem(text)) {117 if (options.includeOrderedLists !== false && hasOrderedListItem(text)) {
93 return true;118 return true;
94 }119 }