Commit 0af690d
authored
feat: implement autofit table column width algorithm (SD-2502) (#2929)
* test: lock down table width import/export and grid invariants
* refactor: materialize effective table layout attrs in pm-adapter
* feat: add pure AutoFit column algorithm module
* refactor: normalize TableBlock data into AutoFit working-grid input
* feat: add table cell content metrics for AutoFit measurement
* fix: build fallback logical grids from row skips and spans
* feat: switch table measurement to full fixed-vs-autofit width resolution
* feat: pin width-authoring table edits to fixed layout
* test: lock down current autofit divergences
* refactor: formalize shared working table grid model
* feat: implement pure fixed-width table solver
* refactor: route fixed-layout table measurement through the new fixed solver
* refactor: adapt autofit content metrics to fixed-pass baseline inputs
* feat: rework pure autofit solver to consume fixed-pass output
* feat: switch runtime autofit measurement to fixed-plus-overrides solver
* fix(autofit): redistribute widths by content demand in no-trigger path
* fix(table-resize): keep tcW and borders aligned with authored widths
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.
* fix(table-resize): recompute nested tableWidth from grid on width edits
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.
* fix(autofit): skip rowspan-occupied columns when placing later-row cells
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.
* fix(table-attr-sync): emit tableCellSpacing as { value: <px>, type: '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.
* fix(table-attr-sync): restore index signature on updatedTableProps
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.
* fix(tables-adapter): sync first-row tcW when setting a column width
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.
* fix(tables-adapter): sync first-row tcW when distributing column widths
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.
* fix(autofit): coalesce repeated trigger cells by strongest demand
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.
* test(behavior): cover width-authoring sync flow (SD-2502)
Lock the new TableResizeOverlay -> table-attr-sync path: a column drag must
mirror the resolved span width into tableCellProperties.cellWidth (twips, dxa)
on every affected row, and flip the table to fixed-layout so the AutoFit
measuring solver respects the authored grid on the next pass. Existing
tests/tables/resize.spec.ts only checks the grid attr; this adds the
per-cell tcW mirror that the new layout-engine modules depend on.
Also uploaded rendering/sd-2502-autofit-table-algorithm.docx to the corpus
for layout/visual auto-discovery (Word-native, tblLayout omitted = autofit
per ECMA, gridSpan=2, long unbreakable token forces column growth).
* fix(autofit): preserve protected column widths when no slack basis remains
distributeRemainingSlack fell back to a uniform targetWidth / N spread
whenever the proportional basis collapsed to 0, ignoring the
growableColumns set passed in by the caller. Protected columns (e.g.
ones already locked to a span trigger) were silently overwritten back
to the uniform target, undoing the trigger.
Honor growableColumns in the zero-basis branch: leave widths untouched
when no growable columns remain, otherwise spread the remaining slack
equally across only the growable indexes so protected columns keep
their resolved widths.
Regression test: 2-col table with a span trigger pulling both columns
to a summed max of 240 — totalWidth should stay at 240 instead of
expanding back to the preferred 300.
* fix(autofit): include working grid and fixed layout in result cache key
The autofit table-result cache key only hashed maxWidth, cellMetricKeys,
and layoutEpoch, so tables with identical cell metrics but different
row placements or fixed-layout starting widths collided and reused
stale cached results. Hash the working-grid placements (cell starts,
spans, preferred widths, skipped columns, logical column count) and the
fixed-layout result alongside the existing inputs.
* chore: remove dead getMeasuredCellBorderWidthPx helper
* chore(autofit): explicitly type working-grid cells in cache-key serializer
No behavior change — narrows row.cells to WorkingTableCellInput inside
buildAutoFitTableResultCacheKey so the per-cell field accesses resolve
through the working-grid contract.
* fix(pm-adapter): skip gridBefore/gridAfter placeholder cells in table 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.
* fix(measuring): accumulate token widths across runs in autofit min-width
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.
* feat(measuring): preserve authored fixed-table grid when columns sum 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.
* feat(measuring): preserve authored grid for AutoFit tables with tblW=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.
* feat(measuring): preserve authored grid for AutoFit tables with explicit 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.
* fix(measuring): only preserve authored AutoFit grids when columns are 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.
* fix(measuring): drop trailing placeholder grid columns and tolerate near-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.
* fix(measuring): preserve uniform explicit-tblW AutoFit grids when cells 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.1 parent 38a1785 commit 0af690d
26 files changed
Lines changed: 7604 additions & 470 deletions
File tree
- packages
- layout-engine
- measuring/dom/src
- pm-adapter/src/converters
- super-editor/src/editors/v1
- components
- core/super-converter
- helpers
- v3/handlers/w/tbl
- document-api-adapters
- __conformance__
- helpers
- tests/regression
- tests/behavior/tests/tables
Lines changed: 872 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 1224 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 657 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 576 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 380 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
0 commit comments