Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions frontend/src/utils/markdownTableNormalize.test.ts
Original file line number Diff line number Diff line change
@@ -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}`);
});
22 changes: 21 additions & 1 deletion frontend/src/utils/markdownTableNormalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down