diff --git a/frontend/src/utils/markdownTableNormalize.test.ts b/frontend/src/utils/markdownTableNormalize.test.ts new file mode 100644 index 00000000000..66cb2af73cf --- /dev/null +++ b/frontend/src/utils/markdownTableNormalize.test.ts @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { normalizeSpuriousTablePrefixes } from './markdownTableNormalize.ts'; + +const brokenTable = '| | |\n| --- | --- |\n| Name | Value |\n| alpha | beta |'; +const normalizedTable = '| Name | Value |\n| --- | --- |\n| alpha | beta |'; + +test('repairs spurious table prefixes outside code', () => { + assert.equal(normalizeSpuriousTablePrefixes(brokenTable), normalizedTable); + assert.equal(normalizeSpuriousTablePrefixes(normalizedTable), normalizedTable); +}); + +for (const fence of ['```', '~~~', '````', '~~~~']) { + test(`preserves literal table examples inside ${fence} fences`, () => { + const source = `${fence}markdown\n${brokenTable}\n${fence}`; + assert.equal(normalizeSpuriousTablePrefixes(source), source); + }); +} + +test('preserves indented code containing pipe-delimited rows', () => { + for (const indent of [' ', '\t']) { + const source = brokenTable.split('\n').map(line => indent + line).join('\n'); + assert.equal(normalizeSpuriousTablePrefixes(source), source); + } +}); + +test('resumes table repair after a code fence closes', () => { + const code = ` ~~~markdown\n${brokenTable}\n ~~~~ `; + assert.equal( + normalizeSpuriousTablePrefixes(`${code}\n\n${brokenTable}`), + `${code}\n\n${normalizedTable}`, + ); +}); + +test('shorter, opposite-character and nonblank-suffix fences do not close code', () => { + const source = `\`\`\`\`markdown\n${brokenTable}\n\`\`\`\n~~~\n\`\`\`\`suffix\n${brokenTable}\n\`\`\`\``; + assert.equal(normalizeSpuriousTablePrefixes(source), source); +}); + +test('an unclosed fence protects the rest of the document', () => { + const source = `~~~markdown\n${brokenTable}\n\n${brokenTable}`; + assert.equal(normalizeSpuriousTablePrefixes(source), source); +}); + +test('a backtick in the info string does not start a backtick fence', () => { + const source = `\`\`\`invalid\`info\n${brokenTable}`; + assert.equal(normalizeSpuriousTablePrefixes(source), `\`\`\`invalid\`info\n${normalizedTable}`); +}); diff --git a/frontend/src/utils/markdownTableNormalize.ts b/frontend/src/utils/markdownTableNormalize.ts index 46e5dbb8e46..bd05ca33dfb 100644 --- a/frontend/src/utils/markdownTableNormalize.ts +++ b/frontend/src/utils/markdownTableNormalize.ts @@ -17,6 +17,10 @@ function splitRowCells(line: string): string[] { } function isTableRow(line: string): boolean { + // Four-space and tab-indented rows are literal code, not Markdown tables. + if (!/^ {0,3}\|/.test(line)) { + return false; + } const stripped = line.trim(); return stripped.startsWith('|') && stripped.includes('|', 1); } @@ -50,12 +54,28 @@ function normalizeTableBlock(block: string[]): string[] { return rows; } -/** Fix MarkItDown-style tables: empty row + separator before real rows. */ +/** Fix MarkItDown-style tables outside literal fenced and indented code. */ export function normalizeSpuriousTablePrefixes(content: string): string { const lines = content.split('\n'); const out: string[] = []; let i = 0; + let fence: string | undefined; while (i < lines.length) { + const marker = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(lines[i]); + if (fence) { + if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && /^[ \t\r]*$/.test(marker[2])) { + fence = undefined; + } + out.push(lines[i]); + i += 1; + continue; + } + if (marker && (marker[1][0] === '~' || !marker[2].includes('`'))) { + fence = marker[1]; + out.push(lines[i]); + i += 1; + continue; + } if (!isTableRow(lines[i])) { out.push(lines[i]); i += 1;