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