feat: implement autofit table column width algorithm (SD-2502)#2929
Conversation
The new fixed-layout solver treats first-row w:tcW as authoritative over
the authored grid, so column-resize edits that only updated colwidth
were reverted by the next measure pass. Mirror the new span width into
tableCellProperties.cellWidth on every affected cell so the solver
observes the authored change.
Also stop syncExtractedTableAttrs from promoting `borders: null`: the
table extension's renderDOM calls Object.keys(borders), which threw on
null and broke the render cycle after every width-authoring edit. Keep
the PM schema default shape ({}) when no borders are set.
buildWidthAuthoringTableAttrs now derives tableProperties.tableWidth from the authored grid (or clears it when none exists) so column resizes and tablesSetColumnWidthAdapter no longer leave stale totals for the fixed solver and DOCX export to consume.
The working-grid normalizer started every row at column 0, so a cell in row N whose first free column was occupied by an earlier row's rowspan landed in the wrong logical column and shifted the rest of the row. Track active rowspans across rows and advance past occupied columns for both cells and gridBefore/gridAfter skips.
…dxa' }
syncExtractedTableAttrs was writing { w: String(twips), type }, which
diverged from the importer's { value: <px>, type: 'dxa' } shape and
blanked out cell spacing after any width-authoring edit. Convert through
twipsToPixels and use the value field so the promoted shape matches.
|
Status: PASS All OOXML elements and attributes in the changed test file check out against ECMA-376. Elements verified:
One behavior worth noting (not a violation): the decode tests assert that |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 00350b57e9
ℹ️ 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".
Without an explicit type, TS narrowed the object literal to
{ tableLayout: string }, dropping the Record<string, unknown> index
signature from the spread operands. That broke the subsequent
updatedTableProps.tableWidth assignment and `delete` added by the
tableWidth-recompute fix, failing `tsc -b`.
Annotate the binding as Record<string, unknown> so both operations
remain well-typed. No behavior change.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Mirror the new span width into first-row tableCellProperties.cellWidth in tablesSetColumnWidthAdapter so the fixed solver (which reads first- row tcW as authoritative) observes the edit instead of reverting it on the next measure pass. Span width is resolved from the mutated grid, falling back to pixel colwidths when the grid is missing. Same class of bug the column-resize overlay fix addressed, now on the document-api path.
Mirror distributed span widths into first-row tableCellProperties.cellWidth so the fixed solver (which reads first-row tcW as authoritative) observes the edit. Hoist grid normalization above the cell loop so the mutated grid is available to the first-row sync. Same fix as the set-column-width path, now on the distribute path.
collectTriggerCells deduped {startColumn, span, cellIndex}, collapsing
matching span-triggers from different rows to an arbitrary winner and
losing the stronger content demand. Tag cells with rowIndex so dedup
preserves all rows, then keep the strongest cell per {startColumn, span}
by preferredWidth / max content width.
…le-column-width-algorithm
|
@luccas-harbour I think this PR regresses some docs in an unintended way. Check my comment in linear pls. |
… converter Importer emits __placeholder cells to represent wBefore/wAfter spacing, but those should not appear as real cells in the TableBlock. Filter them out in parseTableRow while preserving gridBefore/gridAfter row attrs.
Min-token measurement split each run independently, so a token spanning multiple runs (e.g. "EXHIBIT\u00a0\u201cA\u201d" rendered as three styled runs) was measured as separate fragments. Track an in-progress token across runs and only flush at whitespace, hyphen, or explicit line break boundaries, treating non-breakable separators as part of the token.
…to tblW Fixed-layout tables whose authored grid is complete and already sums to the requested table width now skip per-cell tcW reconciliation and use the authored column widths directly. Incomplete grids and cases where the grid disagrees with tblW continue through the existing fixed-layout solver. Cache key includes the new flag so cached results don't leak across the two paths.
…auto When an AutoFit table has tblW=auto and a complete authored grid, treat those column widths as preferred geometry: skip the maximum/content- weighted redistribution passes so the table doesn't expand toward column maxima beyond the authored shape. Content-minimum growth and shrink-to- target still apply, so columns can grow when content forces it. Cache key includes the new flag.
…cit tblW that matches the grid Extend the preferred-grid preservation path to AutoFit tables with an explicit tblW whose authored column widths already sum to that width. Like the tblW=auto case, these grids skip the maximum/content-weighted redistribution passes so the authored shape is kept unless content minimums force growth. Cache key includes the new flag.
… non-uniform A uniform authored grid (all columns equal width) carries no shape information from the author — it's the default Word emits when no per- column intent exists. Treating those as preferred geometry suppressed content-driven redistribution. Restrict preserveAutoGrid and preserveExplicitAutoGrid to grids with at least one column whose width differs from the rest.
…ear-tblW authored grids Imported tables sometimes carry a trailing ~0px grid column (often paired with a tiny gridAfter/wAfter on the first row) that no real cell occupies. Trim those unoccupied <=1px columns from preferredColumnWidths, ignore gridAfter when wAfter sums to placeholder width, and clamp cell spans so they don't extend into the trimmed region. Authored-grid preservation also accepts grids that fall slightly under tblW (within 5%), since the trimmed placeholder leaves the remaining grid just shy of the authored total.
…ls request concrete widths The non-uniform-grid heuristic skipped preservation for uniform authored grids on the assumption they were Word's default placeholder. But a table whose cells each carry an explicit dxa tcW request is intentional geometry, even when the resulting columns happen to be equal. Allow preserveExplicitAutoGrid in that case while still excluding uniform grids whose cells use auto tcW.
|
@harbournick I pushed some more commits addressing these problems. I added more details to the linear ticket. |
…le-column-width-algorithm
|
🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.18 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.3.0-next.63 |
|
🎉 This PR is included in @superdoc-dev/react v1.2.0-next.61 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-cli v0.8.0-next.36 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.30.0-next.20 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.8.0-next.22 |
|
🎉 This PR is included in superdoc-cli v0.8.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.8.0 |
|
🎉 This PR is included in @superdoc-dev/mcp v0.3.0 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.31.0 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.3.0 |
|
🎉 This PR is included in @superdoc-dev/react v1.3.0 The release is available on GitHub release |
Summary
Implements the ECMA-376 AutoFit column-width algorithm (SD-2502) and rewires
table measurement around a clean fixed-vs-autofit split.
layout-engine/measuring/dom/:fixed-table-columns.ts— fixed-width resolver (first-roww:tcW+explicit grid), deterministic and DOM-free.
autofit-columns.ts— AutoFit resolver that starts from the fixed-passresult and redistributes by per-cell content demand (min/max widths from
table-autofit-metrics.ts).measuring/dom/src/index.ts) now delegates tothose solvers instead of carrying its own width logic; the file drops
~350 lines of ad-hoc reconciliation.
tableLayout/tableWidthattrs once at conversion time, so every downstream consumer sees the
same authoritative shape.
width) are pinned to
tableLayout: fixedand now mirror authored widthsinto
tcW/tblGridthrough a sharedtable-attr-synchelper, so thenew solver's first-row-tcW precedence doesn't revert user edits.
w:gridBefore/w:gridAfterskips and cell spans, fixing a class ofDOCX tables that previously collapsed to the wrong column count.
Word parity
This is not a byte-exact reimplementation of Word's AutoFit. ECMA-376 does
not fully specify the algorithm — the spec explicitly calls out that the
described behavior is advisory and the final column widths are up to the
consumer application. Word itself diverges from the spec in several edge
cases (content-measurement heuristics, minimum-width fallbacks, rowspan
handling under tight constraints). This implementation follows the spec
where it is prescriptive and picks a consistent, documented behavior
where the spec leaves latitude, so rendered widths may differ from Word
in those cases. The tradeoffs are captured in the solver's unit tests.
Notable fixes caught during integration
fix(autofit): redistribute widths by content demand in no-trigger path— tables that didn't hit a redistribution trigger were rendering with
stale fixed-pass widths; AutoFit now always redistributes.
fix(table-resize): keep tcW and borders aligned with authored widths— column-resize edits were being reverted by the next measure pass
because only
colwidthwas updated; also stopssyncExtractedTableAttrsfrom promotingborders: null(the tableextension's
renderDOMcalledObject.keyson it and threw).