@react-x11/components 0.14.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/code-editor/node.d.ts +13 -0
  2. package/dist/code-editor/node.d.ts.map +1 -1
  3. package/dist/code-editor/node.js +23 -5
  4. package/dist/code-editor/node.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +171 -26
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +616 -216
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +25 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +307 -19
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/content.d.ts +11 -6
  14. package/dist/html/css/content.d.ts.map +1 -1
  15. package/dist/html/css/content.js +171 -12
  16. package/dist/html/css/content.js.map +1 -1
  17. package/dist/html/css/parse.d.ts.map +1 -1
  18. package/dist/html/css/parse.js +62 -9
  19. package/dist/html/css/parse.js.map +1 -1
  20. package/dist/html/css/shapes.d.ts +18 -0
  21. package/dist/html/css/shapes.d.ts.map +1 -1
  22. package/dist/html/css/shapes.js +21 -1
  23. package/dist/html/css/shapes.js.map +1 -1
  24. package/dist/html/css/style.d.ts +42 -5
  25. package/dist/html/css/style.d.ts.map +1 -1
  26. package/dist/html/css/style.js +74 -12
  27. package/dist/html/css/style.js.map +1 -1
  28. package/dist/html/css/ua.js +15 -0
  29. package/dist/html/css/ua.js.map +1 -1
  30. package/dist/html/css/values.d.ts +5 -0
  31. package/dist/html/css/values.d.ts.map +1 -1
  32. package/dist/html/css/values.js +18 -9
  33. package/dist/html/css/values.js.map +1 -1
  34. package/dist/html/index.d.ts.map +1 -1
  35. package/dist/html/index.js +7 -0
  36. package/dist/html/index.js.map +1 -1
  37. package/dist/html/layout/block.d.ts +29 -9
  38. package/dist/html/layout/block.d.ts.map +1 -1
  39. package/dist/html/layout/block.js +141 -52
  40. package/dist/html/layout/block.js.map +1 -1
  41. package/dist/html/layout/boxes.d.ts +16 -2
  42. package/dist/html/layout/boxes.d.ts.map +1 -1
  43. package/dist/html/layout/boxes.js +144 -23
  44. package/dist/html/layout/boxes.js.map +1 -1
  45. package/dist/html/layout/css-grid.d.ts.map +1 -1
  46. package/dist/html/layout/css-grid.js +11 -9
  47. package/dist/html/layout/css-grid.js.map +1 -1
  48. package/dist/html/layout/flex.d.ts.map +1 -1
  49. package/dist/html/layout/flex.js +1631 -221
  50. package/dist/html/layout/flex.js.map +1 -1
  51. package/dist/html/layout/inline.d.ts +15 -3
  52. package/dist/html/layout/inline.d.ts.map +1 -1
  53. package/dist/html/layout/inline.js +23 -8
  54. package/dist/html/layout/inline.js.map +1 -1
  55. package/dist/html/layout/table.d.ts +27 -1
  56. package/dist/html/layout/table.d.ts.map +1 -1
  57. package/dist/html/layout/table.js +886 -324
  58. package/dist/html/layout/table.js.map +1 -1
  59. package/dist/html/node.d.ts +35 -4
  60. package/dist/html/node.d.ts.map +1 -1
  61. package/dist/html/node.js +118 -26
  62. package/dist/html/node.js.map +1 -1
  63. package/dist/html/paint.d.ts.map +1 -1
  64. package/dist/html/paint.js +558 -178
  65. package/dist/html/paint.js.map +1 -1
  66. package/dist/html/resources.js +14 -1
  67. package/dist/html/resources.js.map +1 -1
  68. package/dist/html/svg.d.ts +23 -7
  69. package/dist/html/svg.d.ts.map +1 -1
  70. package/dist/html/svg.js +264 -59
  71. package/dist/html/svg.js.map +1 -1
  72. package/dist/html/widgets.d.ts.map +1 -1
  73. package/dist/html/widgets.js +57 -4
  74. package/dist/html/widgets.js.map +1 -1
  75. package/package.json +3 -3
  76. package/src/code-editor/node.ts +24 -5
  77. package/src/html/css/cascade.ts +697 -221
  78. package/src/html/css/color.ts +345 -18
  79. package/src/html/css/content.ts +173 -15
  80. package/src/html/css/parse.ts +60 -8
  81. package/src/html/css/shapes.ts +35 -1
  82. package/src/html/css/style.ts +122 -17
  83. package/src/html/css/ua.ts +15 -0
  84. package/src/html/css/values.ts +23 -9
  85. package/src/html/index.ts +7 -0
  86. package/src/html/layout/block.ts +149 -58
  87. package/src/html/layout/boxes.ts +171 -27
  88. package/src/html/layout/css-grid.ts +12 -9
  89. package/src/html/layout/flex.ts +1843 -212
  90. package/src/html/layout/inline.ts +27 -7
  91. package/src/html/layout/table.ts +961 -332
  92. package/src/html/node.ts +151 -27
  93. package/src/html/paint.ts +692 -234
  94. package/src/html/resources.ts +13 -1
  95. package/src/html/svg.ts +299 -56
  96. package/src/html/widgets.ts +61 -4
@@ -25,6 +25,7 @@ import {
25
25
  cyclicWidth,
26
26
  exactMinContent,
27
27
  layoutBlockIn,
28
+ layoutSubtree,
28
29
  measureIntrinsicWidth,
29
30
  moveContent,
30
31
  moveTo,
@@ -33,16 +34,31 @@ import {
33
34
  import type { LayoutContext } from './block.js';
34
35
  import { firstBaselineIn } from './inline.js';
35
36
  import { tableGrid } from './grid.js';
36
- import type { Cell } from './grid.js';
37
+ import type { Cell, TableGrid } from './grid.js';
37
38
 
38
- /** Lay out a table's insides. Returns the content height. */
39
+ /**
40
+ * Lay out a table's insides. Returns the content height.
41
+ *
42
+ * A table in a cell is measured and laid out from inside the layout of the
43
+ * table around it, so nested tables hold every level's frames on the stack
44
+ * at once. So this is the phases and nothing else, and what waits on the
45
+ * stack while a cell's content is measured (`measureCell`) and laid out
46
+ * (`layoutCells`) is the few values passed between them; the columns'
47
+ * arithmetic (`sizeColumns`, `autoColumns`) and the rows' (`placeRows`)
48
+ * are done before or after, in frames of their own. As one function, every
49
+ * local of every phase in the one frame the interpreter keeps whole, a
50
+ * level of nested tables took 4 KB of the stack where it takes 2.8, and
51
+ * `display: table-cell` boxes 254 deep, as deep as the parser lets a
52
+ * document go, ran out of the 984 KB V8 gives the main thread: the
53
+ * document came out blank. (The box builder stops them at 128 now, for
54
+ * paint's sake, `MAX_DEPTH`.)
55
+ */
39
56
  export function layoutTable(
40
57
  table: Box,
41
58
  ctx: LayoutContext,
42
59
  contentWidth: number,
43
60
  ): number {
44
- const { rows, captions, cells, columnCount, columnBoxes, columnGroups } =
45
- tableGrid(table);
61
+ const grid = tableGrid(table);
46
62
  table.captionTop = 0;
47
63
  table.captionBottom = 0;
48
64
  // a percentage of a width that is not known is `auto`: the table is as
@@ -52,38 +68,11 @@ export function layoutTable(
52
68
  // — and one a grid stretches across its area is as wide as that, as a
53
69
  // table with a width of its own is (`STRETCHED_ACROSS`)
54
70
  const auto = own && !STRETCHED_ACROSS.has(table);
55
- if (!columnCount) {
56
- // With no columns, an auto table is as wide as its widest caption can
57
- // be and its `min-width` asks, as one with columns is at the least
58
- // (17.5.2), rather than as the room on offer: a caption over no cells
59
- // was centred in the width of the page.
60
- if (auto) {
61
- let inner = captions.length
62
- ? captionMinimum(captions, ctx) - table.horizontalExtra
63
- : 0;
64
- const min = resolveOrNull(table.style.minWidth, contentWidth);
65
- if (min !== null) {
66
- inner = Math.max(
67
- inner,
68
- table.style.boxSizing === 'border-box'
69
- ? min - table.horizontalExtra
70
- : min,
71
- );
72
- }
73
- table.width = Math.max(0, inner) + table.horizontalExtra;
74
- }
75
- layoutCaptions(captions, ctx, table);
71
+ if (!grid.columnCount) {
72
+ layoutColumnless(table, grid.captions, ctx, contentWidth, auto);
76
73
  return 0;
77
74
  }
78
75
 
79
- const style = table.style;
80
- // between the columns, and between the rows (CSS 2.1 17.6.1)
81
- const apart = style.borderCollapse !== 'collapse';
82
- const spacing = apart ? style.borderSpacing : 0;
83
- const rowSpacing = apart ? style.borderSpacingY : 0;
84
- const gaps = spacing * (columnCount + 1);
85
- const available = Math.max(0, contentWidth - gaps);
86
-
87
76
  // While the columns are sized, a percentage in a cell's padding is of
88
77
  // nothing: the width it is of is the one the columns are being sized to
89
78
  // come to (CSS Sizing 3, 5.2.1), and a cell asks of its column what its
@@ -91,31 +80,92 @@ export function layoutTable(
91
80
  // widths, below. Taken of the table's width here, and of the cell's own
92
81
  // where the cell was laid out, a padded cell's column was wider than
93
82
  // its share and its padding narrower.
94
- for (const cell of cells) resolveEdges(cell.box, 0);
83
+ for (const cell of grid.cells) resolveEdges(cell.box, 0);
95
84
 
96
85
  // a fixed layout needs a width to be fixed to; with `auto` a table is laid
97
86
  // out by its contents after all (CSS 2.1 17.5.2.1), stretched or not
98
- const fixed = style.tableLayout === 'fixed' && !own;
99
- // a percentage of the table's width less its spacing (CSS 2.1 17.5.2.1)
100
- const set = columnWidths(columnBoxes, columnGroups, available);
101
- const widths = fixed
102
- ? fixedColumns(cells, set, columnCount, available, spacing)
103
- : autoColumns(
104
- cells,
105
- set,
106
- columnCount,
107
- available,
108
- ctx,
109
- contentWidth,
110
- !auto,
111
- spacing,
87
+ const fixed = table.style.tableLayout === 'fixed' && !own;
88
+ if (!fixed) for (const cell of grid.cells) measureCell(cell.box, ctx);
89
+ const columns = sizeColumns(table, grid, contentWidth, fixed, auto, ctx);
90
+ layoutCaptions(grid.captions, ctx, table);
91
+ const natural = layoutCells(grid.cells, columns, ctx);
92
+ return placeRows(table, grid, columns, natural);
93
+ }
94
+
95
+ /** A table with no columns, and its captions. */
96
+ function layoutColumnless(
97
+ table: Box,
98
+ captions: Box[],
99
+ ctx: LayoutContext,
100
+ contentWidth: number,
101
+ auto: boolean,
102
+ ): void {
103
+ // With no columns, an auto table is as wide as its widest caption can
104
+ // be and its `min-width` asks, as one with columns is at the least
105
+ // (17.5.2), rather than as the room on offer: a caption over no cells
106
+ // was centred in the width of the page.
107
+ if (auto) {
108
+ let inner = captions.length
109
+ ? captionMinimum(captions, ctx) - table.horizontalExtra
110
+ : 0;
111
+ const min = resolveOrNull(table.style.minWidth, contentWidth);
112
+ if (min !== null) {
113
+ inner = Math.max(
114
+ inner,
115
+ table.style.boxSizing === 'border-box'
116
+ ? min - table.horizontalExtra
117
+ : min,
112
118
  );
119
+ }
120
+ table.width = Math.max(0, inner) + table.horizontalExtra;
121
+ }
122
+ layoutCaptions(captions, ctx, table);
123
+ TABLE_CONTENT.set(
124
+ table,
125
+ table.captionTop + table.captionBottom + table.verticalExtra,
126
+ );
127
+ }
128
+
129
+ /** Where a table's columns are (`sizeColumns`). */
130
+ interface Columns {
131
+ widths: number[];
132
+ /** Each column's left edge. */
133
+ columnX: number[];
134
+ /** Whether `visibility: collapse` takes each column out, and any. */
135
+ gone: boolean[];
136
+ anyGone: boolean;
137
+ /** Where the columns end: past the last one left in, and the spacing
138
+ * after it. */
139
+ end: number;
140
+ /** Between the columns, and between the rows (CSS 2.1 17.6.1). */
141
+ spacing: number;
142
+ rowSpacing: number;
143
+ }
144
+
145
+ /** A table's columns sized and placed, and the table as wide as they
146
+ * make it. Its cells are measured already (`measureCell`). */
147
+ function sizeColumns(
148
+ table: Box,
149
+ grid: TableGrid,
150
+ contentWidth: number,
151
+ fixed: boolean,
152
+ auto: boolean,
153
+ ctx: LayoutContext,
154
+ ): Columns {
155
+ const { cells, captions, columnCount, columnBoxes, columnGroups } = grid;
156
+ const style = table.style;
157
+ // between the columns, and between the rows (CSS 2.1 17.6.1)
158
+ const apart = style.borderCollapse !== 'collapse';
159
+ const spacing = apart ? style.borderSpacing : 0;
160
+ const rowSpacing = apart ? style.borderSpacingY : 0;
161
+ const gaps = spacing * (columnCount + 1);
162
+ const available = Math.max(0, contentWidth - gaps);
113
163
 
114
- // --- place ---------------------------------------------------------------
115
164
  // An auto table is at least as wide as its widest caption can be (CSS 2.1
116
- // 17.5.2), and as its `min-width` asks; the columns share what that adds.
165
+ // 17.5.2), and as its `min-width` asks; the columns share what that adds
166
+ // as they share a width of the table's own (`autoColumns`).
167
+ let room = 0;
117
168
  if (auto) {
118
- let room = 0;
119
169
  if (captions.length) {
120
170
  room = captionMinimum(captions, ctx) - table.horizontalExtra - gaps;
121
171
  }
@@ -125,17 +175,37 @@ export function layoutTable(
125
175
  style.boxSizing === 'border-box' ? min - table.horizontalExtra : min;
126
176
  room = Math.max(room, inner - gaps);
127
177
  }
128
- const sum = widths.reduce((a, b) => a + b, 0);
129
- if (room > sum) {
130
- for (let c = 0; c < columnCount; c += 1) {
131
- widths[c] +=
132
- sum > 0
133
- ? (widths[c] / sum) * (room - sum)
134
- : (room - sum) / columnCount;
135
- }
136
- }
137
178
  }
179
+ // A percentage, a column's or a cell's, is of the table's width less
180
+ // its spacing: CSS Tables 3's assignable table width, which Blink takes
181
+ // one of in either layout, so a 50% cell in a table of 300 spaced 2 is
182
+ // 147. In an auto table it is of the width the columns come to, and
183
+ // helps decide it (`autoColumns`).
184
+ const set = columnWidths(columnBoxes, columnGroups, available, fixed);
185
+ const widths = fixed
186
+ ? fixedColumns(
187
+ cells,
188
+ set,
189
+ columnPercents(columnBoxes, columnGroups),
190
+ columnLeast(columnBoxes, columnGroups),
191
+ columnCount,
192
+ available,
193
+ spacing,
194
+ )
195
+ : autoColumns(
196
+ cells,
197
+ set,
198
+ columnLeast(columnBoxes, columnGroups),
199
+ columnPercents(columnBoxes, columnGroups),
200
+ columnCount,
201
+ available,
202
+ !auto,
203
+ spacing,
204
+ room,
205
+ percentagesWiden(table, contentWidth),
206
+ );
138
207
 
208
+ // --- place ---------------------------------------------------------------
139
209
  // A column that `visibility: collapse` takes out of the table has been
140
210
  // sized with the rest, and is no room at all where they are placed (CSS
141
211
  // 2.1 17.5.5): no width, and no spacing after it. Its cells are still
@@ -171,48 +241,88 @@ export function layoutTable(
171
241
  tableContentWidth = used;
172
242
  table.width = used + table.horizontalExtra;
173
243
  }
174
- layoutCaptions(captions, ctx, table);
175
-
176
- const top = table.contentY + table.captionTop;
177
- let y = top;
178
- const rowTop: number[] = new Array<number>(rows.length);
179
- const rowHeight: number[] = new Array<number>(rows.length).fill(0);
180
- y += rowSpacing;
244
+ return { widths, columnX, gone, anyGone, end: x, spacing, rowSpacing };
245
+ }
181
246
 
182
- // Size every cell at its column width first, so a row's height is the
183
- // tallest cell in it rather than the first one that was measured. What
184
- // its content came to is kept apart from a height the cell sets: it is
185
- // that content `vertical-align` moves in the cell.
186
- //
187
- // A percentage in a cell's padding is of the width of its row: the
188
- // columns and the spacing between them, without the spacing either side
189
- // and the table's own padding. CSS 2.1 8.4 has it of the containing
190
- // block's width and names none for a cell; the row's is what browsers
191
- // take, and what the table's content box is wherever there is no
192
- // spacing. Every column counts, one `visibility: collapse` takes out
193
- // too, so that taking it out changes no row's height.
194
- let rowWidth = spacing * Math.max(0, columnCount - 1);
247
+ /**
248
+ * Every cell laid out at its columns' width, all of them before a row is
249
+ * sized, so a row's height is the tallest cell in it rather than the first
250
+ * one that was measured. What its content came to is kept apart from a
251
+ * height the cell sets: it is that content `vertical-align` moves in the
252
+ * cell.
253
+ *
254
+ * A percentage in a cell's padding is of the width of its row: the
255
+ * columns and the spacing between them, without the spacing either side
256
+ * and the table's own padding. CSS 2.1 8.4 has it of the containing
257
+ * block's width and names none for a cell; the row's is what browsers
258
+ * take, and what the table's content box is wherever there is no
259
+ * spacing. Every column counts, one `visibility: collapse` takes out
260
+ * too, so that taking it out changes no row's height.
261
+ *
262
+ * Returns what each cell's content came to.
263
+ */
264
+ function layoutCells(
265
+ cells: Cell[],
266
+ columns: Columns,
267
+ ctx: LayoutContext,
268
+ ): number[] {
269
+ const { widths, spacing } = columns;
270
+ let rowWidth = spacing * Math.max(0, widths.length - 1);
195
271
  for (const columnWidth of widths) rowWidth += columnWidth;
196
272
  const natural: number[] = new Array<number>(cells.length);
197
273
  for (let i = 0; i < cells.length; i += 1) {
198
274
  const cell = cells[i];
199
275
  const width = spannedWidth(widths, cell, spacing);
200
- ctx.layoutSubtree(cell.box, width, rowWidth);
276
+ layoutSubtree(cell.box, ctx, width, rowWidth);
201
277
  natural[i] = CELL_CONTENT.get(cell.box) ?? cell.box.height;
202
278
  // A height a cell sets is a least one: its content is laid out as a
203
279
  // block's, which keeps a height it sets and lets the content run out of
204
280
  // it, and a cell does not. `<td height="10">` with a line in it is as
205
281
  // tall as the line (CSS 2.1 17.5.3), where it was 10px with the line
206
282
  // hanging out of it.
207
- if (natural[i] > cell.box.height) cell.box.height = natural[i];
283
+ //
284
+ // The height is of the box `box-sizing` names, and a border-box one
285
+ // holds at least the padding and border; a cell's `min-height` and
286
+ // `max-height` count for nothing here, as in Blink (`ComputeRowData`),
287
+ // so the height its own layout clamped to them is not kept. The padding
288
+ // went on a border-box height a second time, and a cell set 80px tall
289
+ // was 100px. (Quirks mode takes every cell's height as a border box;
290
+ // `<Html>` has no quirks mode.)
291
+ cell.box.height = natural[i];
208
292
  const specified = resolveOrNull(cell.box.style.height, NaN);
209
293
  if (specified !== null) {
294
+ const extra = cell.box.verticalExtra;
295
+ const set = Math.max(0, specified);
210
296
  cell.box.height = Math.max(
211
297
  cell.box.height,
212
- specified + cell.box.verticalExtra,
298
+ cell.box.style.boxSizing === 'border-box'
299
+ ? Math.max(set, extra)
300
+ : set + extra,
213
301
  );
214
302
  }
215
303
  }
304
+ return natural;
305
+ }
306
+
307
+ /** The rows' heights, and the rows and the cells placed, once every cell
308
+ * is laid out at its columns' width (`layoutCells`) and its content came
309
+ * to `natural`. Returns the rows' height. */
310
+ function placeRows(
311
+ table: Box,
312
+ grid: TableGrid,
313
+ columns: Columns,
314
+ natural: number[],
315
+ ): number {
316
+ const { rows, cells } = grid;
317
+ const { widths, columnX, gone, anyGone, spacing, rowSpacing } = columns;
318
+ const columnCount = widths.length;
319
+ const style = table.style;
320
+ const top = table.contentY + table.captionTop;
321
+ let y = top;
322
+ const rowTop: number[] = new Array<number>(rows.length);
323
+ const rowHeight: number[] = new Array<number>(rows.length).fill(0);
324
+ y += rowSpacing;
325
+
216
326
  // A cell aligned on the baseline — every value but `top`, `middle` and
217
327
  // `bottom` — hangs its first line on its first row's baseline, the lowest
218
328
  // of theirs from the row's top (CSS 2.1 17.5.3), and the row is as tall as
@@ -220,17 +330,23 @@ export function layoutTable(
220
330
  // needs the lift and its content, and a height it sets is a least one
221
331
  // beside that rather than more room under the lift: two cells set
222
332
  // `height: 80px` with text of two sizes in them are a row 80px tall, where
223
- // the lift on top of the height made it 104px. A cell spanning rows needs
224
- // the same of them, which is where Chrome differs: it sizes a spanning
225
- // cell by its content and height alone and lets the lifted content hang
226
- // out of it, and the rows here "encompass the cell" (17.5.3).
227
- const lift = baselineLifts(cells, rows.length, natural);
333
+ // the lift on top of the height made it 104px.
334
+ //
335
+ // A cell spanning rows asks its first row for its baseline and nothing
336
+ // under it, and the rows it spans for its content and height alone, as
337
+ // Chrome sizes it (Blink's `RowBaselineTabulator` gives it an ascent and
338
+ // no descent): the lifted content hangs out of the cell. 17.5.3 asks only
339
+ // that the rows encompass the cell, whose box is as tall as its content.
340
+ const { lift, own: ascent } = baselineLifts(cells, rows.length, natural);
228
341
  const needs = (i: number): number =>
229
- Math.max(cells[i].box.height, lift[i] + natural[i]);
342
+ cells[i].rowSpan > 1
343
+ ? cells[i].box.height
344
+ : Math.max(cells[i].box.height, lift[i] + natural[i]);
230
345
  for (let i = 0; i < cells.length; i += 1) {
231
346
  const cell = cells[i];
232
- if (cell.rowSpan > 1) continue;
233
- rowHeight[cell.row] = Math.max(rowHeight[cell.row], needs(i));
347
+ const need = cell.rowSpan > 1 ? lift[i] + ascent[i] : needs(i);
348
+ // NaN for a spanning cell with no baseline, which Math.max would keep
349
+ if (need > rowHeight[cell.row]) rowHeight[cell.row] = need;
234
350
  }
235
351
  for (let r = 0; r < rows.length; r += 1) {
236
352
  const specified = resolveOrNull(rows[r].style.height, NaN);
@@ -238,18 +354,59 @@ export function layoutTable(
238
354
  }
239
355
  // A row-spanning cell only forces height when the rows it covers do not
240
356
  // already provide it — otherwise a `rowspan="3"` cell would make each of
241
- // its three rows as tall as all of it.
357
+ // its three rows as tall as all of it — and what they come short of it
358
+ // is shared out over them (`growRows`), where it all went to the last.
359
+ // Blink takes the cells in an order of its own: one inside another's
360
+ // rows first, so that the outer one is given what the inner left it; of
361
+ // two over the same rows, the taller; and else from the top down.
362
+ const spanning: number[] = [];
363
+ const lastRow = (cell: Cell): number =>
364
+ Math.min(rows.length - 1, cell.row + cell.rowSpan - 1);
242
365
  for (let i = 0; i < cells.length; i += 1) {
243
- const cell = cells[i];
244
- if (cell.rowSpan <= 1) continue;
245
- const last = Math.min(rows.length - 1, cell.row + cell.rowSpan - 1);
246
- let covered = 0;
247
- for (let r = cell.row; r <= last; r += 1) covered += rowHeight[r];
248
- covered += rowSpacing * (last - cell.row);
249
- const missing = needs(i) - covered;
250
- if (missing > 0) rowHeight[last] += missing;
366
+ if (cells[i].rowSpan > 1) spanning.push(i);
367
+ }
368
+ if (spanning.length) {
369
+ const starts: boolean[] = new Array<boolean>(rows.length).fill(false);
370
+ for (const i of spanning) {
371
+ if (lastRow(cells[i]) > cells[i].row) starts[cells[i].row] = true;
372
+ }
373
+ spanning.sort((a, b) => {
374
+ const p = cells[a];
375
+ const q = cells[b];
376
+ const pEnd = lastRow(p);
377
+ const qEnd = lastRow(q);
378
+ if (p.row === q.row && pEnd === qEnd) return needs(b) - needs(a);
379
+ if (p.row >= q.row && pEnd <= qEnd) return -1;
380
+ if (q.row >= p.row && qEnd <= pEnd) return 1;
381
+ return p.row - q.row;
382
+ });
383
+ const sizing = rowSizing(rows, cells);
384
+ for (const i of spanning) {
385
+ const cell = cells[i];
386
+ const last = lastRow(cell);
387
+ let covered = 0;
388
+ for (let r = cell.row; r <= last; r += 1) covered += rowHeight[r];
389
+ covered += rowSpacing * (last - cell.row);
390
+ const missing = needs(i) - covered;
391
+ if (missing > 0) {
392
+ growRows(rowHeight, sizing, missing, null, cell.row, last, starts);
393
+ }
394
+ }
251
395
  }
252
396
 
397
+ // What the rows come to before any height is shared out to them, the
398
+ // spacing above, below and between them in (`TABLE_CONTENT`)
399
+ let rowsOwn = rowSpacing;
400
+ for (let r = 0; r < rows.length; r += 1) {
401
+ if (rows[r].style.visibility !== 'collapse') {
402
+ rowsOwn += rowHeight[r] + rowSpacing;
403
+ }
404
+ }
405
+ TABLE_CONTENT.set(
406
+ table,
407
+ rowsOwn + table.captionTop + table.captionBottom + table.verticalExtra,
408
+ );
409
+
253
410
  // A table's own height, within its least and greatest, is a least
254
411
  // height: what its rows come short of it goes to them (CSS 2.1 17.5.3,
255
412
  // which leaves how to its user agents: `growRows`). So is the height a
@@ -257,9 +414,13 @@ export function layoutTable(
257
414
  // `USED_HEIGHT`), which is its wrapper box's, captions and all (CSS
258
415
  // Flexbox 1, 4): taken as the table's box alone, a table stretched down
259
416
  // its line kept its rows at their content's height, and a cell had
260
- // nothing for `vertical-align` to centre its content in.
417
+ // nothing for `vertical-align` to centre its content in. That one is its
418
+ // height in place of its own, which the flex layout started from: flexed
419
+ // down a column shorter than a height of its own, it is as short as that,
420
+ // where its rows let it be (Blink: "A table interprets forced block-size
421
+ // as the block-size of its rows", `flex_layout_algorithm.cc`).
261
422
  const wanted = resolveOrNull(style.height, table.percentHeightBase);
262
- const given = FLEXED_HEIGHT.get(table) ?? USED_HEIGHT.get(table) ?? NaN;
423
+ const given = givenHeight(table);
263
424
  if (rows.length) {
264
425
  const extraBox = table.verticalExtra;
265
426
  const set =
@@ -268,10 +429,9 @@ export function layoutTable(
268
429
  : style.boxSizing === 'border-box'
269
430
  ? wanted
270
431
  : wanted + extraBox;
271
- let inner = clampHeight(table, set) - extraBox;
272
- if (Number.isFinite(given)) {
273
- inner = Math.max(inner, given - table.captionTop - table.captionBottom);
274
- }
432
+ const inner = Number.isFinite(given)
433
+ ? given - table.captionTop - table.captionBottom
434
+ : clampHeight(table, set) - extraBox;
275
435
  let total = 0;
276
436
  for (const h of rowHeight) total += h;
277
437
  const extra = inner - total - rowSpacing * (rows.length + 1);
@@ -383,7 +543,7 @@ export function layoutTable(
383
543
  // left edge to the last one's right, the spacing around them outside it
384
544
  // (CSS 2.1 17.5.1), as it is above and below.
385
545
  const gridX = table.contentX + spacing;
386
- const gridWidth = Math.max(0, x - spacing - gridX);
546
+ const gridWidth = Math.max(0, columns.end - spacing - gridX);
387
547
  for (let r = 0; r < rows.length; r += 1) {
388
548
  const row = rows[r];
389
549
  row.x = gridX;
@@ -407,6 +567,20 @@ export function layoutTable(
407
567
  return y - top;
408
568
  }
409
569
 
570
+ /**
571
+ * A table's height as its rows and captions came to in its last layout,
572
+ * border box, apart from a height it sets or is given: the least it can be
573
+ * at the width it was laid out at. What a flex layout holds a table item to
574
+ * down a column (`flex.ts`'s `autoMinimums`).
575
+ */
576
+ export const TABLE_CONTENT = new WeakMap<Box, number>();
577
+
578
+ /** The content height a flex or grid layout gave a table, its captions in
579
+ * (`FLEXED_HEIGHT`, `USED_HEIGHT`), and else NaN. */
580
+ export function givenHeight(table: Box): number {
581
+ return FLEXED_HEIGHT.get(table) ?? USED_HEIGHT.get(table) ?? NaN;
582
+ }
583
+
410
584
  /** Whether `visibility: collapse` takes a column out: its own, or the
411
585
  * column group's that holds it with no column box of its own. */
412
586
  function collapsedColumn(column: Box | null, group: Box | null): boolean {
@@ -485,137 +659,291 @@ function spannedWidth(widths: number[], cell: Cell, spacing: number): number {
485
659
  /**
486
660
  * `table-layout: fixed`: the columns and the first row decide, and the rest
487
661
  * of the table is not measured at all, which is the whole point of it (CSS
488
- * 2.1 17.5.2.1). A column with a `width` sets its own; failing that, a cell
489
- * of the first row with one sets the columns it spans, by its border box and
490
- * less the spacing between them; the columns left share what remains, and
491
- * where nothing remains, what is left of a table wider than its columns is
492
- * shared among all of them.
662
+ * 2.1 17.5.2.1). Each column is set to a length, to a percentage of the
663
+ * table's width less its spacing, or to neither. A `<col>` set to a length
664
+ * has it whatever its cells say; otherwise a cell of the first row sets
665
+ * the column it is in, its percentage over the `<col>`'s where it is the
666
+ * larger. A cell spanning columns gives each that has no length an even
667
+ * share of its border box less the spacing between them, and each that
668
+ * has neither an even share of its percentage: `<td colspan=2
669
+ * style="width:60%">` sets two columns to 30%, and one 200 wide over a
670
+ * `<col>` of 50 and a column of nothing makes that 99, where it was the
671
+ * 148 the `<col>` left. All of this is Blink's, which CSS 2.1 leaves to
672
+ * the browser.
673
+ *
674
+ * The table's width is shared out as Blink shares it too
675
+ * (`SynchronizeAssignableTableInlineSizeAndColumnsFixed`): the lengths
676
+ * first, then the percentages, then what is left to the columns set to
677
+ * neither. Percentages that come to more than 100% are scaled until they
678
+ * come to that, and percentages that come to more than the lengths leave
679
+ * are scaled down to it. Where no column is set to neither, what is left
680
+ * goes to the lengths, or failing those to the percentages. Only a column's
681
+ * least widens the table: its length, or a `<col>`'s `min-width`. Taken
682
+ * as lengths, two cells of 60% made a table of 300 one of 359, where each
683
+ * is 147 and the table 300; and a length of 100 beside a 60% column
684
+ * shared what the two left with it, 106.4 and 187.6, where the length
685
+ * takes it all, 117.6 and 176.4.
493
686
  */
494
687
  function fixedColumns(
495
688
  cells: Cell[],
496
689
  columnWidths: (number | null)[],
690
+ columnPercents: (number | null)[],
691
+ columnLeast: (number | null)[],
497
692
  columnCount: number,
498
693
  available: number,
499
694
  spacing: number,
500
695
  ): number[] {
501
- const widths: number[] = new Array<number>(columnCount).fill(0);
502
- const set: boolean[] = new Array<boolean>(columnCount).fill(false);
503
- for (let c = 0; c < columnCount; c += 1) {
504
- const px = columnWidths[c];
505
- if (px === null) continue;
506
- widths[c] = px;
507
- set[c] = true;
508
- }
696
+ // each column's length, its percentage, and what a cell with the
697
+ // percentage adds to it where its padding and borders are not in it
698
+ const length = columnWidths.map((px, c) =>
699
+ columnPercents[c] === null ? px : null,
700
+ );
701
+ const percent = columnPercents.slice();
702
+ const added: number[] = new Array<number>(columnCount).fill(0);
509
703
  for (const cell of cells) {
510
704
  if (cell.row > 0) break;
511
- const px = ownWidth(cell.box.style, available);
512
- if (px === null) continue;
513
705
  const box = cell.box;
514
- const outer =
515
- box.style.boxSizing === 'border-box'
516
- ? Math.max(px, box.horizontalExtra)
517
- : px + box.horizontalExtra;
518
- const last = Math.min(columnCount, cell.column + cell.colSpan);
519
- let taken = spacing * (last - cell.column - 1);
520
- let open = 0;
521
- for (let c = cell.column; c < last; c += 1) {
522
- if (set[c]) taken += widths[c];
523
- else open += 1;
524
- }
525
- if (!open) continue;
526
- const each = Math.max(0, outer - taken) / open;
527
- for (let c = cell.column; c < last; c += 1) {
528
- if (set[c]) continue;
529
- widths[c] = each;
530
- set[c] = true;
531
- }
532
- }
533
- let used = 0;
534
- let unset = 0;
706
+ const first = cell.column;
707
+ const last = Math.min(columnCount, first + cell.colSpan);
708
+ const span = last - first;
709
+ const share = ownPercent(box.style);
710
+ if (share !== null) {
711
+ for (let c = first; c < last; c += 1) {
712
+ if (length[c] !== null) continue;
713
+ if (span > 1) {
714
+ if (percent[c] === null) percent[c] = share / span;
715
+ } else if (percent[c] === null || share > (percent[c] as number)) {
716
+ percent[c] = share;
717
+ added[c] =
718
+ box.style.boxSizing === 'border-box' ? 0 : box.horizontalExtra;
719
+ }
720
+ }
721
+ continue;
722
+ }
723
+ const px = ownWidth(box.style, available);
724
+ if (px === null) continue;
725
+ // A length within a length `max-width`, which a `min-width` raises
726
+ // and does nothing else to: a fixed table does not measure its cells'
727
+ // content, so a least width has nothing to hold up (CSS Tables 3,
728
+ // 3.8.2 and 3.8.3, as Blink reads them). Unheld, a cell `width: 300px;
729
+ // max-width: 100px` took 300 of the table.
730
+ const outer = Math.min(outerWidth(box, px), cellLimits(box).cap);
731
+ const each = Math.max(0, outer - spacing * (span - 1)) / span;
732
+ for (let c = first; c < last; c += 1) {
733
+ if (length[c] === null) length[c] = each;
734
+ }
735
+ }
736
+
737
+ // The table is as wide as the least its columns come to, which is what
738
+ // widens it past its own width: the lengths, and the `min-width` of each
739
+ // `<col>` set to anything else.
740
+ let least = 0;
741
+ let total = 0;
742
+ for (let c = 0; c < columnCount; c += 1) {
743
+ const floor = columnLeast[c] ?? 0;
744
+ const px = length[c];
745
+ if (percent[c] === null && px !== null) {
746
+ length[c] = Math.max(px, floor);
747
+ least += length[c] as number;
748
+ } else {
749
+ least += floor;
750
+ }
751
+ if (percent[c] !== null) total += percent[c] as number;
752
+ }
753
+ const target = Math.max(available, least);
754
+ const scale = total > 100 ? 100 / total : 1;
755
+ const ofTarget: number[] = new Array<number>(columnCount).fill(0);
756
+ let lengths = 0;
757
+ let percents = 0;
758
+ let percentCount = 0;
759
+ let autoCount = 0;
760
+ let zeroCount = 0;
535
761
  for (let c = 0; c < columnCount; c += 1) {
536
- if (set[c]) used += widths[c];
537
- else unset += 1;
762
+ const share = percent[c];
763
+ const px = length[c];
764
+ if (share !== null) {
765
+ ofTarget[c] = Math.max(
766
+ columnLeast[c] ?? 0,
767
+ ((share * scale) / 100) * target + added[c],
768
+ );
769
+ percents += ofTarget[c];
770
+ percentCount += 1;
771
+ } else if (px === null) {
772
+ autoCount += 1;
773
+ } else if (px > 0) {
774
+ lengths += px;
775
+ } else {
776
+ zeroCount += 1;
777
+ }
538
778
  }
539
- const remaining = Math.max(0, available - used);
540
- if (unset) {
779
+
780
+ const widths: number[] = new Array<number>(columnCount).fill(0);
781
+ let assigned = 0;
782
+ // the lengths fit, since they widen the table; they grow into what the
783
+ // percentages leave where no column is set to neither
784
+ if (lengths > 0) {
785
+ const room = Math.max(0, target - percents);
786
+ const grow = !autoCount && lengths < room ? room / lengths : 1;
541
787
  for (let c = 0; c < columnCount; c += 1) {
542
- if (!set[c]) widths[c] = remaining / unset;
788
+ const px = length[c];
789
+ if (percent[c] !== null || px === null || !(px > 0)) continue;
790
+ widths[c] = px * grow;
791
+ assigned += widths[c];
543
792
  }
544
- } else if (remaining > 0 && columnCount) {
793
+ }
794
+ if (assigned >= target) return widths;
795
+ // the percentages are scaled down to what the lengths leave, and up to
796
+ // it where no column is set to neither
797
+ if (percentCount) {
798
+ const room = target - assigned;
799
+ const toRoom = percents > room || (!autoCount && percents < room);
545
800
  for (let c = 0; c < columnCount; c += 1) {
546
- widths[c] +=
547
- used > 0 ? (widths[c] / used) * remaining : remaining / columnCount;
801
+ if (percent[c] === null) continue;
802
+ widths[c] = !toRoom
803
+ ? ofTarget[c]
804
+ : percents > 0
805
+ ? (ofTarget[c] * room) / percents
806
+ : room / percentCount;
807
+ assigned += widths[c];
808
+ }
809
+ }
810
+ // The columns set to neither share what is left. One set to 0 is set to
811
+ // a length that gets nothing, unless every column is set to 0.
812
+ const rest = target - assigned;
813
+ const all = zeroCount === columnCount;
814
+ if (!all && !autoCount) return widths;
815
+ for (let c = 0; c < columnCount; c += 1) {
816
+ if (percent[c] !== null) continue;
817
+ if (all || length[c] === null) {
818
+ widths[c] = rest / (all ? zeroCount : autoCount);
548
819
  }
549
820
  }
550
821
  return widths;
551
822
  }
552
823
 
824
+ /**
825
+ * A cell's max-content and min-content widths, for `autoColumns`: two probe
826
+ * layouts, unconstrained for max-content, and at no width for min-content
827
+ * (`MIN_CONTENT_PROBE`) — which the line breaker answers by breaking at
828
+ * every opportunity, so the widest line is the longest unbreakable word.
829
+ *
830
+ * Cached on the box, because intrinsic widths are width-independent by
831
+ * definition — measuring them per layout pass made a *resize* of a 500-row
832
+ * table cost as much as its first layout (223ms of re-probing measured
833
+ * against 10ms with the cache). The cache's invalidation rule is the box's
834
+ * lifetime: any DOM or style change rebuilds the tree.
835
+ *
836
+ * Every cell is measured before the columns are sized, and not in the loop
837
+ * that sizes them, so that the frame a table in the cell is measured under
838
+ * is this small one and not `autoColumns`'s (`layoutTable`).
839
+ */
840
+ function measureCell(box: Box, ctx: LayoutContext): void {
841
+ if (box.intrinsicMaxContent >= 0) return;
842
+ box.intrinsicMaxContent = measureIntrinsicWidth(box, ctx, Infinity);
843
+ // where its words say it exactly, read from the layout just made
844
+ box.intrinsicMinContent =
845
+ (ctx.fonts && exactMinContent(box, ctx.fonts)) ??
846
+ measureIntrinsicWidth(box, ctx, MIN_CONTENT_PROBE, true);
847
+ }
848
+
553
849
  /**
554
850
  * The auto algorithm: measure every cell's max-content and min-content
555
- * width, then distribute.
851
+ * width, gather them into columns, and share the table's width out over
852
+ * them, as CSS Tables 3 has it (3.9.3, "width distribution") and Blink
853
+ * implements it (`DistributeInlineSizeToComputedInlineSizeAuto`).
556
854
  *
557
- * The distribution is the part worth stating. When the max-content widths
855
+ * The distribution is the part worth stating. A column is set to a
856
+ * percentage, set to a length, or neither, and the table's width is shared
857
+ * out in steps, each tried only where the one before falls short: every
858
+ * column at its least; then the percentage columns grown towards their
859
+ * percentage of the table; then the columns set to a length grown towards
860
+ * it; then the rest towards their max-content. When the max-content widths
558
861
  * fit, they are used — a small table is exactly as wide as its contents.
559
- * When they do not, the surplus over each column's minimum is scaled down in
560
- * proportion, so a column of long prose gives up more than a column of
561
- * dates. A column with an explicit `width` is honoured before either.
862
+ * When they do not, the surplus over each column's minimum is scaled down
863
+ * in proportion, so a column of long prose gives up more than a column of
864
+ * dates. What a table has over all of that goes to the columns set to
865
+ * nothing, or else to those set to a length, or else to the percentages.
866
+ *
867
+ * What a cell asks is its content within its length limits (CSS Tables 3,
868
+ * 3.8.2): its outer min-content width is `max(min-width, min-content)`
869
+ * and its outer max-content width `max(min-width, min-content,
870
+ * min(max-width, max-content))`. A `max-width` caps the min-content too,
871
+ * as Blink has it where the draft does not (`CreateCellInlineConstraint`),
872
+ * so that `td { max-width: 50px }` over a long word, or a line that does
873
+ * not wrap and ends in an ellipsis, makes a column 50 wide and not the
874
+ * word's. Neither limit sets the column's width, which only a `width`
875
+ * does: a cell `min-width: 100px` over 10px of content in a table of 600
876
+ * takes its share of the rest, as one 100 wide would.
877
+ *
878
+ * A percentage is kept a percentage until the table's width is known, and
879
+ * is of that, less its spacing: resolved against the room on offer and
880
+ * held as a length, `<col style="width:20%">` beside a column of 100 made
881
+ * a table of 180 in 400 of room, where it is 125 — the 100 is the other
882
+ * 80% of it. That is how a table's percentages make it wider than its
883
+ * content (`gridMax`), and what `widen` says it may (`percentagesWiden`).
884
+ * `room` is what its captions and its `min-width` hold an auto table to,
885
+ * and is shared out with the rest: added on in proportion afterwards, a
886
+ * caption 200 wide over a column of 50 and one set to 50 made them 100
887
+ * each, where the column set to nothing takes it all.
562
888
  */
563
889
  function autoColumns(
564
890
  cells: Cell[],
565
891
  columnWidths: (number | null)[],
892
+ columnLeast: (number | null)[],
893
+ columnPercents: (number | null)[],
566
894
  columnCount: number,
567
895
  available: number,
568
- ctx: LayoutContext,
569
- containingWidth: number,
570
896
  fill: boolean,
571
897
  spacing: number,
898
+ room: number,
899
+ widen: boolean,
572
900
  ): number[] {
573
901
  const max: number[] = new Array<number>(columnCount).fill(0);
574
902
  const min: number[] = new Array<number>(columnCount).fill(0);
575
903
  const explicit: (number | null)[] = new Array<number | null>(
576
904
  columnCount,
577
905
  ).fill(null);
906
+ // each column's percentage: its own, or the most a cell of it asks
907
+ const percent = columnPercents.slice();
578
908
  const spanning: Cell[] = [];
909
+ // each cell's outer min-content and max-content, within its limits
910
+ const asks = new Map<Cell, { min: number; max: number }>();
579
911
 
580
912
  for (const cell of cells) {
581
- // Two probe layouts per cell: unconstrained for max-content, and at no
582
- // width for min-content (`MIN_CONTENT_PROBE`) — which the line breaker
583
- // answers by breaking at every opportunity, so the widest line is the
584
- // longest unbreakable word.
585
- //
586
- // Cached on the box, because intrinsic widths are width-independent by
587
- // definition — measuring them per layout pass made a *resize* of a
588
- // 500-row table cost as much as its first layout (223ms of re-probing
589
- // measured against 10ms with the cache). The cache's invalidation rule
590
- // is the box's lifetime: any DOM or style change rebuilds the tree.
591
- if (cell.box.intrinsicMaxContent < 0) {
592
- cell.box.intrinsicMaxContent = measureIntrinsicWidth(
593
- cell.box,
594
- ctx,
595
- Infinity,
596
- );
597
- // where its words say it exactly, read from the layout just made
598
- cell.box.intrinsicMinContent =
599
- (ctx.fonts && exactMinContent(cell.box, ctx.fonts)) ??
600
- measureIntrinsicWidth(cell.box, ctx, MIN_CONTENT_PROBE, true);
601
- }
913
+ // what its content asks, measured before (`measureCell`)
914
+ const { floor, cap } = cellLimits(cell.box);
915
+ const low = Math.min(Math.max(cell.box.intrinsicMinContent, floor), cap);
916
+ const ask = {
917
+ min: low,
918
+ max: Math.max(low, Math.min(cell.box.intrinsicMaxContent, cap)),
919
+ };
920
+ asks.set(cell, ask);
602
921
  if (cell.colSpan > 1) {
603
922
  spanning.push(cell);
604
923
  continue;
605
924
  }
606
- max[cell.column] = Math.max(max[cell.column], cell.box.intrinsicMaxContent);
607
- min[cell.column] = Math.max(min[cell.column], cell.box.intrinsicMinContent);
608
- const width = cellWidth(cell, containingWidth);
609
- if (width !== null) {
610
- explicit[cell.column] = Math.max(explicit[cell.column] ?? 0, width);
611
- }
925
+ const c = cell.column;
926
+ max[c] = Math.max(max[c], ask.max);
927
+ min[c] = Math.max(min[c], ask.min);
928
+ // a percentage is the column's, 0% among them, as Blink keeps it
929
+ const share = ownPercent(cell.box.style);
930
+ if (share !== null) percent[c] = Math.max(percent[c] ?? 0, share);
931
+ const width = cellWidth(cell);
932
+ if (width !== null) explicit[c] = Math.max(explicit[c] ?? 0, width);
612
933
  }
613
934
 
614
935
  // a column's own width counts as its cells' do: at least that, and its
615
- // content's max where that is wider (CSS 2.1 17.5.2.2, step 2)
936
+ // content's max where that is wider (CSS 2.1 17.5.2.2, step 2); and its
937
+ // `min-width` alone is the least it and its content are, as a cell's
938
+ // is, and sets no width (CSS Tables 3, 3.8.2)
616
939
  for (let c = 0; c < columnCount; c += 1) {
617
940
  const own = columnWidths[c];
618
941
  if (own !== null) explicit[c] = Math.max(explicit[c] ?? 0, own);
942
+ const floor = columnLeast[c];
943
+ if (floor !== null) {
944
+ min[c] = Math.max(min[c], floor);
945
+ max[c] = Math.max(max[c], floor);
946
+ }
619
947
  }
620
948
 
621
949
  // A spanning cell comes after the cells of one column, the narrower
@@ -626,173 +954,452 @@ function autoColumns(
626
954
  // they have none. Taken in document order and spread evenly, a spanning
627
955
  // cell in a first row was shared out before the cells of one column
628
956
  // under it were seen, and a column one of them set to 5px took half.
957
+ //
958
+ // A column with a percentage is first that much of what the cell asks,
959
+ // less the spacing, as Blink's distribution over the span resolves it;
960
+ // what is left goes to the columns set to nothing, or else to those set
961
+ // to a length, or else to the percentages, by them: `<td colspan=2>` 100
962
+ // wide over a 50% column and one set to nothing, each of 10, makes them
963
+ // 50 each. A spanning cell's own percentage goes first to the columns it
964
+ // spans that have none, as much more as the ones that have come short
965
+ // of it, in proportion to their content (Blink's
966
+ // `DistributeColspanCellToColumnsAuto`): `<td colspan=2 style="width:
967
+ // 50%">` over two columns makes each 25%.
629
968
  spanning.sort((a, b) => a.colSpan - b.colSpan);
630
969
  const grow = (into: number[], want: number, cell: Cell): void => {
631
- const last = Math.min(columnCount - 1, cell.column + cell.colSpan - 1);
632
- let have = spacing * (last - cell.column);
633
- for (let c = cell.column; c <= last; c += 1) have += into[c];
970
+ const first = cell.column;
971
+ const last = Math.min(columnCount - 1, first + cell.colSpan - 1);
972
+ const between = spacing * (last - first);
973
+ let have = between;
974
+ for (let c = first; c <= last; c += 1) have += into[c];
634
975
  if (!(want > have)) return;
635
- let free = false;
636
- for (let c = cell.column; c <= last && !free; c += 1) {
637
- free = explicit[c] === null;
976
+ for (let c = first; c <= last; c += 1) {
977
+ const share = percent[c];
978
+ if (share === null) continue;
979
+ const size = (share / 100) * (want - between);
980
+ if (size > into[c]) {
981
+ have += size - into[c];
982
+ into[c] = size;
983
+ }
638
984
  }
985
+ if (!(want > have)) return;
986
+ let takes = (c: number): boolean =>
987
+ explicit[c] === null && percent[c] === null;
988
+ let any = false;
989
+ for (let c = first; c <= last && !any; c += 1) any = takes(c);
990
+ if (!any) takes = (c) => percent[c] === null;
991
+ for (let c = first; c <= last && !any; c += 1) any = takes(c);
992
+ if (!any) takes = () => true;
993
+ const byPercent = !any;
994
+ const weightOf = (c: number): number =>
995
+ byPercent ? (percent[c] as number) : max[c];
639
996
  let weight = 0;
640
997
  let count = 0;
641
- for (let c = cell.column; c <= last; c += 1) {
642
- if (free && explicit[c] !== null) continue;
643
- weight += max[c];
998
+ for (let c = first; c <= last; c += 1) {
999
+ if (!takes(c)) continue;
1000
+ weight += weightOf(c);
644
1001
  count += 1;
645
1002
  }
646
- for (let c = cell.column; c <= last; c += 1) {
647
- if (free && explicit[c] !== null) continue;
648
- into[c] +=
649
- weight > 0 ? ((want - have) * max[c]) / weight : (want - have) / count;
1003
+ const extra = want - have;
1004
+ for (let c = first; c <= last; c += 1) {
1005
+ if (!takes(c)) continue;
1006
+ into[c] += weight > 0 ? (extra * weightOf(c)) / weight : extra / count;
650
1007
  }
651
1008
  };
652
1009
 
1010
+ for (const cell of spanning) {
1011
+ const share = ownPercent(cell.box.style);
1012
+ if (share !== null) spreadPercent(percent, max, share, cell, columnCount);
1013
+ }
653
1014
  // what the columns' content alone asks, before their set widths: the
654
1015
  // least a column gives way to when the table has less room than its
655
1016
  // cells' widths want, as beside a float
656
1017
  const least = min.slice();
657
- for (const cell of spanning) grow(least, cell.box.intrinsicMinContent, cell);
1018
+ for (const cell of spanning) grow(least, asks.get(cell)!.min, cell);
1019
+ // A column set to a length is at least that wide, where there is room;
1020
+ // one with a percentage too is that, and its length is only the most
1021
+ // its content asks, which is what its percentage holds the table to
658
1022
  for (let c = 0; c < columnCount; c += 1) {
659
- if (explicit[c] !== null) {
660
- max[c] = Math.max(min[c], explicit[c] as number);
661
- min[c] = Math.max(min[c], Math.min(explicit[c] as number, max[c]));
662
- }
1023
+ const own = explicit[c];
1024
+ if (own === null) continue;
1025
+ max[c] = Math.max(min[c], own);
1026
+ if (percent[c] === null) min[c] = Math.max(min[c], Math.min(own, max[c]));
663
1027
  }
664
1028
  // and with them: a spanning cell's own width is the least it is, as a
665
1029
  // single cell's is (step 1), so a cell over three columns set to 100px,
666
1030
  // with 20px between them, has 60 to share, where it had none
667
1031
  for (const cell of spanning) {
668
- const own = cellWidth(cell, containingWidth) ?? 0;
669
- grow(min, Math.max(cell.box.intrinsicMinContent, own), cell);
670
- grow(max, Math.max(cell.box.intrinsicMaxContent, own), cell);
1032
+ const own = cellWidth(cell) ?? 0;
1033
+ const ask = asks.get(cell)!;
1034
+ grow(min, Math.max(ask.min, own), cell);
1035
+ grow(max, Math.max(ask.max, own), cell);
671
1036
  }
672
1037
  for (let c = 0; c < columnCount; c += 1) min[c] = Math.min(min[c], max[c]);
673
1038
 
674
- const totalMax = max.reduce((a, b) => a + b, 0);
675
- if (totalMax <= available) {
676
- // Everything fits. A table with a width of its own still fills it (CSS
677
- // 2.1 17.5.2.2): the columns not set to a width take the rest in
678
- // proportion to what they asked for, or every column does where all are
679
- // set. Returned as asked, `<table width="600">` drew its cells at their
680
- // content's width and left the rest of its 600 empty.
681
- if (!fill || available <= totalMax) return max;
682
- const set = explicit.every((w) => w !== null);
683
- let weight = 0;
1039
+ // The percentages come to no more than 100, the columns' in order:
1040
+ // where they would, a column has what is left, which may be none
1041
+ let total = 0;
1042
+ for (let c = 0; c < columnCount; c += 1) {
1043
+ const share = percent[c];
1044
+ if (share === null) continue;
1045
+ percent[c] = share + total > 100 ? 100 - total : share;
1046
+ total += percent[c] as number;
1047
+ }
1048
+
1049
+ // How wide the table is: the room on offer where it has a width of its
1050
+ // own, and where it has not, as wide as its columns ask, up to the room
1051
+ // (CSS Tables 3, 3.9.2) — wide enough for its percentages too
1052
+ const most = gridMax(max, percent, total, widen);
1053
+ let target = fill ? available : Math.min(available, most);
1054
+ target = Math.max(target, room);
1055
+ if (!Number.isFinite(target)) target = Math.max(most, room);
1056
+
1057
+ // The guesses, each a sum over the columns (3.9.3): every column at its
1058
+ // least; the percentages at theirs; the lengths at theirs; and every
1059
+ // column at its max-content, the percentages still at theirs
1060
+ const sized: number[] = new Array<number>(columnCount).fill(0);
1061
+ let atLeast = 0;
1062
+ let atPercent = 0;
1063
+ let atSet = 0;
1064
+ let atMax = 0;
1065
+ for (let c = 0; c < columnCount; c += 1) {
1066
+ atLeast += least[c];
1067
+ const share = percent[c];
1068
+ if (share !== null) {
1069
+ // a percentage of the border box, so never narrower than its content
1070
+ sized[c] = Math.max(min[c], (share / 100) * target);
1071
+ atPercent += sized[c];
1072
+ atSet += sized[c];
1073
+ atMax += sized[c];
1074
+ } else {
1075
+ atPercent += least[c];
1076
+ atSet += min[c];
1077
+ atMax += max[c];
1078
+ }
1079
+ }
1080
+ if (target <= atLeast) {
1081
+ // Less room than the cells' content needs: a table never goes narrower
1082
+ // than its words (CSS 2.1 17.5.2.2), which it overflows.
1083
+ return least;
1084
+ }
1085
+ if (target <= atPercent) {
1086
+ // The percentages grow from their least towards their size, each by as
1087
+ // much as it has to go, and the rest stay at theirs
1088
+ const deficit = target - atLeast;
1089
+ let increase = 0;
684
1090
  let count = 0;
685
1091
  for (let c = 0; c < columnCount; c += 1) {
686
- if (!set && explicit[c] !== null) continue;
687
- weight += max[c];
1092
+ if (percent[c] === null) continue;
1093
+ increase += sized[c] - least[c];
688
1094
  count += 1;
689
1095
  }
690
- const extra = available - totalMax;
691
- return max.map((m, c) =>
692
- !set && explicit[c] !== null
693
- ? m
694
- : m + (weight > 0 ? (extra * m) / weight : extra / count),
1096
+ return least.map((l, c) =>
1097
+ percent[c] === null
1098
+ ? l
1099
+ : l +
1100
+ (increase > 0
1101
+ ? (deficit * (sized[c] - l)) / increase
1102
+ : deficit / count),
695
1103
  );
696
1104
  }
697
- const totalMin = min.reduce((a, b) => a + b, 0);
698
- if (totalMin >= available) {
1105
+ if (target <= atSet) {
699
1106
  // Less room than the cells' widths want: a width a cell was set to
700
- // gives way first, down to what its content asks, and a table never
701
- // goes narrower than its words (CSS 2.1 17.5.2.2), which it overflows.
702
- const totalLeast = least.reduce((a, b) => a + b, 0);
703
- if (totalLeast >= available) return least;
704
- const give = totalMin - totalLeast;
705
- return min.map(
706
- (m, c) => m - ((m - least[c]) / give) * (totalMin - available),
1107
+ // gives way first, down to what its content asks.
1108
+ const give = atSet - atPercent;
1109
+ return min.map((m, c) =>
1110
+ percent[c] !== null
1111
+ ? sized[c]
1112
+ : m - ((m - least[c]) / give) * (atSet - target),
707
1113
  );
708
1114
  }
709
- const slack = available - totalMin;
710
- const surplus = totalMax - totalMin;
711
- return max.map((m, c) => min[c] + ((m - min[c]) / surplus) * slack);
1115
+ if (target < atMax) {
1116
+ const slack = target - atSet;
1117
+ const surplus = atMax - atSet;
1118
+ return max.map((m, c) =>
1119
+ percent[c] !== null
1120
+ ? sized[c]
1121
+ : min[c] + ((m - min[c]) / surplus) * slack,
1122
+ );
1123
+ }
1124
+ // Everything fits. A table with a width of its own still fills it (CSS
1125
+ // 2.1 17.5.2.2), and an auto table its captions and `min-width`: the
1126
+ // columns set to nothing take the rest in proportion to what they asked
1127
+ // for; where there are none, the columns set to a length; and where
1128
+ // every column has a percentage, those, by it. Returned as asked,
1129
+ // `<table width="600">` drew its cells at their content's width and left
1130
+ // the rest of its 600 empty.
1131
+ let takes: (c: number) => boolean = (c) =>
1132
+ percent[c] === null && explicit[c] === null;
1133
+ let any = false;
1134
+ for (let c = 0; c < columnCount && !any; c += 1) any = takes(c);
1135
+ if (!any) takes = (c) => percent[c] === null;
1136
+ for (let c = 0; c < columnCount && !any; c += 1) any = takes(c);
1137
+ const byPercent = !any;
1138
+ if (byPercent) takes = () => true;
1139
+ const weightOf = (c: number): number =>
1140
+ byPercent ? (percent[c] as number) : max[c];
1141
+ let weight = 0;
1142
+ let count = 0;
1143
+ for (let c = 0; c < columnCount; c += 1) {
1144
+ if (!takes(c)) continue;
1145
+ weight += weightOf(c);
1146
+ count += 1;
1147
+ }
1148
+ const extra = target - atMax;
1149
+ return max.map((m, c) => {
1150
+ const base = percent[c] !== null ? sized[c] : m;
1151
+ if (!takes(c)) return base;
1152
+ return base + (weight > 0 ? (extra * weightOf(c)) / weight : extra / count);
1153
+ });
712
1154
  }
713
1155
 
1156
+ /** What Blink makes the most of a table's percentages can make it,
1157
+ * kTableMaxInlineSize, where the columns with one come to 100% and there
1158
+ * are others with content: no width is wide enough. */
1159
+ const PERCENT_TABLE_MOST = 1_000_000;
1160
+
714
1161
  /**
715
- * The width each column is set to, or null: a column's own `width`, or its
716
- * group's shared among the columns of a group that has none of its own
717
- * (HTML's `<colgroup span>`); and a group's `width` spread over its columns
718
- * where theirs come to less (CSS 2.1 17.5.2.2, step 4). Each within its
719
- * `min-width` and `max-width`, which apply to columns and column groups as
720
- * to any block (10.4), so `min-width` alone sets one.
1162
+ * The widest an auto table's columns make it, less its spacing (CSS
1163
+ * Tables 3, 3.9.2, GRIDMAX): what they ask, and what their percentages
1164
+ * need where they may count (`widen`). The draft says that a percentage
1165
+ * is a constraint the table should try to satisfy and no more; Blink's
1166
+ * `ComputeGridInlineMinMax` widens the table until it can: a column's
1167
+ * content is no more than its percentage of the table, and the columns
1168
+ * with none are the rest of it. Chrome's: a 20% column over 10 of content
1169
+ * beside one of 100 is a table of 125, and two 30% columns of 10 one of
1170
+ * 33.33.
1171
+ */
1172
+ function gridMax(
1173
+ max: number[],
1174
+ percent: (number | null)[],
1175
+ total: number,
1176
+ widen: boolean,
1177
+ ): number {
1178
+ let most = 0;
1179
+ for (const m of max) most += m;
1180
+ if (!widen || !(total > 0)) return most;
1181
+ let estimate = 0;
1182
+ let rest = 0;
1183
+ for (let c = 0; c < max.length; c += 1) {
1184
+ const share = percent[c];
1185
+ if (share !== null && share > 0) {
1186
+ if (max[c] > 0) estimate = Math.max(estimate, (max[c] * 100) / share);
1187
+ } else {
1188
+ rest += max[c];
1189
+ }
1190
+ }
1191
+ const others =
1192
+ rest > 0
1193
+ ? total >= 100 - 1e-6
1194
+ ? PERCENT_TABLE_MOST
1195
+ : (rest * 100) / (100 - total)
1196
+ : 0;
1197
+ return Math.max(most, others, estimate);
1198
+ }
1199
+
1200
+ /** A spanning cell's percentage, over the columns it spans: what it has
1201
+ * more than theirs goes to the ones with none, in proportion to their
1202
+ * content, or evenly where they have none. */
1203
+ function spreadPercent(
1204
+ percent: (number | null)[],
1205
+ max: number[],
1206
+ share: number,
1207
+ cell: Cell,
1208
+ columnCount: number,
1209
+ ): void {
1210
+ const last = Math.min(columnCount - 1, cell.column + cell.colSpan - 1);
1211
+ let taken = 0;
1212
+ let weight = 0;
1213
+ let count = 0;
1214
+ for (let c = cell.column; c <= last; c += 1) {
1215
+ const own = percent[c];
1216
+ if (own !== null) taken += own;
1217
+ else {
1218
+ weight += max[c];
1219
+ count += 1;
1220
+ }
1221
+ }
1222
+ const surplus = share - taken;
1223
+ if (!(surplus > 0) || !count) return;
1224
+ for (let c = cell.column; c <= last; c += 1) {
1225
+ if (percent[c] !== null) continue;
1226
+ percent[c] = weight > 0 ? (surplus * max[c]) / weight : surplus / count;
1227
+ }
1228
+ }
1229
+
1230
+ /**
1231
+ * Whether an auto table's percentages may make it wider than its content
1232
+ * (`gridMax`), as Blink's `AllowColumnPercentages` has it: not where its
1233
+ * width is `max-content`, and where it is measured for what it asks of a
1234
+ * table cell, a flex box or a grid around it, not either — which is the
1235
+ * width it is measured at being no width at all (`Infinity`). Laid out,
1236
+ * a table always may; and measured for a float or an inline block, which
1237
+ * are as wide as it asks, it may too. Chrome's: a table of a 20% column
1238
+ * and a 100px one asks 110 of a cell around it and 125 of a float.
1239
+ */
1240
+ function percentagesWiden(table: Box, contentWidth: number): boolean {
1241
+ if (table.style.widthKeyword === 'max-content') return false;
1242
+ if (Number.isFinite(contentWidth)) return true;
1243
+ for (let box = table.parent; box; box = box.parent) {
1244
+ if (box.kind === 'table-cell' || box.kind === 'flex') return false;
1245
+ }
1246
+ return true;
1247
+ }
1248
+
1249
+ /**
1250
+ * The width each column is set to, or null, from the `<col>` and the
1251
+ * `<colgroup>` it is in. A group is its columns' default and no more: the
1252
+ * width specified for a column is its column's, or else its group's (CSS
1253
+ * Tables 3, 3.8.3), so a `<col>` with a width of its own keeps it however
1254
+ * wide the group is, and one with none takes the group's whole, not a
1255
+ * share of it. A group with no `<col>` stands for each column it spans
1256
+ * (`<colgroup span>`), and each is as wide as the group says. That is
1257
+ * HTML 4's "a default width for each column" and Blink's
1258
+ * (`ColumnConstraintsBuilder`), where CSS 2.1 17.5.2.2 spread a group's
1259
+ * width over its columns wherever theirs came to less: a group of 100
1260
+ * over two columns of 20 made each 50, and over two of none made each 50,
1261
+ * where Chrome has 20 and 100.
1262
+ *
1263
+ * Only a length is a default, raised to the group's `min-width` and the
1264
+ * `<col>`'s, and only in an auto table, as Blink has it: a fixed table
1265
+ * takes no group's width for a `<col>`, and its columns share the table as
1266
+ * though the group set none. A `<col>` with a percentage keeps it in a
1267
+ * fixed table, of the table's width; in an auto table a percentage is no
1268
+ * length (`columnPercents`), and a `<col>` with one takes its group's
1269
+ * length beside it, as Blink's `CreateColumn` does — `<colgroup
1270
+ * style="width:100px"><col style="width:20%">` is a column that needs 100
1271
+ * and is a fifth of the table, which makes the table 500.
721
1272
  */
722
1273
  function columnWidths(
723
1274
  columnBoxes: (Box | null)[],
724
1275
  columnGroups: (Box | null)[],
725
1276
  base: number,
1277
+ fixed: boolean,
726
1278
  ): (number | null)[] {
727
- const widths = columnBoxes.map((column) =>
728
- column ? partWidth(column, base) : null,
729
- );
730
- let group: Box | null = null;
731
- for (let c = 0; c <= columnGroups.length; c += 1) {
732
- if (c < columnGroups.length && columnGroups[c] === group) continue;
733
- // the group that ends here, over its columns from `start`
734
- if (group) {
735
- const width = partWidth(group, base);
736
- let start = c - 1;
737
- while (start > 0 && columnGroups[start - 1] === group) start -= 1;
738
- if (width !== null) {
739
- let have = 0;
740
- for (let i = start; i < c; i += 1) have += widths[i] ?? 0;
741
- if (have < width) {
742
- const each = (width - have) / (c - start);
743
- for (let i = start; i < c; i += 1) {
744
- widths[i] = (widths[i] ?? 0) + each;
745
- }
746
- }
747
- }
748
- }
749
- group = c < columnGroups.length ? columnGroups[c] : null;
750
- }
751
- return widths;
1279
+ const read = (box: Box): number | null =>
1280
+ fixed || !isPct(box.style.width) ? partWidth(box, base) : null;
1281
+ return columnBoxes.map((column, c) => {
1282
+ const group = columnGroups[c];
1283
+ if (!column) return group ? read(group) : null;
1284
+ const own = read(column);
1285
+ if (own !== null || fixed || !group || isPct(group.style.width)) return own;
1286
+ const width = read(group);
1287
+ return width === null ? null : Math.max(width, partLeast(column) ?? 0);
1288
+ });
752
1289
  }
753
1290
 
754
- /** A column's or a column group's width, within its limits, or null where
755
- * it sets none. The limits are lengths: a percentage in them does what it
756
- * does in a cell's (`cellWidth`). */
1291
+ /** The percentage each column is set to, or null: its `<col>`'s, or its
1292
+ * group's where the group stands for it with no `<col>`. A group's
1293
+ * percentage is no default for the `<col>`s in it, and a column set to
1294
+ * 0% has none, as Blink has both (`ColumnConstraintsBuilder`,
1295
+ * `CreateColumn`). */
1296
+ function columnPercents(
1297
+ columnBoxes: (Box | null)[],
1298
+ columnGroups: (Box | null)[],
1299
+ ): (number | null)[] {
1300
+ return columnBoxes.map((column, c) => {
1301
+ const part = column ?? columnGroups[c];
1302
+ return (part && ownPercent(part.style)) || null;
1303
+ });
1304
+ }
1305
+
1306
+ /** The least each column is, where a `min-width` says (`partLeast`): its
1307
+ * `<col>`'s, or, where it has none, the group it is in. A group's
1308
+ * `min-width` does not reach the `<col>`s in it, as in Blink, which makes
1309
+ * a group's limits a column's only where the group stands for it:
1310
+ * `<colgroup style="min-width:100px">` over two `<col>`s of 10px content
1311
+ * is 20 wide, where the 100 was spread over them. */
1312
+ function columnLeast(
1313
+ columnBoxes: (Box | null)[],
1314
+ columnGroups: (Box | null)[],
1315
+ ): (number | null)[] {
1316
+ return columnBoxes.map((column, c) => {
1317
+ const part = column ?? columnGroups[c];
1318
+ return part ? partLeast(part) : null;
1319
+ });
1320
+ }
1321
+
1322
+ /** A column's or a column group's width, or null where it sets none: its
1323
+ * `width`, raised to a length `min-width`. A `max-width` does nothing to
1324
+ * it. CSS Tables 3, 3.8.2 makes a column's outer min-content width
1325
+ * `max(min-width, width)`, which is at least as wide as any `max-width`
1326
+ * could hold it to, and Blink reads no `max-width` on a column at all
1327
+ * (`CreateColumn`): `<col style="width:100px; max-width:50px">` is 100
1328
+ * wide, where it was 50. A percentage `max-width` holds a percentage
1329
+ * `width` (`ownWidth`), and a percentage `min-width` is ignored. */
757
1330
  function partWidth(box: Box, base: number): number | null {
758
- const style = box.style;
759
- const width = ownWidth(style, base);
760
- const min = typeof style.minWidth === 'number' ? style.minWidth : 0;
761
- if (width === null && !(min > 0)) return null;
762
- const max = typeof style.maxWidth === 'number' ? style.maxWidth : null;
763
- let out = width ?? 0;
764
- if (max !== null) out = Math.min(out, max);
765
- return Math.max(0, out, min);
766
- }
767
-
768
- /** A cell's own `width`, its padding and borders in, or null. A
769
- * percentage is a share of the table its padding and borders are part
770
- * of, as browsers read it: added on, a 90% cell and a 10% one came to
771
- * more than the table, which then took it back from both. Held within its
772
- * `min-width` and `max-width` as a column's is (`partWidth`), which CSS
773
- * 2.1 leaves undefined and every browser does: a cell set `width: 3in;
774
- * max-width: 1in` is an inch wide, and one with `min-width` alone is as
775
- * wide as that.
776
- *
777
- * Where they are lengths. A percentage `min-width` is ignored, and a
778
- * percentage `max-width` holds a percentage `width` and nothing else
779
- * (CSS Tables 3, 3.8.2, and `ownWidth`): a cell `width: 100px;
780
- * max-width: 10%` in a table of 600 is 100 wide, where it was 60, and
781
- * one set `min-width: 150%` takes no more than its content asks. */
782
- function cellWidth(cell: Cell, base: number): number | null {
783
- const style = cell.box.style;
784
- const len = style.width;
785
- const width = ownWidth(style, base);
786
- const min = typeof style.minWidth === 'number' ? style.minWidth : 0;
787
- if (width === null && !(min > 0)) return null;
788
- const max = typeof style.maxWidth === 'number' ? style.maxWidth : null;
789
- let own = width ?? 0;
790
- if (max !== null) own = Math.min(own, max);
791
- own = Math.max(0, own, min);
792
- if (isPct(len) && own === width) {
793
- return Math.max(width, cell.box.horizontalExtra);
794
- }
795
- return own + cell.box.horizontalExtra;
1331
+ const width = ownWidth(box.style, base);
1332
+ if (width === null) return null;
1333
+ return Math.max(0, width, partLeast(box) ?? 0);
1334
+ }
1335
+
1336
+ /** The least a column or a column group is, where a length `min-width`
1337
+ * says: what its cells ask is held up to it, and it sets no width of its
1338
+ * own. A fixed table's columns ask nothing, so there it is only the least
1339
+ * the table is (`fixedColumns`). Read as a width, `<col
1340
+ * style="min-width:100px">` held a column of prose to 100 in an auto
1341
+ * table, where it is as wide as the prose is. */
1342
+ function partLeast(box: Box): number | null {
1343
+ const min = box.style.minWidth;
1344
+ return typeof min === 'number' && min > 0 ? min : null;
1345
+ }
1346
+
1347
+ /** A cell's own length `width`, its padding and borders in, or null. A
1348
+ * length is held within a length `max-width`, which CSS 2.1 leaves
1349
+ * undefined and every browser does: a cell set `width: 3in; max-width:
1350
+ * 1in` is an inch wide, and one `width: 100px; max-width: 10%` 100, a
1351
+ * percentage limit holding a percentage only (CSS Tables 3, 3.8.2). A
1352
+ * `min-width` sets no width, and holds up what the cell asks instead. A
1353
+ * percentage is its column's (`ownPercent`). */
1354
+ function cellWidth(cell: Cell): number | null {
1355
+ const box = cell.box;
1356
+ if (isPct(box.style.width)) return null;
1357
+ const width = lengthAgainst(box.style.width, NaN);
1358
+ if (width === null) return null;
1359
+ return Math.min(outerWidth(box, width), cellLimits(box).cap);
1360
+ }
1361
+
1362
+ /** A cell's, a column's or a column group's percentage `width`, or null
1363
+ * where it is none: no more than a percentage `max-width` (CSS Tables 3,
1364
+ * 3.8.2, as `ownWidth`), and not a `calc()` that adds a length to it,
1365
+ * which is `auto` (`tableWidth`). In an auto table it is a share of the
1366
+ * table that the cell's padding and borders are part of, as browsers
1367
+ * read it: added on, a 90% cell and a 10% one came to more than the
1368
+ * table, which then took it back from both. In a fixed one a
1369
+ * `content-box` cell's are added to it, as Blink's
1370
+ * `percent_border_padding` has them (`fixedColumns`). A length
1371
+ * `max-width` does not hold it: `width: 50%; max-width: 100px` is half
1372
+ * the table, as in Blink, which keeps a percentage apart from the lengths
1373
+ * that cap what a cell's content asks. */
1374
+ function ownPercent(style: ComputedStyle): number | null {
1375
+ const width = style.width;
1376
+ if (!isPct(width) || width.px || width.of) return null;
1377
+ const limit = style.maxWidth;
1378
+ if (limit === 'none' || !isPct(limit) || limit.px || limit.of) {
1379
+ return width.pct;
1380
+ }
1381
+ return Math.min(width.pct, limit.pct);
1382
+ }
1383
+
1384
+ /** A cell's length `min-width` and `max-width` as border-box widths, the
1385
+ * limit raised to the least, or 0 and Infinity where it has none: what
1386
+ * its column sizes its content within (`autoColumns`), and a fixed table
1387
+ * its width (`fixedColumns`). As Blink's `InlineSizesFromStyle`. */
1388
+ function cellLimits(box: Box): { floor: number; cap: number } {
1389
+ const { minWidth, maxWidth } = box.style;
1390
+ const floor = typeof minWidth === 'number' ? outerWidth(box, minWidth) : 0;
1391
+ const cap =
1392
+ typeof maxWidth === 'number'
1393
+ ? Math.max(outerWidth(box, maxWidth), floor)
1394
+ : Infinity;
1395
+ return { floor, cap };
1396
+ }
1397
+
1398
+ /** A width a cell's style gives, as its border box. */
1399
+ function outerWidth(box: Box, width: number): number {
1400
+ return box.style.boxSizing === 'border-box'
1401
+ ? Math.max(width, box.horizontalExtra)
1402
+ : width + box.horizontalExtra;
796
1403
  }
797
1404
 
798
1405
  /** A cell's, a column's or a column group's `width` as the columns read
@@ -831,21 +1438,22 @@ function lengthAgainst(len: Len, base: number): number | null {
831
1438
  * increase the height of the cell box": taken from the set height, a cell
832
1439
  * holding one block of 30px in 80px hung the text beside it from 80px. An
833
1440
  * empty cell has nothing to align and says nothing about the row's, as in a
834
- * browser.
1441
+ * browser. `own` is each cell's baseline from its top, NaN where it has none
1442
+ * to give.
835
1443
  */
836
1444
  function baselineLifts(
837
1445
  cells: Cell[],
838
1446
  rowCount: number,
839
1447
  natural: number[],
840
- ): number[] {
1448
+ ): { lift: number[]; own: number[] } {
841
1449
  const lift = new Array<number>(cells.length).fill(0);
1450
+ const own = new Array<number>(cells.length).fill(NaN);
842
1451
  let any = false;
843
1452
  for (const cell of cells) {
844
1453
  const va = cell.box.style.verticalAlign;
845
1454
  if (va !== 'top' && va !== 'middle' && va !== 'bottom') any = true;
846
1455
  }
847
- if (!any) return lift;
848
- const own = new Array<number>(cells.length).fill(NaN);
1456
+ if (!any) return { lift, own };
849
1457
  const row = new Array<number>(rowCount).fill(-Infinity);
850
1458
  for (let i = 0; i < cells.length; i += 1) {
851
1459
  const box = cells[i].box;
@@ -862,7 +1470,7 @@ function baselineLifts(
862
1470
  for (let i = 0; i < cells.length; i += 1) {
863
1471
  if (own[i] === own[i]) lift[i] = row[cells[i].row] - own[i];
864
1472
  }
865
- return lift;
1473
+ return { lift, own };
866
1474
  }
867
1475
 
868
1476
  /** Whether a cell has anything in flow in it. */
@@ -904,32 +1512,48 @@ function rowSizing(rows: Box[], cells: Cell[]): RowSizing[] {
904
1512
 
905
1513
  /**
906
1514
  * The height a table has past its rows' given out among them, as browsers
907
- * give it (the CSS Tables 3 draft leaves it open, csswg-drafts#4418): first
908
- * to the rows a percentage sets, up to it; then to the rows nothing sets
909
- * that have content, in proportion to their heights; then, where every row
910
- * with content is set, to the empty rows, those nothing sets first, evenly;
911
- * and else to every row with content, in proportion. A row whose cells set
912
- * its height keeps it while an empty row beside it takes the rest.
1515
+ * give it (the CSS Tables 3 draft leaves it open, csswg-drafts#4418; this is
1516
+ * Blink's `DistributeExcessBlockSizeToRows`): first to the rows a percentage
1517
+ * of `base` sets, up to it; then to the rows nothing sets that have
1518
+ * content, in proportion to their heights; then, where every row with
1519
+ * content is set, to the empty rows, those nothing sets first, evenly; and
1520
+ * else to every row with content, in proportion. A row whose cells set its
1521
+ * height keeps it while an empty row beside it takes the rest.
1522
+ *
1523
+ * What a cell spanning the rows `first` to `last` needs past them goes out
1524
+ * the same way, with three differences, and `starts` says which rows a
1525
+ * spanning cell starts in. A percentage is of nothing yet (`base` is null),
1526
+ * so a row one sets is a row nothing sets. Before anything else, the rows
1527
+ * after the first that another spanning cell starts in take it all,
1528
+ * evenly. And empty rows take none of it unless every row is empty, when
1529
+ * the last takes it.
913
1530
  */
914
1531
  function growRows(
915
1532
  heights: number[],
916
1533
  sizing: RowSizing[],
917
1534
  extra: number,
918
- base: number,
1535
+ base: number | null,
1536
+ first = 0,
1537
+ last = heights.length - 1,
1538
+ starts: boolean[] | null = null,
919
1539
  ): void {
920
- const n = heights.length;
921
- const deficit = heights.map((h, r) => {
922
- const pct = sizing[r].percent;
923
- return pct ? Math.max(0, (pct / 100) * base - h) : 0;
924
- });
1540
+ const deficit: number[] = [];
925
1541
  const empty: number[] = [];
926
1542
  const nothingSetEmpty: number[] = [];
927
1543
  const full: number[] = [];
928
1544
  const nothingSetFull: number[] = [];
1545
+ const spanned: number[] = [];
929
1546
  let setFull = 0;
930
- for (let r = 0; r < n; r += 1) {
931
- const set = sizing[r].constrained || sizing[r].percent !== null;
932
- if (heights[r] === 0 && deficit[r] === 0) {
1547
+ let owed = 0;
1548
+ for (let r = first; r <= last; r += 1) {
1549
+ const pct = sizing[r].percent;
1550
+ const short =
1551
+ pct && base !== null ? Math.max(0, (pct / 100) * base - heights[r]) : 0;
1552
+ deficit.push(short);
1553
+ owed += short;
1554
+ const set = pct !== null ? base !== null : sizing[r].constrained;
1555
+ if (starts?.[r] && r > first) spanned.push(r);
1556
+ if (heights[r] === 0 && short === 0) {
933
1557
  empty.push(r);
934
1558
  if (!set) nothingSetEmpty.push(r);
935
1559
  } else {
@@ -938,10 +1562,11 @@ function growRows(
938
1562
  else nothingSetFull.push(r);
939
1563
  }
940
1564
  }
941
- const owed = deficit.reduce((a, b) => a + b, 0);
942
1565
  if (owed > 0) {
943
1566
  const give = Math.min(owed, extra);
944
- for (let r = 0; r < n; r += 1) heights[r] += (give * deficit[r]) / owed;
1567
+ for (let r = first; r <= last; r += 1) {
1568
+ heights[r] += (give * deficit[r - first]) / owed;
1569
+ }
945
1570
  extra -= give;
946
1571
  if (!(extra > 0)) return;
947
1572
  }
@@ -953,8 +1578,12 @@ function growRows(
953
1578
  even || !(sum > 0) ? extra / which.length : (extra * heights[r]) / sum;
954
1579
  }
955
1580
  };
956
- if (nothingSetFull.length) grow(nothingSetFull, false);
957
- else if (empty.length && empty.length + setFull === n) {
1581
+ if (spanned.length) grow(spanned, true);
1582
+ else if (nothingSetFull.length) grow(nothingSetFull, false);
1583
+ else if (starts) {
1584
+ if (full.length) grow(full, false);
1585
+ else heights[last] += extra;
1586
+ } else if (empty.length && empty.length + setFull === last - first + 1) {
958
1587
  grow(nothingSetEmpty.length ? nothingSetEmpty : empty, true);
959
1588
  } else grow(full, false);
960
1589
  }