fix(mobile): two more table parsers hold a pipe in a cell (OTA phase C follow-up) (#22114)

* test(mobile): pin escaped pipes in mobile markdown table cells

The mobile preview parser splits a table row on every pipe, so a cell
that escaped one becomes two cells and keeps the backslash.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): read table rows through the shared row splitter

The editor's markdown-table-rows already splits on unescaped pipes only
and unescapes the cell; it has no imports of its own, so owning the rule
once costs the preview parser nothing.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin escaped-pipe rows in PR comment tables

Its splitter strips the trailing pipe before walking escapes and reads
`\\|` as an escaped pipe, so a row ending in `\|` loses the pipe and a
cell holding a backslash swallows the separator after it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): split PR comment table rows on unescaped pipes only

Its own delimiter grammar stays local: a single dash still opens a table
here, which the editor's three-dash separator would reject.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(config): repin the session route closure at 4,332 modules

markdown-table-rows.ts joins through the PR comment renderer. Measured on
this head: 4,332 modules / 990 local, and it is the only file under
rich-markdown/ in the closure, so nothing came with it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo Hong
2026-09-21 20:26:41 -04:00
committed by GitHub
parent 0677271709
commit 226f4a0775
5 changed files with 57 additions and 45 deletions
@@ -229,8 +229,16 @@ const MERMAID_PACKAGE = 'node_modules/mermaid/'
* diffed to name the difference: those two leave and nothing joins. The same measurement, taken
* before #22067 landed, is how this branch read main's pin of 4,330 as three modules stale — the
* three the paragraph above names.
*
* Then the two other table parsers gave up their own row splitters and read the editor's
* `src/components/rich-markdown/markdown-table-rows.ts` instead, which the session page reaches
* through the PR comment renderer. It is the one module that joins, and the only one it can be: it
* imports nothing, and no other file under `rich-markdown/` is in the closure beside it.
*
* modules 4331 -> 4332 (+1)
* local modules 989 -> 990 (+1)
*/
const SESSION_ROUTE_MODULES = 4331
const SESSION_ROUTE_MODULES = 4332
/** What the page enters this route through once the route is a switch with a `.web.tsx` sibling. */
const ROUTE_ENTRY = [
@@ -45,6 +45,26 @@ describe('parseMobileMarkdown', () => {
])
})
it('keeps an escaped pipe inside the cell that escaped it', () => {
expect(parseMobileMarkdown('| Cmd | Note |\n| --- | --- |\n| a \\| b | c |')).toEqual([
{
type: 'table',
headers: ['Cmd', 'Note'],
rows: [['a | b', 'c']]
}
])
})
it('ends a cell at the pipe following an escaped backslash', () => {
expect(parseMobileMarkdown('| A | B |\n| --- | --- |\n| x\\\\|y |')).toEqual([
{
type: 'table',
headers: ['A', 'B'],
rows: [['x\\', 'y']]
}
])
})
it('parses standalone HTTPS images without folding them into paragraphs', () => {
expect(parseMobileMarkdown('![Screenshot](https://example.com/screen.png)')).toEqual([
{
@@ -1,3 +1,5 @@
import { isTableSeparator, splitTableRow } from './rich-markdown/markdown-table-rows'
export type MobileMarkdownBlock =
| { type: 'paragraph'; text: string }
| { type: 'heading'; level: number; text: string }
@@ -11,20 +13,6 @@ export type MobileMarkdownBlock =
const HEADING = /^(#{1,6})\s+(.+)$/
const CODE_FENCE = /^```([A-Za-z0-9_-]+)?\s*$/
function splitTableRow(line: string): string[] {
return line
.trim()
.replace(/^\|/, '')
.replace(/\|$/, '')
.split('|')
.map((cell) => cell.trim())
}
function isTableSeparator(line: string): boolean {
const cells = splitTableRow(line)
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell))
}
export function parseMobileMarkdown(content: string): MobileMarkdownBlock[] {
const lines = content.replace(/\r\n?/g, '\n').split('\n')
const blocks: MobileMarkdownBlock[] = []
@@ -135,6 +135,30 @@ describe('parseMarkdownBlocks tables', () => {
])
})
it('keeps an escaped pipe that ends a row carrying no closing pipe', () => {
const md = ['A | B', '--- | ---', 'x | y \\|'].join('\n')
expect(parseMarkdownBlocks(md)).toEqual([
{
kind: 'table',
headers: ['A', 'B'],
align: ['left', 'left'],
rows: [['x', 'y |']]
}
])
})
it('ends a cell at the pipe following an escaped backslash', () => {
const md = ['| A | B |', '| --- | --- |', '| x\\\\|y |'].join('\n')
expect(parseMarkdownBlocks(md)).toEqual([
{
kind: 'table',
headers: ['A', 'B'],
align: ['left', 'left'],
rows: [['x\\', 'y']]
}
])
})
it('does not treat prose containing a pipe as a table (no delimiter row)', () => {
expect(parseMarkdownBlocks('this | that is just text')).toEqual([
{ kind: 'paragraph', text: 'this | that is just text' }
@@ -1,4 +1,5 @@
import { createMarkdownInlineMatcher } from '../markdown-inline-matcher'
import { splitTableRow } from '../rich-markdown/markdown-table-rows'
// Tiny, dependency-free markdown model for PR comment bodies. We render GitHub
// markdown without a third-party RN markdown library (the previous dependency hung
@@ -194,36 +195,7 @@ function parseLines(content: string): MarkdownBlock[] {
return blocks
}
// Splits a `| a | b |` table row into trimmed cells. Tolerates missing outer
// pipes and escaped `\|` inside cells. Total: never throws on odd input.
function splitTableRow(line: string): string[] {
const cells: string[] = []
let cell = ''
let trimmed = line.trim()
if (trimmed.startsWith('|')) {
trimmed = trimmed.slice(1)
}
if (trimmed.endsWith('|')) {
trimmed = trimmed.slice(0, -1)
}
for (let j = 0; j < trimmed.length; j += 1) {
const ch = trimmed[j]
if (ch === '\\' && trimmed[j + 1] === '|') {
cell += '|'
j += 1
continue
}
if (ch === '|') {
cells.push(cell.trim())
cell = ''
continue
}
cell += ch
}
cells.push(cell.trim())
return cells
}
// A single dash is a delimiter cell here, unlike the editor's three-dash separator.
function isTableDelimiter(line: string): boolean {
return splitTableRow(line).every((cell) => /^:?-+:?$/.test(cell))
}