Skip to content

Commit d8d3752

Browse files
committed
fix: table blockMatch algo
1 parent f2480c6 commit d8d3752

1 file changed

Lines changed: 107 additions & 6 deletions

File tree

packages/core/src/y/extensions/blockMatchNodes.ts

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,94 @@ import { $prosemirrorDelta } from "@y/prosemirror";
88
* `blockContent blockGroup?`) this is its block-content type (paragraph,
99
* heading, image, ...).
1010
*/
11-
const firstChildName = (
11+
const firstChild = (
1212
d: schema.Unwrap<typeof $prosemirrorDelta>,
13-
): string | null => {
13+
): schema.Unwrap<typeof $prosemirrorDelta> | null => {
1414
for (const op of (d as any).children) {
1515
if (delta.$insertOp.check(op)) {
1616
for (const it of op.insert) {
1717
if (delta.$deltaAny.check(it)) {
18-
return it.name;
18+
return it;
1919
}
2020
}
2121
}
2222
}
2323
return null;
2424
};
2525

26+
function getTableDimensions(
27+
d: schema.Unwrap<typeof $prosemirrorDelta>,
28+
): { rows: number; cols: number } | null {
29+
if (d.name !== "table") {
30+
return null;
31+
}
32+
33+
// Collect all rows with their cells' colspan/rowspan values.
34+
const rows: Array<Array<{ colspan: number; rowspan: number }>> = [];
35+
for (const op of (d as any).children) {
36+
if (delta.$insertOp.check(op)) {
37+
for (const tr of op.insert as Array<
38+
schema.Unwrap<typeof $prosemirrorDelta>
39+
>) {
40+
if (tr.name !== "tableRow") {
41+
return null;
42+
}
43+
const cells: Array<{ colspan: number; rowspan: number }> = [];
44+
for (const trOp of (tr as any).children) {
45+
if (delta.$insertOp.check(trOp)) {
46+
for (const td of trOp.insert as Array<
47+
schema.Unwrap<typeof $prosemirrorDelta>
48+
>) {
49+
if (td.name !== "tableCell" && td.name !== "tableHeader") {
50+
return null;
51+
}
52+
cells.push({
53+
colspan: Number(td.attrs.colspan) || 1,
54+
rowspan: Number(td.attrs.rowspan) || 1,
55+
});
56+
}
57+
}
58+
}
59+
rows.push(cells);
60+
}
61+
}
62+
}
63+
64+
if (rows.length === 0) {
65+
return null;
66+
}
67+
68+
// Build an occupancy grid to determine the true column count.
69+
// Each entry in `grid[r]` tracks which columns are already occupied
70+
// (by a cell from a previous row with rowspan > 1).
71+
const grid: boolean[][] = [];
72+
for (let r = 0; r < rows.length; r++) {
73+
if (!grid[r]) {
74+
grid[r] = [];
75+
}
76+
let col = 0;
77+
for (const cell of rows[r]) {
78+
// Skip columns already occupied by a rowspan from above.
79+
while (grid[r][col]) {
80+
col++;
81+
}
82+
// Mark all slots this cell occupies.
83+
for (let dr = 0; dr < cell.rowspan; dr++) {
84+
if (!grid[r + dr]) {
85+
grid[r + dr] = [];
86+
}
87+
for (let dc = 0; dc < cell.colspan; dc++) {
88+
grid[r + dr][col + dc] = true;
89+
}
90+
}
91+
col += cell.colspan;
92+
}
93+
}
94+
95+
const numCols = Math.max(...grid.map((row) => row.length));
96+
return { rows: rows.length, cols: numCols };
97+
}
98+
2699
/**
27100
* BlockNote's node-pairing policy for y-prosemirror's `matchNodes` option
28101
* (forwarded to `lib0/delta.diff`). This is the schema-specific bit that lives
@@ -48,6 +121,34 @@ const firstChildName = (
48121
export const blockMatchNodes = (
49122
a: schema.Unwrap<typeof $prosemirrorDelta>,
50123
b: schema.Unwrap<typeof $prosemirrorDelta>,
51-
): boolean =>
52-
a.name === b.name &&
53-
(a.name !== "blockContainer" || firstChildName(a) === firstChildName(b));
124+
): boolean => {
125+
if (a.name !== b.name) {
126+
return false;
127+
}
128+
129+
if (a.name !== "blockContainer") {
130+
return true;
131+
}
132+
133+
const childA = firstChild(a);
134+
const childB = firstChild(b);
135+
136+
if (childA?.name !== childB?.name) {
137+
return false;
138+
}
139+
140+
if (childA?.name === "table" && childB?.name === "table") {
141+
const dimA = getTableDimensions(childA);
142+
const dimB = getTableDimensions(childB);
143+
if (
144+
dimA !== null &&
145+
dimB !== null &&
146+
dimA.rows !== dimB.rows &&
147+
dimA.cols !== dimB.cols
148+
) {
149+
return false;
150+
}
151+
}
152+
153+
return true;
154+
};

0 commit comments

Comments
 (0)