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
52 changes: 46 additions & 6 deletions src/layout-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ const { ColSpanCell, RowSpanCell } = Cell;
return yConflict && xConflict;
}

function conflictExists(rows, x, y) {
function conflictExists(rows, x, y, occupied) {
if (occupied) {
return occupied[y][x] === 1;
}
let i_max = Math.min(rows.length - 1, y);
let cell = { x: x, y: y };
for (let i = 0; i <= i_max; i++) {
Expand All @@ -78,9 +81,9 @@ const { ColSpanCell, RowSpanCell } = Cell;
return false;
}

function allBlank(rows, y, xMin, xMax) {
function allBlank(rows, y, xMin, xMax, occupied) {
for (let x = xMin; x < xMax; x++) {
if (conflictExists(rows, x, y)) {
if (conflictExists(rows, x, y, occupied)) {
return false;
}
}
Expand Down Expand Up @@ -124,21 +127,57 @@ const { ColSpanCell, RowSpanCell } = Cell;
row.splice(x, 0, cell);
}

function markOccupied(occupied, cell) {
if (occupied) {
let yMax = Math.min(occupied.length, cell.y + (cell.rowSpan || 1));
for (let y = cell.y; y < yMax; y++) {
occupied[y].fill(1, cell.x, cell.x + (cell.colSpan || 1));
}
}
}

function indexOccupiedCells(table, width) {
// Preserve the existing conflict checks for malformed coordinates or spans.
if (
width === 0 ||
!Number.isInteger(width) ||
!table.every((row, y) =>
row.every(
(cell) =>
cell.y === y &&
Number.isInteger(cell.x) &&
cell.x >= 0 &&
Number.isInteger(cell.rowSpan || 1) &&
(cell.rowSpan || 1) > 0 &&
Number.isInteger(cell.colSpan || 1) &&
(cell.colSpan || 1) > 0
)
)
) {
return null;
}

let occupied = table.map(() => Buffer.alloc(width));
table.forEach((row) => row.forEach((cell) => markOccupied(occupied, cell)));
return occupied;
}

function fillInTable(table) {
let h_max = maxHeight(table);
let w_max = maxWidth(table);
let occupied = indexOccupiedCells(table, w_max);
debug(`Max rows: ${h_max}; Max cols: ${w_max}`);
for (let y = 0; y < h_max; y++) {
for (let x = 0; x < w_max; x++) {
if (!conflictExists(table, x, y)) {
if (!conflictExists(table, x, y, occupied)) {
let opts = { x: x, y: y, colSpan: 1, rowSpan: 1 };
x++;
while (x < w_max && !conflictExists(table, x, y)) {
while (x < w_max && !conflictExists(table, x, y, occupied)) {
opts.colSpan++;
x++;
}
let y2 = y + 1;
while (y2 < h_max && allBlank(table, y2, opts.x, opts.x + opts.colSpan)) {
while (y2 < h_max && allBlank(table, y2, opts.x, opts.x + opts.colSpan, occupied)) {
opts.rowSpan++;
y2++;
}
Expand All @@ -147,6 +186,7 @@ const { ColSpanCell, RowSpanCell } = Cell;
cell.y = opts.y;
warn(`Missing cell at ${cell.y}-${cell.x}.`);
insertCell(cell, table[y]);
markOccupied(occupied, cell);
}
}
}
Expand Down
104 changes: 104 additions & 0 deletions test/table-layout-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,110 @@ describe('tableLayout', function () {
return cell;
}

it('does not repeatedly inspect preceding rows when checking occupied cells', function () {
let spanReads = 0;
let cells = Array.from({ length: 50 }, (_, y) =>
Array.from({ length: 5 }, (_, x) => {
let cell = mc('value', y, x);
Object.defineProperty(cell, 'rowSpan', {
get() {
spanReads++;
return 1;
},
});
return cell;
})
);

fillInTable(cells);

// Bound work by the number of cells, rather than by machine-dependent timings.
expect(spanReads).toBeLessThanOrEqual(50 * 5 * 10);
expect(cells.every((row) => row.length === 5)).toBe(true);
});

it('preserves gap rectangles and warning order around staggered spans', function () {
const debug = require('../src/debug');
let cells = [
[mc({ content: 'a', rowSpan: 3 }, 0, 0), mc({ content: 'b', colSpan: 2 }, 0, 3)],
[mc({ content: 'c', rowSpan: 2 }, 1, 2)],
[],
[mc('d', 3, 1)],
];

debug.reset();
debug.setDebugLevel(1);
try {
fillInTable(cells);

expect(
cells.map((row) => row.map((cell) => [cell.x, cell.y, cell.colSpan, cell.rowSpan, cell.content]))
).toEqual([
[
[0, 0, 1, 3, 'a'],
[1, 0, 2, 1, ''],
[3, 0, 2, 1, 'b'],
],
[
[1, 1, 1, 2, ''],
[2, 1, 1, 2, 'c'],
[3, 1, 2, 3, ''],
],
[],
[
[0, 3, 1, 1, ''],
[1, 3, 1, 1, 'd'],
[2, 3, 1, 1, ''],
],
]);
expect(debug.debugMessages()).toEqual([
'Missing cell at 0-1.',
'Missing cell at 1-1.',
'Missing cell at 1-3.',
'Missing cell at 3-0.',
'Missing cell at 3-2.',
]);
} finally {
debug.setDebugLevel(0);
debug.reset();
}
});

it('preserves the existing fractional-span conflict behavior', function () {
let cells = [[mc({ content: 'a', rowSpan: 1.5 }, 0, 0)], [mc('b', 1, 1)]];

fillInTable(cells);

expect(cells.map((row) => row.map((cell) => [cell.x, cell.y, cell.content]))).toEqual([
[
[0, 0, 'a'],
[1, 0, ''],
],
[
[0, 1, ''],
[1, 1, 'b'],
],
]);
expect(cells[0][0].rowSpan).toBe(1.5);
});

it('only checks existing rows when a span extends below the table', function () {
let cells = [[mc({ content: 'a', rowSpan: 4 }, 0, 0)], [], []];

fillInTable(cells);

expect(cells.map((row) => row.length)).toEqual([1, 0, 0]);
expect(cells[0][0].rowSpan).toBe(4);
});

it('leaves an empty table unchanged', function () {
let cells = [[], []];

fillInTable(cells);

expect(cells).toEqual([[], []]);
});

it('will blank out individual cells', function () {
let cells = [[mc('a', 0, 1)], [mc('b', 1, 0)]];
fillInTable(cells);
Expand Down