Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -105,28 +105,27 @@ export function handleTableCellNode({
const rows = table.elements.filter((el) => el.name === 'w:tr');
const currentRowIndex = rows.findIndex((r) => r === row);
const remainingRows = rows.slice(currentRowIndex + 1);

const cellsInRow = row.elements.filter((el) => el.name === 'w:tc');
let cellIndex = cellsInRow.findIndex((el) => el === node);
let rowspan = 1;
const startColumn = Number.isFinite(columnIndex) ? columnIndex : 0;

// Iterate through all remaining rows after the current cell, and find all cells that need to be merged
// Continue the merge by matching cells on the logical table grid, not by raw tc index.
// This keeps vertical merges aligned when rows have gridBefore or different preceding spans.
for (let remainingRow of remainingRows) {
const firstCell = remainingRow.elements.findIndex((el) => el.name === 'w:tc');
const cellAtIndex = remainingRow.elements[firstCell + cellIndex];
const cellAtColumn = findTableCellAtColumn(remainingRow, startColumn);

if (!cellAtIndex) break;
if (!cellAtColumn) break;

const vMerge = getTableCellVMerge(cellAtIndex);
const vMerge = getTableCellVMerge(cellAtColumn);

if (!vMerge || vMerge === 'restart') {
// We have reached the end of the vertically merged cells
break;
}

// This cell is part of a merged cell, merge it (remove it from its row)
// This cell is part of a merged cell. Mark it consumed so the row encoder skips it
// but still advances the column index (grid geometry is preserved).
rowspan++;
remainingRow.elements.splice(firstCell + cellIndex, 1);
markTableCellAsVMergeConsumed(cellAtColumn);
}
attributes['rowspan'] = rowspan;
}
Expand Down Expand Up @@ -256,6 +255,46 @@ const getTableCellVMerge = (node) => {
return vMerge.attributes?.['w:val'] || 'continue';
};

const getGridBefore = (row) => {
const trPr = row.elements?.find((el) => el.name === 'w:trPr');
const gridBefore = trPr?.elements?.find((el) => el.name === 'w:gridBefore');
const raw = gridBefore?.attributes?.['w:val'];
const value = typeof raw === 'string' ? parseInt(raw, 10) : raw;
return Number.isFinite(value) && value > 0 ? value : 0;
};

const getTableCellGridSpan = (node) => {
if (!node || node.name !== 'w:tc') return 1;
const tcPr = node.elements?.find((el) => el.name === 'w:tcPr');
const gridSpan = tcPr?.elements?.find((el) => el.name === 'w:gridSpan');
const raw = gridSpan?.attributes?.['w:val'];
const value = typeof raw === 'string' ? parseInt(raw, 10) : raw;
return Number.isFinite(value) && value > 0 ? value : 1;
};

const findTableCellAtColumn = (row, targetColumn) => {
const cells = row.elements?.filter((el) => el.name === 'w:tc') ?? [];
let currentColumn = getGridBefore(row);

for (const cell of cells) {
const colSpan = getTableCellGridSpan(cell);
if (targetColumn >= currentColumn && targetColumn < currentColumn + colSpan) {
return cell._vMergeConsumed ? null : cell;
}
currentColumn += colSpan;
}

return null;
};

/** Mark a w:tc as consumed by a vertical merge so the row encoder skips it but advances column index. */
const markTableCellAsVMergeConsumed = (node) => {
if (node?.name === 'w:tc') {
node._vMergeConsumed = true;
node._vMergeConsumedGridSpan = getTableCellGridSpan(node);
}
};

/**
* Process the margins for a table cell
* @param {Object} inlineMargins
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,144 @@ describe('legacy-handle-table-cell-node', () => {
expect(out.attrs.rowspan).toBe(3);
});

it('resolves vertical merge continuations by logical grid column when rows use gridBefore', () => {
const cellNode = {
name: 'w:tc',
elements: [
{
name: 'w:tcPr',
elements: [
{ name: 'w:vMerge', attributes: { 'w:val': 'restart' } },
{ name: 'w:shd', attributes: { 'w:fill': '006A72' } },
],
},
{ name: 'w:p' },
],
};

const row1 = {
name: 'w:tr',
elements: [
{
name: 'w:trPr',
elements: [{ name: 'w:gridBefore', attributes: { 'w:val': '1' } }],
},
cellNode,
{ name: 'w:tc', elements: [{ name: 'w:p' }] },
],
};

const row2 = {
name: 'w:tr',
elements: [
{
name: 'w:trPr',
elements: [{ name: 'w:gridBefore', attributes: { 'w:val': '1' } }],
},
{
name: 'w:tc',
elements: [{ name: 'w:tcPr', elements: [{ name: 'w:vMerge' }] }, { name: 'w:p' }],
},
{ name: 'w:tc', elements: [{ name: 'w:p' }] },
],
};

const table = { name: 'w:tbl', elements: [row1, row2] };
const params = {
docx: {},
nodeListHandler: { handler: vi.fn(() => 'CONTENT') },
path: [],
editor: createEditorStub(),
};

const out = handleTableCellNode({
params,
node: cellNode,
table,
row: row1,
columnIndex: 1,
columnWidth: null,
allColumnWidths: [90, 100, 110],
_referencedStyles: null,
});

expect(out.attrs.background).toEqual({ color: '006A72' });
expect(out.attrs.rowspan).toBe(2);
const row2Cells = row2.elements.filter((el) => el.name === 'w:tc');
expect(row2Cells).toHaveLength(2);
expect(row2Cells[0]._vMergeConsumed).toBe(true);
});

it('preserves later merge-column alignment after removing an earlier continuation cell', () => {
const firstRestart = {
name: 'w:tc',
elements: [
{ name: 'w:tcPr', elements: [{ name: 'w:vMerge', attributes: { 'w:val': 'restart' } }] },
{ name: 'w:p' },
],
};
const secondRestart = {
name: 'w:tc',
elements: [
{
name: 'w:tcPr',
elements: [
{ name: 'w:vMerge', attributes: { 'w:val': 'restart' } },
{ name: 'w:shd', attributes: { 'w:fill': '006A72' } },
],
},
{ name: 'w:p' },
],
};
const firstContinue = {
name: 'w:tc',
elements: [{ name: 'w:tcPr', elements: [{ name: 'w:vMerge' }] }, { name: 'w:p' }],
};
const secondContinue = {
name: 'w:tc',
elements: [{ name: 'w:tcPr', elements: [{ name: 'w:vMerge' }] }, { name: 'w:p' }],
};

const row1 = { name: 'w:tr', elements: [firstRestart, secondRestart] };
const row2 = { name: 'w:tr', elements: [firstContinue, secondContinue] };
const table = { name: 'w:tbl', elements: [row1, row2] };
const params = {
docx: {},
nodeListHandler: { handler: vi.fn(() => 'CONTENT') },
path: [],
editor: createEditorStub(),
};

const outFirst = handleTableCellNode({
params,
node: firstRestart,
table,
row: row1,
columnIndex: 0,
columnWidth: null,
allColumnWidths: [90, 100],
_referencedStyles: null,
});

const outSecond = handleTableCellNode({
params,
node: secondRestart,
table,
row: row1,
columnIndex: 1,
columnWidth: null,
allColumnWidths: [90, 100],
_referencedStyles: null,
});

expect(outFirst.attrs.rowspan).toBe(2);
expect(outSecond.attrs.rowspan).toBe(2);
expect(outSecond.attrs.background).toEqual({ color: '006A72' });
const row2Cells = row2.elements.filter((el) => el.name === 'w:tc');
expect(row2Cells).toHaveLength(2);
expect(row2Cells.every((tc) => tc._vMergeConsumed)).toBe(true);
});

it('blends percentage table shading into a solid background color', () => {
const cellNode = { name: 'w:tc', elements: [{ name: 'w:p' }] };
const row = { name: 'w:tr', elements: [cellNode] };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ const encode = (params, encodedAttrs) => {
skipOccupiedColumns();

const startColumn = currentColumnIndex;

// Cell was consumed by a vertical merge (rowspan) above; skip encoding but preserve column advance
if (node._vMergeConsumed && Number.isFinite(node._vMergeConsumedGridSpan)) {
currentColumnIndex = startColumn + node._vMergeConsumedGridSpan;
Comment on lines +107 to +108

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid double-advancing columns for consumed merge cells

When this branch runs, skipOccupiedColumns() has already consumed the same merged columns via activeRowSpans, so incrementing currentColumnIndex by _vMergeConsumedGridSpan advances a second time. In rows that contain a vertical-merge continuation plus later cells (for example [continue, normal]), the next real cell is encoded one column too far, which miscomputes columnWidth and can misalign subsequent merge/colspan handling.

Useful? React with 👍 / 👎.

return;
}

const columnWidth = gridColumnWidths?.[startColumn] || null;

const result = tcTranslator.encode({
Expand Down
Loading