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