mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 16:02:24 +00:00
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:
@@ -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('')).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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user