@quario/layout 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/layout.js ADDED
@@ -0,0 +1,1913 @@
1
+ /**
2
+ * Laying the event stream out on pages: items flow as wrapped blocks, tables
3
+ * are measured whole and emitted row by row with the header replayed after
4
+ * every break, and group headers wait for the content they introduce and then
5
+ * travel with it — a page a group instance continues onto opens with the same
6
+ * headers the page that introduced it did.
7
+ *
8
+ * Page state is the drawing surface's (`canvas.js`), so most of what follows
9
+ * takes a `Canvas` and nothing else. Pagination is the exception, and honestly
10
+ * so: `turn`, `item` and the table's `Grid` take the flow, because a page turn
11
+ * is now the moment the open groups get their headers back. What stays here is
12
+ * the band flow's own bookkeeping — the headers still waiting, the ones a turn
13
+ * replays, the space owed before the next group instance, and the outline marks
14
+ * it anchors as it goes. `flow` at the foot of the file is what owns that
15
+ * record: it hands out every handler a render walks the stream through, and a
16
+ * `finish` that flushes what is left.
17
+ * Nothing else can reach them, and no caller adds a handler of its own, so the
18
+ * order those handlers impose is not an order a caller can get wrong.
19
+ *
20
+ * Page furniture is here too, both halves of it: how much of the page a
21
+ * document's bands claim before anything is placed, and where they are drawn
22
+ * on each finished page once the count is known.
23
+ */
24
+ import { display, isReportBand, text } from "quario";
25
+ import { balance } from "./balance.js";
26
+ import { adopt, frame, measuring } from "./canvas.js";
27
+ import { familyName } from "./fonts.js";
28
+ import { intrinsic } from "./image.js";
29
+ import { CELL_PAD, NO_PAD, WHOLE, insetOf, paintBox, sliceInset, unbox } from "./box.js";
30
+ import { BAND, GUTTER, LEAD, col, merge, roled, shift, sizeOf } from "./style.js";
31
+ import { atoms, dress, heightOf, wrap } from "./text.js";
32
+
33
+ /** @typedef {import('./balance.js').Unit} Unit */
34
+ /** @typedef {import('./canvas.js').Canvas} Canvas */
35
+ /** @typedef {import('./canvas.js').Frame} Frame */
36
+ /** @typedef {import('./text.js').Line} Line */
37
+
38
+ // `open` is a stack, one entry per group instance still open: the header items
39
+ // it holds back until content arrives, and — once those are drawn — the blocks
40
+ // a page turn replays them from. `gap` is the space owed before the next group
41
+ // instance, `marks` the outline entries, which `anchor` positions from the
42
+ // cursor, and `starts` the page indexes where a `reset: "page"` sequence
43
+ // begins (the document itself is the sequence that starts at page 0).
44
+ /**
45
+ * @typedef {{ held: any[], blocks: Block[] }} Group
46
+ */
47
+ /**
48
+ * @typedef {{ canvas: Canvas, open: Group[], gap: number, marks: any[],
49
+ * starts: number[], region: Region | null, pending: Pending | null,
50
+ * measured: WeakMap<any, Measured>,
51
+ * pin: number | null, skipGap: boolean, inHeader: boolean,
52
+ * route: (event: any, block?: Block | null) => void }} Flow
53
+ */
54
+ /**
55
+ * What flow spacing reads of the page: the cursor and whether a strip is open.
56
+ * Page furniture has a canvas and no region, so the skip helpers take this
57
+ * rather than the whole flow.
58
+ * @typedef {{ canvas: Canvas, region: Region | null }} Cursor
59
+ */
60
+
61
+ // One flow unit and its presentation; `h` is the full flow height. A text
62
+ // item's is its wrapped lines; an image item's is the picture, sized here so
63
+ // everything downstream -- fitting, holding a group header back, replaying it
64
+ // on a continued page -- reads one height and never a second kind of unit.
65
+ // One block does not always measure its own: nothing outside `splitBlock` sees
66
+ // a slot's part before that height is handed down, and a part never flows -- it
67
+ // is drawn where `drawSplit` puts it and nowhere else.
68
+ /**
69
+ * @typedef {{ bytes: Uint8Array, format: string, w: number, h: number }} Picture
70
+ */
71
+ /**
72
+ * `align` is absent on a split, which has none of its own: alignment is a
73
+ * slot's, reached through the style layering.
74
+ * `path` names the schema node the block draws, when the event carried one:
75
+ * the box the canvas records for the hit-test.
76
+ * @typedef {{ lines: Line[], picture?: Picture, parts?: SlotPart[],
77
+ * bg: import('./style.js').Color | null, align?: any, h: number, style?: any,
78
+ * path?: string,
79
+ * inset: { t: number, r: number, b: number, l: number } }} Block
80
+ */
81
+ /**
82
+ * One slot of a split, as laid out: the block it drew to and the width it was
83
+ * measured at. There is no offset — a slot's is the sum of the widths before
84
+ * it, which `drawSplit` accumulates as it walks them.
85
+ * @typedef {{ block: Block, w: number }} SlotPart
86
+ */
87
+
88
+ // How `fit` sizes a picture against the width it has: `natural` is the
89
+ // image's own size in points, never wider than the content box; `width`
90
+ // scales to the content box either way. The ratio is kept in both, so a
91
+ // height is never anything but the width's consequence.
92
+ /** @type {(event: any, avail: number) => Picture} */
93
+ let pictureOf = (event, avail) => {
94
+ let { w, h } = intrinsic(event.bytes, event.format);
95
+ let width = event.fit === "width" ? avail : Math.min(w, avail);
96
+ return { bytes: event.bytes, format: event.format, w: width, h: (h * width) / w };
97
+ };
98
+
99
+ // `under` is the enclosing style a slot's own layers over — a split's, when
100
+ // this is one of its slots. Null everywhere else, where nothing encloses.
101
+ /** @type {(canvas: Canvas, event: any, avail: number, under?: any) => Block} */
102
+ let blockOf = (canvas, event, avail, under = null) => {
103
+ let text = merge(unbox(under), unbox(event.style));
104
+ let box = event.style;
105
+ let bg = col((box || {}).background);
106
+ let inset = insetOf(box, NO_PAD);
107
+ let inner = Math.max(avail - inset.l - inset.r, 1);
108
+ if (event.type === "split") return splitBlock(canvas, event, inner, text, bg, box, inset);
109
+ if (event.type === "image") {
110
+ let picture = pictureOf(event, inner);
111
+ return {
112
+ lines: [],
113
+ picture,
114
+ bg,
115
+ align: text.align,
116
+ h: picture.h + inset.t + inset.b,
117
+ style: box,
118
+ path: event.path,
119
+ inset,
120
+ };
121
+ }
122
+ let size = sizeOf(text, canvas.base);
123
+ let lines = dress(wrap(canvas, atoms(canvas, event.tokens, text), inner, size), text);
124
+ return {
125
+ lines,
126
+ bg,
127
+ align: text.align,
128
+ h: heightOf(lines) + inset.t + inset.b,
129
+ style: box,
130
+ path: event.path,
131
+ inset,
132
+ };
133
+ };
134
+
135
+ // Scale a picture block down to the room it has. Best-effort, and only ever
136
+ // reached by an image no page could hold whole (SCHEMA.md).
137
+ /** @type {(block: Block, room: number) => void} */
138
+ let shrink = (block, room) => {
139
+ let picture = /** @type {Picture} */ (block.picture);
140
+ let inner = Math.max(room - block.inset.t - block.inset.b, 1);
141
+ picture.w *= inner / picture.h;
142
+ picture.h = inner;
143
+ block.h = inner + block.inset.t + block.inset.b;
144
+ };
145
+
146
+ // Draw one block whole at the cursor. A picture's background fills the
147
+ // image's own box rather than the width of the flow -- there is no line for
148
+ // it to sit behind. Text is placed line by line as each is drawn; a picture
149
+ // is one box, so where it sits in the width it was given is worked out once.
150
+ /** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
151
+ let drawPicture = (canvas, block, x, avail) => {
152
+ let inset = block.inset;
153
+ let { bytes, format, w, h } = /** @type {Picture} */ (block.picture);
154
+ let outer = w + inset.l + inset.r;
155
+ let at = x + shift(block.align, avail - outer);
156
+ paintBox(canvas, at, canvas.y, outer, block.h, block.style, block.bg);
157
+ canvas.box(block.path, at, canvas.y, outer, block.h);
158
+ canvas.picture(bytes, format, at + inset.l, canvas.y - inset.t - h, w, h);
159
+ canvas.y -= block.h;
160
+ canvas.fresh = false;
161
+ };
162
+
163
+ // Draw one block of lines at the cursor -- a whole item, or one slice of an
164
+ // item a page could not hold. `slice` says which one this is, and nothing
165
+ // else distinguishes the two cases: the slice carries the lines it took and
166
+ // the height they make, and the box, the hit box and the horizontal inset
167
+ // are this one path's for both. It defaults to `WHOLE`, so an item that
168
+ // never broke reads as a first slice with no more to come, and gets its
169
+ // four sides.
170
+ /**
171
+ * @type {(canvas: Canvas, block: Block, x: number, avail: number,
172
+ * slice?: import('./box.js').Slice) => void}
173
+ */
174
+ let drawLines = (canvas, block, x, avail, slice = WHOLE) => {
175
+ let inset = sliceInset(block.inset, slice);
176
+ paintBox(canvas, x, canvas.y, avail, block.h, block.style, block.bg, slice);
177
+ canvas.box(block.path, x, canvas.y, avail, block.h);
178
+ let y = canvas.y - inset.t;
179
+ let inner = Math.max(avail - inset.l - inset.r, 1);
180
+ for (let line of block.lines) {
181
+ canvas.drawLine(line, x + inset.l, y, inner, block.align);
182
+ y -= line.h;
183
+ }
184
+ canvas.y -= block.h;
185
+ canvas.fresh = false;
186
+ };
187
+
188
+ // An authored share fixes its slot; the width-less ones divide what is left,
189
+ // evenly. Evenly rather than by natural width, because a split is a line of
190
+ // furniture whose geometry should read off the document — the same answer the
191
+ // HTML target's `flex:1` gives, so one definition places alike in both.
192
+ // Each slot is measured at its own share and drawn from a common top, so the
193
+ // split is as tall as its tallest slot and the line's geometry does not move
194
+ // with the data. Every slot is then given that height, so its box -- painted
195
+ // and hit alike -- is the split's rather than its own content's: the rule a
196
+ // table cell already follows, a cell box being painted at the row's height.
197
+ // A shorter slot keeping its own height left daylight under every bordered
198
+ // cell in a row whose slots were not sized alike. Only the height is handed
199
+ // down: lines still start at the top of the box, and a picture is neither
200
+ // scaled nor moved, `fit` having no vertical mode.
201
+ // The split's own declarations are the layer under each slot's, so a split
202
+ // styled `bold` reads the same here as it does through CSS inheritance in the
203
+ // HTML target and the `under` layer in XLSX. It has no `align` of its own:
204
+ // alignment is a slot's, reached through that layering.
205
+ /**
206
+ * @type {(canvas: Canvas, event: any, inner: number, text: any,
207
+ * bg: any, box: any, inset: { t: number, r: number, b: number, l: number }) => Block}
208
+ */
209
+ let splitBlock = (canvas, event, inner, text, bg, box, inset) => {
210
+ let widths = slotWidths(event.slots, inner);
211
+ /** @type {SlotPart[]} */
212
+ let parts = [];
213
+ let h = 0;
214
+ for (let [i, slot] of event.items.entries()) {
215
+ let block = blockOf(canvas, slot, Math.max(widths[i], 1), text);
216
+ if (block.h > h) h = block.h;
217
+ parts.push({ block, w: widths[i] });
218
+ }
219
+ for (let part of parts) part.block.h = h;
220
+ return { lines: [], parts, bg, h: h + inset.t + inset.b, style: box, path: event.path, inset };
221
+ };
222
+
223
+ /** @type {(slots: { width?: number }[], avail: number) => number[]} */
224
+ let slotWidths = (slots, avail) => {
225
+ let autos = 0,
226
+ spent = 0;
227
+ /** @type {(number | null)[]} */
228
+ let fixed = slots.map(({ width }) => {
229
+ // Bound to a local first: `Number.isFinite` does not narrow a property.
230
+ if (width === undefined || !Number.isFinite(width)) return (autos++, null);
231
+ let share = (width / 100) * avail;
232
+ spent += share;
233
+ return share;
234
+ });
235
+ let each = autos ? Math.max(avail - spent, 0) / autos : 0;
236
+ return fixed.map((width) => width ?? each);
237
+ };
238
+
239
+ // A split reaches this target as a bracket around ordinary item and image
240
+ // events. Folding it into one event is what lets the rest of the target treat
241
+ // it as a single block -- measured, kept together, and paged by exactly the
242
+ // machinery every other block goes through. Stateful rather than a transform
243
+ // over an array, because the body walk is streaming; the page-band pass drives
244
+ // the same folder over its own list, so the two cannot drift.
245
+ // The event types that become a `Block`. They are also exactly the three the
246
+ // split folder owns: item and image reach `route` through it, so a slot's are
247
+ // collected rather than placed, and `split` is minted by the folder and never
248
+ // arrives from the stream at all.
249
+ let BLOCKS = new Set(["item", "image", "split"]);
250
+
251
+ /** @type {(emit: (event: any) => void) => (event: any) => void} */
252
+ let splitFolder = (emit) => {
253
+ /** @type {{ event: any, items: any[] } | null} */
254
+ let bracket = null;
255
+ // The engine emits the bracket as one array, so `split-end` always has its
256
+ // opening and this never reads a null.
257
+ let close = () => {
258
+ let { event, items } = /** @type {{ event: any, items: any[] }} */ (bracket);
259
+ bracket = null;
260
+ emit({
261
+ type: "split",
262
+ role: event.role,
263
+ style: event.style,
264
+ slots: event.slots,
265
+ path: event.path,
266
+ items,
267
+ });
268
+ };
269
+ return (event) => {
270
+ if (event.type === "split-start") bracket = { event, items: [] };
271
+ else if (event.type === "split-end") close();
272
+ else if (bracket) bracket.items.push(event);
273
+ else emit(event);
274
+ };
275
+ };
276
+
277
+ /** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
278
+ let drawSplit = (canvas, block, x, avail) => {
279
+ let inset = block.inset;
280
+ let yTop = canvas.y;
281
+ paintBox(canvas, x, yTop, avail, block.h, block.style, block.bg);
282
+ canvas.box(block.path, x, yTop, avail, block.h);
283
+ let top = yTop - inset.t;
284
+ let at = x + inset.l;
285
+ for (let part of block.parts || []) {
286
+ canvas.y = top;
287
+ drawBlock(canvas, part.block, at, part.w);
288
+ at += part.w;
289
+ }
290
+ canvas.y = yTop - block.h;
291
+ canvas.fresh = false;
292
+ };
293
+
294
+ /** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
295
+ let drawBlock = (canvas, block, x, avail) =>
296
+ (block.parts ? drawSplit : block.picture ? drawPicture : drawLines)(canvas, block, x, avail);
297
+
298
+ // A region is a run of strips: what a columned node's content flows down
299
+ // (CONTEXT.md "Strip"). `index` is the strip being filled, `top` the y every
300
+ // strip on this page starts from, `ends` where the finished ones stopped, and
301
+ // `floor` the bottom the current strip fills to — the page bottom while
302
+ // filling, a shorter target while balancing. `owner` is the depth of the group
303
+ // that declared the count, or -1 for the report root, and `nesting` how deep
304
+ // the buffer has walked while it holds — see `nesting()` below, its one reader.
305
+ /**
306
+ * @typedef {{ width: number, count: number, index: number, top: number,
307
+ * height: number | null, ends: number[], owner: number, nesting: number,
308
+ * held: any[] | null, measured: number }} Region
309
+ */
310
+ /**
311
+ * @typedef {{ count: number, owner: number }} Pending
312
+ */
313
+
314
+ // Where the cursor's column starts and how wide it is: the strip's, inside a
315
+ // region, and the page's content box outside one. Every drawing call reads
316
+ // these rather than `canvas.margin`/`canvas.content` — the canvas keeps
317
+ // presenting the page, because `pageBox` re-derives page furniture from it.
318
+ /** @type {(state: Flow) => number} */
319
+ let originOf = (state) =>
320
+ state.canvas.margin + (state.region ? state.region.index * (state.region.width + GUTTER) : 0);
321
+ /** @type {(state: Flow) => number} */
322
+ let widthOf = (state) => (state.region ? state.region.width : state.canvas.content);
323
+ // The bottom the cursor fills to. A balanced strip's floor sits above the page
324
+ // bottom and is a target rather than a bound: a fresh strip still takes at
325
+ // least one line, so a block that fits the page is placed even where it
326
+ // overruns. The last strip answers to the page instead — a balanced share is
327
+ // rounded, so the strips before it may each stop a little short, and the
328
+ // remainder has to land somewhere.
329
+ /** @type {(state: Flow) => number} */
330
+ let floorOf = (state) => {
331
+ let region = state.region;
332
+ if (!region || region.height == null || region.index === region.count - 1)
333
+ return state.canvas.bottom;
334
+ return region.top - region.height;
335
+ };
336
+ // The y a fresh column starts from: a strip's shared top inside a region.
337
+ /** @type {(state: Flow) => number} */
338
+ let ceilOf = (state) => (state.region ? state.region.top : state.canvas.top);
339
+ // Has this column taken anything yet? Answered positionally, because the only
340
+ // thing that marks a page used is the cursor moving off its top, and the
341
+ // drawing primitives that move it take a `Canvas` rather than a `Flow`.
342
+ // `canvas.fresh` stays the *page*'s own answer — it is what keeps `turn` from
343
+ // leaving an empty page behind — so a strip claims no second meaning for it.
344
+ // A block of no height would read fresh here and used there; none is reachable
345
+ // today, since every item wraps to at least one line.
346
+ /** @type {(state: Cursor) => boolean} */
347
+ let freshOf = (state) => (state.region ? state.canvas.y === state.region.top : state.canvas.fresh);
348
+
349
+ /** @type {(canvas: Canvas, h: number, line: Line, floor: number) => boolean} */
350
+ let hasRoom = (canvas, h, line, floor) => canvas.y - h - line.h >= floor;
351
+
352
+ // A fresh column always takes at least one line, or nothing would ever fit —
353
+ // which is also what makes a balanced floor a target rather than a bound.
354
+ /** @type {(fresh: boolean, j: number, i: number) => boolean} */
355
+ let takeLine = (fresh, j, i) => j === i && fresh;
356
+
357
+ /**
358
+ * @type {(canvas: Canvas, lines: Line[], i: number, floor: number,
359
+ * fresh: boolean) => { j: number, h: number }}
360
+ */
361
+ let grow = (canvas, lines, i, floor, fresh) => {
362
+ let j = i,
363
+ h = 0,
364
+ n = lines.length;
365
+ while (j < n) {
366
+ if (!hasRoom(canvas, h, lines[j], floor) && !takeLine(fresh, j, i)) break;
367
+ h += lines[j++].h;
368
+ }
369
+ return { j, h };
370
+ };
371
+
372
+ /** @type {(n: number, j: number, i: number) => boolean} */
373
+ let orphan = (n, j, i) => n - j === 1 && j - i >= 2;
374
+
375
+ /** @type {(i: number, j: number, n: number, fresh: boolean) => boolean} */
376
+ let widow = (i, j, n, fresh) => i === 0 && j - i === 1 && n >= 2 && !fresh;
377
+
378
+ // Never leave a single line behind (an orphan) or take one across alone
379
+ // (a widow): give one back, or move the whole block down.
380
+ /**
381
+ * @type {(lines: Line[], i: number, j: number, h: number,
382
+ * fresh: boolean) => { j: number, h: number }}
383
+ */
384
+ let mend = (lines, i, j, h, fresh) => {
385
+ let n = lines.length;
386
+ if (j >= n) return { j, h };
387
+ if (orphan(n, j, i)) h -= lines[--j].h;
388
+ if (widow(i, j, n, fresh)) j = i;
389
+ return { j, h };
390
+ };
391
+
392
+ // Lines `i..j` fit in this column, occupying height `h`; `j === i` means the
393
+ // column is spent.
394
+ /**
395
+ * @type {(canvas: Canvas, block: Block, i: number, floor: number,
396
+ * fresh: boolean) => { j: number, h: number }}
397
+ */
398
+ let fitLines = (canvas, block, i, floor, fresh) => {
399
+ let { j, h } = grow(canvas, block.lines, i, floor, fresh);
400
+ return mend(block.lines, i, j, h, fresh);
401
+ };
402
+
403
+ /** @type {(canvas: Canvas, block: Block, floor: number) => boolean} */
404
+ let whole = (canvas, block, floor) => canvas.y - block.h >= floor;
405
+
406
+ /** @type {(style: any, name: string) => number} */
407
+ let spaceOf = (style, name) => {
408
+ let n = style?.[name];
409
+ return Number.isFinite(n) && n > 0 ? n : 0;
410
+ };
411
+
412
+ /** @type {(event: any) => boolean} */
413
+ let pageBand = (event) => event.role === "page-header" || event.role === "page-footer";
414
+ /** @type {(state: Cursor, event: any) => boolean} */
415
+ let keepLead = (state, event) => pageBand(event) || !freshOf(state);
416
+ /** @type {(state: Cursor, event: any) => number} */
417
+ let leadOf = (state, event) => (keepLead(state, event) ? spaceOf(event.style, "spaceBefore") : 0);
418
+ /** @type {(state: Cursor, event: any, name: string) => void} */
419
+ let skipFlow = (state, event, name) => {
420
+ state.canvas.y -= name === "spaceBefore" ? leadOf(state, event) : spaceOf(event.style, name);
421
+ };
422
+ /** @type {(state: Cursor, event: any) => number} */
423
+ let flowPad = (state, event) => leadOf(state, event) + spaceOf(event.style, "spaceAfter");
424
+
425
+ // The unsplittable blocks -- a picture and a split -- move whole to a fresh
426
+ // column rather than breaking, so a balanced floor never cuts one: only the
427
+ // page's own floor bears on it. A picture that no column can hold is scaled
428
+ // down to the one it lands on, since obeying the guarantee literally would
429
+ // mean dropping it (SCHEMA.md, best-effort); a split has nothing to scale, so
430
+ // one taller than any column renders in full past the bottom margin, the
431
+ // posture a paragraph line taller than a page already has.
432
+ //
433
+ // A split also *cannot* be sliced: its own `lines` are empty -- the content
434
+ // lives in its slots -- so the slicing path would draw nothing and leave the
435
+ // cursor where it stood, taking everything after it down with it.
436
+ /** @type {(state: Flow, block: Block) => void} */
437
+ let placeWhole = (state, block) => {
438
+ advance(state);
439
+ let canvas = state.canvas;
440
+ let room = canvas.y - canvas.bottom;
441
+ if (block.picture && block.h > room) shrink(block, room);
442
+ drawBlock(canvas, block, originOf(state), widthOf(state));
443
+ };
444
+
445
+ // One slice of a broken item, shaped as a block the whole-block drawer takes:
446
+ // the lines it carries, and the height those make once the insets the break
447
+ // left it are counted. The unmasked `inset` rides along, because `drawLines`
448
+ // derives the same masked one from the same slice -- a zero top on a middle
449
+ // slice must not be confused with an author who declared none.
450
+ /**
451
+ * @type {(block: Block, lines: Line[], h: number,
452
+ * slice: import('./box.js').Slice) => Block}
453
+ */
454
+ let sliceOf = (block, lines, h, slice) => {
455
+ let inset = sliceInset(block.inset, slice);
456
+ return { ...block, lines, h: h + inset.t + inset.b };
457
+ };
458
+
459
+ // The floor one slice must fit above. The bottom inset is reserved on every
460
+ // slice rather than mended back once the last one is known: which slice is
461
+ // last depends on how many lines fit, which depends on the reservation. The
462
+ // price is a middle slice stopping `paddingBottom` short of the break and
463
+ // drawing nothing there, which is whitespace; the price of not reserving is a
464
+ // bottom border stroked below the bottom margin, which is ink.
465
+ //
466
+ // The top inset joins it on the first slice. Folding both into the floor is
467
+ // exactly seeding them into the height -- one number either way -- so `grow`,
468
+ // `mend` and `fitLines` stay line arithmetic and know nothing about the box.
469
+ /** @type {(state: Flow, block: Block, first: boolean) => number} */
470
+ let sliceFloor = (state, block, first) =>
471
+ floorOf(state) + block.inset.b + (first ? block.inset.t : 0);
472
+
473
+ /** @type {(state: Flow, block: Block, i: number, n: number, first: boolean) => number} */
474
+ let paintSlice = (state, block, i, n, first) => {
475
+ let canvas = state.canvas;
476
+ let { j, h } = fitLines(canvas, block, i, sliceFloor(state, block, first), freshOf(state));
477
+ if (j === i) {
478
+ advance(state);
479
+ return i;
480
+ }
481
+ let slice = { first, more: j < n };
482
+ drawLines(
483
+ canvas,
484
+ sliceOf(block, block.lines.slice(i, j), h, slice),
485
+ originOf(state),
486
+ widthOf(state),
487
+ slice,
488
+ );
489
+ if (slice.more) advance(state);
490
+ return j;
491
+ };
492
+
493
+ // A column that took nothing leaves `first` where it was: the slice that owns
494
+ // the top inset is the one that draws lines, not the one that gave up on them.
495
+ /** @type {(state: Flow, block: Block) => void} */
496
+ let slice = (state, block) => {
497
+ let n = block.lines.length,
498
+ i = 0,
499
+ first = true;
500
+ while (i < n) {
501
+ let next = paintSlice(state, block, i, n, first);
502
+ if (next > i) first = false;
503
+ i = next;
504
+ }
505
+ };
506
+
507
+ // Flow a block down the column, breaking where `fitLines` says. Each slice is
508
+ // a box and then its lines, drawn by the path a whole block goes through.
509
+ /** @type {(state: Flow, event: any, measured?: Block | null) => void} */
510
+ let item = (state, event, measured = null) => {
511
+ let canvas = state.canvas;
512
+ let avail = widthOf(state);
513
+ let block = measured || blockOf(canvas, event, avail);
514
+ let padded = { ...block, h: block.h + flowPad(state, event) };
515
+ if (whole(canvas, padded, floorOf(state))) {
516
+ skipFlow(state, event, "spaceBefore");
517
+ drawBlock(canvas, block, originOf(state), avail);
518
+ skipFlow(state, event, "spaceAfter");
519
+ return;
520
+ }
521
+ overflow(state, block);
522
+ skipFlow(state, event, "spaceAfter");
523
+ };
524
+
525
+ // What a block does when it does not fit where it stands. A picture and a
526
+ // split never break -- each moves whole to a fresh column; anything else flows
527
+ // down it, breaking where `fitLines` says.
528
+ /** @type {(state: Flow, block: Block) => void} */
529
+ let overflow = (state, block) =>
530
+ block.picture || block.parts ? placeWhole(state, block) : slice(state, block);
531
+
532
+ // The height a page turn carries over: the drawn headers of the groups still
533
+ // open, which every page their instances continue onto replays. Computed rather
534
+ // than kept — a stored total is a second copy of `open` that can drift from it.
535
+ // Never "running": CONTEXT.md spends that word on the accumulators under `run`.
536
+ //
537
+ // What a page has left after it is what `flush` measures a run against, so a
538
+ // repeat always leaves room for the content unit it introduces. That room is
539
+ // measured from the group's *first* unit, though: a later item with lines
540
+ // taller than what is left is drawn past the bottom margin all the same, as a
541
+ // line taller than a page always was. SCHEMA.md says so rather than guarding
542
+ // it — the alternative is dropping content.
543
+ /** @type {(state: Flow) => number} */
544
+ let carried = (state) =>
545
+ state.open.reduce(
546
+ (total, group) => total + group.blocks.reduce((height, block) => height + block.h, 0),
547
+ 0,
548
+ );
549
+
550
+ // Break unless the page is still fresh, where a turn would only leave an empty
551
+ // page behind. Every break goes through here — the elective ones and the
552
+ // overflows alike — because a fresh page owes the open groups their headers
553
+ // back, and one owner for that is one place to get it right.
554
+ //
555
+ // The replayed run is not content, and `fresh` stays true across it: that is
556
+ // what keeps `fitLines` taking at least one line here (so a run of turns always
557
+ // advances), keeps `put` from breaking straight back out, and keeps a second
558
+ // turn a no-op. The blocks are the ones `flush` drew, repainted exactly as
559
+ // `carryOver` repaints a table's header row — `adopt` moves only the top and
560
+ // bottom, so the content width is fixed for the render and a block measured on
561
+ // page one is valid on page nine.
562
+ /** @type {(state: Flow) => void} */
563
+ let turn = (state) => {
564
+ let canvas = state.canvas;
565
+ if (canvas.fresh) return;
566
+ canvas.newPage();
567
+ replayOpen(state);
568
+ canvas.fresh = true;
569
+ if (state.region) restrip(state.region, canvas.y);
570
+ };
571
+
572
+ // What a fresh page owes the instances still open: their drawn headers again.
573
+ // Under a region the replay goes into the first strip, at the width the blocks
574
+ // were measured at — they are the region's content continuing, not page
575
+ // furniture, and drawing them across the page would straddle every strip below
576
+ // them. The strips then restart under the run.
577
+ /** @type {(state: Flow) => void} */
578
+ let replayOpen = (state) => {
579
+ if (state.region) state.region.index = 0;
580
+ let x = originOf(state),
581
+ avail = widthOf(state);
582
+ for (let group of state.open)
583
+ for (let block of group.blocks) drawBlock(state.canvas, block, x, avail);
584
+ };
585
+
586
+ // Every strip on a page shares a top and, while balancing, a floor. Reset
587
+ // together so a page turn and a region opening say the same thing once.
588
+ /** @type {(region: Region, top: number) => void} */
589
+ let restrip = (region, top) => {
590
+ region.index = 0;
591
+ region.top = top;
592
+ region.ends = [];
593
+ };
594
+
595
+ // The column break: move to the next strip, and only turn the page once the
596
+ // last one is spent. Outside a region this is a page turn and nothing else,
597
+ // which is why every break in this file goes through here.
598
+ /** @type {(state: Flow) => void} */
599
+ let advance = (state) => {
600
+ let region = state.region;
601
+ if (!region) return turn(state);
602
+ // Nothing in this column yet: advancing would leave it empty and the next
603
+ // one would refuse the same block, so the caller must place it here.
604
+ if (freshOf(state)) return;
605
+ region.ends.push(state.canvas.y);
606
+ if (region.index + 1 < region.count) {
607
+ region.index++;
608
+ state.canvas.y = region.top;
609
+ return;
610
+ }
611
+ // Spent: a real page, which restrips through `turn`.
612
+ turn(state);
613
+ };
614
+
615
+ // --- regions ----------------------------------------------------------------
616
+
617
+ // One strip's width: the content box less the gutters, shared out. Needed
618
+ // before a region exists, because a keep-with block is measured against the
619
+ // width the item is about to land in.
620
+ /** @type {(state: Flow, count: number) => number} */
621
+ let stripWidth = (state, count) => (state.canvas.content - GUTTER * (count - 1)) / count;
622
+
623
+ // Open the region a `columns` count owed, at the first content that belongs in
624
+ // it. Deferred to here rather than opened where the count arrived, because the
625
+ // declaring node's own header run is full-width and has to be drawn first —
626
+ // `flush` does that immediately above every call site.
627
+ //
628
+ // It opens buffering: what the strips do with the content depends on how much
629
+ // of it there is, and that is not known until the region ends or outgrows a
630
+ // page. `held` is that buffer; `null` means the region has committed.
631
+ /** @type {(state: Flow) => void} */
632
+ let openRegion = (state) => {
633
+ let owed = state.pending;
634
+ if (!owed || state.region) return;
635
+ state.pending = null;
636
+ let canvas = state.canvas;
637
+ state.region = {
638
+ width: stripWidth(state, owed.count),
639
+ count: owed.count,
640
+ index: 0,
641
+ top: canvas.y,
642
+ height: null,
643
+ ends: [],
644
+ owner: owed.owner,
645
+ // Nothing is placed while the buffer holds, so the open stack stops here
646
+ // and this carries the walk on from it.
647
+ nesting: state.open.length - 1,
648
+ held: [],
649
+ measured: 0,
650
+ };
651
+ };
652
+
653
+ // Close the region: the cursor lands at whichever strip reached lowest, and
654
+ // full-width content resumes under it on the same page (SCHEMA.md).
655
+ /** @type {(state: Flow) => void} */
656
+ let closeRegion = (state) => {
657
+ let region = state.region;
658
+ if (!region) return;
659
+ // Committing replays the buffer, and a replayed footer closes the region on
660
+ // its own way through — so re-read rather than trusting the local.
661
+ if (region.held) commit(state);
662
+ if (!state.region) return;
663
+ let canvas = state.canvas;
664
+ canvas.y = Math.min(canvas.y, ...region.ends);
665
+ state.region = null;
666
+ };
667
+
668
+ // The region's own entries as the things a strip places (`balance.js`). Only
669
+ // text flows; a group instance, a table's headings row, a table row and an
670
+ // image all move whole, and `head` is where each says what it costs when it is
671
+ // the first thing in a strip: an instance drops its opening gap there, a row
672
+ // pays for the column headings the table restates above it, and the other two
673
+ // cost the same wherever they land.
674
+ //
675
+ // Two things this deliberately does not model, each stated because the cost of
676
+ // getting them wrong is a strip that stops early rather than a wrong document.
677
+ // A `total-row` is a row like any other, though it really keeps the last data
678
+ // row with the whole emitted total block (SCHEMA.md): pairing them costs the
679
+ // flat per-entry map this is, to remove an error of one row at the end of a
680
+ // table. And a row taller than
681
+ // any strip is sliced rather than moved (SCHEMA.md, "How a tall row slices");
682
+ // called whole here, it makes every candidate height fail and the region
683
+ // fills, which is the honest answer for a region holding one.
684
+ //
685
+ // Row heights themselves are real, not the buffer's one-line estimate —
686
+ // `remeasure` corrects them when the table closes.
687
+ /** @type {(state: Flow, own: any[]) => Unit[]} */
688
+ let unitsOf = (state, own) => {
689
+ let table = { headings: 0 };
690
+ return own.map((entry) => unitFor(state.canvas, entry, table));
691
+ };
692
+
693
+ // What the open table costs a strip that has to restate it, carried between
694
+ // entries because a row is what pays for the headings and a row cannot see
695
+ // them.
696
+ /**
697
+ * @typedef {{ headings: number }} Restated
698
+ */
699
+ // One entry's cost, by what it is.
700
+ /** @type {(canvas: Canvas, entry: any, table: Restated) => Unit} */
701
+ let unitFor = (canvas, entry, table) => {
702
+ if (entry.span != null) return unsplit(entry.span, headOf(canvas, entry.span));
703
+ return (UNITS[entry.event.type] || flows)(entry, table);
704
+ };
705
+
706
+ /** @type {(h: number, head: number) => Unit} */
707
+ let unsplit = (h, head) => ({ whole: true, h, head });
708
+ /** @type {(entry: any) => Unit} */
709
+ let flows = (entry) => ({ whole: false, h: entry.h, head: entry.h });
710
+
711
+ // What each kind of entry that does not flow costs. A module constant for the
712
+ // reason `WORTH` is one: this runs once per buffered entry. It is not `WORTH`
713
+ // with more names — that answers what an event adds to the running total when
714
+ // it carries no block, so it holds `group-start` (whose gap nothing else
715
+ // counts) and no `image` (which always carries one).
716
+ /** @type {Record<string, (entry: any, table: Restated) => Unit>} */
717
+ let UNITS = {
718
+ // The headings row is a unit itself, and opening the table is where what a
719
+ // later strip owes for restating it is recorded.
720
+ "table-start": (entry, table) => {
721
+ table.headings = entry.h;
722
+ return unsplit(entry.h, entry.h);
723
+ },
724
+ row: restated,
725
+ "total-row": restated,
726
+ image: unbroken,
727
+ split: unbroken,
728
+ };
729
+
730
+ // An unsplittable block owes a strip nothing but its own height.
731
+ /** @type {(entry: any) => Unit} */
732
+ function unbroken(entry) {
733
+ return unsplit(entry.h, entry.h);
734
+ }
735
+
736
+ // A row opening a strip carries the column headings restated above it.
737
+ /** @type {(entry: any, table: Restated) => Unit} */
738
+ function restated(entry, table) {
739
+ return unsplit(entry.h, entry.h + table.headings);
740
+ }
741
+
742
+ // The one number the buffer decides. A region that fits a page balances — the
743
+ // shortest strip height that still holds its units in `count` strips — and a
744
+ // longer one, or one no such height exists for, fills each strip to the page
745
+ // bottom. Filling is the taller floor of the two, so committing to it is
746
+ // committing to nothing: the strips simply take what they take.
747
+ /** @type {(state: Flow, region: Region, own: any[]) => number | null} */
748
+ let stripHeight = (state, region, own) =>
749
+ outgrown(state, region)
750
+ ? null
751
+ : balance(unitsOf(state, own), region.count, roomOf(state, region), rowHeight(state.canvas));
752
+
753
+ /** @type {(state: Flow) => void} */
754
+ let commit = (state) => {
755
+ let region = /** @type {Region} */ (state.region);
756
+ let held = /** @type {any[]} */ (region.held);
757
+ // Marking the spans is also what picks out the region's own children, and
758
+ // the height is decided from those — so it runs before the replay, not with
759
+ // it.
760
+ region.height = stripHeight(state, region, spans(held));
761
+ region.held = null;
762
+ restrip(region, region.top);
763
+ for (let entry of held) {
764
+ if (breaksFor(state, region, entry.span)) advance(state);
765
+ state.route(entry.event, entry.block);
766
+ }
767
+ };
768
+
769
+ // A group instance is the atomic unit inside a region: one that would cross a
770
+ // strip boundary, and would fit a strip of its own, opens the next one
771
+ // instead. An instance no strip can hold answers false and continues, the way
772
+ // anything too tall does (SCHEMA.md). What a fresh strip has to hold is the
773
+ // span less the opening gap, which `settle` drops at a strip head — the same
774
+ // unit `balance.js` packs. Only a buffered `group-start` carries a span;
775
+ // everything else flows.
776
+ /** @type {(state: Flow, region: Region, span: number | undefined) => boolean} */
777
+ let breaksFor = (state, region, span) =>
778
+ span != null &&
779
+ !freshOf(state) &&
780
+ state.canvas.y - span < floorOf(state) &&
781
+ headOf(state.canvas, span) <= roomOf(state, region);
782
+
783
+ // How much one strip of this region can hold, and whether the buffer has more
784
+ // than every strip together could take. The balance-or-fill decision and the
785
+ // early commit that gives up on balancing are the same question, so they are
786
+ // the same predicate.
787
+ /** @type {(state: Flow, region: Region) => number} */
788
+ let roomOf = (state, region) => region.top - state.canvas.bottom;
789
+ /** @type {(state: Flow, region: Region) => boolean} */
790
+ let outgrown = (state, region) => region.measured > roomOf(state, region) * region.count;
791
+
792
+ // Mark every buffered `group-start` with the height of the instance it opens,
793
+ // so the replay can keep it whole. Only the region's own children carry one:
794
+ // an inner instance is held by the outer one it belongs to.
795
+ //
796
+ // The empty stack is also what says an entry is the region's own child, so the
797
+ // same walk hands those back — one traversal answers both questions rather
798
+ // than two tracking the same nesting.
799
+ /** @type {(held: any[]) => any[]} */
800
+ let spans = (held) => {
801
+ /** @type {number[]} */
802
+ let stack = [];
803
+ /** @type {any[]} */
804
+ let own = [];
805
+ for (let [i, entry] of held.entries()) {
806
+ if (!stack.length) own.push(entry);
807
+ nest(held, stack, entry, i);
808
+ if (stack.length) held[stack[0]].span += entry.h;
809
+ }
810
+ return own;
811
+ };
812
+
813
+ // Track the instance nesting one entry at a time. Only the outermost one open
814
+ // carries a span, which is what makes it the region's own child.
815
+ /** @type {(held: any[], stack: number[], entry: any, i: number) => void} */
816
+ let nest = (held, stack, entry, i) => {
817
+ if (entry.event.type === "group-end") return void stack.pop();
818
+ if (entry.event.type !== "group-start") return;
819
+ if (!stack.length) held[i].span = 0;
820
+ stack.push(i);
821
+ };
822
+
823
+ // What a buffered event is worth to the running total when it carries no block
824
+ // of its own: a table row and a group's opening gap are the two that matter,
825
+ // and both are known without laying anything out.
826
+ /** @type {(state: Flow, event: any) => number} */
827
+ let estimate = (state, event) => (WORTH[event.type] || (() => 0))(state.canvas);
828
+
829
+ // What each kind of buffered event is worth. A module constant: `estimate` runs
830
+ // once per buffered event, and rebuilding this table each time would allocate
831
+ // a closure per table row.
832
+ /** @type {(canvas: Canvas) => number} */
833
+ let rowWorth = (canvas) => rowHeight(canvas) + CELL_PAD.t + CELL_PAD.b;
834
+ /** @type {Record<string, (canvas: Canvas) => number>} */
835
+ let WORTH = {
836
+ row: rowWorth,
837
+ "total-row": rowWorth,
838
+ "table-start": rowWorth,
839
+ "group-start": (canvas) => instanceGap(canvas),
840
+ };
841
+
842
+ // What a buffered bracket does to the region's own walk. A table beside the
843
+ // heights above, so the bracket names sit where the other event names are read.
844
+ /** @type {Record<string, number>} */
845
+ let NESTING = { "group-start": 1, "group-end": -1 };
846
+
847
+ // Buffer one event while the region is still deciding, keeping a running total
848
+ // of what it holds. A measured block rides along so the replay never wraps the
849
+ // same text twice; a held header counts towards the total like anything else,
850
+ // but the flush that draws it measures from the event, so only its height is
851
+ // kept. A buffer that outgrows what the strips could hold has answered the
852
+ // question early: it commits to filling, and everything after it streams.
853
+ /** @type {(state: Flow, event: any, block: Block | null) => void} */
854
+ let buffer = (state, event, block) => {
855
+ let region = /** @type {Region} */ (state.region);
856
+ // The one door into the held list, so it is where the buffer's own walk is
857
+ // kept. A bracket buffered here is a bracket the open stack will not see
858
+ // until the replay places it.
859
+ region.nesting += NESTING[event.type] ?? 0;
860
+ let entry = entryFor(state, event, block, region.width);
861
+ /** @type {any[]} */ (region.held).push(entry);
862
+ region.measured += entry.h;
863
+ if (event.type === "table-end") region.measured += remeasure(state, region);
864
+ if (outgrown(state, region)) commit(state);
865
+ };
866
+
867
+ // A table's events reach the buffer one at a time, and each is worth a bare
868
+ // estimate — one line — until the table closes: a row's height is what its
869
+ // cells wrap to, that needs the column widths, and those need every cell in
870
+ // the table. The last event is where the table is finally in hand, so it is
871
+ // measured at the strip width and its entries are corrected. Both readers of
872
+ // those heights want the real ones: the balance model, and `outgrown`.
873
+ //
874
+ // The replay measures the table again when it draws it. That is the price of
875
+ // keeping `table` the one place a table is laid out, and it is paid once per
876
+ // table per render.
877
+ // The measurement is kept, keyed by the opening event, so the replay draws
878
+ // this table without wrapping every cell a second time. Held per flow rather
879
+ // than per module: the keys are one render's events.
880
+ /** @type {(state: Flow, region: Region) => number} */
881
+ let remeasure = (state, region) => {
882
+ let held = /** @type {any[]} */ (region.held);
883
+ let at = held.findLastIndex((entry) => entry.event.type === "table-start");
884
+ if (at < 0) return 0;
885
+ let span = held.slice(at);
886
+ let measured = measureTable(
887
+ state.canvas,
888
+ tableOf(span.map((entry) => entry.event)),
889
+ region.width,
890
+ );
891
+ state.measured.set(span[0].event, measured);
892
+ return retune(span, measured);
893
+ };
894
+
895
+ // Write the measured heights back over the estimates. `span` is the table's
896
+ // events in the order they were buffered — its opening, its rows, an optional
897
+ // total, then its end — and the measurement comes out in that same order, so
898
+ // the two line up by position and the end simply runs off the shorter list.
899
+ // Reports what the correction moved the running total by.
900
+ /** @type {(span: any[], measured: Measured) => number} */
901
+ let retune = (span, measured) => {
902
+ let heights = [measured.head.h, ...measured.laid.map((/** @type {any} */ laid) => laid.h)];
903
+ if (measured.totalRows) for (let row of measured.totalRows) heights.push(row.h);
904
+ let delta = 0;
905
+ for (let [i, h] of heights.entries()) {
906
+ delta += h - span[i].h;
907
+ span[i].h = h;
908
+ }
909
+ return delta;
910
+ };
911
+
912
+ // One buffered entry: the event, the block the replay will draw it from, and
913
+ // what it is worth to the running total. A header keeps its block like
914
+ // anything else — the flush that eventually draws it takes the block from the
915
+ // hold rather than wrapping the text again.
916
+ /** @type {(state: Flow, event: any, block: Block | null, width: number) => any} */
917
+ let entryFor = (state, event, block, width) => {
918
+ let measured = block || measureFor(state, event, width);
919
+ return { event, block: measured, h: measured ? measured.h : estimate(state, event) };
920
+ };
921
+
922
+ // The block a buffered event will be drawn from, measured once at the strip
923
+ // width — already known, which is why this can happen before the region has
924
+ // decided anything. Events that are not blocks contribute an `estimate`.
925
+ /** @type {(state: Flow, event: any, width: number) => Block | null} */
926
+ let measureFor = (state, event, width) =>
927
+ BLOCKS.has(event.type) ? blockOf(state.canvas, event, width) : null;
928
+
929
+ // Give every outline mark still waiting a position: where its content begins.
930
+ /** @type {(state: Flow) => void} */
931
+ let anchor = (state) => {
932
+ for (let i = state.marks.length - 1; i >= 0 && state.marks[i].page < 0; i--) {
933
+ state.marks[i].page = state.canvas.count - 1;
934
+ state.marks[i].y = state.canvas.y;
935
+ // The strip too: two instances in two strips share a y, so a mark that
936
+ // recorded only the page and the cursor would send both bookmarks to the
937
+ // same place.
938
+ state.marks[i].x = originOf(state);
939
+ }
940
+ };
941
+
942
+ /** @type {(state: Flow) => Group[]} */
943
+ let pending = (state) => state.open.filter((group) => group.held.length);
944
+
945
+ // A held run, as blocks. A region measured its headers while buffering them,
946
+ // at this same width, so a replayed run reuses those rather than wrapping the
947
+ // same text a second time (ADR 0027).
948
+ /** @type {(canvas: Canvas, held: any[], avail: number) => Block[]} */
949
+ let blocksOf = (canvas, held, avail) =>
950
+ held.map((entry) => entry.block || blockOf(canvas, entry.event, avail));
951
+
952
+ /** @type {(state: Flow, extra: number, runs: Block[][]) => number} */
953
+ let needed = (state, extra, runs) =>
954
+ state.gap + runs.flat().reduce((total, block) => total + block.h, 0) + extra;
955
+
956
+ /** @type {(state: Flow, need: number) => boolean} */
957
+ let tooTall = (state, need) => need > ceilOf(state) - carried(state) - floorOf(state);
958
+
959
+ // Degenerate: taller than a page — keep nothing, paginate normally, and
960
+ // repeat nothing afterwards. That last part is what bounds the replayed run
961
+ // for good: every other path drew its headers inside the room a page still
962
+ // had, so a run always leaves space for the content it introduces.
963
+ /** @type {(state: Flow, holding: Group[]) => void} */
964
+ let dropHeld = (state, holding) => {
965
+ let held = holding.flatMap((group) => group.held);
966
+ for (let group of holding) group.held = [];
967
+ state.gap = 0;
968
+ anchor(state);
969
+ for (let entry of held) item(state, entry.event, entry.block);
970
+ };
971
+
972
+ /** @type {(state: Flow, need: number) => void} */
973
+ let settle = (state, need) => {
974
+ if (state.canvas.y - need < floorOf(state)) advance(state);
975
+ // The half-line gap is dropped at the head of a column, exactly as it is at
976
+ // the head of a page: three strips of labels start level or they read as a
977
+ // ragged grid.
978
+ if (!freshOf(state)) state.canvas.y -= state.gap;
979
+ state.gap = 0;
980
+ anchor(state);
981
+ };
982
+
983
+ // Drawn and handed back one instance at a time. Nothing in here turns a page
984
+ // — the turn above already did — so an instance taking its blocks the moment
985
+ // they are drawn can never have them replayed on top of themselves.
986
+ /** @type {(state: Flow, holding: Group[], runs: Block[][], avail: number) => void} */
987
+ let drawHeld = (state, holding, runs, avail) => {
988
+ let x = originOf(state);
989
+ for (let [i, group] of holding.entries()) {
990
+ for (let block of runs[i]) drawBlock(state.canvas, block, x, avail);
991
+ group.blocks.push(...runs[i]);
992
+ group.held = [];
993
+ }
994
+ };
995
+
996
+ /** @type {(state: Flow, extra: number) => void} */
997
+ let flush = (state, extra) => {
998
+ let runsFor = pending(state);
999
+ if (!holding(state)) return;
1000
+ let canvas = state.canvas;
1001
+ let avail = widthOf(state);
1002
+ // Measured per instance, so each one takes back exactly the blocks its own
1003
+ // headers were drawn from: no flat list to slice, no order to keep in step.
1004
+ let runs = runsFor.map((group) => blocksOf(canvas, group.held, avail));
1005
+ let need = needed(state, extra, runs);
1006
+ // Against what a fresh column has left once the page has replayed what it
1007
+ // carries. Inside a region that is a strip, and the replay sits above it.
1008
+ if (tooTall(state, need)) return dropHeld(state, runsFor);
1009
+ settle(state, need);
1010
+ drawHeld(state, runsFor, runs, avail);
1011
+ };
1012
+
1013
+ // Enough of an item that its group header is never left introducing nothing.
1014
+ let KEEP_LINES = 2;
1015
+ /** @type {(block: Block) => number} */
1016
+ let keepWith = (block) =>
1017
+ block.picture ? block.h : Math.min(block.h, heightOf(block.lines.slice(0, KEEP_LINES)));
1018
+
1019
+ // Flush any pending header with this item's opening, then place the item. The
1020
+ // block is measured once here and handed on rather than wrapped twice.
1021
+ /** @type {(state: Flow, event: any, measured?: Block | null) => void} */
1022
+ let placeItem = (state, event, measured = null) => {
1023
+ let block = measured || keepBlock(state, event, widthOf(state));
1024
+ if (block && holding(state)) flush(state, keepWith(block) + flowPad(state, event));
1025
+ item(state, event, block);
1026
+ };
1027
+
1028
+ // The block a held header run must keep company with: null when nothing is
1029
+ // waiting on it, or when this event is a header itself rather than the content
1030
+ // one introduces. The width is the caller's, because a region measures against
1031
+ // the strip its content is about to land in rather than the page.
1032
+ /** @type {(state: Flow, event: any, width: number) => Block | null} */
1033
+ let keepBlock = (state, event, width) =>
1034
+ holding(state) && event.role !== "group-header" ? measureFor(state, event, width) : null;
1035
+
1036
+ // Is anything waiting on this item's arrival — a header run, or the gap an
1037
+ // instance opens with?
1038
+ /** @type {(state: Flow) => boolean} */
1039
+ let holding = (state) => state.open.some((group) => group.held.length) || state.gap > 0;
1040
+
1041
+ // A half-line gap before each instance (dropped at a page top by `flush`)
1042
+ // keeps groups reading as blocks. Every instance becomes an outline entry.
1043
+ // `break: "page"` turns the page first — pending ancestor headers stay
1044
+ // buffered across it, so they open the fresh page with their content and
1045
+ // their own marks anchor there. `reset: "page"` turns the same way, then
1046
+ // starts a new `page.number` / `page.total` sequence on the page it opens.
1047
+ // A duplicate start on a page that already began a sequence is a zero-length
1048
+ // range; `numbered` skips it.
1049
+ /** @type {(state: Flow, event: any) => void} */
1050
+ let openGroup = (state, event) => {
1051
+ breakFor(state, event);
1052
+ if (event.columns) state.pending = { count: event.columns, owner: event.depth };
1053
+ state.gap = state.skipGap ? 0 : Math.max(state.gap, instanceGap(state.canvas));
1054
+ state.skipGap = false;
1055
+ state.open.push({ held: [], blocks: [] });
1056
+ state.marks.push(markFor(event));
1057
+ };
1058
+
1059
+ // A real page, inside a region as much as outside one: ADR 0013 declined a
1060
+ // `break: "column"`, so this never means the next strip. `turn` restrips.
1061
+ /** @type {(state: Flow, event: any) => void} */
1062
+ let breakFor = (state, event) => {
1063
+ if (event.break === "page" || event.reset === "page") turn(state);
1064
+ if (event.reset === "page") state.starts.push(state.canvas.count - 1);
1065
+ };
1066
+
1067
+ /** @type {(event: any) => any} */
1068
+ let markFor = (event) => ({
1069
+ // display(), not String(): a Date group key must title its outline bookmark
1070
+ // with the same ISO 8601 UTC text its cells render, on every machine.
1071
+ title: event.name + ": " + display(event.key),
1072
+ titled: false,
1073
+ depth: event.depth,
1074
+ page: -1,
1075
+ y: 0,
1076
+ });
1077
+
1078
+ // Headers wait for the content they introduce, so one is never stranded at a
1079
+ // page bottom. The instance's first header titles its outline entry.
1080
+ /** @type {(state: Flow, event: any, title: string | null, block?: Block | null) => void} */
1081
+ let holdHeader = (state, event, title, block = null) => {
1082
+ let mark = state.marks[state.marks.length - 1];
1083
+ // A picture has no words to title an outline entry with, so it holds its
1084
+ // place in the run and leaves the title to the next header that has some.
1085
+ if (mark && !mark.titled && title != null) {
1086
+ mark.titled = true;
1087
+ mark.title = title;
1088
+ }
1089
+ state.open[state.open.length - 1].held.push({ event, block });
1090
+ };
1091
+
1092
+ // A table row's floor height and the gap a group instance opens with: named
1093
+ // because the region's estimator reads both, and a constant with two readers
1094
+ // and no name drifts between them.
1095
+ /** @type {(canvas: Canvas) => number} */
1096
+ let rowHeight = (canvas) => LEAD * canvas.base;
1097
+ /** @type {(canvas: Canvas) => number} */
1098
+ let instanceGap = (canvas) => 0.5 * LEAD * canvas.base;
1099
+
1100
+ // What a fresh strip has to hold to take an instance: the span less the
1101
+ // opening gap, which `settle` drops at a strip head. This is the one fact the
1102
+ // balance model and the placement rule have to agree on, so both read it here
1103
+ // rather than each subtracting the gap for itself.
1104
+ /** @type {(canvas: Canvas, span: number) => number} */
1105
+ let headOf = (canvas, span) => span - instanceGap(canvas);
1106
+
1107
+ // --- tables -----------------------------------------------------------------
1108
+
1109
+ // Pre-measure one cell: wrapped lines per column width come later; natural
1110
+ // width first (no wrapping except hard breaks). `mt`/`mb` go unread: SCHEMA.md
1111
+ // gives flow spacing no meaning inside a table row, in either target.
1112
+ /** @type {(canvas: Canvas, cell: any, rowStyle: any) => any} */
1113
+ let cellOf = (canvas, cell, rowStyle) => {
1114
+ let style = merge(unbox(rowStyle), unbox(cell.style));
1115
+ let inset = insetOf(cell.style, CELL_PAD);
1116
+ let list = atoms(canvas, cell.tokens, style);
1117
+ let natural = wrap(canvas, list, Infinity, sizeOf(style, canvas.base)).reduce(
1118
+ (widest, line) => Math.max(widest, line.w),
1119
+ 0,
1120
+ );
1121
+ return {
1122
+ list,
1123
+ natural: natural + inset.l + inset.r,
1124
+ align: style.align,
1125
+ bg: col(style.background),
1126
+ underline: !!style.underline,
1127
+ strikethrough: !!style.strikethrough,
1128
+ inset,
1129
+ style: cell.style,
1130
+ path: cell.path,
1131
+ };
1132
+ };
1133
+
1134
+ /** @type {(canvas: Canvas, cells: any[], widths: number[]) => { cells: any[], h: number }} */
1135
+ let rowOf = (canvas, cells, widths) => {
1136
+ let h = rowHeight(canvas);
1137
+ let out = cells.map((cell, i) => {
1138
+ let inner = Math.max(widths[i] - cell.inset.l - cell.inset.r, 1);
1139
+ // A glyph or a hard break occupies; spaces alone are an empty cell.
1140
+ let lines = cell.list.some(
1141
+ (/** @type {{ hard: boolean, space: boolean }} */ atom) => atom.hard || !atom.space,
1142
+ )
1143
+ ? dress(wrap(canvas, cell.list, inner, cell.list[0].size), cell)
1144
+ : [];
1145
+ let cellHeight = heightOf(lines) + cell.inset.t + cell.inset.b;
1146
+ if (cellHeight > h) h = cellHeight;
1147
+ return { ...cell, lines };
1148
+ });
1149
+ return { cells: out, h };
1150
+ };
1151
+
1152
+ /** @type {(canvas: Canvas, cell: any, bg: any, x: number, y: number, w: number, h: number) => void} */
1153
+ let fillCell = (canvas, cell, bg, x, y, w, h) => {
1154
+ if (cell.bg && cell.bg !== bg) canvas.rect(cell.bg, x, y, w, h);
1155
+ };
1156
+
1157
+ /** @type {(canvas: Canvas, cell: any, lines: any[], x: number, y: number, w: number) => void} */
1158
+ let writeCell = (canvas, cell, lines, x, y, w) => {
1159
+ let inset = cell.inset;
1160
+ for (let line of lines) {
1161
+ canvas.drawLine(line, x + inset.l, y, w - inset.l - inset.r, cell.align);
1162
+ y -= line.h;
1163
+ }
1164
+ };
1165
+
1166
+ // Paint a band of cells `h` tall and advance past it. `lines[i]` is what
1167
+ // column `i` draws here: its whole cell, or one slice of a row too tall to
1168
+ // fit — which is why `slice` is a parameter. It is what `vPad` measures
1169
+ // with, so the ink a slice lays down and the height it was given come from
1170
+ // one answer: a continuation starts flush, and only the slice that owns a
1171
+ // side strokes it.
1172
+ /**
1173
+ * @type {(canvas: Canvas, cells: any[], lines: any[][], h: number,
1174
+ * slice: import('./box.js').Slice, xOffsets: number[], widths: number[],
1175
+ * bg: any) => void}
1176
+ */
1177
+ let paintCells = (canvas, cells, lines, h, slice, xOffsets, widths, bg) => {
1178
+ if (bg) canvas.rect(bg, xOffsets[0], canvas.y - h, sum(widths), h);
1179
+ for (let [i, cell] of cells.entries()) {
1180
+ fillCell(canvas, cell, bg, xOffsets[i], canvas.y - h, widths[i], h);
1181
+ paintBox(canvas, xOffsets[i], canvas.y, widths[i], h, cell.style, null, slice);
1182
+ canvas.box(cell.path, xOffsets[i], canvas.y, widths[i], h);
1183
+ let y0 = canvas.y - (slice.first ? cell.inset.t : 0);
1184
+ writeCell(canvas, cell, lines[i], xOffsets[i], y0, widths[i]);
1185
+ }
1186
+ canvas.y -= h;
1187
+ canvas.fresh = false;
1188
+ };
1189
+
1190
+ /**
1191
+ * @type {(canvas: Canvas, row: TableRow, xOffsets: number[],
1192
+ * widths: number[], bg: any) => void}
1193
+ */
1194
+ let drawRow = (canvas, row, xOffsets, widths, bg) => {
1195
+ let yTop = canvas.y;
1196
+ paintCells(
1197
+ canvas,
1198
+ row.cells,
1199
+ row.cells.map((cell) => cell.lines),
1200
+ row.h,
1201
+ WHOLE,
1202
+ xOffsets,
1203
+ widths,
1204
+ bg,
1205
+ );
1206
+ paintBox(canvas, xOffsets[0], yTop, sum(widths), row.h, row.style, null);
1207
+ };
1208
+
1209
+ // The widest of each column's header, rows and totals, padding included.
1210
+ /** @type {(cols: any[], header: any[], rows: any[], totals: { cells: any[] }[]) => number[]} */
1211
+ let naturalWidths = (cols, header, rows, totals) =>
1212
+ cols.map((_, i) => {
1213
+ let widest = header[i].natural;
1214
+ for (let row of rows) widest = Math.max(widest, row.cells[i].natural);
1215
+ for (let row of totals) widest = Math.max(widest, row.cells[i].natural);
1216
+ return widest;
1217
+ });
1218
+
1219
+ /** @type {(values: number[]) => number} */
1220
+ let sum = (values) => values.reduce((total, value) => total + value, 0);
1221
+
1222
+ // An authored `width` percentage fixes its column; the rest share what is left
1223
+ // in proportion to their natural widths. Three cases, one return each — the
1224
+ // fall-through is all-authored within budget, honoured exactly.
1225
+ //
1226
+ // There is no over-commitment case: the engine rejects a document whose fixed
1227
+ // shares leave the width-less columns nothing, so `left` is positive whenever
1228
+ // an auto column exists and the fixed shares never exceed the content width.
1229
+ // That invariant is asserted rather than trusted — falling through with auto
1230
+ // columns unplaced would draw them at zero width, an invisible failure — the
1231
+ // same fail-loud posture as the measuring canvas's `newPage`.
1232
+ /** @type {(cols: any[], natural: number[], avail: number) => number[]} */
1233
+ let columnWidths = (cols, natural, avail) => {
1234
+ let fixed = cols.map((column) => Number.isFinite(column.width));
1235
+ if (!fixed.some(Boolean)) {
1236
+ // Natural widths always carry the cell padding, so the sum is never zero.
1237
+ let wanted = sum(natural);
1238
+ return natural.map((width) => (width * avail) / wanted);
1239
+ }
1240
+ let widths = cols.map((col, i) => (fixed[i] ? (col.width / 100) * avail : 0));
1241
+ let auto = natural.map((width, i) => (fixed[i] ? 0 : width));
1242
+ let left = avail - sum(widths);
1243
+ let wanted = sum(auto);
1244
+ // Room for the auto columns: they share what the fixed ones left.
1245
+ if (wanted > 0) {
1246
+ if (left <= 0) throw new Error("over-committed column widths reached the layout");
1247
+ return widths.map((width, i) => (fixed[i] ? width : (auto[i] * left) / wanted));
1248
+ }
1249
+ return widths;
1250
+ };
1251
+
1252
+ // One table being emitted: what `carryOver`, `sliced` and `put` all need.
1253
+ /**
1254
+ * @typedef {{ cells: any[], h: number, style?: any }} TableRow
1255
+ * @typedef {{ state: Flow, head: TableRow,
1256
+ * xOffsets: number[], widths: number[], x: number, avail: number }} Grid
1257
+ */
1258
+
1259
+ // Continue in a fresh column: a table that spans columns or pages restates its
1260
+ // headings, under whatever headers the open groups replay above them. Both
1261
+ // callers reach here on a column something is already drawn in, so the break
1262
+ // always breaks.
1263
+ //
1264
+ // The headings replay at a strip head as well as a page head, unlike a group
1265
+ // header (SCHEMA.md): a column of figures under nothing is unreadable, where a
1266
+ // group header is merely absent.
1267
+ /** @type {(row: { style?: any }) => any} */
1268
+ let rowFill = (row) => col((row.style || {}).background);
1269
+
1270
+ /** @type {(grid: Grid) => void} */
1271
+ let carryOver = (grid) => {
1272
+ advance(grid.state);
1273
+ rebase(grid);
1274
+ drawRow(grid.state.canvas, grid.head, grid.xOffsets, grid.widths, rowFill(grid.head));
1275
+ };
1276
+
1277
+ // Move the grid to the column the cursor is now in. A table's widths are its
1278
+ // own for its whole life -- every strip is the same width -- but where its
1279
+ // columns start is the strip's, so a break across strips rebases them.
1280
+ /** @type {(grid: Grid) => void} */
1281
+ let rebase = (grid) => {
1282
+ grid.x = originOf(grid.state);
1283
+ grid.avail = widthOf(grid.state);
1284
+ grid.xOffsets = grid.widths.map((_, i) => grid.x + sum(grid.widths.slice(0, i)));
1285
+ };
1286
+
1287
+ // The least of a row that can go on a page, so a slice always advances.
1288
+ /** @type {(canvas: Canvas, row: { cells: any[] }) => number} */
1289
+ let firstSlice = (canvas, row) =>
1290
+ Math.max(
1291
+ rowHeight(canvas),
1292
+ ...row.cells
1293
+ .filter((/** @type {any} */ cell) => cell.lines.length)
1294
+ .map((/** @type {any} */ cell) => cell.inset.t + cell.lines[0].h),
1295
+ );
1296
+
1297
+ /** @type {(cell: any, next: number[], i: number, used: number, cap: number) => any[]} */
1298
+ let takeLines = (cell, next, i, used, cap) => {
1299
+ /** @type {any[]} */
1300
+ let out = [];
1301
+ while (next[i] < cell.lines.length) {
1302
+ let h = cell.lines[next[i]].h;
1303
+ if (out.length && used + h > cap) break;
1304
+ used += h;
1305
+ out.push(cell.lines[next[i]++]);
1306
+ }
1307
+ return out;
1308
+ };
1309
+
1310
+ /** @type {(row: { cells: any[] }, next: number[]) => boolean} */
1311
+ let stillMore = (row, next) =>
1312
+ row.cells.some((/** @type {any} */ cell, /** @type {number} */ i) => next[i] < cell.lines.length);
1313
+
1314
+ // The vertical padding one slice of a cell owes, read off the same helper the
1315
+ // drawer strokes its sides from: the height a slice is given and the ink it
1316
+ // lays down cannot disagree, because there is nothing to keep in step.
1317
+ /** @type {(cell: any, slice: import('./box.js').Slice) => number} */
1318
+ let vPad = (cell, slice) => {
1319
+ let inset = sliceInset(cell.inset, slice);
1320
+ return inset.t + inset.b;
1321
+ };
1322
+
1323
+ /** @type {(taken: any[][], cells: any[], slice: import('./box.js').Slice) => number} */
1324
+ let sliceH = (taken, cells, slice) => {
1325
+ let h = 0;
1326
+ for (let [i, lines] of taken.entries()) {
1327
+ let cellH = vPad(cells[i], slice) + heightOf(lines);
1328
+ if (cellH > h) h = cellH;
1329
+ }
1330
+ return h;
1331
+ };
1332
+
1333
+ // A row no page can hold degrades to plain flow: each page takes what fits of
1334
+ // each column, always at least one line, with the header replayed between.
1335
+ // Each column reserves its own bottom inset out of the room, the same trade
1336
+ // `sliceFloor` makes for an item: a slice that stops short leaves whitespace,
1337
+ // one that does not leaves a bottom border under the bottom margin.
1338
+ //
1339
+ // The row's own box slices with the cells rather than going missing, which is
1340
+ // what it did while `drawRow` was the only caller painting it.
1341
+ /** @type {(grid: Grid, row: TableRow, bg: any) => void} */
1342
+ let sliced = (grid, row, bg) => {
1343
+ let canvas = grid.state.canvas;
1344
+ let next = row.cells.map(() => 0); // first line of each column not yet placed
1345
+ let first = true;
1346
+ for (;;) {
1347
+ let cap = canvas.y - canvas.bottom;
1348
+ let taken = row.cells.map((/** @type {any} */ cell, /** @type {number} */ i) =>
1349
+ takeLines(cell, next, i, first ? cell.inset.t : 0, cap - cell.inset.b),
1350
+ );
1351
+ let slice = { first, more: stillMore(row, next) };
1352
+ let h = sliceH(taken, row.cells, slice);
1353
+ let yTop = canvas.y;
1354
+ paintCells(canvas, row.cells, taken, h, slice, grid.xOffsets, grid.widths, bg);
1355
+ paintBox(canvas, grid.xOffsets[0], yTop, sum(grid.widths), h, row.style, null, slice);
1356
+ if (!slice.more) return;
1357
+ carryOver(grid);
1358
+ first = false;
1359
+ }
1360
+ };
1361
+
1362
+ /** @type {(grid: Grid) => number} */
1363
+ let pageCap = (grid) =>
1364
+ ceilOf(grid.state) - carried(grid.state) - floorOf(grid.state) - grid.head.h;
1365
+
1366
+ /** @type {(row: { h: number }, keep: number, cap: number) => number} */
1367
+ let companion = (row, keep, cap) => (row.h + keep > cap ? 0 : keep);
1368
+
1369
+ /** @type {(canvas: Canvas, row: any, cap: number, floor: number) => boolean} */
1370
+ let worthBreak = (canvas, row, cap, floor) =>
1371
+ row.h <= cap || canvas.y - floor < Math.min(firstSlice(canvas, row), cap);
1372
+
1373
+ /** @type {(state: Flow, row: any, keep: number, cap: number) => boolean} */
1374
+ let shouldBreak = (state, row, keep, cap) => {
1375
+ let canvas = state.canvas,
1376
+ floor = floorOf(state);
1377
+ return canvas.y - row.h - keep < floor && worthBreak(canvas, row, cap, floor) && !freshOf(state);
1378
+ };
1379
+
1380
+ // Place one row, breaking first when that helps. `keep` is a companion row it
1381
+ // must not be parted from — the total riding with the last data row.
1382
+ /**
1383
+ * @type {(grid: Grid, row: TableRow, bg: any,
1384
+ * keep?: number) => void}
1385
+ */
1386
+ let put = (grid, row, bg, keep = 0) => {
1387
+ let canvas = grid.state.canvas;
1388
+ // The tallest row a fresh column holds, after the replayed group headers and
1389
+ // the replayed column headings. A companion over that can never share a
1390
+ // column, so it stops being a reason to break.
1391
+ let cap = pageCap(grid);
1392
+ keep = companion(row, keep, cap);
1393
+ // A row that will be sliced anyway is only worth breaking for while this
1394
+ // column cannot take even its first slice — beyond that a break buys nothing
1395
+ // and leaves a headings-only column. Clamped to `cap`: a first slice no fresh
1396
+ // column could fit either is no reason to go looking for one.
1397
+ let broke = shouldBreak(grid.state, row, keep, cap);
1398
+ if (broke) carryOver(grid);
1399
+ if (canvas.y - row.h < canvas.bottom) return sliced(grid, row, bg);
1400
+ drawRow(canvas, row, grid.xOffsets, grid.widths, bg);
1401
+ };
1402
+
1403
+ /** @type {(canvas: Canvas, row: any) => { cells: any[], style: any }} */
1404
+ let bodyRow = (canvas, row) => ({
1405
+ cells: row.cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, row.style)),
1406
+ style: row.style,
1407
+ });
1408
+
1409
+ /** @type {(canvas: Canvas, buffered: any) => any} */
1410
+ let totalRowsOf = (canvas, buffered) =>
1411
+ buffered.totals.map((/** @type {any} */ row) => ({
1412
+ cells: row.cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, row.style)),
1413
+ style: row.style,
1414
+ }));
1415
+
1416
+ /** @type {(laid: any[], totalRows: any[]) => number} */
1417
+ let firstH = (laid, totalRows) => (laid.length ? laid[0].h : totalRows.length ? totalRows[0].h : 0);
1418
+
1419
+ /** @type {(totalRows: any[]) => number} */
1420
+ let totalsH = (totalRows) => totalRows.reduce((h, row) => h + row.h, 0);
1421
+
1422
+ /** @type {(grid: Grid, laid: any[], totalRows: any[]) => void} */
1423
+ let emitRows = (grid, laid, totalRows) => {
1424
+ let last = laid.length - 1;
1425
+ let keep = totalsH(totalRows);
1426
+ for (let [i, row] of laid.entries()) put(grid, row, rowFill(row), i === last ? keep : 0);
1427
+ for (let row of totalRows) put(grid, row, rowFill(row), 0);
1428
+ };
1429
+
1430
+ // Lay out a buffered table: measure every cell, allocate the columns, then
1431
+ // emit the header, the rows and the total, breaking pages as needed.
1432
+ // A buffered table, in the one shape `table` lays out: its columns come from
1433
+ // the opening event, its rows are the row events themselves, and its total is
1434
+ // the total row's cells. Both readers build it here — the replay, collecting
1435
+ // events as they arrive, and the region buffer, reading them back off what it
1436
+ // held — so a new table event kind cannot reach one and miss the other.
1437
+ /** @type {(events: any[]) => any} */
1438
+ let tableOf = (events) => {
1439
+ let opening = events[0];
1440
+ let totals = events.filter((event) => event.type === "total-row");
1441
+ return {
1442
+ // A header cell arrives without a path of its own; the column's is what
1443
+ // its box names, so it rides on the cell from here, where both readers
1444
+ // assemble the table.
1445
+ columns: opening.columns.map((/** @type {any} */ col) => ({
1446
+ ...col,
1447
+ header: { ...col.header, path: col.path },
1448
+ })),
1449
+ headerStyle: opening.style,
1450
+ rows: events.filter((event) => event.type === "row"),
1451
+ totals: totals.map((row) => ({ cells: row.cells, style: row.style })),
1452
+ };
1453
+ };
1454
+
1455
+ /**
1456
+ * @typedef {{ widths: number[], head: any, laid: any[], totalRows: any[] }} Measured
1457
+ */
1458
+ // Measure a buffered table at a given width: the column widths every cell has
1459
+ // a say in, then each row laid out against them. Everything here is
1460
+ // arithmetic over the cells, so the region's buffer can ask for it before
1461
+ // anything is drawn — which is the only way a row's real height is known
1462
+ // early enough to balance on (bead `quario-cgk`).
1463
+ /** @type {(canvas: Canvas, buffered: any, avail: number) => Measured} */
1464
+ let measureTable = (canvas, buffered, avail) => {
1465
+ let cols = /** @type {any[]} */ (buffered.columns);
1466
+ let header = cols.map((/** @type {any} */ col) =>
1467
+ cellOf(canvas, col.header, buffered.headerStyle),
1468
+ );
1469
+ let rows = buffered.rows.map((/** @type {any} */ row) => bodyRow(canvas, row));
1470
+ let totals = totalRowsOf(canvas, buffered);
1471
+ let widths = columnWidths(cols, naturalWidths(cols, header, rows, totals), avail);
1472
+ return {
1473
+ widths,
1474
+ head: { ...rowOf(canvas, header, widths), style: buffered.headerStyle },
1475
+ laid: rows.map((/** @type {any} */ row) => ({
1476
+ ...rowOf(canvas, row.cells, widths),
1477
+ style: row.style,
1478
+ })),
1479
+ totalRows: totals.map((/** @type {any} */ row) => ({
1480
+ ...rowOf(canvas, row.cells, widths),
1481
+ style: row.style,
1482
+ })),
1483
+ };
1484
+ };
1485
+
1486
+ /** @type {(state: Flow, buffered: any) => void} */
1487
+ let table = (state, buffered) => {
1488
+ let canvas = state.canvas;
1489
+ // A table inside a region is measured against the strip, not the page: an
1490
+ // authored `width` percentage is a share of the column it lands in
1491
+ // (SCHEMA.md, "Page columns").
1492
+ let avail = widthOf(state);
1493
+ let x = originOf(state);
1494
+ // Measured already if a region buffered this table: the balance model needed
1495
+ // its real row heights, at this same width, and that is the same
1496
+ // measurement — so the replay draws it without wrapping every cell again.
1497
+ let { widths, head, laid, totalRows } =
1498
+ state.measured.get(buffered.opening) || measureTable(canvas, buffered, avail);
1499
+ let xOffsets = widths.map((_, i) => x + sum(widths.slice(0, i)));
1500
+
1501
+ /** @type {Grid} */
1502
+ let grid = { state, head, xOffsets, widths, x, avail };
1503
+
1504
+ // A pending group header keeps the table's header row and its first row.
1505
+ flush(state, grid.head.h + firstH(laid, totalRows));
1506
+ if (canvas.y - grid.head.h < floorOf(state)) {
1507
+ advance(state);
1508
+ rebase(grid);
1509
+ }
1510
+ drawRow(canvas, grid.head, grid.xOffsets, widths, rowFill(grid.head));
1511
+ // The last data row keeps the whole emitted total block with it, so a
1512
+ // total is never stranded alone at a page top.
1513
+ emitRows(grid, laid, totalRows);
1514
+ };
1515
+
1516
+ // --- page bands -------------------------------------------------------------
1517
+
1518
+ // Lay page band items out at a fixed spot (no page breaks): used per page
1519
+ // with the real `{ number, total }`, and once as a probe to reserve height.
1520
+ /** @type {(canvas: Canvas, items: any[], yTop: number) => number} */
1521
+ let band = (canvas, items, yTop) => {
1522
+ let avail = canvas.content;
1523
+ let saved = canvas.y,
1524
+ fresh = canvas.fresh;
1525
+ canvas.y = yTop;
1526
+ canvas.fresh = false;
1527
+ // Folded straight into the draw, the way the body walk folds straight into
1528
+ // `route`: the folder emits in order and nothing here needs the band whole.
1529
+ let fold = splitFolder((event) => {
1530
+ skipFlow({ canvas, region: null }, event, "spaceBefore");
1531
+ drawBlock(canvas, blockOf(canvas, event, avail), canvas.margin, avail);
1532
+ skipFlow({ canvas, region: null }, event, "spaceAfter");
1533
+ });
1534
+ for (let event of items) fold(event);
1535
+ let h = yTop - canvas.y;
1536
+ canvas.y = saved;
1537
+ canvas.fresh = fresh;
1538
+ return h;
1539
+ };
1540
+
1541
+ // Measure a band without drawing it, to reserve its height. The trial render
1542
+ // goes onto a measuring canvas of its own, which can draw nothing and holds
1543
+ // nothing through which a document could be reached — so it runs while a live
1544
+ // canvas is open on one, with no live state for it to disturb.
1545
+ /** @type {(geo: Frame, fonts: any, items: any[]) => number} */
1546
+ let probe = (geo, fonts, items) => band(measuring(geo, fonts), items, 0);
1547
+
1548
+ // The pages every band is probed on, and the whole of what reservation assumes
1549
+ // about where a band varies. Two anchors cover each visibility edge — a band
1550
+ // hidden on the cover (`visible: '=page.number > 1'`) and a cover-only
1551
+ // masthead alike reserve their true height — and the total of two is a
1552
+ // stand-in for a count no one knows yet, which SCHEMA.md's cover-page note is
1553
+ // about: a band that varies with the real page count, in visibility or in
1554
+ // height, is measured as though the document were two pages long.
1555
+ let PROBES = [
1556
+ { number: 1, total: 2 },
1557
+ { number: 2, total: 2 },
1558
+ ];
1559
+
1560
+ /**
1561
+ * The content box a render's body flows through: the page box less what its
1562
+ * page bands need, each band's height plus the gap that separates it from the
1563
+ * body. A declared band costs that gap whether or not it measured anything —
1564
+ * the gap belongs to the declaration, and a band drawing nothing on either
1565
+ * probed page may still draw on page three.
1566
+ *
1567
+ * Pure, and measured against the page box rather than a live canvas: what a
1568
+ * band needs is a property of the page.
1569
+ *
1570
+ * @type {(geo: Frame, fonts: any, bands: any) => Frame}
1571
+ */
1572
+ let reserve = (geo, fonts, bands) => {
1573
+ let height = (/** @type {(page: any) => any[]} */ items) =>
1574
+ Math.max(...PROBES.map((page) => probe(geo, fonts, items(page))));
1575
+ return {
1576
+ ...geo,
1577
+ top: geo.top - (bands.header ? height(bands.header) + BAND : 0),
1578
+ bottom: geo.bottom + (bands.footer ? height(bands.footer) + BAND : 0),
1579
+ };
1580
+ };
1581
+
1582
+ // The page box a canvas presents, before any band narrowed it. `frame` owns how
1583
+ // `top`/`bottom` fall out of a page and a margin — "so no caller and no suite
1584
+ // has to restate it", as it says next door — and both halves of page furniture
1585
+ // want it: reservation to measure against, the draw pass to hang the header
1586
+ // from.
1587
+ /** @type {(canvas: Canvas) => Frame} */
1588
+ let pageBox = (canvas) =>
1589
+ Object.assign(frame(canvas.width, canvas.height, canvas.margin, canvas.base, canvas.family), {
1590
+ locale: canvas.locale,
1591
+ currency: canvas.currency,
1592
+ timeZone: canvas.timeZone,
1593
+ });
1594
+
1595
+ // One finished page's furniture, drawn in the strips `reserve` left for it:
1596
+ // the header hanging from the top margin, the footer resting on the bottom
1597
+ // one, a gap between each and the body either way. The other half of that
1598
+ // constant lives directly above, which is the point of the two sitting here
1599
+ // together — the pass that draws the bands cannot drift from the reservation
1600
+ // that made room for them.
1601
+ /** @type {(canvas: Canvas, bands: any, anchor: any) => void} */
1602
+ let furniture = (canvas, bands, anchor) => {
1603
+ if (bands.header) band(canvas, bands.header(anchor), pageBox(canvas).top);
1604
+ if (bands.footer) band(canvas, bands.footer(anchor), canvas.bottom - BAND);
1605
+ };
1606
+
1607
+ // One `{ number, total }` per finished page, from the sequence starts the
1608
+ // band flow recorded. A document with no `reset` is one sequence; each
1609
+ // resetting instance starts another at the page it opened.
1610
+ /** @type {(count: number, starts: number[]) => { number: number, total: number }[]} */
1611
+ let numbered = (count, starts) => {
1612
+ let pages = Array(count);
1613
+ let ends = starts.concat(count);
1614
+ for (let s = 0; s < starts.length; s++) {
1615
+ let lo = ends[s];
1616
+ let total = ends[s + 1] - lo;
1617
+ for (let i = lo; i < lo + total; i++) pages[i] = { number: i - lo + 1, total };
1618
+ }
1619
+ return pages;
1620
+ };
1621
+
1622
+ // --- the band flow ----------------------------------------------------------
1623
+
1624
+ /**
1625
+ * The band flow (CONTEXT.md): one render's consumer of the banded walk. It owns
1626
+ * the placement state — what is held back, what a fresh page owes the groups
1627
+ * still open, what is owed before the next instance, what is not yet anchored —
1628
+ * and hands out the handlers the walk driver dispatches to, the opening event's
1629
+ * among them: the page bands come off the content box there,
1630
+ * while the driver still guarantees no second event has been pulled and this
1631
+ * band flow's own first page is open and empty.
1632
+ *
1633
+ * `finish` flushes whatever the last event left pending and returns what the
1634
+ * passes over the finished pages still need: the outline marks — the only way
1635
+ * to reach them, so an outline read off a band flow that still owes a flush is
1636
+ * not a mistake this seam leaves open — the opening event it settled,
1637
+ * handed back whole rather than read into, and the per-page `{ number, total }`
1638
+ * anchors the furniture pass draws with.
1639
+ *
1640
+ * @type {(canvas: Canvas) => { handlers: Record<string, (event: any) => void>,
1641
+ * finish: () => { marks: any[], opening: any,
1642
+ * pages: { number: number, total: number }[] } }}
1643
+ */
1644
+ let flow = (canvas) => {
1645
+ /** @type {Flow} */
1646
+ let state = {
1647
+ canvas,
1648
+ open: [],
1649
+ gap: 0,
1650
+ marks: [],
1651
+ starts: [0],
1652
+ region: null,
1653
+ pending: null,
1654
+ // What a region already measured, keyed by the table's opening event, so
1655
+ // the replay draws that table without wrapping every cell a second time.
1656
+ // Per flow, because the keys are one render's events.
1657
+ measured: new WeakMap(),
1658
+ pin: null,
1659
+ skipGap: false,
1660
+ inHeader: false,
1661
+ // Assigned below, once `route` exists: a buffered region replays through
1662
+ // it, and one entry point is what makes the replay take the path the
1663
+ // content would have taken had it never been held.
1664
+ route: () => {},
1665
+ };
1666
+ // The body walk's own folder, emitting each finished event onward to `route`
1667
+ // below. Declared here and bound after `route` exists.
1668
+ /** @type {(event: any) => void} */
1669
+ let fold;
1670
+ // A table is buffered whole before it is laid out: column widths come from
1671
+ // every cell in it, so the last row has to be in hand first.
1672
+ /** @type {any[]} */
1673
+ let collected = [];
1674
+ // The opening event, kept for the caller: this band flow reserves the page
1675
+ // bands off it, and the passes over the finished pages want the rest.
1676
+ /** @type {any} */
1677
+ let opening = null;
1678
+ // The body opens its own first page. Not through `turn`: a canvas that has
1679
+ // drawn nothing is fresh already, and would be left with no page at all.
1680
+ canvas.newPage();
1681
+
1682
+ // What each event does once it is placed for real. A buffered region replays
1683
+ // through this same table, so held content lays out by exactly the path it
1684
+ // would have taken had it never been held.
1685
+ // A block with no title of its own: held back under a group header, placed
1686
+ // anywhere else. Both the image and the split flow this way.
1687
+ /** @type {(event: any, block: Block | null) => void} */
1688
+ let heldOrPlaced = (event, block) =>
1689
+ event.role === "group-header"
1690
+ ? holdHeader(state, event, null, block)
1691
+ : placeItem(state, event, block);
1692
+
1693
+ /** @type {Record<string, (event: any, block: Block | null) => void>} */
1694
+ let placed = {
1695
+ // A header is held rather than placed, and holding is bookkeeping the
1696
+ // replay has to see in its original order — which is why it routes like
1697
+ // everything else instead of reaching the open stack straight away.
1698
+ item: (event, block) =>
1699
+ event.role === "group-header"
1700
+ ? holdHeader(state, event, text(event.tokens), block)
1701
+ : placeItem(state, event, block),
1702
+ // An image flows exactly as an item does, held back in a group header and
1703
+ // placed anywhere else -- the block it becomes carries a height like any
1704
+ // other. It wears no role default: those describe text.
1705
+ image: heldOrPlaced,
1706
+ // A split flows exactly as an image does, for the same reason: one block,
1707
+ // held back in a group header and placed anywhere else. It wears no role
1708
+ // default of its own -- its slots carry theirs.
1709
+ split: heldOrPlaced,
1710
+ "group-start": (event) => openGroup(state, event),
1711
+ "group-end": () => {
1712
+ flush(state, 0);
1713
+ state.open.pop();
1714
+ },
1715
+ // A table is collected as the events it arrived as, and shaped by `tableOf`
1716
+ // once its last one lands — the same constructor the region buffer reads
1717
+ // its own held events back through.
1718
+ "table-start": (event) => void (collected = [event]),
1719
+ row: (event) => void collected.push(event),
1720
+ "total-row": (event) => void collected.push(event),
1721
+ "table-end": () => {
1722
+ table(state, { ...tableOf(collected), opening: collected[0] });
1723
+ collected = [];
1724
+ },
1725
+ };
1726
+ /** @type {(event: any, block: Block | null) => void} */
1727
+ let place = (event, block) => placed[event.type](event, block);
1728
+
1729
+ // Which depth owes or holds the strips, whether or not any content has
1730
+ // opened them yet: an instance that emits nothing still closes with a
1731
+ // full-width footer, and measuring that at a strip width would wrap it to a
1732
+ // column it is not drawn in.
1733
+ let owner = () => (state.region ? state.region.owner : state.pending ? state.pending.owner : -2);
1734
+ // The nesting the router is at. While the buffer holds, routing runs ahead of
1735
+ // placement: nothing has been placed, so `state.open` is frozen at the depth
1736
+ // the region opened at and only the buffer knows where the walk really is.
1737
+ // Once `commit` nulls `held`, the replay *is* the placement timeline and the
1738
+ // open stack is exact again.
1739
+ let nesting = () =>
1740
+ state.region && state.region.held ? state.region.nesting : state.open.length - 1;
1741
+ /** @type {(event: any) => boolean} */
1742
+ let ownFooter = (event) => event.role === "group-footer" && owner() === nesting();
1743
+
1744
+ /** @type {(event: any) => boolean} */
1745
+ let stillHeader = (event) =>
1746
+ event.role === "report-header" || (event.type === "split-end" && state.inHeader);
1747
+ /** @type {(event: any) => void} */
1748
+ let snapPin = (event) => {
1749
+ if (state.pin == null) return;
1750
+ if (stillHeader(event)) {
1751
+ state.inHeader = true;
1752
+ return;
1753
+ }
1754
+ if (state.canvas.y < state.pin) throw Error("header: content taller than height");
1755
+ state.canvas.y = state.pin;
1756
+ state.pin = null;
1757
+ state.inHeader = false;
1758
+ };
1759
+
1760
+ // Route one event: the declaring node's own bands stay full-width, region
1761
+ // content opens the strips owed to it, and anything arriving while a region
1762
+ // is still deciding is buffered rather than placed.
1763
+ /** @type {(event: any, block?: Block | null) => void} */
1764
+ let route = (event, block = null) => {
1765
+ snapPin(event);
1766
+ if (fullBand(event)) fullWidth(event, block);
1767
+ else if (deciding()) buffer(state, event, block);
1768
+ else if (opens(event)) begin(event);
1769
+ else place(event, block);
1770
+ };
1771
+
1772
+ state.route = route;
1773
+ fold = splitFolder(route);
1774
+
1775
+ // Everything drawn across the page rather than down a strip: the declaring
1776
+ // node's own bands, its footer among them.
1777
+ /** @type {(event: any) => boolean} */
1778
+ let fullBand = (event) => ownFooter(event) || isReportBand(event.role);
1779
+
1780
+ // Does this event open the strips a `columns` count owes? A header does not:
1781
+ // it is held, never placed, so the run it belongs to is drawn by the flush
1782
+ // its own content triggers — which is what leaves a columned node's header
1783
+ // above the strips.
1784
+ /** @type {(event: any) => boolean} */
1785
+ let opens = (event) => Boolean(state.pending) && event.role !== "group-header";
1786
+
1787
+ // The instance that owns the strips is the one ending here, so they close
1788
+ // before anything resumes full-width under them. A count the instance never
1789
+ // reached any content for is simply dropped.
1790
+ /** @type {(depth: number) => void} */
1791
+ let endOwned = (depth) => {
1792
+ if (owner() !== depth) return;
1793
+ if (state.region) closeRegion(state);
1794
+ else state.pending = null;
1795
+ };
1796
+
1797
+ // Is the open region still buffering, rather than laying anything out?
1798
+ let deciding = () => Boolean(state.region && state.region.held);
1799
+
1800
+ // The declaring node's own bands, above and below its strips. A count still
1801
+ // owed survives a report header — that band arrives before the body it
1802
+ // columns — but not the owner's own footer, which is the end of what it
1803
+ // would have columned.
1804
+ /** @type {(event: any, block: Block | null) => void} */
1805
+ let fullWidth = (event, block) => {
1806
+ // Asked before the region closes, because that is what it reads.
1807
+ let own = ownFooter(event);
1808
+ closeRegion(state);
1809
+ if (own) state.pending = null;
1810
+ place(event, block);
1811
+ };
1812
+
1813
+ // The first content of a columned node. The node's own header run is drawn
1814
+ // full-width here, before the region exists, which is what keeps it above
1815
+ // the strips; the region then opens at the cursor that left behind. The run
1816
+ // still keeps company with the content it introduces, measured at the strip
1817
+ // width because that is where the content is about to land.
1818
+ /** @type {(event: any) => void} */
1819
+ let begin = (event) => {
1820
+ let owed = /** @type {Pending} */ (state.pending);
1821
+ let block = keepBlock(state, event, stripWidth(state, owed.count));
1822
+ flush(state, block ? keepWith(block) : 0);
1823
+ openRegion(state);
1824
+ buffer(state, event, block);
1825
+ };
1826
+
1827
+ // The report default onto the canvas, before anything is measured. It is
1828
+ // narrowed to `family` and `size`, and each replaces this target's own
1829
+ // baseline outright: row heights and band gaps scale with the document's
1830
+ // type rather than staying at a size nothing is set in, and text declaring
1831
+ // no family is set in the document's. Lifting the pair here is the whole of
1832
+ // this target's reading of docs/adr/0033 -- the default reaches a node as a
1833
+ // fallback the canvas carries, never as a layer merged into its style, so a
1834
+ // document-wide fact costs no allocation however many cells a report has.
1835
+ //
1836
+ // `sizeOf` and `familyName` are this target's one reading each of what a
1837
+ // declared size and family amount to, so they read the default here too --
1838
+ // both reach this unchecked from a computed style, and two spellings of that
1839
+ // leniency would drift. Each falls back to what the canvas already carries,
1840
+ // so a default declaring one of the pair leaves the other alone, and one
1841
+ // whose value is unusable leaves this target's own baseline standing.
1842
+ /** @type {(style: any) => void} */
1843
+ let adoptDefault = (style) => {
1844
+ if (!style) return;
1845
+ canvas.base = sizeOf(style, canvas.base);
1846
+ canvas.family = familyName(style) || canvas.family;
1847
+ };
1848
+
1849
+ /** @type {(event: any) => void} */
1850
+ let pinHeader = (event) => {
1851
+ if (event.headerHeight == null) return;
1852
+ let pin = canvas.y - event.headerHeight;
1853
+ if (pin < canvas.bottom) throw Error("header: height exceeds the first page's body");
1854
+ state.pin = pin;
1855
+ state.skipGap = true;
1856
+ state.inHeader = true;
1857
+ };
1858
+
1859
+ return {
1860
+ handlers: {
1861
+ "report-start": (event) => {
1862
+ opening = event;
1863
+ canvas.locale = event.locale;
1864
+ canvas.currency = event.currency;
1865
+ canvas.timeZone = event.timeZone;
1866
+ adoptDefault(event.style);
1867
+ if (event.columns) state.pending = { count: event.columns, owner: -1 };
1868
+ if (!event.page) {
1869
+ pinHeader(event);
1870
+ return;
1871
+ }
1872
+ // A frame of its own, never the canvas itself: `measuring` builds
1873
+ // from whatever it is handed, so handing it this canvas would spread
1874
+ // the page passes onto a measurement — and reaching a document is the
1875
+ // one thing a measurement must not be able to do. What a band needs is
1876
+ // a property of the page box in any case, not of a content box
1877
+ // something may already have narrowed.
1878
+ adopt(canvas, reserve(pageBox(canvas), canvas.fonts, event.page));
1879
+ pinHeader(event);
1880
+ },
1881
+ // Everything else routes: `placed` above says what each event does, and
1882
+ // one entry point is what lets a buffered region replay through it. The
1883
+ // three the folder owns are left out — `split` is not a stream event at
1884
+ // all (the folder mints it) and item and image reach `route` through it.
1885
+ ...Object.fromEntries(
1886
+ Object.keys(placed)
1887
+ .filter((type) => !BLOCKS.has(type))
1888
+ .map((type) => [type, (/** @type {any} */ event) => route(event)]),
1889
+ ),
1890
+ // Item, image and both bracket events all go through the folder, which
1891
+ // routes what is not inside a split straight onward. The item is the one
1892
+ // that wears a band-role default on the way in.
1893
+ "split-start": (event) => fold(event),
1894
+ "split-end": (event) => fold(event),
1895
+ item: (event) => fold(roled(event)),
1896
+ image: (event) => fold(event),
1897
+ "group-end": (event) => {
1898
+ endOwned(event.depth);
1899
+ route(event);
1900
+ },
1901
+ },
1902
+ finish: () => {
1903
+ snapPin({ type: "report-end" });
1904
+ // A root region reaches here undrained when a report declares `columns`
1905
+ // and no full-width footer followed its body.
1906
+ closeRegion(state);
1907
+ flush(state, 0);
1908
+ return { marks: state.marks, opening, pages: numbered(canvas.count, state.starts) };
1909
+ },
1910
+ };
1911
+ };
1912
+
1913
+ export { flow, furniture };