Skip to content

Commit 81f5ec7

Browse files
committed
Fix table cell alignment for missing and misaligned cells
The table style is determined by the header row. After normalizing the header (filling empty cells between consecutive delimiters), extract two properties: the column count and whether the header starts with a delimiter. For each data row, normalization proceeds in three steps. First, fill in empty cells between consecutive delimiters (this handles empty cells in the middle of a row). Second, if the header has no leading delimiter but the row does, insert an empty cell after the row's leading delimiter (this corrects the one-position shift caused by the row having a leading pipe that the header lacks. Third, if the row has fewer cells than the header column count, pad with empty cells at the end (this handles rows that are shorter than the header because trailing pipes or cells were omitted). The same normalization is applied both in the renderer and the indexer. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
1 parent c75f48d commit 81f5ec7

File tree

3 files changed

+45
-7
lines changed

3 files changed

+45
-7
lines changed

client/markdown_renderer/markdown_render.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,18 @@ function render(
421421
body: cleanTags(mapRender(t.children!)),
422422
};
423423
case "TableRow": {
424-
normalizeTableRow(t);
424+
const table = t.parent;
425+
const header = table?.children?.find((c) => c.type === "TableHeader");
426+
let columnCount = 0;
427+
let headerHasLeadingDelim: boolean | undefined;
428+
if (header) {
429+
normalizeTableRow(header);
430+
headerHasLeadingDelim = header.children?.[0]?.type === "TableDelimiter";
431+
for (const c of header.children ?? []) {
432+
if (c.type === "TableCell") columnCount++;
433+
}
434+
}
435+
normalizeTableRow(t, columnCount, headerHasLeadingDelim);
425436
return {
426437
name: "tr",
427438
body: cleanTags(mapRender(t.children!), true),

plug-api/lib/tree.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,14 @@ export function nodeAtPos(tree: ParseTree, pos: number): ParseTree | null {
191191
return null;
192192
}
193193

194-
// Ensure a TableRow has a TableCell between every pair of TableDelimiters
195-
export function normalizeTableRow(row: ParseTree): void {
194+
// Ensure a TableRow/TableHeader has a TableCell between every pair of
195+
// TableDelimiters, and optionally pad to match columnCount.
196+
// headerHasLeadingDelim indicates whether the header starts with a delimiter.
197+
export function normalizeTableRow(
198+
row: ParseTree,
199+
columnCount?: number,
200+
headerHasLeadingDelim?: boolean,
201+
): void {
196202
const children = row.children;
197203
if (!children) return;
198204
const normalized: ParseTree[] = [];
@@ -210,6 +216,26 @@ export function normalizeTableRow(row: ParseTree): void {
210216
normalized.push(child);
211217
}
212218
row.children = normalized;
219+
220+
// Fix leading-pipe mismatch: row has leading delimiter but header doesn't
221+
if (headerHasLeadingDelim === false) {
222+
if (row.children.length > 0 && row.children[0].type === "TableDelimiter") {
223+
// Insert empty cell after the leading delimiter
224+
row.children.splice(1, 0, { type: "TableCell", children: [] });
225+
}
226+
}
227+
228+
// Pad trailing empty cells to match header column count
229+
if (columnCount !== undefined) {
230+
let cellCount = 0;
231+
for (const child of row.children) {
232+
if (child.type === "TableCell") cellCount++;
233+
}
234+
while (cellCount < columnCount) {
235+
row.children.push({ type: "TableCell", children: [] });
236+
cellCount++;
237+
}
238+
}
213239
}
214240

215241
// Turn ParseTree back into text

plugs/index/table.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,21 @@ export function indexTables(
6363
).forEach(
6464
(table) => {
6565
const rows = collectNodesOfType(table, "TableRow");
66-
const header = collectNodesOfType(table, "TableHeader")[0]; //Use first header. As per markdown spec there can only be exactly one
66+
const header = collectNodesOfType(table, "TableHeader")[0];
67+
normalizeTableRow(header);
68+
const headerHasLeadingDelim =
69+
header.children?.[0]?.type === "TableDelimiter";
6770
const headerLabels = collectNodesOfType(header, "TableCell").map((cell) =>
6871
concatChildrenTexts(cell.children!)
6972
).map(cleanHeaderFieldName);
70-
//console.log("Header labels", headerLabels);
7173

7274
for (const row of rows) {
7375
const tags = new Set<string>();
7476
collectNodesOfType(row, "Hashtag").forEach((h) => {
75-
// Push tag to the list, removing the initial #
7677
tags.add(extractHashtag(h.children![0].text!));
7778
});
7879

79-
normalizeTableRow(row);
80+
normalizeTableRow(row, headerLabels.length, headerHasLeadingDelim);
8081

8182
const cells = collectNodesOfType(row, "TableCell");
8283

0 commit comments

Comments
 (0)