@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.
- package/dist/code-editor/node.d.ts +13 -0
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +23 -5
- package/dist/code-editor/node.js.map +1 -1
- package/dist/html/css/cascade.d.ts +209 -23
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +873 -175
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +25 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +307 -19
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/content.d.ts +11 -6
- package/dist/html/css/content.d.ts.map +1 -1
- package/dist/html/css/content.js +171 -12
- package/dist/html/css/content.js.map +1 -1
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +62 -9
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/shapes.d.ts +62 -0
- package/dist/html/css/shapes.d.ts.map +1 -0
- package/dist/html/css/shapes.js +214 -0
- package/dist/html/css/shapes.js.map +1 -0
- package/dist/html/css/style.d.ts +42 -5
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +75 -27
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.js +15 -0
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +5 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +18 -9
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +7 -0
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +38 -7
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +160 -50
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +29 -2
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +159 -20
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +69 -19
- package/dist/html/layout/css-grid.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +1648 -225
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +15 -3
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +23 -8
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/table.d.ts +27 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +907 -323
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +36 -4
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +174 -37
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +4 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +769 -204
- package/dist/html/paint.js.map +1 -1
- package/dist/html/resources.js +14 -1
- package/dist/html/resources.js.map +1 -1
- package/dist/html/svg.d.ts +50 -17
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +501 -83
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.d.ts.map +1 -1
- package/dist/html/widgets.js +57 -4
- package/dist/html/widgets.js.map +1 -1
- package/package.json +3 -3
- package/src/code-editor/node.ts +24 -5
- package/src/html/css/cascade.ts +987 -176
- package/src/html/css/color.ts +345 -18
- package/src/html/css/content.ts +173 -15
- package/src/html/css/parse.ts +60 -8
- package/src/html/css/shapes.ts +273 -0
- package/src/html/css/style.ts +123 -30
- package/src/html/css/ua.ts +15 -0
- package/src/html/css/values.ts +23 -9
- package/src/html/index.ts +7 -0
- package/src/html/layout/block.ts +169 -56
- package/src/html/layout/boxes.ts +198 -24
- package/src/html/layout/css-grid.ts +89 -22
- package/src/html/layout/flex.ts +1863 -219
- package/src/html/layout/inline.ts +27 -7
- package/src/html/layout/table.ts +989 -333
- package/src/html/node.ts +204 -37
- package/src/html/paint.ts +999 -252
- package/src/html/resources.ts +13 -1
- package/src/html/svg.ts +600 -78
- package/src/html/widgets.ts +61 -4
package/src/html/layout/table.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
|
66
|
+
const own =
|
|
48
67
|
table.style.width === AUTO || cyclicWidth(table.style, contentWidth);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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' && !
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
for (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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 =
|
|
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,
|
|
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).
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
516
|
-
|
|
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
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
526
|
-
|
|
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,
|
|
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.
|
|
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
|
|
539
|
-
* proportion, so a column of long prose gives up more than a column of
|
|
540
|
-
* dates.
|
|
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
|
-
//
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
611
|
-
|
|
612
|
-
|
|
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
|
|
615
|
-
|
|
616
|
-
|
|
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 =
|
|
621
|
-
if (
|
|
622
|
-
weight +=
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
|
648
|
-
|
|
649
|
-
grow(
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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 (
|
|
666
|
-
|
|
1092
|
+
if (percent[c] === null) continue;
|
|
1093
|
+
increase += sized[c] - least[c];
|
|
667
1094
|
count += 1;
|
|
668
1095
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
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
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
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
|
|
707
|
-
|
|
708
|
-
)
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
/**
|
|
734
|
-
*
|
|
735
|
-
*
|
|
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
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
* percentage
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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.
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
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(
|
|
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
|
-
:
|
|
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
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
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
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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 =
|
|
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 (
|
|
930
|
-
else if (
|
|
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
|
}
|