Skip to content

Commit b04947a

Browse files
heavygeeHAPI
andcommitted
fix(web): harden remark-repair-tables against code-span pipes and mixed fences
- countSourceCells: strip backtick code spans before counting column boundaries — a header like | `a | b` | c | is 2 columns, not 3 - repairMarkdownTables: track fenceChar ('`'|'~'|null) instead of a boolean toggle so ``` inside ~~~ no longer incorrectly flips fence state - add 2 tests: code-span-with-pipe in header, backtick inside tilde fence - fix stale comment in markdown-text.tsx (plugin reads file.value, not AST nodes) - drop no-op .trimStart() (padSeparatorLine already returns a trimmed string) via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
1 parent e7a0e17 commit b04947a

3 files changed

Lines changed: 35 additions & 10 deletions

File tree

web/src/components/assistant-ui/markdown-text.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ import type { MarkdownTextPrimitiveProps } from '@assistant-ui/react-markdown'
3030

3131
// ── Plugin array ────────────────────────────────────────────────────────────
3232
// Order: remarkGfm → remarkRepairTables → remarkNonHttpsAutolink → remarkStripCjkAutolink → remarkMath → remarkDisableIndentedCode → remarkFilePathLinks
33-
// remarkRepairTables must run immediately after remarkGfm — it uses the parsed
34-
// table nodes plus file.value position data to detect and repair off-by-one
35-
// separator rows before other transforms run.
33+
// remarkRepairTables must run immediately after remarkGfm — it reads file.value
34+
// (raw source) to pad short separator rows before remark-gfm parses the table.
3635
// remarkNonHttpsAutolink must run BEFORE remarkStripCjkAutolink so that the
3736
// CJK strip plugin sees the new link nodes and can trim trailing CJK punctuation
3837
// from them. Both must come before remarkMath (to avoid treating TeX as URI).

web/src/lib/remark-repair-tables.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,26 @@ describe('repairMarkdownTables (string)', () => {
7373
expect(repairMarkdownTables(input)).toBe(input)
7474
})
7575

76+
it('does not pad a valid table whose header contains a code span with a pipe', () => {
77+
// | `a | b` | c | is a 2-column header; separator has 2 cells — valid
78+
const input = '| `a | b` | c |\n|---|---|\n| x | y |\n'
79+
expect(repairMarkdownTables(input)).toBe(input)
80+
})
81+
82+
it('does not flip fence state when ``` appears inside a ~~~ block', () => {
83+
const input = [
84+
'~~~',
85+
'```',
86+
'| A | B | C |',
87+
'|---|---|',
88+
'| x | y | z |',
89+
'```',
90+
'~~~',
91+
'',
92+
].join('\n')
93+
expect(repairMarkdownTables(input)).toBe(input)
94+
})
95+
7696
it('repairs a broken table after a fenced code block closes', () => {
7797
const input = [
7898
'```',

web/src/lib/remark-repair-tables.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import type { VFile } from 'vfile'
1818
// ── Helpers ──────────────────────────────────────────────────────────────────
1919

2020
/** Count pipe-delimited cells in one table row line of raw source.
21+
* Strips backtick code spans first so pipes inside them are not counted.
2122
* Skips escaped pipes (\|) which are literal characters, not cell boundaries. */
2223
function countSourceCells(line: string): number {
23-
const trimmed = line.trim()
24+
// Replace code spans with a placeholder so any | inside them is invisible
25+
const trimmed = line.trim().replace(/`[^`]*`/g, '\x00')
2426
const inner = trimmed.startsWith('|') ? trimmed.slice(1) : trimmed
2527
const stripped = inner.endsWith('|') ? inner.slice(0, -1) : inner
2628
let cells = 1
@@ -90,15 +92,19 @@ function padSeparatorLine(sepLine: string, targetCols: number): string | null {
9092
export function repairMarkdownTables(source: string): string {
9193
const lines = source.split('\n')
9294
let changed = false
93-
let inFence = false
95+
// Track the fence character separately so ``` inside ~~~ (or vice versa)
96+
// does not incorrectly flip the fence state.
97+
let fenceChar: '`' | '~' | null = null
9498

9599
for (let i = 0; i < lines.length; i++) {
96-
// Toggle fenced-code state on ``` / ~~~ opening/closing lines
97-
if (/^\s*(```|~~~)/.test(lines[i])) {
98-
inFence = !inFence
100+
const fenceMatch = lines[i].match(/^\s*(`{3,}|~{3,})/)
101+
if (fenceMatch) {
102+
const ch = fenceMatch[1][0] as '`' | '~'
103+
if (fenceChar === null) { fenceChar = ch }
104+
else if (ch === fenceChar) { fenceChar = null }
99105
continue
100106
}
101-
if (inFence) continue
107+
if (fenceChar !== null) continue
102108
if (i === 0) continue
103109

104110
const sep = lines[i]
@@ -116,7 +122,7 @@ export function repairMarkdownTables(source: string): string {
116122
if (repaired !== null) {
117123
// Preserve original leading whitespace so indented tables are unchanged
118124
const prefix = sep.match(/^\s*/)?.[0] ?? ''
119-
lines[i] = prefix + repaired.trimStart()
125+
lines[i] = prefix + repaired
120126
changed = true
121127
}
122128
}

0 commit comments

Comments
 (0)