Skip to content

Conversation

@tupizz
Copy link
Contributor

@tupizz tupizz commented Feb 10, 2026

Summary

Fixes three interrelated table rendering bugs that caused columns to disappear, cells to overflow page boundaries, and autofit tables to render at incorrect widths.

Closes SD-1797, SD-1859, SD-1895

SD-1797: Autofit tables losing columns

  • Column count derived from cells.length instead of colSpan sums — rows with rowspan continuations have fewer physical cells, dropping the rightmost column
  • Cell colwidth values (scaled from tcW during import) preferred over authoritative w:tblGrid values
  • Auto-layout tables not scaled up to fill available page width
  • Page breaks splitting rowspan groups instead of keeping them together
CleanShot 2026-02-12 at 11 46 14

SD-1859: Percent-width table cell overflow in mixed-orientation docs

  • Tables measured at the widest section's content width (e.g. 864px landscape) but rendered in narrower sections (e.g. 624px portrait)
  • Cell X positions used rescaled column widths correctly, but cell widths still used the original measurement values — causing cells to overflow the fragment container
CleanShot.2026-02-12.at.11.53.59.mp4

SD-1895: Autofit DOCX tables rendering narrow

  • Same root cause as SD-1797 (column width priority): tcW cell hints took priority over w:tblGrid, making tables render at their hint widths instead of filling the page
CleanShot 2026-02-12 at 11 44 43

Changes

Column width priority (measuring/dom/index.ts)

  • maxCellCount now sums colSpan values per row instead of counting physical cells
  • Auto-layout tables scale both up and down to fill effectiveTargetWidth

Per-fragment column rescaling (layout-table.ts, incrementalLayout.ts)

  • New rescaleColumnWidths() proportionally scales column widths when measured width exceeds fragment width
  • TableFragment.columnWidths added to contracts — carries rescaled widths per fragment

Cell width from column widths (renderTableRow.ts, renderTableCell.ts)

  • renderTableRow computes cell width by summing columnWidths[gridStart..gridStart+colSpan]
  • renderTableCell accepts optional cellWidth parameter, using it instead of cellMeasure.width
  • renderTableFragment uses fragment.columnWidths ?? measure.columnWidths as effectiveColumnWidths

Page break improvements (layout-table.ts)

  • Rowspan-aware split point: tracks maxRowspanEnd and lastCleanFitRow to avoid splitting rowspan groups
  • Ghost cell rendering for rowspan continuations across page breaks
  • Header-skip for cantSplit rows when repeated headers would prevent fitting

Visual regression tests

  • percent-width-table-overflow — validates 100% width table doesn't overflow in portrait pages
  • autofit-table-fill-width — validates auto-layout tables fill page width (programmatic)
  • autofit-table-docx-rendering — validates SD-1895 DOCX table renders correctly

Test plan

  • pnpm --filter @superdoc/layout-engine test -- --run layout-table — rowspan-aware breaks, rescaleColumnWidths
  • pnpm --filter @superdoc/painter-dom test -- --run renderTableFragment — effectiveColumnWidths, cell width from columnWidths
  • pnpm --filter @superdoc/layout-bridge test — measurement constraint tests
  • TypeScript compiles (pnpm build)
  • Visual tests pass on chromium, firefox, webkit
  • Manual: upload mixed-orientation DOCX → verify table fits within portrait page bounds
  • Manual: upload SD-1895 autofit DOCX → verify table fills page width

Fix autofit tables dropping columns when rows use rowspan/colspan patterns
and improve page break splitting to match Word behavior.

Column rendering fixes:
- Prefer grid values over colwidth for column widths (grid sums to page width)
- Use colSpan sums for column count instead of physical cell count
- Distribute colspan tcW proportionally across grid columns during DOCX import

Page break improvements:
- Rowspan-aware split point selection (prefer breaks at rowspan boundaries)
- Ghost cell rendering for rowspan cells spanning page breaks
- Skip header repeat when it would prevent a cantSplit row from fitting
- Fix fullPageHeight to use actual content area (contentBottom - topMargin)

Cell height refinement:
- Skip last paragraph spacing.after in cell measuring and rendering

SD-1797
@github-actions
Copy link
Contributor

Based on my review of the code changes and my knowledge of ECMA-376 WordprocessingML specification, here is my assessment:

Status: PASS

The changes in legacy-handle-table-cell-node.js appear to be spec-compliant. Here's what the code is doing:

What Changed

The modification (lines 73-90, 97-98) adds logic to handle cells with w:gridSpan (colspan) where the cell's w:tcW (table cell width) exceeds the sum of the grid columns it spans.

OOXML Spec Compliance

The implementation correctly handles the ECMA-376 table model:

  1. w:tcW (Table Cell Width): Lines 63-69 correctly extract the cell width from w:tcPr/w:tcW/@w:w and convert from twips to pixels. The spec allows this attribute on table cells.

  2. w:gridSpan (Grid Span): Line 41 correctly reads w:tcPr/w:gridSpan/@w:val which specifies how many grid columns the cell spans. This is a valid ECMA-376 attribute.

  3. Width Resolution Logic: The code computes total grid width (lines 87-89) and only scales when tcW > gridSpanTotal + 1 (line 90). This matches Word's behavior where cell width hints can exceed grid totals for autofit tables.

  4. Proportional Distribution: Lines 97-98 distribute the excess width proportionally across spanned columns. While the spec doesn't mandate this exact algorithm, it's a reasonable interpretation of how to reconcile conflicting width values.

Minor Observations

  • The +1 tolerance in line 90 (cellOwnWidth > gridSpanTotal + 1) is a practical rounding buffer, not from the spec, but this is reasonable for handling floating-point precision.
  • The fallback defaultColwidth = 100 (line 94) is an implementation default when grid columns are missing, which is acceptable.

The code correctly interprets the relationship between w:tcW, w:gridSpan, and w:tblGrid elements as defined in ECMA-376 Part 1 §17.4.

For details on these elements, see https://ooxml.dev/spec?q=tcW and https://ooxml.dev/spec?q=gridSpan.

…797, SD-1859, SD-1895)

Fix three table rendering bugs:

- SD-1797: Autofit tables losing columns due to wrong column width
  priority, incorrect colspan counting, missing scale-up logic, and
  page breaks splitting rowspan groups
- SD-1859: Percent-width tables overflowing in portrait pages of
  mixed-orientation documents because cell widths used measurement
  values instead of rescaled fragment column widths
- SD-1895: Autofit tables from DOCX rendering narrow instead of
  filling page width due to cell width hints taking priority over
  the authoritative tblGrid values

Add three visual regression tests covering all fixed scenarios.
@tupizz tupizz changed the title fix(tables): fix autofit table column rendering and page break splitting fix(tables): fix autofit column scaling, cell width overflow, and page break splitting Feb 12, 2026
…7, SD-1859, SD-1895)

Introduce new visual tests to validate fixes for table rendering bugs:

- SD-1797: Ensure autofit tables with colspan preserve all columns.
- SD-1859: Verify percent-width tables fit within portrait page bounds in mixed-orientation documents.
- SD-1895: Confirm autofit tables fill the page width as expected.

Tests are conditionally skipped if the required documents are not available.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes several table rendering/layout issues across measurement, layout, and DOM painting to prevent column loss, cell overflow in mixed-orientation docs, and incorrect autofit scaling; also improves table splitting behavior and adds regression/visual coverage.

Changes:

  • Correct table measurement/scaling (colspan-aware column count; scale auto-layout tables up/down to effective width; adjust spacing.after handling).
  • Add per-fragment column width rescaling for clamped tables and use those widths when computing/rendering cell widths.
  • Improve page splitting for rowspans (clean split points + ghost cells) and add regression + visual tests/data.

Reviewed changes

Copilot reviewed 20 out of 24 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/visual/tests/rendering/table-fixes.spec.ts Adds visual rendering coverage for SD-1797/1859/1895 using doc-based screenshots.
packages/super-editor/src/tests/regression/table-autofit-colspan.test.js Adds editor-level regression test ensuring colspan/autofit tables preserve grid columns.
packages/super-editor/src/tests/data/table.docx Adds DOCX fixture for table-related tests.
packages/super-editor/src/tests/data/table-autofit-colspan.docx Adds DOCX fixture reproducing colspan/autofit grid truncation.
packages/super-editor/src/tests/data/pci-table.docx Adds DOCX fixture for rowspan/column-count scenarios.
packages/super-editor/src/core/super-converter/v3/handlers/w/tc/helpers/legacy-handle-table-cell-node.js Adjusts cell colwidth generation to optionally scale tcW across grid spans when tcW exceeds grid total.
packages/layout-engine/pm-adapter/src/converters/table.ts Changes width-priority to prefer OOXML tblGrid over cell colwidth when userEdited is false.
packages/layout-engine/pm-adapter/src/converters/table.test.ts Updates converter tests to reflect new grid-over-colwidth priority and twips→px expectations.
packages/layout-engine/painters/dom/src/table/renderTableRow.ts Computes rendered cell width from (possibly rescaled) columnWidths and passes it to cell renderer.
packages/layout-engine/painters/dom/src/table/renderTableFragment.ts Uses per-fragment columnWidths when present; adds ghost-cell rendering for rowspan continuations.
packages/layout-engine/painters/dom/src/table/renderTableFragment.test.ts Adds tests ensuring fragment.columnWidths drives rendered cell widths.
packages/layout-engine/painters/dom/src/table/renderTableCell.ts Accepts optional computed cellWidth; changes spacing.after behavior for last paragraph in a cell.
packages/layout-engine/painters/dom/src/table/renderTableCell.test.ts Updates tests to reflect last-paragraph spacing.after behavior change.
packages/layout-engine/measuring/dom/src/index.ts Makes max column count colspan-aware; scales auto-layout tables up/down; updates cell spacing.after measurement.
packages/layout-engine/measuring/dom/src/index.test.ts Adds tests for colspan/rowspan column preservation and updated spacing.after measurement expectations.
packages/layout-engine/layout-engine/src/layout-table.ts Adds rescaleColumnWidths for clamped tables; improves split-point selection for rowspans; adds header-skip for cantSplit interactions.
packages/layout-engine/layout-engine/src/layout-table.test.ts Adds tests validating per-fragment column width rescaling behavior.
packages/layout-engine/layout-bridge/test/resolveMeasurementConstraints.test.ts Adds tests ensuring measurement width uses max section content width in mixed-orientation docs.
packages/layout-engine/layout-bridge/src/incrementalLayout.ts Rescales fragment column widths during incremental layout when tables are clamped.
packages/layout-engine/contracts/src/index.ts Extends TableFragment contract with optional per-fragment columnWidths.
devtools/visual-testing/tests/interactions/stories/tables/percent-width-table-overflow.ts Adds interaction story verifying percent-width tables don’t overflow in portrait sections.
devtools/visual-testing/tests/interactions/stories/tables/autofit-table-fill-width.ts Adds interaction story verifying inserted auto-layout tables fill available width.
devtools/visual-testing/tests/interactions/stories/tables/autofit-table-docx-rendering.ts Adds interaction story validating DOCX autofit tables fill page width.
devtools/visual-testing/pnpm-lock.yaml Updates lockfile for local superdoc tarball integrity.
Files not reviewed (1)
  • devtools/visual-testing/pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +22 to +27
const table = findFirstTable(editor.state.doc);
expect(table).toBeDefined();

// The table has a 4-column grid
const grid = Array.isArray(table.attrs?.grid) ? table.attrs.grid : [];
expect(grid.length).toBe(4);
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

expect(table).toBeDefined() will still pass when table is null (Vitest only checks for !== undefined). Since findFirstTable() returns null when not found, this assertion won't fail in that case; use a null-safe assertion (e.g. toBeTruthy() / not.toBeNull()), so the test correctly fails when no table is present.

Copilot uses AI. Check for mistakes.
Comment on lines +418 to +440

if (hasExplicitBorders && tableBorders) {
// Use cell's borders, with table top border for continuation
applyBorder(ghostDiv, 'Top', cellBordersAttr.top ?? borderValueToSpec(tableBorders.top));
applyBorder(
ghostDiv,
'Left',
cellBordersAttr.left ?? borderValueToSpec(isFirstCol ? tableBorders.left : tableBorders.insideV),
);
applyBorder(
ghostDiv,
'Right',
cellBordersAttr.right ?? borderValueToSpec(isLastCol ? tableBorders.right : tableBorders.insideV),
);
if (effectiveEnd <= fragment.toRow && spanEndRow <= fragment.toRow) {
applyBorder(ghostDiv, 'Bottom', cellBordersAttr.bottom ?? borderValueToSpec(tableBorders.insideH));
}
} else if (tableBorders) {
// Resolve from table borders
applyBorder(ghostDiv, 'Top', borderValueToSpec(tableBorders.top));
applyBorder(ghostDiv, 'Left', borderValueToSpec(isFirstCol ? tableBorders.left : tableBorders.insideV));
applyBorder(ghostDiv, 'Right', borderValueToSpec(isLastCol ? tableBorders.right : tableBorders.insideV));
if (effectiveEnd <= fragment.toRow && spanEndRow <= fragment.toRow) {
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Ghost cell border resolution currently only runs when tableBorders is present; if a table has no tableBorders but a cell has explicit borders, the ghost cell renders borderless. Also, for rowspans that continue past this fragment, the ghost cell never draws a bottom border at the fragment boundary, which can leave a visible gap when fragment.continuesOnNext closes the table on the page break. Consider matching renderTableRow’s continuation/explicit-border logic for ghost cells (handle explicit borders without table borders, and apply a bottom border when the fragment ends on a page break).

Suggested change
if (hasExplicitBorders && tableBorders) {
// Use cell's borders, with table top border for continuation
applyBorder(ghostDiv, 'Top', cellBordersAttr.top ?? borderValueToSpec(tableBorders.top));
applyBorder(
ghostDiv,
'Left',
cellBordersAttr.left ?? borderValueToSpec(isFirstCol ? tableBorders.left : tableBorders.insideV),
);
applyBorder(
ghostDiv,
'Right',
cellBordersAttr.right ?? borderValueToSpec(isLastCol ? tableBorders.right : tableBorders.insideV),
);
if (effectiveEnd <= fragment.toRow && spanEndRow <= fragment.toRow) {
applyBorder(ghostDiv, 'Bottom', cellBordersAttr.bottom ?? borderValueToSpec(tableBorders.insideH));
}
} else if (tableBorders) {
// Resolve from table borders
applyBorder(ghostDiv, 'Top', borderValueToSpec(tableBorders.top));
applyBorder(ghostDiv, 'Left', borderValueToSpec(isFirstCol ? tableBorders.left : tableBorders.insideV));
applyBorder(ghostDiv, 'Right', borderValueToSpec(isLastCol ? tableBorders.right : tableBorders.insideV));
if (effectiveEnd <= fragment.toRow && spanEndRow <= fragment.toRow) {
const shouldDrawBottomBorder =
effectiveEnd <= fragment.toRow &&
(spanEndRow <= fragment.toRow || fragment.continuesOnNext === true);
if (hasExplicitBorders) {
if (tableBorders) {
// Use cell's borders, with table borders for continuation
applyBorder(ghostDiv, 'Top', cellBordersAttr.top ?? borderValueToSpec(tableBorders.top));
applyBorder(
ghostDiv,
'Left',
cellBordersAttr.left ?? borderValueToSpec(isFirstCol ? tableBorders.left : tableBorders.insideV),
);
applyBorder(
ghostDiv,
'Right',
cellBordersAttr.right ?? borderValueToSpec(isLastCol ? tableBorders.right : tableBorders.insideV),
);
if (shouldDrawBottomBorder) {
applyBorder(ghostDiv, 'Bottom', cellBordersAttr.bottom ?? borderValueToSpec(tableBorders.insideH));
}
} else {
// No table borders; use only explicit cell borders
if (cellBordersAttr.top !== undefined) {
applyBorder(ghostDiv, 'Top', cellBordersAttr.top);
}
if (cellBordersAttr.left !== undefined) {
applyBorder(ghostDiv, 'Left', cellBordersAttr.left);
}
if (cellBordersAttr.right !== undefined) {
applyBorder(ghostDiv, 'Right', cellBordersAttr.right);
}
if (shouldDrawBottomBorder && cellBordersAttr.bottom !== undefined) {
applyBorder(ghostDiv, 'Bottom', cellBordersAttr.bottom);
}
}
} else if (tableBorders) {
// Resolve from table borders
applyBorder(ghostDiv, 'Top', borderValueToSpec(tableBorders.top));
applyBorder(ghostDiv, 'Left', borderValueToSpec(isFirstCol ? tableBorders.left : tableBorders.insideV));
applyBorder(ghostDiv, 'Right', borderValueToSpec(isLastCol ? tableBorders.right : tableBorders.insideV));
if (shouldDrawBottomBorder) {

Copilot uses AI. Check for mistakes.
const hasPage2 = await page.evaluate(() => {
const pages = document.querySelectorAll('.superdoc-page');
if (pages.length > 1) {
const container = document.querySelector('.harness-main');
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Unused variable container.

Suggested change
const container = document.querySelector('.harness-main');

Copilot uses AI. Check for mistakes.
// force a page break so the remaining rows start on the next page instead of
// continuing to fill the current page with another fragment.
if (forcePageBreak && currentRow < block.rows.length) {
state = advanceColumn(state);
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The value assigned to state here is unused.

Suggested change
state = advanceColumn(state);
advanceColumn(state);

Copilot uses AI. Check for mistakes.
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a97b2c396

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

…endering tests

Modified the visual tests for table rendering issues (SD-1797, SD-1859, SD-1895) to improve document availability checks. Tests now skip with a more generic message if the required documents are not found, enhancing clarity and maintainability.
@github-actions
Copy link
Contributor

Visual diffs detected

Pixel differences were found in visual tests. This is not blocking — reproduce locally with cd tests/visual && pnpm docs:download && pnpm test to review diffs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant