diff --git a/web/src/components/MemoContent/Table.tsx b/web/src/components/MemoContent/Table.tsx index 9ad1a8684..dbe1fc36e 100644 --- a/web/src/components/MemoContent/Table.tsx +++ b/web/src/components/MemoContent/Table.tsx @@ -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( diff --git a/web/src/utils/markdown-table.test.ts b/web/src/utils/markdown-table.test.ts index c4a145c69..37ab970ca 100644 --- a/web/src/utils/markdown-table.test.ts +++ b/web/src/utils/markdown-table.test.ts @@ -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 + // (? { const original = `| Name | Age | | ----- | --- | diff --git a/web/src/utils/markdown-table.ts b/web/src/utils/markdown-table.ts index 1103776c5..4c6a55ab8 100644 --- a/web/src/utils/markdown-table.ts +++ b/web/src/utils/markdown-table.ts @@ -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(/(? { + 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"), }; }