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