@react-x11/components 0.13.0 → 0.14.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.
Files changed (134) hide show
  1. package/dist/html/controls.d.ts +29 -2
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +70 -2
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +42 -1
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +336 -16
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +18 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +27 -0
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/parse.d.ts +18 -4
  14. package/dist/html/css/parse.d.ts.map +1 -1
  15. package/dist/html/css/parse.js +66 -28
  16. package/dist/html/css/parse.js.map +1 -1
  17. package/dist/html/css/shapes.d.ts +44 -0
  18. package/dist/html/css/shapes.d.ts.map +1 -0
  19. package/dist/html/css/shapes.js +194 -0
  20. package/dist/html/css/shapes.js.map +1 -0
  21. package/dist/html/css/style.d.ts +128 -15
  22. package/dist/html/css/style.d.ts.map +1 -1
  23. package/dist/html/css/style.js +547 -144
  24. package/dist/html/css/style.js.map +1 -1
  25. package/dist/html/css/transform.d.ts +85 -0
  26. package/dist/html/css/transform.d.ts.map +1 -0
  27. package/dist/html/css/transform.js +425 -0
  28. package/dist/html/css/transform.js.map +1 -0
  29. package/dist/html/css/ua.d.ts.map +1 -1
  30. package/dist/html/css/ua.js +14 -2
  31. package/dist/html/css/ua.js.map +1 -1
  32. package/dist/html/css/values.d.ts +4 -0
  33. package/dist/html/css/values.d.ts.map +1 -1
  34. package/dist/html/css/values.js.map +1 -1
  35. package/dist/html/fonts.d.ts +55 -5
  36. package/dist/html/fonts.d.ts.map +1 -1
  37. package/dist/html/fonts.js +284 -23
  38. package/dist/html/fonts.js.map +1 -1
  39. package/dist/html/layout/axes.d.ts +14 -0
  40. package/dist/html/layout/axes.d.ts.map +1 -0
  41. package/dist/html/layout/axes.js +28 -0
  42. package/dist/html/layout/axes.js.map +1 -0
  43. package/dist/html/layout/block.d.ts +104 -12
  44. package/dist/html/layout/block.d.ts.map +1 -1
  45. package/dist/html/layout/block.js +410 -39
  46. package/dist/html/layout/block.js.map +1 -1
  47. package/dist/html/layout/boxes.d.ts +72 -1
  48. package/dist/html/layout/boxes.d.ts.map +1 -1
  49. package/dist/html/layout/boxes.js +40 -2
  50. package/dist/html/layout/boxes.js.map +1 -1
  51. package/dist/html/layout/cache.d.ts +1 -1
  52. package/dist/html/layout/cache.d.ts.map +1 -1
  53. package/dist/html/layout/cache.js +6 -0
  54. package/dist/html/layout/cache.js.map +1 -1
  55. package/dist/html/layout/css-grid.d.ts.map +1 -1
  56. package/dist/html/layout/css-grid.js +144 -24
  57. package/dist/html/layout/css-grid.js.map +1 -1
  58. package/dist/html/layout/flex.d.ts.map +1 -1
  59. package/dist/html/layout/flex.js +585 -85
  60. package/dist/html/layout/flex.js.map +1 -1
  61. package/dist/html/layout/inline.d.ts +3 -0
  62. package/dist/html/layout/inline.d.ts.map +1 -1
  63. package/dist/html/layout/inline.js +144 -54
  64. package/dist/html/layout/inline.js.map +1 -1
  65. package/dist/html/layout/multicol.d.ts +42 -0
  66. package/dist/html/layout/multicol.d.ts.map +1 -0
  67. package/dist/html/layout/multicol.js +897 -0
  68. package/dist/html/layout/multicol.js.map +1 -0
  69. package/dist/html/layout/shaping.d.ts +7 -0
  70. package/dist/html/layout/shaping.d.ts.map +1 -1
  71. package/dist/html/layout/shaping.js +25 -20
  72. package/dist/html/layout/shaping.js.map +1 -1
  73. package/dist/html/layout/table.d.ts.map +1 -1
  74. package/dist/html/layout/table.js +97 -31
  75. package/dist/html/layout/table.js.map +1 -1
  76. package/dist/html/node.d.ts +4 -0
  77. package/dist/html/node.d.ts.map +1 -1
  78. package/dist/html/node.js +319 -47
  79. package/dist/html/node.js.map +1 -1
  80. package/dist/html/paint.d.ts +83 -5
  81. package/dist/html/paint.d.ts.map +1 -1
  82. package/dist/html/paint.js +1474 -217
  83. package/dist/html/paint.js.map +1 -1
  84. package/dist/html/svg.d.ts +39 -9
  85. package/dist/html/svg.d.ts.map +1 -1
  86. package/dist/html/svg.js +482 -37
  87. package/dist/html/svg.js.map +1 -1
  88. package/dist/html/widgets.js +87 -7
  89. package/dist/html/widgets.js.map +1 -1
  90. package/dist/html/woff2.d.ts +14 -0
  91. package/dist/html/woff2.d.ts.map +1 -0
  92. package/dist/html/woff2.js +608 -0
  93. package/dist/html/woff2.js.map +1 -0
  94. package/dist/maps/controller.d.ts +17 -1
  95. package/dist/maps/controller.d.ts.map +1 -1
  96. package/dist/maps/controller.js +27 -7
  97. package/dist/maps/controller.js.map +1 -1
  98. package/dist/richtext/node.d.ts +8 -1
  99. package/dist/richtext/node.d.ts.map +1 -1
  100. package/dist/richtext/node.js +5 -1
  101. package/dist/richtext/node.js.map +1 -1
  102. package/dist/richtext/runs.d.ts +25 -6
  103. package/dist/richtext/runs.d.ts.map +1 -1
  104. package/dist/richtext/runs.js +240 -24
  105. package/dist/richtext/runs.js.map +1 -1
  106. package/package.json +1 -1
  107. package/src/html/controls.ts +85 -2
  108. package/src/html/css/cascade.ts +366 -8
  109. package/src/html/css/color.ts +29 -0
  110. package/src/html/css/parse.ts +71 -31
  111. package/src/html/css/shapes.ts +239 -0
  112. package/src/html/css/style.ts +652 -147
  113. package/src/html/css/transform.ts +459 -0
  114. package/src/html/css/ua.ts +15 -2
  115. package/src/html/css/values.ts +4 -0
  116. package/src/html/fonts.ts +312 -29
  117. package/src/html/layout/axes.ts +62 -0
  118. package/src/html/layout/block.ts +466 -37
  119. package/src/html/layout/boxes.ts +95 -3
  120. package/src/html/layout/cache.ts +6 -0
  121. package/src/html/layout/css-grid.ts +169 -32
  122. package/src/html/layout/flex.ts +686 -79
  123. package/src/html/layout/inline.ts +149 -55
  124. package/src/html/layout/multicol.ts +977 -0
  125. package/src/html/layout/shaping.ts +27 -18
  126. package/src/html/layout/table.ts +105 -35
  127. package/src/html/node.ts +354 -48
  128. package/src/html/paint.ts +1804 -315
  129. package/src/html/svg.ts +573 -36
  130. package/src/html/widgets.ts +95 -9
  131. package/src/html/woff2.ts +612 -0
  132. package/src/maps/controller.ts +28 -7
  133. package/src/richtext/node.ts +13 -2
  134. package/src/richtext/runs.ts +313 -35
@@ -0,0 +1,977 @@
1
+ /**
2
+ * Multi-column layout (CSS Multi-column 1), over the layout the block pass
3
+ * makes of a container's content as one column: a strip a column wide and
4
+ * as tall as it all comes to. What is here decides where the strip breaks,
5
+ * and moves each piece of it to its column.
6
+ *
7
+ * Moved rather than laid out again, because a break changes nothing about a
8
+ * box but where it is: every column is the same width, so a paragraph's
9
+ * lines are the ones the strip gave it, and a box that a break misses is the
10
+ * same box in another place. A box a break falls inside keeps one rect,
11
+ * which takes in all its pieces as a browser's bounding rect of it does, and
12
+ * its pieces beside it (`COLUMN_PIECES`); the lines of a paragraph go each to
13
+ * its column (`COLUMN_LINES`).
14
+ *
15
+ * Where the breaks go is CSS Fragmentation 3's: between the lines of a block
16
+ * and between sibling boxes, never inside a line, a replaced element, a
17
+ * table, a flex box or a grid, a box that clips or scrolls, or one that
18
+ * says `break-inside: avoid` (4.1); `orphans` and `widows` lines of a block
19
+ * kept together at either side of one (4.3), and a margin next to one
20
+ * dropped where the break was not forced (5.2). A box with a height and
21
+ * nothing in it has no line or box for a break to fall inside, and is cut
22
+ * wherever a column ends; so is the room clearance leaves above a box or a
23
+ * line, which is no margin. How tall the columns are is left to the user
24
+ * agent where they balance (Multi-column 1, 7), and this does as Blink
25
+ * does: the content's height over the number of columns, no less than the
26
+ * tallest thing that cannot break, and then, while that many columns do not
27
+ * hold it, taller by the least amount that lets one more thing into a
28
+ * column — the smallest "space shortage" any break left
29
+ * (`NGColumnLayoutAlgorithm`, `StretchColumnBlockSize`).
30
+ *
31
+ * What this cannot do for having laid the content out once: a float that
32
+ * cannot be cut stays with the lines that were set beside it, where a
33
+ * browser takes it to the next column and sets those lines again, wider;
34
+ * and a table, a flex box and a grid go to a column whole, where a browser
35
+ * breaks them between their rows.
36
+ */
37
+ import { CONTAIN_SIZE, scrolls } from '../css/style.js';
38
+ import { gapOf } from '../css/values.js';
39
+ import { contained, establishesBFC, moveTo, transformed } from './block.js';
40
+ import {
41
+ CLEARED_FROM,
42
+ COLUMN_LINES,
43
+ COLUMN_PIECES,
44
+ COLUMN_ROWS,
45
+ columned,
46
+ } from './boxes.js';
47
+ import type { Box, ColumnPiece, LineBox, LineText } from './boxes.js';
48
+
49
+ /** A multicol container's columns: how many, how wide, and the gap. */
50
+ export interface Columns {
51
+ count: number;
52
+ width: number;
53
+ gap: number;
54
+ }
55
+
56
+ /** Slack for sums of fractions of a pixel that should have come out even. */
57
+ const EPS = 1 / 128;
58
+
59
+ /** How far past the rows of its column a text's layout is left to draw at
60
+ * an end no other column's rows are beyond: a glyph taller than its line. */
61
+ const REACH = 2048;
62
+
63
+ /**
64
+ * The columns a multicol container's content box of a width is set in (CSS
65
+ * Multi-column 1, 3.4): as many as `column-count` says, or as many
66
+ * `column-width` wide as fit, or the fewer of the two, and each as wide as
67
+ * leaves a gap between them. Null for a box that is none, and for one laid
68
+ * out at no width limit, whose content is one column as wide as it likes.
69
+ */
70
+ export function columnsOf(box: Box, available: number): Columns | null {
71
+ const style = box.style;
72
+ if (!style.columns || box.kind !== 'block') return null;
73
+ if (!Number.isFinite(available)) return null;
74
+ // `normal` is an em between columns (4.1)
75
+ const gap = style.columnGapNormal
76
+ ? style.fontSize
77
+ : gapOf(style.columnGap, available);
78
+ let count: number;
79
+ if (style.columnWidth === null) count = style.columnCount ?? 1;
80
+ else {
81
+ const fit = Math.max(
82
+ 1,
83
+ Math.floor((available + gap) / (style.columnWidth + gap)),
84
+ );
85
+ count = style.columnCount === null ? fit : Math.min(style.columnCount, fit);
86
+ }
87
+ return {
88
+ count,
89
+ gap,
90
+ width: Math.max(0, (available - (count - 1) * gap) / count),
91
+ };
92
+ }
93
+
94
+ /** Something a break may come before and not inside: a line, or a box that
95
+ * is not broken. */
96
+ interface Unit {
97
+ top: number;
98
+ bottom: number;
99
+ /** The block it is a line of. */
100
+ block: Box | null;
101
+ /** Whether it is a box with nothing in it, which a break may fall
102
+ * anywhere in: there is no line or box there for it to fall inside. */
103
+ sliced: boolean;
104
+ /** Its own height, before the edges of the boxes it starts and ends were
105
+ * added to it, and where it ends without those it ends. */
106
+ size: number;
107
+ end: number;
108
+ group: Group;
109
+ }
110
+
111
+ /** The units that overlap down the strip, which go together: one alone, or
112
+ * a float and the lines beside it. */
113
+ interface Group {
114
+ top: number;
115
+ bottom: number;
116
+ /** The block it is a line of, where it is one line and nothing else. */
117
+ block: Box | null;
118
+ /** Whether it is one box a break may fall anywhere in. */
119
+ sliced: boolean;
120
+ /** Whether nothing in it has a height: the edges of boxes, at most. */
121
+ empty: boolean;
122
+ /** Where it ends, without the bottom edges of the boxes it ends. */
123
+ end: number;
124
+ /** The column it starts in, and the one it ends in: the same, unless it
125
+ * is sliced. */
126
+ column: number;
127
+ last: number;
128
+ }
129
+
130
+ interface Gathered {
131
+ units: Unit[];
132
+ lines: Map<LineBox, Unit>;
133
+ /** The boxes that go whole. */
134
+ boxes: Map<Box, Unit>;
135
+ /** The boxes a break may fall inside: the first and the last unit in
136
+ * each. */
137
+ spans: Map<Box, [Unit, Unit]>;
138
+ /** The boxes with a float in them that is set in a column on its own
139
+ * account: they do not move as one with what they hold. */
140
+ loose: Set<Box>;
141
+ }
142
+
143
+ const NO_GROUP: Group = {
144
+ top: 0,
145
+ bottom: 0,
146
+ block: null,
147
+ sliced: false,
148
+ empty: true,
149
+ end: 0,
150
+ column: 0,
151
+ last: 0,
152
+ };
153
+
154
+ /** Whether no break may fall inside a box, whatever it holds: it is
155
+ * monolithic, or says so. It goes to a column with all that is in it. */
156
+ function whole(box: Box): boolean {
157
+ if (box.kind !== 'block') return true;
158
+ if (box.style.breakInside === 'avoid') return true;
159
+ return establishesBFC(box) || transformed(box.style);
160
+ }
161
+
162
+ /**
163
+ * Whether a box that holds nothing to break between may be cut anywhere: a
164
+ * block with a height and nothing in it, floated or not, which a browser
165
+ * carries on from one column to the next (CSS Fragmentation 3, 4.1 makes
166
+ * only what cannot be cut monolithic: replaced content, and a box that
167
+ * scrolls or clips).
168
+ */
169
+ function sliceable(box: Box): boolean {
170
+ if (box.kind !== 'block' || box.marker) return false;
171
+ const style = box.style;
172
+ if (style.breakInside === 'avoid' || scrolls(style)) return false;
173
+ if (transformed(style) || style.columns) return false;
174
+ // size containment makes a box monolithic (CSS Containment 2, 3.1)
175
+ if (contained(box, CONTAIN_SIZE)) return false;
176
+ if (box.lines?.length) return false;
177
+ for (const child of box.children) {
178
+ if (flows(child)) return false;
179
+ }
180
+ return true;
181
+ }
182
+
183
+ /** Whether a block's child is a box in its flow. */
184
+ function flows(child: Box): boolean {
185
+ switch (child.kind) {
186
+ case 'text':
187
+ case 'break':
188
+ case 'inline':
189
+ case 'marker':
190
+ return false;
191
+ default:
192
+ return !child.outOfFlow && !child.isFloat;
193
+ }
194
+ }
195
+
196
+ /** The floats and the out-of-flow boxes in a block's inline content, at
197
+ * any depth of inline box. */
198
+ function eachAside(box: Box, visit: (aside: Box) => void): void {
199
+ for (const child of box.children) {
200
+ if (child.isFloat || child.outOfFlow) visit(child);
201
+ else if (child.kind === 'inline') eachAside(child, visit);
202
+ }
203
+ }
204
+
205
+ /** How far down a box that goes whole reaches: its own bottom, and the
206
+ * floats that hang out of it where it does not hold them. */
207
+ function reachOf(box: Box): number {
208
+ let bottom = box.y + box.height;
209
+ if (box.kind !== 'block' || establishesBFC(box)) return bottom;
210
+ const walk = (node: Box): void => {
211
+ for (const child of node.children) {
212
+ if (child.outOfFlow) continue;
213
+ if (child.isFloat) {
214
+ bottom = Math.max(
215
+ bottom,
216
+ child.y + child.height + Math.max(0, child.marginBottom),
217
+ );
218
+ } else if (child.kind === 'inline' || child.kind === 'block') walk(child);
219
+ }
220
+ };
221
+ walk(box);
222
+ return bottom;
223
+ }
224
+
225
+ /**
226
+ * A float that goes whole, as a unit with the lines and the boxes beside
227
+ * it, which it is set in a column with. One that can be cut anywhere is
228
+ * none: it stands beside the flow, which breaks as it would without it,
229
+ * and is cut where the columns end (`cutFloat`).
230
+ */
231
+ function floatUnit(float: Box, from: Box, into: Gathered): void {
232
+ if (columned.any) COLUMN_PIECES.delete(float);
233
+ into.loose.add(from);
234
+ if (sliceable(float)) return;
235
+ const unit: Unit = {
236
+ top: float.y - Math.max(0, float.marginTop),
237
+ bottom: float.y + float.height + Math.max(0, float.marginBottom),
238
+ block: null,
239
+ sliced: false,
240
+ size: 0,
241
+ end: 0,
242
+ group: NO_GROUP,
243
+ };
244
+ into.units.push(made(unit));
245
+ into.boxes.set(float, unit);
246
+ }
247
+
248
+ /**
249
+ * Room in the flow that nothing stands in and no margin makes: what
250
+ * clearance moved a box or a line down by. A break drops a margin beside
251
+ * it (CSS Fragmentation 3, 5.2) and not this, which the columns take as
252
+ * they would a box with nothing in it, cut wherever one ends.
253
+ */
254
+ function space(
255
+ top: number,
256
+ bottom: number,
257
+ /** The block whose lines it is between. */
258
+ block: Box | null,
259
+ into: Gathered,
260
+ ): Unit {
261
+ const unit: Unit = {
262
+ top,
263
+ bottom,
264
+ block,
265
+ sliced: true,
266
+ size: 0,
267
+ end: 0,
268
+ group: NO_GROUP,
269
+ };
270
+ into.units.push(made(unit));
271
+ return unit;
272
+ }
273
+
274
+ /**
275
+ * The units of a box's content, in the strip, and which box or line each
276
+ * is: a line for each line, a box for each box that goes whole, and the
277
+ * content of any other block in its turn — whose own top edge goes with its
278
+ * first and whose bottom with its last, since no break comes between a box's
279
+ * edge and what it holds. Answers the first and the last in the flow.
280
+ */
281
+ function gather(box: Box, into: Gathered): [Unit, Unit] | null {
282
+ let first: Unit | null = null;
283
+ let last: Unit | null = null;
284
+ const lines = box.lines;
285
+ if (lines?.length) {
286
+ let under = box.contentY;
287
+ for (const line of lines) {
288
+ // room a line was set below — a `<br clear>`, a line that did not
289
+ // fit beside a float — is no margin for a break to drop
290
+ if (line.y - under > EPS) {
291
+ const room = space(under, line.y, box, into);
292
+ first ??= room;
293
+ }
294
+ under = line.y + line.height;
295
+ const unit: Unit = {
296
+ top: line.y,
297
+ bottom: line.y + line.height,
298
+ block: box,
299
+ sliced: false,
300
+ size: 0,
301
+ end: 0,
302
+ group: NO_GROUP,
303
+ };
304
+ into.units.push(made(unit));
305
+ into.lines.set(line, unit);
306
+ first ??= unit;
307
+ last = unit;
308
+ }
309
+ eachAside(box, (aside) => {
310
+ if (aside.isFloat) floatUnit(aside, box, into);
311
+ });
312
+ return first && last ? [first, last] : null;
313
+ }
314
+ for (const child of box.children) {
315
+ if (child.isFloat && !child.outOfFlow) {
316
+ floatUnit(child, box, into);
317
+ continue;
318
+ }
319
+ if (!flows(child)) continue;
320
+ if (columned.any) COLUMN_PIECES.delete(child);
321
+ // the room clearance set it down by, which is none of its margin
322
+ const clear = CLEARED_FROM.get(child);
323
+ if (clear !== undefined && child.y - clear > EPS) {
324
+ const room = space(clear, child.y, null, into);
325
+ first ??= room;
326
+ }
327
+ if (whole(child)) {
328
+ const unit: Unit = {
329
+ top: child.y,
330
+ bottom: reachOf(child),
331
+ block: null,
332
+ sliced: sliceable(child),
333
+ size: 0,
334
+ end: 0,
335
+ group: NO_GROUP,
336
+ };
337
+ into.units.push(made(unit));
338
+ into.boxes.set(child, unit);
339
+ first ??= unit;
340
+ last = unit;
341
+ continue;
342
+ }
343
+ const span = gather(child, into);
344
+ if (into.loose.has(child)) into.loose.add(box);
345
+ if (!span) {
346
+ // nothing in its flow to break between: it goes whole, or is cut
347
+ const unit: Unit = {
348
+ top: child.y,
349
+ bottom: child.y + child.height,
350
+ block: null,
351
+ sliced: sliceable(child),
352
+ size: 0,
353
+ end: 0,
354
+ group: NO_GROUP,
355
+ };
356
+ into.units.push(made(unit));
357
+ into.boxes.set(child, unit);
358
+ first ??= unit;
359
+ last = unit;
360
+ continue;
361
+ }
362
+ span[0].top = Math.min(span[0].top, child.y);
363
+ span[1].bottom = Math.max(span[1].bottom, child.y + child.height);
364
+ into.spans.set(child, span);
365
+ first ??= span[0];
366
+ last = span[1];
367
+ }
368
+ return first && last ? [first, last] : null;
369
+ }
370
+
371
+ /** The units in groups, down the strip: each group what a break may come
372
+ * before. Units that overlap are one group, which no break divides. */
373
+ /** Note a unit's own extent, as it is made. */
374
+ function made(unit: Unit): Unit {
375
+ unit.size = unit.bottom - unit.top;
376
+ unit.end = unit.bottom;
377
+ return unit;
378
+ }
379
+
380
+ function groupsOf(units: Unit[]): Group[] {
381
+ // in the order of their tops, which is the document's but for a float
382
+ const sorted = units.slice().sort((a, b) => a.top - b.top);
383
+ const groups: Group[] = [];
384
+ let group: Group | null = null;
385
+ for (const unit of sorted) {
386
+ if (group && unit.top < group.bottom - EPS) {
387
+ group.bottom = Math.max(group.bottom, unit.bottom);
388
+ group.block = null;
389
+ group.sliced = false;
390
+ group.empty &&= unit.size <= EPS;
391
+ group.end = Math.max(group.end, unit.end);
392
+ } else {
393
+ group = {
394
+ top: unit.top,
395
+ bottom: unit.bottom,
396
+ block: unit.block,
397
+ sliced: unit.sliced,
398
+ empty: unit.size <= EPS,
399
+ end: unit.end,
400
+ column: 0,
401
+ last: 0,
402
+ };
403
+ groups.push(group);
404
+ }
405
+ unit.group = group;
406
+ }
407
+ return groups;
408
+ }
409
+
410
+ interface Filled {
411
+ /** The group each column starts with, where the column starts down the
412
+ * strip, and whether it starts inside that group, which the column
413
+ * before cut. */
414
+ starts: number[];
415
+ tops: number[];
416
+ cut: boolean[];
417
+ /** The least any break missed fitting what came after it by. */
418
+ shortage: number;
419
+ /** Whether something stands in a column it is taller than. */
420
+ over: boolean;
421
+ }
422
+
423
+ /** The most columns content is set in: past it, the rest stays in the
424
+ * last. A column a fraction of a pixel tall would make one for every such
425
+ * fraction of a box that can be cut anywhere. */
426
+ const MOST_COLUMNS = 2000;
427
+
428
+ /**
429
+ * The groups set in columns of a height: each column filled until the next
430
+ * group does not fit, and broken there — or before there, where that would
431
+ * leave fewer lines of a block than `orphans` at the foot of the column, or
432
+ * fewer than `widows` at the head of the next: `orphans` first, and then as
433
+ * many of `widows` as that leaves, which is how Blink weighs the two (a
434
+ * block of four lines with `widows: 3` breaks two and two; CSS says only
435
+ * that both are to be honoured, 4.3). What stands
436
+ * first in a column stays there however tall, and the first `orphans` lines
437
+ * of a block with it. A box that can be cut anywhere is cut at the foot of
438
+ * the column, and goes on at the head of the next.
439
+ */
440
+ function fill(
441
+ groups: Group[],
442
+ top: number,
443
+ height: number,
444
+ /** Whether the columns can be no taller: the container's own height, or
445
+ * its `max-height`, is what they are. */
446
+ fixed: boolean,
447
+ ): Filled {
448
+ const starts = [0];
449
+ const tops = [top];
450
+ const cut = [false];
451
+ let shortage = Infinity;
452
+ let over = false;
453
+ let head = 0;
454
+ let columnTop = top;
455
+ /** Whether the group at the column's head is one the column before
456
+ * cut: it goes on from the column's top. */
457
+ let goesOn = false;
458
+ let i = 0;
459
+ const missed = (index: number): void => {
460
+ const by = groups[index].bottom - columnTop - height;
461
+ if (by > EPS && by < shortage) shortage = by;
462
+ };
463
+ /** Whether nothing with a height has been set in the column before a
464
+ * group: the top edges of the boxes it starts, at most — or, `blank`,
465
+ * not even those. */
466
+ const bare = (index: number, blank: boolean): boolean => {
467
+ for (let k = head; k < index; k += 1) {
468
+ const before = groups[k];
469
+ if (blank ? before.bottom - before.top > EPS : !before.empty) {
470
+ return false;
471
+ }
472
+ }
473
+ return !goesOn;
474
+ };
475
+ const next = (at: number, from: number, inside: boolean): void => {
476
+ starts.push(at);
477
+ tops.push(from);
478
+ cut.push(inside);
479
+ head = at;
480
+ columnTop = from;
481
+ goesOn = inside;
482
+ i = at;
483
+ };
484
+ while (i < groups.length) {
485
+ const group = groups[i];
486
+ if (
487
+ group.bottom - columnTop <= height + EPS ||
488
+ // nothing at all, which takes no room wherever it is
489
+ group.bottom - group.top <= EPS ||
490
+ starts.length >= MOST_COLUMNS
491
+ ) {
492
+ i += 1;
493
+ goesOn = false;
494
+ continue;
495
+ }
496
+ missed(i);
497
+ if (group.sliced && height >= 1) {
498
+ // what fits of it stays, and the rest goes on; none of it fits where
499
+ // it starts at the column's foot, and then all of it goes on
500
+ const from = goesOn ? columnTop : group.top;
501
+ const foot = columnTop + height;
502
+ if (foot - from > EPS) next(i, foot, true);
503
+ else next(i, group.top, false);
504
+ continue;
505
+ }
506
+ if (
507
+ i === head ||
508
+ bare(i, true) ||
509
+ (group.end - columnTop <= height + EPS && bare(i, false))
510
+ ) {
511
+ // first in its column, or after nothing that takes any room, where
512
+ // it stays however tall; or all of it fits but the bottom edge of a
513
+ // box it ends, and nothing with a height has been set in the column
514
+ // to break before it: the edge hangs out (where Blink breaks before
515
+ // the edge, as a last resort)
516
+ over = true;
517
+ i += 1;
518
+ goesOn = false;
519
+ continue;
520
+ }
521
+ let at = i;
522
+ const block = group.block;
523
+ if (block) {
524
+ // the lines of its block this one is among, and which of them it is
525
+ const run = linesAbout(groups, i);
526
+ const k = run.indexOf(i);
527
+ const count = run.length;
528
+ const orphans = Math.min(block.style.orphans, count);
529
+ const widows = Math.min(block.style.widows, count);
530
+ const a = run[0];
531
+ if (a >= head && k > 0 && k < orphans) {
532
+ if (a === head && !fixed) {
533
+ // at the head of the column already: they stay, and hang out,
534
+ // for a taller column to take them in
535
+ missed(run[orphans - 1]);
536
+ over = true;
537
+ i = run[orphans - 1] + 1;
538
+ goesOn = false;
539
+ continue;
540
+ }
541
+ // too few would stay: the block goes to the next column whole —
542
+ // or, at the head of a column that can be no taller, breaks where
543
+ // it must, with fewer
544
+ missed(run[orphans - 1]);
545
+ if (a !== head) at = a;
546
+ } else if (k > 0 && count - k < widows) {
547
+ // Too few would go: more go with them, as many as leave `orphans`
548
+ // behind, which may be fewer than `widows` asks for. Of the lines
549
+ // set from this column on, where the block goes on from a column
550
+ // before — and there, where not even all of them are enough, it
551
+ // breaks where it would (as Blink breaks each: seven lines, three
552
+ // to a column, under `widows: 5` are two, three and two).
553
+ const lo = a >= head ? 0 : run.findIndex((line) => line >= head);
554
+ const keep = count - lo - Math.min(block.style.widows, count - lo);
555
+ if (lo === 0 || keep > 0) {
556
+ const stay = Math.max(orphans, keep);
557
+ if (stay < k - lo) at = run[lo + stay];
558
+ }
559
+ }
560
+ }
561
+ next(at, groups[at].top, false);
562
+ }
563
+ return { starts, tops, cut, shortage, over };
564
+ }
565
+
566
+ /** The lines of a block that a line is among, in order: the groups about
567
+ * it that are lines of its block, past the room between them. */
568
+ function linesAbout(groups: Group[], i: number): number[] {
569
+ const block = groups[i].block;
570
+ const run = [i];
571
+ for (let k = i - 1; k >= 0 && groups[k].block === block; k -= 1) {
572
+ if (!groups[k].sliced) run.unshift(k);
573
+ }
574
+ for (let k = i + 1; k < groups.length && groups[k].block === block; k += 1) {
575
+ if (!groups[k].sliced) run.push(k);
576
+ }
577
+ return run;
578
+ }
579
+
580
+ /**
581
+ * The tallest thing in the strip no break may fall inside, which a column
582
+ * is no shorter than: a box that goes whole, a line, and the lines of a
583
+ * block that `orphans` keeps together at its start and `widows` at its end
584
+ * (as Blink's initial balancing pass takes them,
585
+ * `PropagateTallestUnbreakableBlockSize`).
586
+ */
587
+ function tallestOf(groups: Group[]): number {
588
+ let tallest = 0;
589
+ for (let i = 0; i < groups.length; i += 1) {
590
+ const group = groups[i];
591
+ const block = group.block;
592
+ if (group.sliced) continue;
593
+ if (!block) {
594
+ tallest = Math.max(tallest, group.bottom - group.top);
595
+ continue;
596
+ }
597
+ const run = linesAbout(groups, i);
598
+ const count = run.length;
599
+ const orphans = Math.min(block.style.orphans, count);
600
+ const widows = Math.min(block.style.widows, count);
601
+ tallest = Math.max(
602
+ tallest,
603
+ groups[run[orphans - 1]].bottom - group.top,
604
+ groups[run[count - 1]].bottom - groups[run[count - widows]].top,
605
+ );
606
+ i = run[count - 1];
607
+ }
608
+ return tallest;
609
+ }
610
+
611
+ /**
612
+ * Set a multicol container's content, laid out as one strip at its content
613
+ * box's top, in its columns, and answer their height — the container's
614
+ * content height, where it has none of its own.
615
+ *
616
+ * `strip` is the strip's height, its first and last margins in it, and
617
+ * `limit` the tallest a column may be: the container's own height, or its
618
+ * `max-height`. Past it the content goes on in columns beyond the
619
+ * container's edge (8.2). Under `column-fill: auto` the columns are that
620
+ * tall, each filled before the next is started, and where nothing limits
621
+ * them the first holds everything (7.1).
622
+ */
623
+ export function layoutColumns(
624
+ box: Box,
625
+ columns: Columns,
626
+ strip: number,
627
+ limit: number,
628
+ /** Where the strip starts: the content box's top, or under a box that
629
+ * spans the columns (`spansColumns`). */
630
+ top = box.contentY,
631
+ ): number {
632
+ const gathered: Gathered = {
633
+ units: [],
634
+ lines: new Map(),
635
+ boxes: new Map(),
636
+ spans: new Map(),
637
+ loose: new Set(),
638
+ };
639
+ gather(box, gathered);
640
+ const groups = groupsOf(gathered.units);
641
+ // as far down as the content goes, past a box with a height it overflows:
642
+ // what stands out of a box takes room in a column as what is in it does
643
+ for (const group of groups) strip = Math.max(strip, group.bottom - top);
644
+ let height: number;
645
+ let filled: Filled;
646
+ if (box.style.columnFill === 'auto') {
647
+ height = Number.isFinite(limit) ? limit : strip;
648
+ filled = fill(groups, top, height, height >= limit);
649
+ // a container with a `max-height` and less than a column of content
650
+ // is as tall as its content
651
+ if (filled.starts.length === 1) height = Math.min(height, strip);
652
+ } else {
653
+ // balanced: the strip shared out, and no shorter than the tallest
654
+ // thing that cannot break; then as much taller as lets the next thing
655
+ // in, until the columns there are hold it all
656
+ height = Math.min(
657
+ limit,
658
+ Math.max(strip / columns.count, tallestOf(groups)),
659
+ );
660
+ filled = fill(groups, top, height, height >= limit);
661
+ for (let pass = 0; pass < 4096; pass += 1) {
662
+ if (filled.starts.length <= columns.count && !filled.over) break;
663
+ if (height >= limit || !Number.isFinite(filled.shortage)) break;
664
+ height = Math.min(limit, height + filled.shortage);
665
+ filled = fill(groups, top, height, height >= limit);
666
+ }
667
+ }
668
+ // all in one column where it started, and nothing to move — but a float
669
+ // that is cut where the column ends, which may reach past it
670
+ if (!gathered.loose.size && (!groups.length || filled.starts.length === 1)) {
671
+ return height;
672
+ }
673
+
674
+ // the column each group starts in, and the one it ends in
675
+ const { starts, tops, cut } = filled;
676
+ let c = 0;
677
+ for (let i = 0; i < groups.length; i += 1) {
678
+ while (
679
+ c + 1 < starts.length &&
680
+ starts[c + 1] <= i &&
681
+ !(starts[c + 1] === i && cut[c + 1])
682
+ ) {
683
+ c += 1;
684
+ }
685
+ groups[i].column = c;
686
+ while (c + 1 < starts.length && starts[c + 1] === i && cut[c + 1]) c += 1;
687
+ groups[i].last = c;
688
+ }
689
+ const step = columns.width + columns.gap;
690
+ // the first column at the start of the line: the right, right to left
691
+ const rtl = box.style.direction === 'rtl';
692
+ const across = (c: number): number => (rtl ? -c * step : c * step);
693
+ const origin = rtl ? box.contentWidth - columns.width : 0;
694
+ place(box, null, {
695
+ gathered,
696
+ tops,
697
+ height,
698
+ dx: (c) => origin + across(c),
699
+ dy: (c) => top - tops[c],
700
+ });
701
+ return height;
702
+ }
703
+
704
+ interface Placing {
705
+ gathered: Gathered;
706
+ /** Where each column starts, down the strip. */
707
+ tops: number[];
708
+ height: number;
709
+ dx(column: number): number;
710
+ dy(column: number): number;
711
+ }
712
+
713
+ function shift(box: Box, column: number, to: Placing): void {
714
+ moveTo(box, box.x + to.dx(column), box.y + to.dy(column));
715
+ }
716
+
717
+ function shiftLine(line: LineBox, dx: number, dy: number): void {
718
+ line.x += dx;
719
+ line.y += dy;
720
+ for (const text of line.texts) {
721
+ text.drawX += dx;
722
+ text.drawY += dy;
723
+ }
724
+ for (const placed of line.atomics) {
725
+ placed.x += dx;
726
+ placed.y += dy;
727
+ moveTo(placed.box, placed.box.x + dx, placed.box.y + dy);
728
+ }
729
+ for (const edge of line.edges ?? []) edge.x += dx;
730
+ }
731
+
732
+ /** The column a point down the strip is in, between boxes: at a break it
733
+ * is in the column the break ends, which is where Blink leaves what is out
734
+ * of the flow and stood before the box the next column starts with. */
735
+ function columnAt(y: number, tops: number[]): number {
736
+ let c = 0;
737
+ while (c + 1 < tops.length && tops[c + 1] < y - EPS) c += 1;
738
+ return c;
739
+ }
740
+
741
+ /**
742
+ * Move what a box holds to its columns: the container's content, with no
743
+ * `span`, or that of a box a break falls inside, which is then its pieces.
744
+ */
745
+ function place(box: Box, span: [Unit, Unit] | null, to: Placing): void {
746
+ const { gathered } = to;
747
+ const first = span ? span[0].group.column : 0;
748
+ const last = span ? span[1].group.last : to.tops.length - 1;
749
+ if (span && first === last && !gathered.loose.has(box)) {
750
+ shift(box, first, to);
751
+ return;
752
+ }
753
+ const was = { x: box.x, y: box.y, width: box.width, height: box.height };
754
+ const lines = box.lines;
755
+ // what is out of the flow goes from where it would have stood, which is
756
+ // kept from this box's corner: in the column of the line it would have
757
+ // been on, or of the boxes it would have been between
758
+ const asides: [Box, number][] = [];
759
+ const aside = (child: Box): void => {
760
+ const at = child.staticPosition;
761
+ if (!child.outOfFlow || !at || at.from !== box) return;
762
+ const y = was.y + at.y;
763
+ let c = columnAt(y, to.tops);
764
+ if (lines?.length) {
765
+ let on = lines[0];
766
+ for (const line of lines) {
767
+ if (line.y > y + EPS) break;
768
+ on = line;
769
+ }
770
+ c = gathered.lines.get(on)!.group.column;
771
+ }
772
+ asides.push([child, Math.min(last, Math.max(first, c))]);
773
+ };
774
+ if (lines?.length) eachAside(box, aside);
775
+ else for (const child of box.children) aside(child);
776
+ if (lines?.length) {
777
+ for (const line of lines) {
778
+ const c = gathered.lines.get(line)!.group.column;
779
+ shiftLine(line, to.dx(c), to.dy(c));
780
+ }
781
+ if (first !== last) {
782
+ COLUMN_LINES.add(lines);
783
+ rowsOf(lines, gathered);
784
+ columned.any = true;
785
+ }
786
+ eachAside(box, (aside) => {
787
+ const unit = gathered.boxes.get(aside);
788
+ if (unit) place(aside, [unit, unit], to);
789
+ else if (aside.isFloat && !aside.outOfFlow) cutFloat(aside, to);
790
+ });
791
+ } else {
792
+ for (const child of box.children) {
793
+ // a box that goes whole, or is cut where it holds nothing
794
+ const unit = gathered.boxes.get(child);
795
+ const inner = unit ? ([unit, unit] as [Unit, Unit]) : undefined;
796
+ const range = inner ?? gathered.spans.get(child);
797
+ if (range) place(child, range, to);
798
+ else if (child.isFloat && !child.outOfFlow) cutFloat(child, to);
799
+ }
800
+ }
801
+ if (span && first === last) {
802
+ // in one column, with a float in it that went its own way: the box
803
+ // itself, which what it holds was moved without
804
+ box.x += to.dx(first);
805
+ box.y += to.dy(first);
806
+ if (box.marker) {
807
+ box.marker.x += to.dx(first);
808
+ box.marker.y += to.dy(first);
809
+ }
810
+ } else if (span) {
811
+ const pieces: ColumnPiece[] = [];
812
+ let x1 = Infinity;
813
+ let y1 = Infinity;
814
+ let x2 = -Infinity;
815
+ let y2 = -Infinity;
816
+ for (let c = first; c <= last; c += 1) {
817
+ const from = c === first ? was.y : to.tops[c];
818
+ const until = c === last ? was.y + was.height : to.tops[c] + to.height;
819
+ const piece: ColumnPiece = {
820
+ x: was.x + to.dx(c),
821
+ y: from + to.dy(c),
822
+ width: was.width,
823
+ height: Math.max(0, until - from),
824
+ wholeY: was.y + to.dy(c),
825
+ wholeHeight: was.height,
826
+ };
827
+ pieces.push(piece);
828
+ x1 = Math.min(x1, piece.x);
829
+ y1 = Math.min(y1, piece.y);
830
+ x2 = Math.max(x2, piece.x + piece.width);
831
+ y2 = Math.max(y2, piece.y + piece.height);
832
+ }
833
+ COLUMN_PIECES.set(box, pieces);
834
+ columned.any = true;
835
+ if (box.marker) {
836
+ box.marker.x += to.dx(first);
837
+ box.marker.y += to.dy(first);
838
+ }
839
+ box.x = x1;
840
+ box.y = y1;
841
+ box.width = x2 - x1;
842
+ box.height = y2 - y1;
843
+ }
844
+ for (const [child, c] of asides) {
845
+ const at = child.staticPosition!;
846
+ const width = at.right - at.x;
847
+ at.x = was.x + at.x + to.dx(c) - box.x;
848
+ at.right = at.x + width;
849
+ at.y = was.y + at.y + to.dy(c) - box.y;
850
+ }
851
+ }
852
+
853
+ /**
854
+ * A float that can be cut anywhere, set in the columns it reaches: from
855
+ * where it starts to the foot of that column, and then down each column
856
+ * after it from the top, as much of it as is left. By how much of it each
857
+ * column has taken, and not by where the strip has it: the flow beside it
858
+ * drops a margin at a break, and the float has none there to drop.
859
+ */
860
+ function cutFloat(float: Box, to: Placing): void {
861
+ const { tops, height } = to;
862
+ let c = 0;
863
+ while (c + 1 < tops.length && tops[c + 1] <= float.y + EPS) c += 1;
864
+ // past the foot of its column, in the margin a break dropped: at the
865
+ // head of the next
866
+ let at = float.y - tops[c];
867
+ if (at >= height - EPS && c + 1 < tops.length) {
868
+ c += 1;
869
+ at = 0;
870
+ }
871
+ const was = {
872
+ x: float.x,
873
+ y: float.y,
874
+ width: float.width,
875
+ height: float.height,
876
+ };
877
+ if (at + was.height <= height + EPS || !(height >= 1)) {
878
+ moveTo(float, was.x + to.dx(c), tops[0] + at);
879
+ return;
880
+ }
881
+ const pieces: ColumnPiece[] = [];
882
+ let taken = 0;
883
+ let x1 = Infinity;
884
+ let x2 = -Infinity;
885
+ let y2 = -Infinity;
886
+ const head = tops[0];
887
+ for (let k = c; taken < was.height - EPS; k += 1) {
888
+ const from = k === c ? at : 0;
889
+ const part = Math.min(was.height - taken, height - from);
890
+ const x = was.x + to.dx(k);
891
+ pieces.push({
892
+ x,
893
+ y: head + from,
894
+ width: was.width,
895
+ height: part,
896
+ wholeY: head + from - taken,
897
+ wholeHeight: was.height,
898
+ });
899
+ x1 = Math.min(x1, x);
900
+ x2 = Math.max(x2, x + was.width);
901
+ y2 = Math.max(y2, head + from + part);
902
+ taken += part;
903
+ if (pieces.length >= MOST_COLUMNS) break;
904
+ }
905
+ COLUMN_PIECES.set(float, pieces);
906
+ columned.any = true;
907
+ float.x = x1;
908
+ float.y = Math.min(head + at, head);
909
+ float.width = x2 - x1;
910
+ float.height = y2 - float.y;
911
+ }
912
+
913
+ /**
914
+ * The rows each text's layout has in each column, for the layouts whose
915
+ * lines columns took apart (`COLUMN_ROWS`).
916
+ */
917
+ function rowsOf(lines: LineBox[], gathered: Gathered): void {
918
+ const byLayout = new Map<LineText['layout'], Map<number, LineText[]>>();
919
+ for (const line of lines) {
920
+ const c = gathered.lines.get(line)!.group.column;
921
+ for (const text of line.texts) {
922
+ let columns = byLayout.get(text.layout);
923
+ if (!columns) byLayout.set(text.layout, (columns = new Map()));
924
+ const texts = columns.get(c);
925
+ if (texts) texts.push(text);
926
+ else columns.set(c, [text]);
927
+ }
928
+ }
929
+ for (const [layout, columns] of byLayout) {
930
+ if (columns.size < 2) continue;
931
+ const end = layout.lines.length - 1;
932
+ for (const texts of columns.values()) {
933
+ let top = Infinity;
934
+ let bottom = -Infinity;
935
+ let open = 0;
936
+ for (const text of texts) {
937
+ const natural = layout.lines[text.layoutLine];
938
+ if (!natural) continue;
939
+ top = Math.min(top, natural.y);
940
+ bottom = Math.max(bottom, natural.y + natural.height);
941
+ if (text.layoutLine === 0) open |= 1;
942
+ if (text.layoutLine === end) open |= 2;
943
+ }
944
+ if (top === Infinity) continue;
945
+ const rows = {
946
+ top: open & 1 ? top - REACH : top,
947
+ bottom: open & 2 ? bottom + REACH : bottom,
948
+ };
949
+ for (const text of texts) COLUMN_ROWS.set(text, rows);
950
+ }
951
+ }
952
+ }
953
+
954
+ /**
955
+ * Whether a box is set across all the columns of the multicol container it
956
+ * is a child of (`column-span: all`, CSS Multi-column 1, 6): a block-level
957
+ * box in its flow. One further in, in a box of the container's, is set in its column
958
+ * as it would be without: the box around it is not broken about it here.
959
+ */
960
+ export function spansColumns(box: Box): boolean {
961
+ return (
962
+ box.style.columnSpan &&
963
+ flows(box) &&
964
+ // block-level: on an inline-block it does nothing
965
+ !box.style.display.startsWith('inline')
966
+ );
967
+ }
968
+
969
+ /** Forget the pieces an earlier layout broke a box's content in, where
970
+ * this one sets it in one column. */
971
+ export function forgetColumns(box: Box): void {
972
+ if (!columned.any) return;
973
+ for (const child of box.children) {
974
+ if (!flows(child) || child.kind !== 'block') continue;
975
+ if (COLUMN_PIECES.delete(child) || !whole(child)) forgetColumns(child);
976
+ }
977
+ }