@react-x11/components 0.14.0 → 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 (97) 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 +209 -23
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +873 -175
  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 +62 -0
  21. package/dist/html/css/shapes.d.ts.map +1 -0
  22. package/dist/html/css/shapes.js +214 -0
  23. package/dist/html/css/shapes.js.map +1 -0
  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 +75 -27
  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 +38 -7
  38. package/dist/html/layout/block.d.ts.map +1 -1
  39. package/dist/html/layout/block.js +160 -50
  40. package/dist/html/layout/block.js.map +1 -1
  41. package/dist/html/layout/boxes.d.ts +29 -2
  42. package/dist/html/layout/boxes.d.ts.map +1 -1
  43. package/dist/html/layout/boxes.js +159 -20
  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 +69 -19
  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 +1648 -225
  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 +907 -323
  58. package/dist/html/layout/table.js.map +1 -1
  59. package/dist/html/node.d.ts +36 -4
  60. package/dist/html/node.d.ts.map +1 -1
  61. package/dist/html/node.js +174 -37
  62. package/dist/html/node.js.map +1 -1
  63. package/dist/html/paint.d.ts +4 -1
  64. package/dist/html/paint.d.ts.map +1 -1
  65. package/dist/html/paint.js +769 -204
  66. package/dist/html/paint.js.map +1 -1
  67. package/dist/html/resources.js +14 -1
  68. package/dist/html/resources.js.map +1 -1
  69. package/dist/html/svg.d.ts +50 -17
  70. package/dist/html/svg.d.ts.map +1 -1
  71. package/dist/html/svg.js +501 -83
  72. package/dist/html/svg.js.map +1 -1
  73. package/dist/html/widgets.d.ts.map +1 -1
  74. package/dist/html/widgets.js +57 -4
  75. package/dist/html/widgets.js.map +1 -1
  76. package/package.json +3 -3
  77. package/src/code-editor/node.ts +24 -5
  78. package/src/html/css/cascade.ts +987 -176
  79. package/src/html/css/color.ts +345 -18
  80. package/src/html/css/content.ts +173 -15
  81. package/src/html/css/parse.ts +60 -8
  82. package/src/html/css/shapes.ts +273 -0
  83. package/src/html/css/style.ts +123 -30
  84. package/src/html/css/ua.ts +15 -0
  85. package/src/html/css/values.ts +23 -9
  86. package/src/html/index.ts +7 -0
  87. package/src/html/layout/block.ts +169 -56
  88. package/src/html/layout/boxes.ts +198 -24
  89. package/src/html/layout/css-grid.ts +89 -22
  90. package/src/html/layout/flex.ts +1863 -219
  91. package/src/html/layout/inline.ts +27 -7
  92. package/src/html/layout/table.ts +989 -333
  93. package/src/html/node.ts +204 -37
  94. package/src/html/paint.ts +999 -252
  95. package/src/html/resources.ts +13 -1
  96. package/src/html/svg.ts +600 -78
  97. package/src/html/widgets.ts +61 -4
@@ -17,11 +17,15 @@ import type { ComputedStyle } from '../css/style.js';
17
17
  import { Box, CLIPPED_CELLS, COLLAPSED_CELLS, isBlank } from './boxes.js';
18
18
  import {
19
19
  CELL_CONTENT,
20
+ FLEXED_HEIGHT,
20
21
  MIN_CONTENT_PROBE,
22
+ STRETCHED_ACROSS,
23
+ USED_HEIGHT,
21
24
  clampHeight,
22
25
  cyclicWidth,
23
26
  exactMinContent,
24
27
  layoutBlockIn,
28
+ layoutSubtree,
25
29
  measureIntrinsicWidth,
26
30
  moveContent,
27
31
  moveTo,
@@ -30,54 +34,45 @@ import {
30
34
  import type { LayoutContext } from './block.js';
31
35
  import { firstBaselineIn } from './inline.js';
32
36
  import { tableGrid } from './grid.js';
33
- import type { Cell } from './grid.js';
37
+ import type { Cell, TableGrid } from './grid.js';
34
38
 
35
- /** 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
+ */
36
56
  export function layoutTable(
37
57
  table: Box,
38
58
  ctx: LayoutContext,
39
59
  contentWidth: number,
40
60
  ): number {
41
- const { rows, captions, cells, columnCount, columnBoxes, columnGroups } =
42
- tableGrid(table);
61
+ const grid = tableGrid(table);
43
62
  table.captionTop = 0;
44
63
  table.captionBottom = 0;
45
64
  // a percentage of a width that is not known is `auto`: the table is as
46
65
  // wide as its columns where its content is measured (`cyclicWidth`)
47
- const auto =
66
+ const own =
48
67
  table.style.width === AUTO || cyclicWidth(table.style, contentWidth);
49
- if (!columnCount) {
50
- // With no columns, an auto table is as wide as its widest caption can
51
- // be and its `min-width` asks, as one with columns is at the least
52
- // (17.5.2), rather than as the room on offer: a caption over no cells
53
- // was centred in the width of the page.
54
- if (auto) {
55
- let inner = captions.length
56
- ? captionMinimum(captions, ctx) - table.horizontalExtra
57
- : 0;
58
- const min = resolveOrNull(table.style.minWidth, contentWidth);
59
- if (min !== null) {
60
- inner = Math.max(
61
- inner,
62
- table.style.boxSizing === 'border-box'
63
- ? min - table.horizontalExtra
64
- : min,
65
- );
66
- }
67
- table.width = Math.max(0, inner) + table.horizontalExtra;
68
- }
69
- layoutCaptions(captions, ctx, table);
68
+ // — and one a grid stretches across its area is as wide as that, as a
69
+ // table with a width of its own is (`STRETCHED_ACROSS`)
70
+ const auto = own && !STRETCHED_ACROSS.has(table);
71
+ if (!grid.columnCount) {
72
+ layoutColumnless(table, grid.captions, ctx, contentWidth, auto);
70
73
  return 0;
71
74
  }
72
75
 
73
- const style = table.style;
74
- // between the columns, and between the rows (CSS 2.1 17.6.1)
75
- const apart = style.borderCollapse !== 'collapse';
76
- const spacing = apart ? style.borderSpacing : 0;
77
- const rowSpacing = apart ? style.borderSpacingY : 0;
78
- const gaps = spacing * (columnCount + 1);
79
- const available = Math.max(0, contentWidth - gaps);
80
-
81
76
  // While the columns are sized, a percentage in a cell's padding is of
82
77
  // nothing: the width it is of is the one the columns are being sized to
83
78
  // come to (CSS Sizing 3, 5.2.1), and a cell asks of its column what its
@@ -85,31 +80,92 @@ export function layoutTable(
85
80
  // widths, below. Taken of the table's width here, and of the cell's own
86
81
  // where the cell was laid out, a padded cell's column was wider than
87
82
  // its share and its padding narrower.
88
- for (const cell of cells) resolveEdges(cell.box, 0);
83
+ for (const cell of grid.cells) resolveEdges(cell.box, 0);
89
84
 
90
85
  // a fixed layout needs a width to be fixed to; with `auto` a table is laid
91
- // out by its contents after all (CSS 2.1 17.5.2.1)
92
- const fixed = style.tableLayout === 'fixed' && !auto;
93
- // a percentage of the table's width less its spacing (CSS 2.1 17.5.2.1)
94
- const set = columnWidths(columnBoxes, columnGroups, available);
95
- const widths = fixed
96
- ? fixedColumns(cells, set, columnCount, available, spacing)
97
- : autoColumns(
98
- cells,
99
- set,
100
- columnCount,
101
- available,
102
- ctx,
103
- contentWidth,
104
- !auto,
105
- spacing,
86
+ // out by its contents after all (CSS 2.1 17.5.2.1), stretched or not
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,
106
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);
107
163
 
108
- // --- place ---------------------------------------------------------------
109
164
  // An auto table is at least as wide as its widest caption can be (CSS 2.1
110
- // 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;
111
168
  if (auto) {
112
- let room = 0;
113
169
  if (captions.length) {
114
170
  room = captionMinimum(captions, ctx) - table.horizontalExtra - gaps;
115
171
  }
@@ -119,17 +175,37 @@ export function layoutTable(
119
175
  style.boxSizing === 'border-box' ? min - table.horizontalExtra : min;
120
176
  room = Math.max(room, inner - gaps);
121
177
  }
122
- const sum = widths.reduce((a, b) => a + b, 0);
123
- if (room > sum) {
124
- for (let c = 0; c < columnCount; c += 1) {
125
- widths[c] +=
126
- sum > 0
127
- ? (widths[c] / sum) * (room - sum)
128
- : (room - sum) / columnCount;
129
- }
130
- }
131
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
+ );
132
207
 
208
+ // --- place ---------------------------------------------------------------
133
209
  // A column that `visibility: collapse` takes out of the table has been
134
210
  // sized with the rest, and is no room at all where they are placed (CSS
135
211
  // 2.1 17.5.5): no width, and no spacing after it. Its cells are still
@@ -165,60 +241,112 @@ export function layoutTable(
165
241
  tableContentWidth = used;
166
242
  table.width = used + table.horizontalExtra;
167
243
  }
168
- layoutCaptions(captions, ctx, table);
169
-
170
- const top = table.contentY + table.captionTop;
171
- let y = top;
172
- const rowTop: number[] = new Array<number>(rows.length);
173
- const rowHeight: number[] = new Array<number>(rows.length).fill(0);
174
- y += rowSpacing;
244
+ return { widths, columnX, gone, anyGone, end: x, spacing, rowSpacing };
245
+ }
175
246
 
176
- // Size every cell at its column width first, so a row's height is the
177
- // tallest cell in it rather than the first one that was measured. What
178
- // its content came to is kept apart from a height the cell sets: it is
179
- // that content `vertical-align` moves in the cell.
180
- //
181
- // A percentage in a cell's padding is of the width of its row: the
182
- // columns and the spacing between them, without the spacing either side
183
- // and the table's own padding. CSS 2.1 8.4 has it of the containing
184
- // block's width and names none for a cell; the row's is what browsers
185
- // take, and what the table's content box is wherever there is no
186
- // spacing. Every column counts, one `visibility: collapse` takes out
187
- // too, so that taking it out changes no row's height.
188
- 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);
189
271
  for (const columnWidth of widths) rowWidth += columnWidth;
190
272
  const natural: number[] = new Array<number>(cells.length);
191
273
  for (let i = 0; i < cells.length; i += 1) {
192
274
  const cell = cells[i];
193
275
  const width = spannedWidth(widths, cell, spacing);
194
- ctx.layoutSubtree(cell.box, width, rowWidth);
276
+ layoutSubtree(cell.box, ctx, width, rowWidth);
195
277
  natural[i] = CELL_CONTENT.get(cell.box) ?? cell.box.height;
196
278
  // A height a cell sets is a least one: its content is laid out as a
197
279
  // block's, which keeps a height it sets and lets the content run out of
198
280
  // it, and a cell does not. `<td height="10">` with a line in it is as
199
281
  // tall as the line (CSS 2.1 17.5.3), where it was 10px with the line
200
282
  // hanging out of it.
201
- 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];
202
292
  const specified = resolveOrNull(cell.box.style.height, NaN);
203
293
  if (specified !== null) {
294
+ const extra = cell.box.verticalExtra;
295
+ const set = Math.max(0, specified);
204
296
  cell.box.height = Math.max(
205
297
  cell.box.height,
206
- specified + cell.box.verticalExtra,
298
+ cell.box.style.boxSizing === 'border-box'
299
+ ? Math.max(set, extra)
300
+ : set + extra,
207
301
  );
208
302
  }
209
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
+
210
326
  // A cell aligned on the baseline — every value but `top`, `middle` and
211
327
  // `bottom` — hangs its first line on its first row's baseline, the lowest
212
328
  // of theirs from the row's top (CSS 2.1 17.5.3), and the row is as tall as
213
- // the cells hung from it need.
214
- const lift = baselineLifts(cells, rows.length);
329
+ // the cells hung from it need. What moves down is the content, so a cell
330
+ // needs the lift and its content, and a height it sets is a least one
331
+ // beside that rather than more room under the lift: two cells set
332
+ // `height: 80px` with text of two sizes in them are a row 80px tall, where
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);
341
+ const needs = (i: number): number =>
342
+ cells[i].rowSpan > 1
343
+ ? cells[i].box.height
344
+ : Math.max(cells[i].box.height, lift[i] + natural[i]);
215
345
  for (let i = 0; i < cells.length; i += 1) {
216
346
  const cell = cells[i];
217
- if (cell.rowSpan > 1) continue;
218
- rowHeight[cell.row] = Math.max(
219
- rowHeight[cell.row],
220
- lift[i] + cell.box.height,
221
- );
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;
222
350
  }
223
351
  for (let r = 0; r < rows.length; r += 1) {
224
352
  const specified = resolveOrNull(rows[r].style.height, NaN);
@@ -226,22 +354,73 @@ export function layoutTable(
226
354
  }
227
355
  // A row-spanning cell only forces height when the rows it covers do not
228
356
  // already provide it — otherwise a `rowspan="3"` cell would make each of
229
- // 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);
230
365
  for (let i = 0; i < cells.length; i += 1) {
231
- const cell = cells[i];
232
- if (cell.rowSpan <= 1) continue;
233
- const last = Math.min(rows.length - 1, cell.row + cell.rowSpan - 1);
234
- let covered = 0;
235
- for (let r = cell.row; r <= last; r += 1) covered += rowHeight[r];
236
- covered += rowSpacing * (last - cell.row);
237
- const missing = lift[i] + cell.box.height - covered;
238
- 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
+ }
239
395
  }
240
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
+
241
410
  // A table's own height, within its least and greatest, is a least
242
411
  // height: what its rows come short of it goes to them (CSS 2.1 17.5.3,
243
- // which leaves how to its user agents: `growRows`)
412
+ // which leaves how to its user agents: `growRows`). So is the height a
413
+ // flex or grid layout gave it, stretched or flexed (`FLEXED_HEIGHT`,
414
+ // `USED_HEIGHT`), which is its wrapper box's, captions and all (CSS
415
+ // Flexbox 1, 4): taken as the table's box alone, a table stretched down
416
+ // its line kept its rows at their content's height, and a cell had
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`).
244
422
  const wanted = resolveOrNull(style.height, table.percentHeightBase);
423
+ const given = givenHeight(table);
245
424
  if (rows.length) {
246
425
  const extraBox = table.verticalExtra;
247
426
  const set =
@@ -250,7 +429,9 @@ export function layoutTable(
250
429
  : style.boxSizing === 'border-box'
251
430
  ? wanted
252
431
  : wanted + extraBox;
253
- const inner = clampHeight(table, set) - extraBox;
432
+ const inner = Number.isFinite(given)
433
+ ? given - table.captionTop - table.captionBottom
434
+ : clampHeight(table, set) - extraBox;
254
435
  let total = 0;
255
436
  for (const h of rowHeight) total += h;
256
437
  const extra = inner - total - rowSpacing * (rows.length + 1);
@@ -362,7 +543,7 @@ export function layoutTable(
362
543
  // left edge to the last one's right, the spacing around them outside it
363
544
  // (CSS 2.1 17.5.1), as it is above and below.
364
545
  const gridX = table.contentX + spacing;
365
- const gridWidth = Math.max(0, x - spacing - gridX);
546
+ const gridWidth = Math.max(0, columns.end - spacing - gridX);
366
547
  for (let r = 0; r < rows.length; r += 1) {
367
548
  const row = rows[r];
368
549
  row.x = gridX;
@@ -386,6 +567,20 @@ export function layoutTable(
386
567
  return y - top;
387
568
  }
388
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
+
389
584
  /** Whether `visibility: collapse` takes a column out: its own, or the
390
585
  * column group's that holds it with no column box of its own. */
391
586
  function collapsedColumn(column: Box | null, group: Box | null): boolean {
@@ -464,137 +659,291 @@ function spannedWidth(widths: number[], cell: Cell, spacing: number): number {
464
659
  /**
465
660
  * `table-layout: fixed`: the columns and the first row decide, and the rest
466
661
  * of the table is not measured at all, which is the whole point of it (CSS
467
- * 2.1 17.5.2.1). A column with a `width` sets its own; failing that, a cell
468
- * of the first row with one sets the columns it spans, by its border box and
469
- * less the spacing between them; the columns left share what remains, and
470
- * where nothing remains, what is left of a table wider than its columns is
471
- * 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.
472
686
  */
473
687
  function fixedColumns(
474
688
  cells: Cell[],
475
689
  columnWidths: (number | null)[],
690
+ columnPercents: (number | null)[],
691
+ columnLeast: (number | null)[],
476
692
  columnCount: number,
477
693
  available: number,
478
694
  spacing: number,
479
695
  ): number[] {
480
- const widths: number[] = new Array<number>(columnCount).fill(0);
481
- const set: boolean[] = new Array<boolean>(columnCount).fill(false);
482
- for (let c = 0; c < columnCount; c += 1) {
483
- const px = columnWidths[c];
484
- if (px === null) continue;
485
- widths[c] = px;
486
- set[c] = true;
487
- }
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);
488
703
  for (const cell of cells) {
489
704
  if (cell.row > 0) break;
490
- const px = ownWidth(cell.box.style, available);
491
- if (px === null) continue;
492
705
  const box = cell.box;
493
- const outer =
494
- box.style.boxSizing === 'border-box'
495
- ? Math.max(px, box.horizontalExtra)
496
- : px + box.horizontalExtra;
497
- const last = Math.min(columnCount, cell.column + cell.colSpan);
498
- let taken = spacing * (last - cell.column - 1);
499
- let open = 0;
500
- for (let c = cell.column; c < last; c += 1) {
501
- if (set[c]) taken += widths[c];
502
- else open += 1;
503
- }
504
- if (!open) continue;
505
- const each = Math.max(0, outer - taken) / open;
506
- for (let c = cell.column; c < last; c += 1) {
507
- if (set[c]) continue;
508
- widths[c] = each;
509
- set[c] = true;
510
- }
511
- }
512
- let used = 0;
513
- 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;
514
761
  for (let c = 0; c < columnCount; c += 1) {
515
- if (set[c]) used += widths[c];
516
- 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
+ }
517
778
  }
518
- const remaining = Math.max(0, available - used);
519
- 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;
520
787
  for (let c = 0; c < columnCount; c += 1) {
521
- 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];
522
792
  }
523
- } 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);
524
800
  for (let c = 0; c < columnCount; c += 1) {
525
- widths[c] +=
526
- 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);
527
819
  }
528
820
  }
529
821
  return widths;
530
822
  }
531
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
+
532
849
  /**
533
850
  * The auto algorithm: measure every cell's max-content and min-content
534
- * 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`).
535
854
  *
536
- * 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
537
861
  * fit, they are used — a small table is exactly as wide as its contents.
538
- * When they do not, the surplus over each column's minimum is scaled down in
539
- * proportion, so a column of long prose gives up more than a column of
540
- * 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.
541
888
  */
542
889
  function autoColumns(
543
890
  cells: Cell[],
544
891
  columnWidths: (number | null)[],
892
+ columnLeast: (number | null)[],
893
+ columnPercents: (number | null)[],
545
894
  columnCount: number,
546
895
  available: number,
547
- ctx: LayoutContext,
548
- containingWidth: number,
549
896
  fill: boolean,
550
897
  spacing: number,
898
+ room: number,
899
+ widen: boolean,
551
900
  ): number[] {
552
901
  const max: number[] = new Array<number>(columnCount).fill(0);
553
902
  const min: number[] = new Array<number>(columnCount).fill(0);
554
903
  const explicit: (number | null)[] = new Array<number | null>(
555
904
  columnCount,
556
905
  ).fill(null);
906
+ // each column's percentage: its own, or the most a cell of it asks
907
+ const percent = columnPercents.slice();
557
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 }>();
558
911
 
559
912
  for (const cell of cells) {
560
- // Two probe layouts per cell: unconstrained for max-content, and at no
561
- // width for min-content (`MIN_CONTENT_PROBE`) — which the line breaker
562
- // answers by breaking at every opportunity, so the widest line is the
563
- // longest unbreakable word.
564
- //
565
- // Cached on the box, because intrinsic widths are width-independent by
566
- // definition — measuring them per layout pass made a *resize* of a
567
- // 500-row table cost as much as its first layout (223ms of re-probing
568
- // measured against 10ms with the cache). The cache's invalidation rule
569
- // is the box's lifetime: any DOM or style change rebuilds the tree.
570
- if (cell.box.intrinsicMaxContent < 0) {
571
- cell.box.intrinsicMaxContent = measureIntrinsicWidth(
572
- cell.box,
573
- ctx,
574
- Infinity,
575
- );
576
- // where its words say it exactly, read from the layout just made
577
- cell.box.intrinsicMinContent =
578
- (ctx.fonts && exactMinContent(cell.box, ctx.fonts)) ??
579
- measureIntrinsicWidth(cell.box, ctx, MIN_CONTENT_PROBE, true);
580
- }
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);
581
921
  if (cell.colSpan > 1) {
582
922
  spanning.push(cell);
583
923
  continue;
584
924
  }
585
- max[cell.column] = Math.max(max[cell.column], cell.box.intrinsicMaxContent);
586
- min[cell.column] = Math.max(min[cell.column], cell.box.intrinsicMinContent);
587
- const width = cellWidth(cell, containingWidth);
588
- if (width !== null) {
589
- explicit[cell.column] = Math.max(explicit[cell.column] ?? 0, width);
590
- }
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);
591
933
  }
592
934
 
593
935
  // a column's own width counts as its cells' do: at least that, and its
594
- // 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)
595
939
  for (let c = 0; c < columnCount; c += 1) {
596
940
  const own = columnWidths[c];
597
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
+ }
598
947
  }
599
948
 
600
949
  // A spanning cell comes after the cells of one column, the narrower
@@ -605,173 +954,452 @@ function autoColumns(
605
954
  // they have none. Taken in document order and spread evenly, a spanning
606
955
  // cell in a first row was shared out before the cells of one column
607
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%.
608
968
  spanning.sort((a, b) => a.colSpan - b.colSpan);
609
969
  const grow = (into: number[], want: number, cell: Cell): void => {
610
- const last = Math.min(columnCount - 1, cell.column + cell.colSpan - 1);
611
- let have = spacing * (last - cell.column);
612
- 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];
613
975
  if (!(want > have)) return;
614
- let free = false;
615
- for (let c = cell.column; c <= last && !free; c += 1) {
616
- 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
+ }
617
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];
618
996
  let weight = 0;
619
997
  let count = 0;
620
- for (let c = cell.column; c <= last; c += 1) {
621
- if (free && explicit[c] !== null) continue;
622
- weight += max[c];
998
+ for (let c = first; c <= last; c += 1) {
999
+ if (!takes(c)) continue;
1000
+ weight += weightOf(c);
623
1001
  count += 1;
624
1002
  }
625
- for (let c = cell.column; c <= last; c += 1) {
626
- if (free && explicit[c] !== null) continue;
627
- into[c] +=
628
- 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;
629
1007
  }
630
1008
  };
631
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
+ }
632
1014
  // what the columns' content alone asks, before their set widths: the
633
1015
  // least a column gives way to when the table has less room than its
634
1016
  // cells' widths want, as beside a float
635
1017
  const least = min.slice();
636
- 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
637
1022
  for (let c = 0; c < columnCount; c += 1) {
638
- if (explicit[c] !== null) {
639
- max[c] = Math.max(min[c], explicit[c] as number);
640
- min[c] = Math.max(min[c], Math.min(explicit[c] as number, max[c]));
641
- }
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]));
642
1027
  }
643
1028
  // and with them: a spanning cell's own width is the least it is, as a
644
1029
  // single cell's is (step 1), so a cell over three columns set to 100px,
645
1030
  // with 20px between them, has 60 to share, where it had none
646
1031
  for (const cell of spanning) {
647
- const own = cellWidth(cell, containingWidth) ?? 0;
648
- grow(min, Math.max(cell.box.intrinsicMinContent, own), cell);
649
- 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);
650
1036
  }
651
1037
  for (let c = 0; c < columnCount; c += 1) min[c] = Math.min(min[c], max[c]);
652
1038
 
653
- const totalMax = max.reduce((a, b) => a + b, 0);
654
- if (totalMax <= available) {
655
- // Everything fits. A table with a width of its own still fills it (CSS
656
- // 2.1 17.5.2.2): the columns not set to a width take the rest in
657
- // proportion to what they asked for, or every column does where all are
658
- // set. Returned as asked, `<table width="600">` drew its cells at their
659
- // content's width and left the rest of its 600 empty.
660
- if (!fill || available <= totalMax) return max;
661
- const set = explicit.every((w) => w !== null);
662
- 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;
663
1090
  let count = 0;
664
1091
  for (let c = 0; c < columnCount; c += 1) {
665
- if (!set && explicit[c] !== null) continue;
666
- weight += max[c];
1092
+ if (percent[c] === null) continue;
1093
+ increase += sized[c] - least[c];
667
1094
  count += 1;
668
1095
  }
669
- const extra = available - totalMax;
670
- return max.map((m, c) =>
671
- !set && explicit[c] !== null
672
- ? m
673
- : 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),
674
1103
  );
675
1104
  }
676
- const totalMin = min.reduce((a, b) => a + b, 0);
677
- if (totalMin >= available) {
1105
+ if (target <= atSet) {
678
1106
  // Less room than the cells' widths want: a width a cell was set to
679
- // gives way first, down to what its content asks, and a table never
680
- // goes narrower than its words (CSS 2.1 17.5.2.2), which it overflows.
681
- const totalLeast = least.reduce((a, b) => a + b, 0);
682
- if (totalLeast >= available) return least;
683
- const give = totalMin - totalLeast;
684
- return min.map(
685
- (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),
686
1113
  );
687
1114
  }
688
- const slack = available - totalMin;
689
- const surplus = totalMax - totalMin;
690
- 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
+ });
1154
+ }
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
+
1161
+ /**
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
+ }
691
1228
  }
692
1229
 
693
1230
  /**
694
- * The width each column is set to, or null: a column's own `width`, or its
695
- * group's shared among the columns of a group that has none of its own
696
- * (HTML's `<colgroup span>`); and a group's `width` spread over its columns
697
- * where theirs come to less (CSS 2.1 17.5.2.2, step 4). Each within its
698
- * `min-width` and `max-width`, which apply to columns and column groups as
699
- * to any block (10.4), so `min-width` alone sets one.
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.
700
1272
  */
701
1273
  function columnWidths(
702
1274
  columnBoxes: (Box | null)[],
703
1275
  columnGroups: (Box | null)[],
704
1276
  base: number,
1277
+ fixed: boolean,
705
1278
  ): (number | null)[] {
706
- const widths = columnBoxes.map((column) =>
707
- column ? partWidth(column, base) : null,
708
- );
709
- let group: Box | null = null;
710
- for (let c = 0; c <= columnGroups.length; c += 1) {
711
- if (c < columnGroups.length && columnGroups[c] === group) continue;
712
- // the group that ends here, over its columns from `start`
713
- if (group) {
714
- const width = partWidth(group, base);
715
- let start = c - 1;
716
- while (start > 0 && columnGroups[start - 1] === group) start -= 1;
717
- if (width !== null) {
718
- let have = 0;
719
- for (let i = start; i < c; i += 1) have += widths[i] ?? 0;
720
- if (have < width) {
721
- const each = (width - have) / (c - start);
722
- for (let i = start; i < c; i += 1) {
723
- widths[i] = (widths[i] ?? 0) + each;
724
- }
725
- }
726
- }
727
- }
728
- group = c < columnGroups.length ? columnGroups[c] : null;
729
- }
730
- 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
+ });
731
1289
  }
732
1290
 
733
- /** A column's or a column group's width, within its limits, or null where
734
- * it sets none. The limits are lengths: a percentage in them does what it
735
- * 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. */
736
1330
  function partWidth(box: Box, base: number): number | null {
737
- const style = box.style;
738
- const width = ownWidth(style, base);
739
- const min = typeof style.minWidth === 'number' ? style.minWidth : 0;
740
- if (width === null && !(min > 0)) return null;
741
- const max = typeof style.maxWidth === 'number' ? style.maxWidth : null;
742
- let out = width ?? 0;
743
- if (max !== null) out = Math.min(out, max);
744
- return Math.max(0, out, min);
745
- }
746
-
747
- /** A cell's own `width`, its padding and borders in, or null. A
748
- * percentage is a share of the table its padding and borders are part
749
- * of, as browsers read it: added on, a 90% cell and a 10% one came to
750
- * more than the table, which then took it back from both. Held within its
751
- * `min-width` and `max-width` as a column's is (`partWidth`), which CSS
752
- * 2.1 leaves undefined and every browser does: a cell set `width: 3in;
753
- * max-width: 1in` is an inch wide, and one with `min-width` alone is as
754
- * wide as that.
755
- *
756
- * Where they are lengths. A percentage `min-width` is ignored, and a
757
- * percentage `max-width` holds a percentage `width` and nothing else
758
- * (CSS Tables 3, 3.8.2, and `ownWidth`): a cell `width: 100px;
759
- * max-width: 10%` in a table of 600 is 100 wide, where it was 60, and
760
- * one set `min-width: 150%` takes no more than its content asks. */
761
- function cellWidth(cell: Cell, base: number): number | null {
762
- const style = cell.box.style;
763
- const len = style.width;
764
- const width = ownWidth(style, base);
765
- const min = typeof style.minWidth === 'number' ? style.minWidth : 0;
766
- if (width === null && !(min > 0)) return null;
767
- const max = typeof style.maxWidth === 'number' ? style.maxWidth : null;
768
- let own = width ?? 0;
769
- if (max !== null) own = Math.min(own, max);
770
- own = Math.max(0, own, min);
771
- if (isPct(len) && own === width) {
772
- return Math.max(width, cell.box.horizontalExtra);
773
- }
774
- 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;
775
1403
  }
776
1404
 
777
1405
  /** A cell's, a column's or a column group's `width` as the columns read
@@ -805,20 +1433,27 @@ function lengthAgainst(len: Len, base: number): number | null {
805
1433
  * row's: 0 for a cell aligned `top`, `middle` or `bottom`, which is placed
806
1434
  * once the row's height is known. A cell's baseline is its first line box's,
807
1435
  * at any depth, or the bottom of its content where it has none (CSS 2.1
808
- * 17.5.3); it is taken as laid out, from the cell's top. An empty cell has
809
- * nothing to align and says nothing about the row's, as in a browser: its
810
- * content's bottom is its height, which would hang every other cell in the
811
- * row from the bottom of a cell given a height and nothing in it.
1436
+ * 17.5.3); it is taken as laid out, from the cell's top. That bottom is of
1437
+ * the content (`natural`) and not of a height the cell sets, which "does not
1438
+ * increase the height of the cell box": taken from the set height, a cell
1439
+ * holding one block of 30px in 80px hung the text beside it from 80px. An
1440
+ * empty cell has nothing to align and says nothing about the row's, as in a
1441
+ * browser. `own` is each cell's baseline from its top, NaN where it has none
1442
+ * to give.
812
1443
  */
813
- function baselineLifts(cells: Cell[], rowCount: number): number[] {
1444
+ function baselineLifts(
1445
+ cells: Cell[],
1446
+ rowCount: number,
1447
+ natural: number[],
1448
+ ): { lift: number[]; own: number[] } {
814
1449
  const lift = new Array<number>(cells.length).fill(0);
1450
+ const own = new Array<number>(cells.length).fill(NaN);
815
1451
  let any = false;
816
1452
  for (const cell of cells) {
817
1453
  const va = cell.box.style.verticalAlign;
818
1454
  if (va !== 'top' && va !== 'middle' && va !== 'bottom') any = true;
819
1455
  }
820
- if (!any) return lift;
821
- const own = new Array<number>(cells.length).fill(NaN);
1456
+ if (!any) return { lift, own };
822
1457
  const row = new Array<number>(rowCount).fill(-Infinity);
823
1458
  for (let i = 0; i < cells.length; i += 1) {
824
1459
  const box = cells[i].box;
@@ -829,13 +1464,13 @@ function baselineLifts(cells: Cell[], rowCount: number): number[] {
829
1464
  own[i] =
830
1465
  first !== null
831
1466
  ? first - box.y
832
- : box.height - box.padBottom - box.borderBottom;
1467
+ : natural[i] - box.padBottom - box.borderBottom;
833
1468
  row[cells[i].row] = Math.max(row[cells[i].row], own[i]);
834
1469
  }
835
1470
  for (let i = 0; i < cells.length; i += 1) {
836
1471
  if (own[i] === own[i]) lift[i] = row[cells[i].row] - own[i];
837
1472
  }
838
- return lift;
1473
+ return { lift, own };
839
1474
  }
840
1475
 
841
1476
  /** Whether a cell has anything in flow in it. */
@@ -877,32 +1512,48 @@ function rowSizing(rows: Box[], cells: Cell[]): RowSizing[] {
877
1512
 
878
1513
  /**
879
1514
  * The height a table has past its rows' given out among them, as browsers
880
- * give it (the CSS Tables 3 draft leaves it open, csswg-drafts#4418): first
881
- * to the rows a percentage sets, up to it; then to the rows nothing sets
882
- * that have content, in proportion to their heights; then, where every row
883
- * with content is set, to the empty rows, those nothing sets first, evenly;
884
- * and else to every row with content, in proportion. A row whose cells set
885
- * 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.
886
1530
  */
887
1531
  function growRows(
888
1532
  heights: number[],
889
1533
  sizing: RowSizing[],
890
1534
  extra: number,
891
- base: number,
1535
+ base: number | null,
1536
+ first = 0,
1537
+ last = heights.length - 1,
1538
+ starts: boolean[] | null = null,
892
1539
  ): void {
893
- const n = heights.length;
894
- const deficit = heights.map((h, r) => {
895
- const pct = sizing[r].percent;
896
- return pct ? Math.max(0, (pct / 100) * base - h) : 0;
897
- });
1540
+ const deficit: number[] = [];
898
1541
  const empty: number[] = [];
899
1542
  const nothingSetEmpty: number[] = [];
900
1543
  const full: number[] = [];
901
1544
  const nothingSetFull: number[] = [];
1545
+ const spanned: number[] = [];
902
1546
  let setFull = 0;
903
- for (let r = 0; r < n; r += 1) {
904
- const set = sizing[r].constrained || sizing[r].percent !== null;
905
- 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) {
906
1557
  empty.push(r);
907
1558
  if (!set) nothingSetEmpty.push(r);
908
1559
  } else {
@@ -911,10 +1562,11 @@ function growRows(
911
1562
  else nothingSetFull.push(r);
912
1563
  }
913
1564
  }
914
- const owed = deficit.reduce((a, b) => a + b, 0);
915
1565
  if (owed > 0) {
916
1566
  const give = Math.min(owed, extra);
917
- 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
+ }
918
1570
  extra -= give;
919
1571
  if (!(extra > 0)) return;
920
1572
  }
@@ -926,8 +1578,12 @@ function growRows(
926
1578
  even || !(sum > 0) ? extra / which.length : (extra * heights[r]) / sum;
927
1579
  }
928
1580
  };
929
- if (nothingSetFull.length) grow(nothingSetFull, false);
930
- 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) {
931
1587
  grow(nothingSetEmpty.length ? nothingSetEmpty : empty, true);
932
1588
  } else grow(full, false);
933
1589
  }