refactor(table): rename resolveTableIndex, fix escapeCell backslash handling, remove redundant cast

- Rename `resolveTableIndex` useMemo value to `currentTableIndex` in Table.tsx
  so the name reflects a computed value rather than an action; update all
  references (callbacks, dependency arrays, JSDoc comment)

- Fix `escapeCell` in markdown-table.ts: replace the single-char lookbehind
  regex `(?<!\\)|` with a character loop that counts consecutive backslashes
  before each pipe and only inserts an escape when the count is even, mirroring
  the parser logic and correctly handling sequences like `\\|`

- Remove redundant `as ColumnAlignment` type assertion in `createEmptyTable`;
  TypeScript infers `\"none\"` correctly via contextual typing from the return type

- Add regression test for the `\\|` round-trip case in markdown-table.test.ts
This commit is contained in:
milvasic 2026-03-24 18:42:47 +01:00
parent 26dec86b70
commit 0ad53f9dd3
3 changed files with 44 additions and 11 deletions

View File

@ -34,8 +34,8 @@ export const Table = ({ children, className, node, ...props }: TableProps) => {
const tables = useMemo(() => findAllTables(memo.content), [memo.content]);
/** Resolve which markdown table index this rendered table corresponds to using AST source positions. */
const resolveTableIndex = useMemo(() => {
/** The index of the markdown table this rendered table corresponds to (from AST source positions). */
const currentTableIndex = useMemo(() => {
const nodeStart = node?.position?.start?.offset;
if (nodeStart == null) return -1;
@ -50,16 +50,16 @@ export const Table = ({ children, className, node, ...props }: TableProps) => {
e.stopPropagation();
e.preventDefault();
if (resolveTableIndex < 0 || resolveTableIndex >= tables.length) return;
if (currentTableIndex < 0 || currentTableIndex >= tables.length) return;
const parsed = parseMarkdownTable(tables[resolveTableIndex].text);
const parsed = parseMarkdownTable(tables[currentTableIndex].text);
if (!parsed) return;
setTableData(parsed);
setTableIndex(resolveTableIndex);
setTableIndex(currentTableIndex);
setDialogOpen(true);
},
[tables, resolveTableIndex],
[tables, currentTableIndex],
);
const handleDeleteClick = useCallback(
@ -67,12 +67,12 @@ export const Table = ({ children, className, node, ...props }: TableProps) => {
e.stopPropagation();
e.preventDefault();
if (resolveTableIndex < 0) return;
if (currentTableIndex < 0) return;
setTableIndex(resolveTableIndex);
setTableIndex(currentTableIndex);
setDeleteDialogOpen(true);
},
[resolveTableIndex],
[currentTableIndex],
);
const handleConfirmEdit = useCallback(

View File

@ -150,6 +150,24 @@ describe("serializeMarkdownTable", () => {
expect(parsed?.rows[0][1]).toBe("baz");
});
it("round-trips a cell whose value has two backslashes before a pipe (\\\\\\\\|)", () => {
// Cell value "foo\\|bar" (two backslashes + pipe) is a valid parsed value
// that comes from markdown "foo\\\\\\|bar". The old escapeCell regex
// (?<!\\)\\| would NOT escape the pipe (it sees one \\ before the | and
// thinks it is already escaped), causing the parser to split on the pipe.
// The fixed escapeCell counts all consecutive backslashes: 2 is even, so
// it correctly inserts an escape backslash.
const data: TableData = {
headers: ["A"],
rows: [["foo\\\\|bar"]],
alignments: ["none"],
};
const md = serializeMarkdownTable(data);
const parsed = parseMarkdownTable(md);
expect(parsed?.headers.length).toBe(1); // pipe must NOT split into extra columns
expect(parsed?.rows[0][0]).toBe("foo\\\\|bar");
});
it("round-trips through parse and serialize", () => {
const original = `| Name | Age |
| ----- | --- |

View File

@ -107,7 +107,22 @@ export function serializeMarkdownTable(data: TableData): string {
const { headers, rows, alignments } = data;
const colCount = headers.length;
const escapeCell = (text: string): string => text.replace(/(?<!\\)\|/g, "\\|");
const escapeCell = (text: string): string => {
let result = "";
for (let i = 0; i < text.length; i++) {
if (text[i] === "|") {
let backslashes = 0;
let j = i - 1;
while (j >= 0 && text[j] === "\\") {
backslashes++;
j--;
}
if (backslashes % 2 === 0) result += "\\";
}
result += text[i];
}
return result;
};
// Calculate maximum width per column (minimum 3 for the separator).
const widths: number[] = [];
@ -214,6 +229,6 @@ export function createEmptyTable(cols = 2, rows = 2): TableData {
return {
headers: Array.from({ length: cols }, (_, i) => `Header ${i + 1}`),
rows: Array.from({ length: rows }, () => Array.from({ length: cols }, () => "")),
alignments: Array.from({ length: cols }, () => "none" as ColumnAlignment),
alignments: Array.from({ length: cols }, () => "none"),
};
}