@quario/layout 0.1.0 → 0.3.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 CHANGED
@@ -21,40 +21,43 @@
21
21
  * document's bands claim before anything is placed, and where they are drawn
22
22
  * on each finished page once the count is known.
23
23
  */
24
- import { display, isReportBand, text } from "quario";
24
+ import { display, imageError, isReportBand, text } from "quario";
25
25
  import { balance } from "./balance.js";
26
- import { adopt, frame, measuring } from "./canvas.js";
27
- import { familyName } from "./fonts.js";
26
+ import { adopt, measuring } from "./canvas.js";
27
+ import { frame } from "./page.js";
28
28
  import { intrinsic } from "./image.js";
29
- import { CELL_PAD, NO_PAD, WHOLE, insetOf, paintBox, sliceInset, unbox } from "./box.js";
30
- import { BAND, GUTTER, LEAD, col, merge, roled, shift, sizeOf } from "./style.js";
29
+ import { CELL_PAD, NO_PAD, WHOLE, insetOf, isWhole, paintBox, sliceInset, unbox } from "./box.js";
30
+ import { BAND, col, GUTTER, LEAD, merge, roled, shift, sizeOf, vshift } from "./style.js";
31
31
  import { atoms, dress, heightOf, wrap } from "./text.js";
32
32
 
33
33
  /** @typedef {import('./balance.js').Unit} Unit */
34
34
  /** @typedef {import('./canvas.js').Canvas} Canvas */
35
- /** @typedef {import('./canvas.js').Frame} Frame */
35
+ /** @typedef {import('./page.js').Frame} Frame */
36
+ /** @typedef {import('./settings.js').Settings} Settings */
36
37
  /** @typedef {import('./text.js').Line} Line */
37
38
 
38
39
  // `open` is a stack, one entry per group instance still open: the header items
39
40
  // 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).
41
+ // a page turn replays them from. Each entry also carries the `gap` and `skip`
42
+ // that stood before it opened, which is what a hollow instance puts back.
43
+ // `gap` is the space owed before the next group instance, `marks` the outline
44
+ // entries, which `anchor` positions from the cursor, and `starts` the page
45
+ // indexes where a `reset: "page"` sequence begins (the document itself is the
46
+ // sequence that starts at page 0).
44
47
  /**
45
- * @typedef {{ held: any[], blocks: Block[] }} Group
48
+ * @typedef {{ held: any[], blocks: Block[], gap: number, skip: boolean }} Group
46
49
  */
47
50
  /**
48
51
  * @typedef {{ canvas: Canvas, open: Group[], gap: number, marks: any[],
49
52
  * 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
+ * pin: number | null, skipGap: boolean, inHeader: boolean }} Flow
53
54
  */
54
55
  /**
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.
56
+ * Where the cursor is and whether a strip is open — the whole of what
57
+ * page-vs-strip geometry depends on, and less than the whole flow. The five
58
+ * accessors under "the cursor" below take this, and so do the flow-spacing
59
+ * helpers beside them, which is how page furniture drives spacing off a canvas
60
+ * and no region at all.
58
61
  * @typedef {{ canvas: Canvas, region: Region | null }} Cursor
59
62
  */
60
63
 
@@ -69,13 +72,16 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
69
72
  * @typedef {{ bytes: Uint8Array, format: string, w: number, h: number }} Picture
70
73
  */
71
74
  /**
72
- * `align` is absent on a split, which has none of its own: alignment is a
73
- * slot's, reached through the style layering.
75
+ * `align` and `valign` are absent on a split, which has none of its own:
76
+ * alignment is a slot's, reached through the style layering.
77
+ * `drop` is how far below the top of the box the content starts: zero for a
78
+ * box that is its content's height, and for a slot the slack its `valign`
79
+ * reads once `splitBlock` has handed the split's height down.
74
80
  * `path` names the schema node the block draws, when the event carried one:
75
81
  * the box the canvas records for the hit-test.
76
82
  * @typedef {{ lines: Line[], picture?: Picture, parts?: SlotPart[],
77
- * bg: import('./style.js').Color | null, align?: any, h: number, style?: any,
78
- * path?: string,
83
+ * bg: import('./style.js').Color | null, align?: any, valign?: any,
84
+ * drop: number, h: number, style?: any, path?: string,
79
85
  * inset: { t: number, r: number, b: number, l: number } }} Block
80
86
  */
81
87
  /**
@@ -85,15 +91,36 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
85
91
  * @typedef {{ block: Block, w: number }} SlotPart
86
92
  */
87
93
 
94
+ // A block no page break may pass through, and the negation of `slice`. The
95
+ // region balancer answers the same question its own way, keyed on the event
96
+ // rather than the block (`UNITS`), so a new kind of one belongs there too.
97
+ /** @type {(block: Block) => boolean} */
98
+ let unsliceable = (block) => Boolean(block.picture || block.parts);
99
+
88
100
  // How `fit` sizes a picture against the width it has: `natural` is the
89
101
  // image's own size in points, never wider than the content box; `width`
90
102
  // scales to the content box either way. The ratio is kept in both, so a
91
103
  // height is never anything but the width's consequence.
92
104
  /** @type {(event: any, avail: number) => Picture} */
93
105
  let pictureOf = (event, avail) => {
94
- let { w, h } = intrinsic(event.bytes, event.format);
106
+ // Located here rather than in `image.js`, which is handed bytes and a
107
+ // format and has never heard of a schema: this is the innermost place that
108
+ // knows both what went wrong and which item asked for it. SCHEMA.md calls
109
+ // this failure a render error, and a render error names its node.
110
+ let sized;
111
+ try {
112
+ sized = intrinsic(event.bytes, event.format);
113
+ } catch (cause) {
114
+ throw imageError(event.path, /** @type {Error} */ (cause).message, cause);
115
+ }
116
+ let { w, h } = sized;
95
117
  let width = event.fit === "width" ? avail : Math.min(w, avail);
96
- return { bytes: event.bytes, format: event.format, w: width, h: (h * width) / w };
118
+ return {
119
+ bytes: event.bytes,
120
+ format: event.format,
121
+ w: width,
122
+ h: (h * width) / w,
123
+ };
97
124
  };
98
125
 
99
126
  // `under` is the enclosing style a slot's own layers over — a split's, when
@@ -113,18 +140,22 @@ let blockOf = (canvas, event, avail, under = null) => {
113
140
  picture,
114
141
  bg,
115
142
  align: text.align,
143
+ valign: text.valign,
144
+ drop: 0,
116
145
  h: picture.h + inset.t + inset.b,
117
146
  style: box,
118
147
  path: event.path,
119
148
  inset,
120
149
  };
121
150
  }
122
- let size = sizeOf(text, canvas.base);
123
- let lines = dress(wrap(canvas, atoms(canvas, event.tokens, text), inner, size), text);
151
+ let size = sizeOf(text, canvas.settings.base);
152
+ let lines = dress(wrap(atoms(canvas.settings, event.tokens, text), inner, size), text);
124
153
  return {
125
154
  lines,
126
155
  bg,
127
156
  align: text.align,
157
+ valign: text.valign,
158
+ drop: 0,
128
159
  h: heightOf(lines) + inset.t + inset.b,
129
160
  style: box,
130
161
  path: event.path,
@@ -155,7 +186,15 @@ let drawPicture = (canvas, block, x, avail) => {
155
186
  let at = x + shift(block.align, avail - outer);
156
187
  paintBox(canvas, at, canvas.y, outer, block.h, block.style, block.bg);
157
188
  canvas.box(block.path, at, canvas.y, outer, block.h);
158
- canvas.picture(bytes, format, at + inset.l, canvas.y - inset.t - h, w, h);
189
+ canvas.picture(
190
+ block.path,
191
+ bytes,
192
+ format,
193
+ at + inset.l,
194
+ canvas.y - inset.t - block.drop - h,
195
+ w,
196
+ h,
197
+ );
159
198
  canvas.y -= block.h;
160
199
  canvas.fresh = false;
161
200
  };
@@ -175,7 +214,7 @@ let drawLines = (canvas, block, x, avail, slice = WHOLE) => {
175
214
  let inset = sliceInset(block.inset, slice);
176
215
  paintBox(canvas, x, canvas.y, avail, block.h, block.style, block.bg, slice);
177
216
  canvas.box(block.path, x, canvas.y, avail, block.h);
178
- let y = canvas.y - inset.t;
217
+ let y = canvas.y - inset.t - block.drop;
179
218
  let inner = Math.max(avail - inset.l - inset.r, 1);
180
219
  for (let line of block.lines) {
181
220
  canvas.drawLine(line, x + inset.l, y, inner, block.align);
@@ -196,12 +235,13 @@ let drawLines = (canvas, block, x, avail, slice = WHOLE) => {
196
235
  // table cell already follows, a cell box being painted at the row's height.
197
236
  // A shorter slot keeping its own height left daylight under every bordered
198
237
  // cell in a row whose slots were not sized alike. Only the height is handed
199
- // down: lines still start at the top of the box, and a picture is neither
200
- // scaled nor moved, `fit` having no vertical mode.
238
+ // down: a slot's lines start at the top of the box unless its `valign` reads
239
+ // the slack, and a picture is never scaled, `fit` having no vertical mode --
240
+ // `valign` moves it within the box, it does not fill it.
201
241
  // The split's own declarations are the layer under each slot's, so a split
202
242
  // styled `bold` reads the same here as it does through CSS inheritance in the
203
- // HTML target and the `under` layer in XLSX. It has no `align` of its own:
204
- // alignment is a slot's, reached through that layering.
243
+ // HTML target and the `under` layer in XLSX. It has no `align` or `valign` of
244
+ // its own: alignment is a slot's, reached through that layering.
205
245
  /**
206
246
  * @type {(canvas: Canvas, event: any, inner: number, text: any,
207
247
  * bg: any, box: any, inset: { t: number, r: number, b: number, l: number }) => Block}
@@ -216,8 +256,20 @@ let splitBlock = (canvas, event, inner, text, bg, box, inset) => {
216
256
  if (block.h > h) h = block.h;
217
257
  parts.push({ block, w: widths[i] });
218
258
  }
219
- for (let part of parts) part.block.h = h;
220
- return { lines: [], parts, bg, h: h + inset.t + inset.b, style: box, path: event.path, inset };
259
+ for (let part of parts) {
260
+ part.block.drop = vshift(part.block.valign, h - part.block.h);
261
+ part.block.h = h;
262
+ }
263
+ return {
264
+ lines: [],
265
+ parts,
266
+ bg,
267
+ drop: 0,
268
+ h: h + inset.t + inset.b,
269
+ style: box,
270
+ path: event.path,
271
+ inset,
272
+ };
221
273
  };
222
274
 
223
275
  /** @type {(slots: { width?: number }[], avail: number) => number[]} */
@@ -311,14 +363,24 @@ let drawBlock = (canvas, block, x, avail) =>
311
363
  * @typedef {{ count: number, owner: number }} Pending
312
364
  */
313
365
 
366
+ // --- the cursor -------------------------------------------------------------
367
+
368
+ // The five below are the whole of how anything reaches page-vs-strip geometry,
369
+ // and they take a `Cursor` — a canvas and the region open on it, if one is —
370
+ // because the region is the only thing that changes any of their answers. The
371
+ // drawing primitives take a bare `Canvas` and read it flat, and that is not an
372
+ // exception to this: a primitive draws where it is told and does not choose a
373
+ // column, so it has nothing to ask. Page furniture reads flat for the same
374
+ // reason — `band` places at the page's own margin and never inside a region.
375
+ // The flow-spacing helpers below take a `Cursor` too: `keepLead` asks
376
+ // `freshOf`, so the spacing and the geometry are one seam, not two.
377
+
314
378
  // Where the cursor's column starts and how wide it is: the strip's, inside a
315
- // region, and the page's content box outside one. Every drawing call reads
316
- // these rather than `canvas.margin`/`canvas.content` the canvas keeps
317
- // presenting the page, because `pageBox` re-derives page furniture from it.
318
- /** @type {(state: Flow) => number} */
379
+ // region, and the page's content box outside one.
380
+ /** @type {(state: Cursor) => number} */
319
381
  let originOf = (state) =>
320
382
  state.canvas.margin + (state.region ? state.region.index * (state.region.width + GUTTER) : 0);
321
- /** @type {(state: Flow) => number} */
383
+ /** @type {(state: Cursor) => number} */
322
384
  let widthOf = (state) => (state.region ? state.region.width : state.canvas.content);
323
385
  // The bottom the cursor fills to. A balanced strip's floor sits above the page
324
386
  // bottom and is a target rather than a bound: a fresh strip still takes at
@@ -326,7 +388,7 @@ let widthOf = (state) => (state.region ? state.region.width : state.canvas.conte
326
388
  // overruns. The last strip answers to the page instead — a balanced share is
327
389
  // rounded, so the strips before it may each stop a little short, and the
328
390
  // remainder has to land somewhere.
329
- /** @type {(state: Flow) => number} */
391
+ /** @type {(state: Cursor) => number} */
330
392
  let floorOf = (state) => {
331
393
  let region = state.region;
332
394
  if (!region || region.height == null || region.index === region.count - 1)
@@ -334,11 +396,11 @@ let floorOf = (state) => {
334
396
  return region.top - region.height;
335
397
  };
336
398
  // The y a fresh column starts from: a strip's shared top inside a region.
337
- /** @type {(state: Flow) => number} */
399
+ /** @type {(state: Cursor) => number} */
338
400
  let ceilOf = (state) => (state.region ? state.region.top : state.canvas.top);
339
401
  // Has this column taken anything yet? Answered positionally, because the only
340
402
  // thing that marks a page used is the cursor moving off its top, and the
341
- // drawing primitives that move it take a `Canvas` rather than a `Flow`.
403
+ // drawing primitives that move it take a `Canvas` rather than a `Cursor`.
342
404
  // `canvas.fresh` stays the *page*'s own answer — it is what keeps `turn` from
343
405
  // leaving an empty page behind — so a strip claims no second meaning for it.
344
406
  // A block of no height would read fresh here and used there; none is reachable
@@ -346,9 +408,6 @@ let ceilOf = (state) => (state.region ? state.region.top : state.canvas.top);
346
408
  /** @type {(state: Cursor) => boolean} */
347
409
  let freshOf = (state) => (state.region ? state.canvas.y === state.region.top : state.canvas.fresh);
348
410
 
349
- /** @type {(canvas: Canvas, h: number, line: Line, floor: number) => boolean} */
350
- let hasRoom = (canvas, h, line, floor) => canvas.y - h - line.h >= floor;
351
-
352
411
  // A fresh column always takes at least one line, or nothing would ever fit —
353
412
  // which is also what makes a balanced floor a target rather than a bound.
354
413
  /** @type {(fresh: boolean, j: number, i: number) => boolean} */
@@ -363,7 +422,7 @@ let grow = (canvas, lines, i, floor, fresh) => {
363
422
  h = 0,
364
423
  n = lines.length;
365
424
  while (j < n) {
366
- if (!hasRoom(canvas, h, lines[j], floor) && !takeLine(fresh, j, i)) break;
425
+ if (!(canvas.y - h - lines[j].h >= floor) && !takeLine(fresh, j, i)) break;
367
426
  h += lines[j++].h;
368
427
  }
369
428
  return { j, h };
@@ -422,7 +481,7 @@ let skipFlow = (state, event, name) => {
422
481
  /** @type {(state: Cursor, event: any) => number} */
423
482
  let flowPad = (state, event) => leadOf(state, event) + spaceOf(event.style, "spaceAfter");
424
483
 
425
- // The unsplittable blocks -- a picture and a split -- move whole to a fresh
484
+ // The unsliceable blocks -- a picture and a split -- move whole to a fresh
426
485
  // column rather than breaking, so a balanced floor never cuts one: only the
427
486
  // page's own floor bears on it. A picture that no column can hold is scaled
428
487
  // down to the one it lands on, since obeying the guarantee literally would
@@ -522,12 +581,12 @@ let item = (state, event, measured = null) => {
522
581
  skipFlow(state, event, "spaceAfter");
523
582
  };
524
583
 
525
- // What a block does when it does not fit where it stands. A picture and a
526
- // split never break -- each moves whole to a fresh column; anything else flows
527
- // down it, breaking where `fitLines` says.
584
+ // What a block does when it does not fit where it stands. An unsliceable block
585
+ // moves whole to a fresh column; anything else flows down it, breaking where
586
+ // `fitLines` says.
528
587
  /** @type {(state: Flow, block: Block) => void} */
529
588
  let overflow = (state, block) =>
530
- block.picture || block.parts ? placeWhole(state, block) : slice(state, block);
589
+ unsliceable(block) ? placeWhole(state, block) : slice(state, block);
531
590
 
532
591
  // The height a page turn carries over: the drawn headers of the groups still
533
592
  // open, which every page their instances continue onto replays. Computed rather
@@ -650,18 +709,14 @@ let openRegion = (state) => {
650
709
  };
651
710
  };
652
711
 
653
- // Close the region: the cursor lands at whichever strip reached lowest, and
654
- // full-width content resumes under it on the same page (SCHEMA.md).
655
- /** @type {(state: Flow) => void} */
656
- let closeRegion = (state) => {
657
- let region = state.region;
658
- if (!region) return;
659
- // Committing replays the buffer, and a replayed footer closes the region on
660
- // its own way through — so re-read rather than trusting the local.
661
- if (region.held) commit(state);
662
- if (!state.region) return;
663
- let canvas = state.canvas;
664
- canvas.y = Math.min(canvas.y, ...region.ends);
712
+ // Land the region: the cursor drops to whichever strip reached lowest, and
713
+ // full-width content resumes under it on the same page (SCHEMA.md). The last
714
+ // reader of `ends`, which `restrip` empties and `advance` fills — closing is
715
+ // the flow's, because a region still holding has to be laid out first, but
716
+ // where the region lands is geometry and belongs here.
717
+ /** @type {(state: Flow, region: Region) => void} */
718
+ let landRegion = (state, region) => {
719
+ state.canvas.y = Math.min(state.canvas.y, ...region.ends);
665
720
  state.region = null;
666
721
  };
667
722
 
@@ -723,11 +778,14 @@ let UNITS = {
723
778
  },
724
779
  row: restated,
725
780
  "total-row": restated,
781
+ // The kinds `unsliceable` names, restated because a strip is costed from the
782
+ // event rather than the block -- this table's `whole` is the balancer's
783
+ // model, not the pagination truth. A third such kind belongs in both places.
726
784
  image: unbroken,
727
785
  split: unbroken,
728
786
  };
729
787
 
730
- // An unsplittable block owes a strip nothing but its own height.
788
+ // An entry no strip may cut owes it nothing but its own height.
731
789
  /** @type {(entry: any) => Unit} */
732
790
  function unbroken(entry) {
733
791
  return unsplit(entry.h, entry.h);
@@ -750,20 +808,19 @@ let stripHeight = (state, region, own) =>
750
808
  ? null
751
809
  : balance(unitsOf(state, own), region.count, roomOf(state, region), rowHeight(state.canvas));
752
810
 
753
- /** @type {(state: Flow) => void} */
811
+ // Commit the region: decide the strip height, stop holding, and hand the held
812
+ // entries back in the order they arrived, for `flow`'s `replay` to walk.
813
+ //
814
+ // Marking the spans is also what picks out the region's own children, and the
815
+ // height is decided from those — so it runs before the replay, not with it.
816
+ /** @type {(state: Flow) => any[]} */
754
817
  let commit = (state) => {
755
818
  let region = /** @type {Region} */ (state.region);
756
819
  let held = /** @type {any[]} */ (region.held);
757
- // Marking the spans is also what picks out the region's own children, and
758
- // the height is decided from those — so it runs before the replay, not with
759
- // it.
760
820
  region.height = stripHeight(state, region, spans(held));
761
821
  region.held = null;
762
822
  restrip(region, region.top);
763
- for (let entry of held) {
764
- if (breaksFor(state, region, entry.span)) advance(state);
765
- state.route(entry.event, entry.block);
766
- }
823
+ return held;
767
824
  };
768
825
 
769
826
  // A group instance is the atomic unit inside a region: one that would cross a
@@ -848,20 +905,47 @@ let NESTING = { "group-start": 1, "group-end": -1 };
848
905
  // of what it holds. A measured block rides along so the replay never wraps the
849
906
  // same text twice; a held header counts towards the total like anything else,
850
907
  // but the flush that draws it measures from the event, so only its height is
851
- // kept. A buffer that outgrows what the strips could hold has answered the
852
- // question early: it commits to filling, and everything after it streams.
853
- /** @type {(state: Flow, event: any, block: Block | null) => void} */
854
- let buffer = (state, event, block) => {
908
+ // kept. Answers whether that event filled it: a buffer that outgrows what the
909
+ // strips could hold has answered the balancing question early, and everything
910
+ // after it streams.
911
+ /** @type {(state: Flow, event: any, payload: Block | Measured | null, hollow: boolean) => boolean} */
912
+ let buffer = (state, event, payload, hollow) => {
855
913
  let region = /** @type {Region} */ (state.region);
856
914
  // The one door into the held list, so it is where the buffer's own walk is
857
915
  // kept. A bracket buffered here is a bracket the open stack will not see
858
916
  // until the replay places it.
859
917
  region.nesting += NESTING[event.type] ?? 0;
860
- let entry = entryFor(state, event, block, region.width);
918
+ let entry = entryFor(state, event, payload, region.width);
861
919
  /** @type {any[]} */ (region.held).push(entry);
862
920
  region.measured += entry.h;
863
- if (event.type === "table-end") region.measured += remeasure(state, region);
864
- if (outgrown(state, region)) commit(state);
921
+ if (event.type === "table-end") region.measured += remeasure(state, region, entry);
922
+ if (hollow) region.measured -= refund(/** @type {any[]} */ (region.held));
923
+ return outgrown(state, region);
924
+ };
925
+
926
+ // Give back what `WORTH` charged the opening of an instance that turned out
927
+ // hollow, and zero the entry so the replay's `spans` walk agrees. Taken as the
928
+ // close lands rather than at commit because `outgrown` is tested on every
929
+ // buffered event: a region carrying a phantom height can give up on balancing
930
+ // before it ever reaches the commit that would have corrected it.
931
+ //
932
+ // The walk back is over brackets only, which is safe precisely because the
933
+ // caller has already established the instance is hollow — there is nothing
934
+ // else between the two ends to walk over. Nested hollow instances refunded
935
+ // themselves on the way in, so each charge is given back exactly once.
936
+ /** @type {(held: any[]) => number} */
937
+ let refund = (held) => {
938
+ let depth = 0;
939
+ for (let i = held.length - 2; i >= 0; i--) {
940
+ if (held[i].event.type === "group-end") depth++;
941
+ else if (depth) depth--;
942
+ else {
943
+ let charged = held[i].h;
944
+ held[i].h = 0;
945
+ return charged;
946
+ }
947
+ }
948
+ return 0;
865
949
  };
866
950
 
867
951
  // A table's events reach the buffer one at a time, and each is worth a bare
@@ -871,52 +955,59 @@ let buffer = (state, event, block) => {
871
955
  // measured at the strip width and its entries are corrected. Both readers of
872
956
  // those heights want the real ones: the balance model, and `outgrown`.
873
957
  //
874
- // The replay measures the table again when it draws it. That is the price of
875
- // keeping `table` the one place a table is laid out, and it is paid once per
876
- // table per render.
877
- // The measurement is kept, keyed by the opening event, so the replay draws
878
- // this table without wrapping every cell a second time. Held per flow rather
879
- // than per module: the keys are one render's events.
880
- /** @type {(state: Flow, region: Region) => number} */
881
- let remeasure = (state, region) => {
958
+ // The measurement then rides the entry that closes the table, so the replay
959
+ // lays it out without wrapping every cell a second time. Nothing outside the
960
+ // buffer holds on to it: `commit` hands the entry to the replay and the
961
+ // replay is the last reader.
962
+ /** @type {(state: Flow, region: Region, closing: any) => number} */
963
+ let remeasure = (state, region, closing) => {
882
964
  let held = /** @type {any[]} */ (region.held);
883
965
  let at = held.findLastIndex((entry) => entry.event.type === "table-start");
884
966
  if (at < 0) return 0;
885
967
  let span = held.slice(at);
886
- let measured = measureTable(
887
- state.canvas,
888
- tableOf(span.map((entry) => entry.event)),
889
- region.width,
890
- );
891
- state.measured.set(span[0].event, measured);
892
- return retune(span, measured);
893
- };
894
-
895
- // Write the measured heights back over the estimates. `span` is the table's
896
- // events in the order they were buffered its opening, its rows, an optional
897
- // total, then its end and the measurement comes out in that same order, so
898
- // the two line up by position and the end simply runs off the shorter list.
968
+ let buffered = tableOf(span.map((entry) => entry.event));
969
+ let measured = measureTable(state.canvas, buffered, region.width);
970
+ closing.payload = measured;
971
+ return retune(span, buffered, measured);
972
+ };
973
+
974
+ // Every measured height filed under the event it was measured from: the
975
+ // opening event pays for the headings row, each row and each total for
976
+ // itself. What has no height of its own is simply absent, which is what keeps
977
+ // a new kind of table event landing between a row and a total costless.
978
+ /** @type {(span: any[], buffered: any, measured: Measured) => Map<any, number>} */
979
+ let heightsOf = (span, buffered, measured) => {
980
+ let by = new Map([[span[0].event, measured.head.h]]);
981
+ for (let [i, row] of buffered.rows.entries()) by.set(row, measured.laid[i].h);
982
+ for (let [i, row] of buffered.totals.entries()) by.set(row, measured.totalRows[i].h);
983
+ return by;
984
+ };
985
+
986
+ // Write those heights back over the estimates. The correction cannot slide:
987
+ // an entry the measurement filed nothing under keeps the estimate it had.
899
988
  // Reports what the correction moved the running total by.
900
- /** @type {(span: any[], measured: Measured) => number} */
901
- let retune = (span, measured) => {
902
- let heights = [measured.head.h, ...measured.laid.map((/** @type {any} */ laid) => laid.h)];
903
- if (measured.totalRows) for (let row of measured.totalRows) heights.push(row.h);
989
+ /** @type {(span: any[], buffered: any, measured: Measured) => number} */
990
+ let retune = (span, buffered, measured) => {
991
+ let by = heightsOf(span, buffered, measured);
904
992
  let delta = 0;
905
- for (let [i, h] of heights.entries()) {
906
- delta += h - span[i].h;
907
- span[i].h = h;
993
+ for (let entry of span) {
994
+ let h = by.get(entry.event) ?? entry.h;
995
+ delta += h - entry.h;
996
+ entry.h = h;
908
997
  }
909
998
  return delta;
910
999
  };
911
1000
 
912
- // One buffered entry: the event, the block the replay will draw it from, and
1001
+ // One buffered entry: the event, what the replay is handed along with it, and
913
1002
  // what it is worth to the running total. A header keeps its block like
914
1003
  // anything else — the flush that eventually draws it takes the block from the
915
- // hold rather than wrapping the text again.
916
- /** @type {(state: Flow, event: any, block: Block | null, width: number) => any} */
917
- let entryFor = (state, event, block, width) => {
918
- let measured = block || measureFor(state, event, width);
919
- return { event, block: measured, h: measured ? measured.h : estimate(state, event) };
1004
+ // hold rather than wrapping the text again. Only a table's closing entry is
1005
+ // ever given a measurement, and `remeasure` puts it there afterwards, so what
1006
+ // reaches here is a block or nothing.
1007
+ /** @type {(state: Flow, event: any, payload: Block | Measured | null, width: number) => any} */
1008
+ let entryFor = (state, event, payload, width) => {
1009
+ let block = /** @type {Block | null} */ (payload) || measureFor(state, event, width);
1010
+ return { event, payload: block, h: block ? block.h : estimate(state, event) };
920
1011
  };
921
1012
 
922
1013
  // The block a buffered event will be drawn from, measured once at the strip
@@ -942,20 +1033,10 @@ let anchor = (state) => {
942
1033
  /** @type {(state: Flow) => Group[]} */
943
1034
  let pending = (state) => state.open.filter((group) => group.held.length);
944
1035
 
945
- // A held run, as blocks. A region measured its headers while buffering them,
946
- // at this same width, so a replayed run reuses those rather than wrapping the
947
- // same text a second time (ADR 0027).
948
- /** @type {(canvas: Canvas, held: any[], avail: number) => Block[]} */
949
- let blocksOf = (canvas, held, avail) =>
950
- held.map((entry) => entry.block || blockOf(canvas, entry.event, avail));
951
-
952
1036
  /** @type {(state: Flow, extra: number, runs: Block[][]) => number} */
953
1037
  let needed = (state, extra, runs) =>
954
1038
  state.gap + runs.flat().reduce((total, block) => total + block.h, 0) + extra;
955
1039
 
956
- /** @type {(state: Flow, need: number) => boolean} */
957
- let tooTall = (state, need) => need > ceilOf(state) - carried(state) - floorOf(state);
958
-
959
1040
  // Degenerate: taller than a page — keep nothing, paginate normally, and
960
1041
  // repeat nothing afterwards. That last part is what bounds the replayed run
961
1042
  // for good: every other path drew its headers inside the room a page still
@@ -1001,20 +1082,29 @@ let flush = (state, extra) => {
1001
1082
  let avail = widthOf(state);
1002
1083
  // Measured per instance, so each one takes back exactly the blocks its own
1003
1084
  // headers were drawn from: no flat list to slice, no order to keep in step.
1004
- let runs = runsFor.map((group) => blocksOf(canvas, group.held, avail));
1085
+ // A held run, as blocks. A region measured its headers while buffering them,
1086
+ // at this same width, so a replayed run reuses those rather than wrapping
1087
+ // the same text a second time (ADR 0027).
1088
+ let runs = runsFor.map((group) =>
1089
+ group.held.map((entry) => entry.block || blockOf(canvas, entry.event, avail)),
1090
+ );
1005
1091
  let need = needed(state, extra, runs);
1006
1092
  // Against what a fresh column has left once the page has replayed what it
1007
1093
  // carries. Inside a region that is a strip, and the replay sits above it.
1008
- if (tooTall(state, need)) return dropHeld(state, runsFor);
1094
+ if (need > ceilOf(state) - carried(state) - floorOf(state)) return dropHeld(state, runsFor);
1009
1095
  settle(state, need);
1010
1096
  drawHeld(state, runsFor, runs, avail);
1011
1097
  };
1012
1098
 
1013
1099
  // Enough of an item that its group header is never left introducing nothing.
1100
+ // An unsliceable block is kept whole -- the same blocks `overflow` moves
1101
+ // rather than slices. There is no first part of one to keep company with: a
1102
+ // split's own `lines` are empty, so clipping to `KEEP_LINES` would reserve
1103
+ // nothing at all and strand the run it was called to protect.
1014
1104
  let KEEP_LINES = 2;
1015
1105
  /** @type {(block: Block) => number} */
1016
1106
  let keepWith = (block) =>
1017
- block.picture ? block.h : Math.min(block.h, heightOf(block.lines.slice(0, KEEP_LINES)));
1107
+ unsliceable(block) ? block.h : Math.min(block.h, heightOf(block.lines.slice(0, KEEP_LINES)));
1018
1108
 
1019
1109
  // Flush any pending header with this item's opening, then place the item. The
1020
1110
  // block is measured once here and handed on rather than wrapped twice.
@@ -1046,16 +1136,52 @@ let holding = (state) => state.open.some((group) => group.held.length) || state.
1046
1136
  // starts a new `page.number` / `page.total` sequence on the page it opens.
1047
1137
  // A duplicate start on a page that already began a sequence is a zero-length
1048
1138
  // range; `numbered` skips it.
1139
+ // Run `seen` on every event before the handler that acts on it. The stream is
1140
+ // the only place hollowness can be read in order: past this point a region may
1141
+ // hold an event back for a commit that lands after its own instance has closed.
1142
+ /** @type {(seen: (event: any) => void, handlers: Record<string, (event: any) => void>) => Record<string, (event: any) => void>} */
1143
+ let tracked = (seen, handlers) =>
1144
+ Object.fromEntries(
1145
+ Object.entries(handlers).map(([type, handle]) => [
1146
+ type,
1147
+ (/** @type {any} */ event) => {
1148
+ seen(event);
1149
+ handle(event);
1150
+ },
1151
+ ]),
1152
+ );
1153
+
1049
1154
  /** @type {(state: Flow, event: any) => void} */
1050
1155
  let openGroup = (state, event) => {
1051
1156
  breakFor(state, event);
1052
1157
  if (event.columns) state.pending = { count: event.columns, owner: event.depth };
1053
- state.gap = state.skipGap ? 0 : Math.max(state.gap, instanceGap(state.canvas));
1158
+ let gap = state.gap;
1159
+ let skip = state.skipGap;
1160
+ state.gap = skip ? 0 : Math.max(state.gap, instanceGap(state.canvas));
1054
1161
  state.skipGap = false;
1055
- state.open.push({ held: [], blocks: [] });
1162
+ state.open.push({ held: [], blocks: [], gap, skip });
1056
1163
  state.marks.push(markFor(event));
1057
1164
  };
1058
1165
 
1166
+ // Put back everything opening this instance displaced, so a hollow one leaves
1167
+ // the flow exactly as it found it. Restoring rather than zeroing is the whole
1168
+ // point: `Math.max` folded any ancestor's gap into this one, and a close that
1169
+ // zeroed would rob the parent of a gap its own opening earned. The pinned
1170
+ // header's `skip` travels the same way — a hollow instance is not the band the
1171
+ // pin was suppressing the gap for, so the suppression passes through it to
1172
+ // whatever lands next. The mark goes too: the outline is what a reader opens,
1173
+ // and a bookmark onto content this instance did not draw is a worse answer
1174
+ // than no bookmark. It is this instance's mark for certain — a non-hollow
1175
+ // child would have made this instance non-hollow, and a hollow one already
1176
+ // took its own back.
1177
+ /** @type {(state: Flow) => void} */
1178
+ let unopen = (state) => {
1179
+ let frame = state.open[state.open.length - 1];
1180
+ state.gap = frame.gap;
1181
+ state.skipGap = frame.skip;
1182
+ state.marks.pop();
1183
+ };
1184
+
1059
1185
  // A real page, inside a region as much as outside one: ADR 0013 declined a
1060
1186
  // `break: "column"`, so this never means the next strip. `turn` restrips.
1061
1187
  /** @type {(state: Flow, event: any) => void} */
@@ -1093,9 +1219,9 @@ let holdHeader = (state, event, title, block = null) => {
1093
1219
  // because the region's estimator reads both, and a constant with two readers
1094
1220
  // and no name drifts between them.
1095
1221
  /** @type {(canvas: Canvas) => number} */
1096
- let rowHeight = (canvas) => LEAD * canvas.base;
1222
+ let rowHeight = (canvas) => LEAD * canvas.settings.base;
1097
1223
  /** @type {(canvas: Canvas) => number} */
1098
- let instanceGap = (canvas) => 0.5 * LEAD * canvas.base;
1224
+ let instanceGap = (canvas) => 0.5 * LEAD * canvas.settings.base;
1099
1225
 
1100
1226
  // What a fresh strip has to hold to take an instance: the span less the
1101
1227
  // opening gap, which `settle` drops at a strip head. This is the one fact the
@@ -1111,10 +1237,14 @@ let headOf = (canvas, span) => span - instanceGap(canvas);
1111
1237
  // gives flow spacing no meaning inside a table row, in either target.
1112
1238
  /** @type {(canvas: Canvas, cell: any, rowStyle: any) => any} */
1113
1239
  let cellOf = (canvas, cell, rowStyle) => {
1114
- let style = merge(unbox(rowStyle), unbox(cell.style));
1240
+ // A row's block reaches its cells with no box in it: the engine resolved
1241
+ // that half onto the cells themselves before the event was emitted
1242
+ // (SCHEMA.md, "Style declarations"), which is why the inset and the ink
1243
+ // both read the cell and a row is never asked for a box it cannot have.
1244
+ let style = merge(rowStyle, unbox(cell.style));
1115
1245
  let inset = insetOf(cell.style, CELL_PAD);
1116
- let list = atoms(canvas, cell.tokens, style);
1117
- let natural = wrap(canvas, list, Infinity, sizeOf(style, canvas.base)).reduce(
1246
+ let list = atoms(canvas.settings, cell.tokens, style);
1247
+ let natural = wrap(list, Infinity, sizeOf(style, canvas.settings.base)).reduce(
1118
1248
  (widest, line) => Math.max(widest, line.w),
1119
1249
  0,
1120
1250
  );
@@ -1122,6 +1252,7 @@ let cellOf = (canvas, cell, rowStyle) => {
1122
1252
  list,
1123
1253
  natural: natural + inset.l + inset.r,
1124
1254
  align: style.align,
1255
+ valign: style.valign,
1125
1256
  bg: col(style.background),
1126
1257
  underline: !!style.underline,
1127
1258
  strikethrough: !!style.strikethrough,
@@ -1134,18 +1265,24 @@ let cellOf = (canvas, cell, rowStyle) => {
1134
1265
  /** @type {(canvas: Canvas, cells: any[], widths: number[]) => { cells: any[], h: number }} */
1135
1266
  let rowOf = (canvas, cells, widths) => {
1136
1267
  let h = rowHeight(canvas);
1268
+ /** @type {number[]} */
1269
+ let owns = [];
1137
1270
  let out = cells.map((cell, i) => {
1138
1271
  let inner = Math.max(widths[i] - cell.inset.l - cell.inset.r, 1);
1139
1272
  // A glyph or a hard break occupies; spaces alone are an empty cell.
1140
1273
  let lines = cell.list.some(
1141
1274
  (/** @type {{ hard: boolean, space: boolean }} */ atom) => atom.hard || !atom.space,
1142
1275
  )
1143
- ? dress(wrap(canvas, cell.list, inner, cell.list[0].size), cell)
1276
+ ? dress(wrap(cell.list, inner, cell.list[0].size), cell)
1144
1277
  : [];
1145
- let cellHeight = heightOf(lines) + cell.inset.t + cell.inset.b;
1146
- if (cellHeight > h) h = cellHeight;
1278
+ let own = heightOf(lines) + cell.inset.t + cell.inset.b;
1279
+ if (own > h) h = own;
1280
+ owns.push(own);
1147
1281
  return { ...cell, lines };
1148
1282
  });
1283
+ // The row's height is known only now, so the slack each cell's `valign`
1284
+ // reads is measured here, as `splitBlock` measures a slot's.
1285
+ for (let [i, cell] of out.entries()) cell.drop = vshift(cell.valign, h - owns[i]);
1149
1286
  return { cells: out, h };
1150
1287
  };
1151
1288
 
@@ -1169,6 +1306,10 @@ let writeCell = (canvas, cell, lines, x, y, w) => {
1169
1306
  // with, so the ink a slice lays down and the height it was given come from
1170
1307
  // one answer: a continuation starts flush, and only the slice that owns a
1171
1308
  // side strokes it.
1309
+ // A row sliced across pages has no one box for `valign` to read the slack
1310
+ // against, so its cells paint from the top (SCHEMA.md, the pdf Layout contract).
1311
+ /** @type {(cell: any, slice: import('./box.js').Slice) => number} */
1312
+ let dropOf = (cell, slice) => (isWhole(slice) ? cell.drop : 0);
1172
1313
  /**
1173
1314
  * @type {(canvas: Canvas, cells: any[], lines: any[][], h: number,
1174
1315
  * slice: import('./box.js').Slice, xOffsets: number[], widths: number[],
@@ -1180,7 +1321,7 @@ let paintCells = (canvas, cells, lines, h, slice, xOffsets, widths, bg) => {
1180
1321
  fillCell(canvas, cell, bg, xOffsets[i], canvas.y - h, widths[i], h);
1181
1322
  paintBox(canvas, xOffsets[i], canvas.y, widths[i], h, cell.style, null, slice);
1182
1323
  canvas.box(cell.path, xOffsets[i], canvas.y, widths[i], h);
1183
- let y0 = canvas.y - (slice.first ? cell.inset.t : 0);
1324
+ let y0 = canvas.y - (slice.first ? cell.inset.t : 0) - dropOf(cell, slice);
1184
1325
  writeCell(canvas, cell, lines[i], xOffsets[i], y0, widths[i]);
1185
1326
  }
1186
1327
  canvas.y -= h;
@@ -1192,7 +1333,6 @@ let paintCells = (canvas, cells, lines, h, slice, xOffsets, widths, bg) => {
1192
1333
  * widths: number[], bg: any) => void}
1193
1334
  */
1194
1335
  let drawRow = (canvas, row, xOffsets, widths, bg) => {
1195
- let yTop = canvas.y;
1196
1336
  paintCells(
1197
1337
  canvas,
1198
1338
  row.cells,
@@ -1203,18 +1343,69 @@ let drawRow = (canvas, row, xOffsets, widths, bg) => {
1203
1343
  widths,
1204
1344
  bg,
1205
1345
  );
1206
- paintBox(canvas, xOffsets[0], yTop, sum(widths), row.h, row.style, null);
1207
1346
  };
1208
1347
 
1348
+ // The column geometry one row sees. A cell covering several columns starts
1349
+ // where the first of them starts and is as wide as all of them together; a row
1350
+ // that spans nothing sees the columns themselves, and pays nothing for the
1351
+ // feature. `null` spans is that row -- every data row, and every total row
1352
+ // whose cells each cover one column.
1353
+ /** @type {(cells: any[]) => number[] | null} */
1354
+ let spansOf = (cells) =>
1355
+ cells.some((cell) => cell.span > 1) ? cells.map((cell) => cell.span || 1) : null;
1356
+ // One value per cell, walking the columns each of them covers. Reached only
1357
+ // for a row that spans, so the closure it takes costs nothing per data row.
1358
+ /** @type {(spans: number[], pick: (at: number, span: number) => number) => number[]} */
1359
+ let overSpans = (spans, pick) => {
1360
+ /** @type {number[]} */
1361
+ let out = [];
1362
+ let at = 0;
1363
+ for (let span of spans) {
1364
+ out.push(pick(at, span));
1365
+ at += span;
1366
+ }
1367
+ return out;
1368
+ };
1369
+ /** @type {(widths: number[], spans: number[] | null) => number[]} */
1370
+ let spanWidths = (widths, spans) =>
1371
+ spans ? overSpans(spans, (at, span) => sum(widths.slice(at, at + span))) : widths;
1372
+ /** @type {(xOffsets: number[], spans: number[] | null) => number[]} */
1373
+ let spanOffsets = (xOffsets, spans) => (spans ? overSpans(spans, (at) => xOffsets[at]) : xOffsets);
1374
+
1209
1375
  // The widest of each column's header, rows and totals, padding included.
1210
- /** @type {(cols: any[], header: any[], rows: any[], totals: { cells: any[] }[]) => number[]} */
1211
- let naturalWidths = (cols, header, rows, totals) =>
1212
- cols.map((_, i) => {
1213
- let widest = header[i].natural;
1214
- for (let row of rows) widest = Math.max(widest, row.cells[i].natural);
1215
- for (let row of totals) widest = Math.max(widest, row.cells[i].natural);
1216
- return widest;
1217
- });
1376
+ //
1377
+ // A cell covering more than one column has no say in their widths (SCHEMA.md):
1378
+ // what a span states is which columns a cell reaches across, never how wide
1379
+ // they are. So a column can end up with no voter at all -- every cell above it
1380
+ // spans over it -- which only an empty table reaches, since a data row never
1381
+ // spans. It opens at the padding floor rather than at nothing, so an empty
1382
+ // table still shows the geometry it promises.
1383
+ /**
1384
+ * @typedef {{ cells: any[], spans: number[] | null }} Voting
1385
+ */
1386
+ /** @type {(cols: any[], header: Voting, rows: Voting[], totals: Voting[]) => number[]} */
1387
+ let naturalWidths = (cols, header, rows, totals) => {
1388
+ /** @type {(number | null)[]} */
1389
+ let widest = cols.map(() => null);
1390
+ /** @type {(at: number, natural: number) => void} */
1391
+ let widen = (at, natural) => {
1392
+ let held = widest[at];
1393
+ if (held === null || natural > held) widest[at] = natural;
1394
+ };
1395
+ /** @type {(row: Voting) => void} */
1396
+ let vote = (row) => {
1397
+ let at = 0;
1398
+ for (let [i, cell] of row.cells.entries()) {
1399
+ let span = row.spans ? row.spans[i] : 1;
1400
+ if (span === 1) widen(at, cell.natural);
1401
+ at += span;
1402
+ }
1403
+ };
1404
+ vote(header);
1405
+ for (let row of rows) vote(row);
1406
+ for (let row of totals) vote(row);
1407
+ return widest.map((width) => (width === null ? CELL_PAD.l + CELL_PAD.r : width));
1408
+ };
1218
1409
 
1219
1410
  /** @type {(values: number[]) => number} */
1220
1411
  let sum = (values) => values.reduce((total, value) => total + value, 0);
@@ -1251,7 +1442,7 @@ let columnWidths = (cols, natural, avail) => {
1251
1442
 
1252
1443
  // One table being emitted: what `carryOver`, `sliced` and `put` all need.
1253
1444
  /**
1254
- * @typedef {{ cells: any[], h: number, style?: any }} TableRow
1445
+ * @typedef {{ cells: any[], h: number, style?: any, spans?: number[] | null }} TableRow
1255
1446
  * @typedef {{ state: Flow, head: TableRow,
1256
1447
  * xOffsets: number[], widths: number[], x: number, avail: number }} Grid
1257
1448
  */
@@ -1267,11 +1458,24 @@ let columnWidths = (cols, natural, avail) => {
1267
1458
  /** @type {(row: { style?: any }) => any} */
1268
1459
  let rowFill = (row) => col((row.style || {}).background);
1269
1460
 
1461
+ // What this row draws against: the grid's own columns, or the merged geometry
1462
+ // a spanning row sees. Read at draw time rather than kept on the row, because
1463
+ // `rebase` moves the offsets under it every time the table crosses a strip.
1464
+ /** @type {(grid: Grid, row: TableRow) => { xOffsets: number[], widths: number[] }} */
1465
+ let gridOf = (grid, row) =>
1466
+ row.spans
1467
+ ? {
1468
+ xOffsets: spanOffsets(grid.xOffsets, row.spans),
1469
+ widths: spanWidths(grid.widths, row.spans),
1470
+ }
1471
+ : grid;
1472
+
1270
1473
  /** @type {(grid: Grid) => void} */
1271
1474
  let carryOver = (grid) => {
1272
1475
  advance(grid.state);
1273
1476
  rebase(grid);
1274
- drawRow(grid.state.canvas, grid.head, grid.xOffsets, grid.widths, rowFill(grid.head));
1477
+ let geo = gridOf(grid, grid.head);
1478
+ drawRow(grid.state.canvas, grid.head, geo.xOffsets, geo.widths, rowFill(grid.head));
1275
1479
  };
1276
1480
 
1277
1481
  // Move the grid to the column the cursor is now in. A table's widths are its
@@ -1335,34 +1539,29 @@ let sliceH = (taken, cells, slice) => {
1335
1539
  // Each column reserves its own bottom inset out of the room, the same trade
1336
1540
  // `sliceFloor` makes for an item: a slice that stops short leaves whitespace,
1337
1541
  // one that does not leaves a bottom border under the bottom margin.
1338
- //
1339
- // The row's own box slices with the cells rather than going missing, which is
1340
- // what it did while `drawRow` was the only caller painting it.
1341
1542
  /** @type {(grid: Grid, row: TableRow, bg: any) => void} */
1342
1543
  let sliced = (grid, row, bg) => {
1343
1544
  let canvas = grid.state.canvas;
1344
1545
  let next = row.cells.map(() => 0); // first line of each column not yet placed
1345
1546
  let first = true;
1346
1547
  for (;;) {
1548
+ // Inside the loop, not above it: `carryOver` rebases the grid at the foot
1549
+ // of every slice, and a merged geometry read once would keep painting the
1550
+ // continuations at the strip this row started in.
1551
+ let geo = gridOf(grid, row);
1347
1552
  let cap = canvas.y - canvas.bottom;
1348
1553
  let taken = row.cells.map((/** @type {any} */ cell, /** @type {number} */ i) =>
1349
1554
  takeLines(cell, next, i, first ? cell.inset.t : 0, cap - cell.inset.b),
1350
1555
  );
1351
1556
  let slice = { first, more: stillMore(row, next) };
1352
1557
  let h = sliceH(taken, row.cells, slice);
1353
- let yTop = canvas.y;
1354
- paintCells(canvas, row.cells, taken, h, slice, grid.xOffsets, grid.widths, bg);
1355
- paintBox(canvas, grid.xOffsets[0], yTop, sum(grid.widths), h, row.style, null, slice);
1558
+ paintCells(canvas, row.cells, taken, h, slice, geo.xOffsets, geo.widths, bg);
1356
1559
  if (!slice.more) return;
1357
1560
  carryOver(grid);
1358
1561
  first = false;
1359
1562
  }
1360
1563
  };
1361
1564
 
1362
- /** @type {(grid: Grid) => number} */
1363
- let pageCap = (grid) =>
1364
- ceilOf(grid.state) - carried(grid.state) - floorOf(grid.state) - grid.head.h;
1365
-
1366
1565
  /** @type {(row: { h: number }, keep: number, cap: number) => number} */
1367
1566
  let companion = (row, keep, cap) => (row.h + keep > cap ? 0 : keep);
1368
1567
 
@@ -1388,7 +1587,7 @@ let put = (grid, row, bg, keep = 0) => {
1388
1587
  // The tallest row a fresh column holds, after the replayed group headers and
1389
1588
  // the replayed column headings. A companion over that can never share a
1390
1589
  // column, so it stops being a reason to break.
1391
- let cap = pageCap(grid);
1590
+ let cap = ceilOf(grid.state) - carried(grid.state) - floorOf(grid.state) - grid.head.h;
1392
1591
  keep = companion(row, keep, cap);
1393
1592
  // A row that will be sliced anyway is only worth breaking for while this
1394
1593
  // column cannot take even its first slice — beyond that a break buys nothing
@@ -1397,38 +1596,21 @@ let put = (grid, row, bg, keep = 0) => {
1397
1596
  let broke = shouldBreak(grid.state, row, keep, cap);
1398
1597
  if (broke) carryOver(grid);
1399
1598
  if (canvas.y - row.h < canvas.bottom) return sliced(grid, row, bg);
1400
- drawRow(canvas, row, grid.xOffsets, grid.widths, bg);
1599
+ let geo = gridOf(grid, row);
1600
+ drawRow(canvas, row, geo.xOffsets, geo.widths, bg);
1401
1601
  };
1402
1602
 
1403
- /** @type {(canvas: Canvas, row: any) => { cells: any[], style: any }} */
1404
- let bodyRow = (canvas, row) => ({
1405
- cells: row.cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, row.style)),
1406
- style: row.style,
1407
- });
1408
-
1409
- /** @type {(canvas: Canvas, buffered: any) => any} */
1410
- let totalRowsOf = (canvas, buffered) =>
1411
- buffered.totals.map((/** @type {any} */ row) => ({
1412
- cells: row.cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, row.style)),
1413
- style: row.style,
1414
- }));
1415
-
1416
1603
  /** @type {(laid: any[], totalRows: any[]) => number} */
1417
1604
  let firstH = (laid, totalRows) => (laid.length ? laid[0].h : totalRows.length ? totalRows[0].h : 0);
1418
1605
 
1419
- /** @type {(totalRows: any[]) => number} */
1420
- let totalsH = (totalRows) => totalRows.reduce((h, row) => h + row.h, 0);
1421
-
1422
1606
  /** @type {(grid: Grid, laid: any[], totalRows: any[]) => void} */
1423
1607
  let emitRows = (grid, laid, totalRows) => {
1424
1608
  let last = laid.length - 1;
1425
- let keep = totalsH(totalRows);
1609
+ let keep = totalRows.reduce((h, row) => h + row.h, 0);
1426
1610
  for (let [i, row] of laid.entries()) put(grid, row, rowFill(row), i === last ? keep : 0);
1427
1611
  for (let row of totalRows) put(grid, row, rowFill(row), 0);
1428
1612
  };
1429
1613
 
1430
- // Lay out a buffered table: measure every cell, allocate the columns, then
1431
- // emit the header, the rows and the total, breaking pages as needed.
1432
1614
  // A buffered table, in the one shape `table` lays out: its columns come from
1433
1615
  // the opening event, its rows are the row events themselves, and its total is
1434
1616
  // the total row's cells. Both readers build it here — the replay, collecting
@@ -1437,18 +1619,17 @@ let emitRows = (grid, laid, totalRows) => {
1437
1619
  /** @type {(events: any[]) => any} */
1438
1620
  let tableOf = (events) => {
1439
1621
  let opening = events[0];
1440
- let totals = events.filter((event) => event.type === "total-row");
1441
1622
  return {
1442
- // A header cell arrives without a path of its own; the column's is what
1443
- // its box names, so it rides on the cell from here, where both readers
1444
- // assemble the table.
1445
- columns: opening.columns.map((/** @type {any} */ col) => ({
1446
- ...col,
1447
- header: { ...col.header, path: col.path },
1448
- })),
1449
- headerStyle: opening.style,
1623
+ columns: opening.columns.map((/** @type {any} */ col) => ({ ...col })),
1624
+ // The header row arrives whole (docs/adr/0053), so both readers -- the
1625
+ // replay and the region buffer -- take the same list rather than each
1626
+ // rebuilding it from the columns.
1627
+ header: opening.header.cells,
1628
+ headerStyle: opening.header.style,
1450
1629
  rows: events.filter((event) => event.type === "row"),
1451
- totals: totals.map((row) => ({ cells: row.cells, style: row.style })),
1630
+ // The events themselves, as `rows` are: a total row is a row, and the
1631
+ // buffer files each corrected height under the event it was measured from.
1632
+ totals: events.filter((event) => event.type === "total-row"),
1452
1633
  };
1453
1634
  };
1454
1635
 
@@ -1463,39 +1644,52 @@ let tableOf = (events) => {
1463
1644
  /** @type {(canvas: Canvas, buffered: any, avail: number) => Measured} */
1464
1645
  let measureTable = (canvas, buffered, avail) => {
1465
1646
  let cols = /** @type {any[]} */ (buffered.columns);
1466
- let header = cols.map((/** @type {any} */ col) =>
1467
- cellOf(canvas, col.header, buffered.headerStyle),
1647
+ /** @type {(cells: any[], style: any, spans: number[] | null) => Voting & { style: any }} */
1648
+ let measured = (cells, style, spans) => ({
1649
+ cells: cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, style)),
1650
+ spans,
1651
+ style,
1652
+ });
1653
+ let header = measured(buffered.header, buffered.headerStyle, spansOf(buffered.header));
1654
+ // A data row's cells are the columns' own, so nothing in one can span
1655
+ // (SCHEMA.md, "Span"). Asking each row anyway would scan every cell of the
1656
+ // table to rediscover what the schema already guarantees.
1657
+ let rows = buffered.rows.map((/** @type {any} */ row) => measured(row.cells, row.style, null));
1658
+ let totals = buffered.totals.map((/** @type {any} */ row) =>
1659
+ measured(row.cells, row.style, spansOf(row.cells)),
1468
1660
  );
1469
- let rows = buffered.rows.map((/** @type {any} */ row) => bodyRow(canvas, row));
1470
- let totals = totalRowsOf(canvas, buffered);
1471
1661
  let widths = columnWidths(cols, naturalWidths(cols, header, rows, totals), avail);
1662
+ // Each row wraps against the geometry it sees, which is the columns' own
1663
+ // unless one of its cells spans.
1664
+ /** @type {(row: Voting & { style: any }) => any} */
1665
+ let laidOut = (row) => ({
1666
+ ...rowOf(canvas, row.cells, spanWidths(widths, row.spans)),
1667
+ spans: row.spans,
1668
+ style: row.style,
1669
+ });
1472
1670
  return {
1473
1671
  widths,
1474
- head: { ...rowOf(canvas, header, widths), style: buffered.headerStyle },
1475
- laid: rows.map((/** @type {any} */ row) => ({
1476
- ...rowOf(canvas, row.cells, widths),
1477
- style: row.style,
1478
- })),
1479
- totalRows: totals.map((/** @type {any} */ row) => ({
1480
- ...rowOf(canvas, row.cells, widths),
1481
- style: row.style,
1482
- })),
1672
+ head: laidOut(header),
1673
+ laid: rows.map(laidOut),
1674
+ totalRows: totals.map(laidOut),
1483
1675
  };
1484
1676
  };
1485
1677
 
1486
- /** @type {(state: Flow, buffered: any) => void} */
1487
- let table = (state, buffered) => {
1678
+ // Lay out one table from the events it arrived as: measure every cell,
1679
+ // allocate the columns, then emit the header, the rows and the total,
1680
+ // breaking pages as needed.
1681
+ /** @type {(state: Flow, events: any[], ready: Measured | null) => void} */
1682
+ let table = (state, events, ready) => {
1488
1683
  let canvas = state.canvas;
1489
1684
  // A table inside a region is measured against the strip, not the page: an
1490
1685
  // authored `width` percentage is a share of the column it lands in
1491
1686
  // (SCHEMA.md, "Page columns").
1492
1687
  let avail = widthOf(state);
1493
1688
  let x = originOf(state);
1494
- // Measured already if a region buffered this table: the balance model needed
1495
- // its real row heights, at this same width, and that is the same
1496
- // measurement — so the replay draws it without wrapping every cell again.
1497
- let { widths, head, laid, totalRows } =
1498
- state.measured.get(buffered.opening) || measureTable(canvas, buffered, avail);
1689
+ // Handed a measurement if a region buffered this table: the balance model
1690
+ // needed its real row heights, at this same width, and that is the same
1691
+ // measurement — so the replay neither shapes the table nor wraps a cell.
1692
+ let { widths, head, laid, totalRows } = ready || measureTable(canvas, tableOf(events), avail);
1499
1693
  let xOffsets = widths.map((_, i) => x + sum(widths.slice(0, i)));
1500
1694
 
1501
1695
  /** @type {Grid} */
@@ -1507,7 +1701,8 @@ let table = (state, buffered) => {
1507
1701
  advance(state);
1508
1702
  rebase(grid);
1509
1703
  }
1510
- drawRow(canvas, grid.head, grid.xOffsets, widths, rowFill(grid.head));
1704
+ let headGeo = gridOf(grid, grid.head);
1705
+ drawRow(canvas, grid.head, headGeo.xOffsets, headGeo.widths, rowFill(grid.head));
1511
1706
  // The last data row keeps the whole emitted total block with it, so a
1512
1707
  // total is never stranded alone at a page top.
1513
1708
  emitRows(grid, laid, totalRows);
@@ -1541,9 +1736,11 @@ let band = (canvas, items, yTop) => {
1541
1736
  // Measure a band without drawing it, to reserve its height. The trial render
1542
1737
  // goes onto a measuring canvas of its own, which can draw nothing and holds
1543
1738
  // nothing through which a document could be reached — so it runs while a live
1544
- // canvas is open on one, with no live state for it to disturb.
1545
- /** @type {(geo: Frame, fonts: any, items: any[]) => number} */
1546
- let probe = (geo, fonts, items) => band(measuring(geo, fonts), items, 0);
1739
+ // canvas is open on one, with no live state for it to disturb. The settings it
1740
+ // measures against are the render's own object, not a copy `Settings` in
1741
+ // `canvas.js` says what that buys.
1742
+ /** @type {(geo: Frame, settings: Settings, items: any[]) => number} */
1743
+ let probe = (geo, settings, items) => band(measuring(geo, settings), items, 0);
1547
1744
 
1548
1745
  // The pages every band is probed on, and the whole of what reservation assumes
1549
1746
  // about where a band varies. Two anchors cover each visibility edge — a band
@@ -1567,11 +1764,11 @@ let PROBES = [
1567
1764
  * Pure, and measured against the page box rather than a live canvas: what a
1568
1765
  * band needs is a property of the page.
1569
1766
  *
1570
- * @type {(geo: Frame, fonts: any, bands: any) => Frame}
1767
+ * @type {(geo: Frame, settings: Settings, bands: any) => Frame}
1571
1768
  */
1572
- let reserve = (geo, fonts, bands) => {
1769
+ let reserve = (geo, settings, bands) => {
1573
1770
  let height = (/** @type {(page: any) => any[]} */ items) =>
1574
- Math.max(...PROBES.map((page) => probe(geo, fonts, items(page))));
1771
+ Math.max(...PROBES.map((page) => probe(geo, settings, items(page))));
1575
1772
  return {
1576
1773
  ...geo,
1577
1774
  top: geo.top - (bands.header ? height(bands.header) + BAND : 0),
@@ -1579,18 +1776,15 @@ let reserve = (geo, fonts, bands) => {
1579
1776
  };
1580
1777
  };
1581
1778
 
1582
- // The page box a canvas presents, before any band narrowed it. `frame` owns how
1583
- // `top`/`bottom` fall out of a page and a margin — "so no caller and no suite
1584
- // has to restate it", as it says next door and both halves of page furniture
1779
+ // The page frame a canvas presents, recovered from the canvas after `adopt`
1780
+ // narrowed it. Only `top`/`bottom` were overwritten there; `width`, `height`
1781
+ // and `margin` are still the page's own, so this re-derives the two that went
1782
+ // through the same `frame` every other consumer reads — a derivation from live
1783
+ // fields, never a copy that could drift from one. Both halves of page furniture
1585
1784
  // want it: reservation to measure against, the draw pass to hang the header
1586
1785
  // from.
1587
1786
  /** @type {(canvas: Canvas) => Frame} */
1588
- let pageBox = (canvas) =>
1589
- Object.assign(frame(canvas.width, canvas.height, canvas.margin, canvas.base, canvas.family), {
1590
- locale: canvas.locale,
1591
- currency: canvas.currency,
1592
- timeZone: canvas.timeZone,
1593
- });
1787
+ let unnarrowed = (canvas) => frame(canvas.width, canvas.height, canvas.margin);
1594
1788
 
1595
1789
  // One finished page's furniture, drawn in the strips `reserve` left for it:
1596
1790
  // the header hanging from the top margin, the footer resting on the bottom
@@ -1600,7 +1794,7 @@ let pageBox = (canvas) =>
1600
1794
  // that made room for them.
1601
1795
  /** @type {(canvas: Canvas, bands: any, anchor: any) => void} */
1602
1796
  let furniture = (canvas, bands, anchor) => {
1603
- if (bands.header) band(canvas, bands.header(anchor), pageBox(canvas).top);
1797
+ if (bands.header) band(canvas, bands.header(anchor), unnarrowed(canvas).top);
1604
1798
  if (bands.footer) band(canvas, bands.footer(anchor), canvas.bottom - BAND);
1605
1799
  };
1606
1800
 
@@ -1633,12 +1827,12 @@ let numbered = (count, starts) => {
1633
1827
  * `finish` flushes whatever the last event left pending and returns what the
1634
1828
  * passes over the finished pages still need: the outline marks — the only way
1635
1829
  * to reach them, so an outline read off a band flow that still owes a flush is
1636
- * not a mistake this seam leaves open — the opening event it settled,
1637
- * handed back whole rather than read into, and the per-page `{ number, total }`
1638
- * anchors the furniture pass draws with.
1830
+ * not a mistake this seam leaves open — and the per-page `{ number, total }`
1831
+ * anchors the furniture pass draws with. The opening event itself the entry
1832
+ * peeked for both halves it is built from, so this seam keeps none of it.
1639
1833
  *
1640
1834
  * @type {(canvas: Canvas) => { handlers: Record<string, (event: any) => void>,
1641
- * finish: () => { marks: any[], opening: any,
1835
+ * finish: () => { marks: any[],
1642
1836
  * pages: { number: number, total: number }[] } }}
1643
1837
  */
1644
1838
  let flow = (canvas) => {
@@ -1651,30 +1845,38 @@ let flow = (canvas) => {
1651
1845
  starts: [0],
1652
1846
  region: null,
1653
1847
  pending: null,
1654
- // What a region already measured, keyed by the table's opening event, so
1655
- // the replay draws that table without wrapping every cell a second time.
1656
- // Per flow, because the keys are one render's events.
1657
- measured: new WeakMap(),
1658
1848
  pin: null,
1659
1849
  skipGap: false,
1660
1850
  inHeader: false,
1661
- // Assigned below, once `route` exists: a buffered region replays through
1662
- // it, and one entry point is what makes the replay take the path the
1663
- // content would have taken had it never been held.
1664
- route: () => {},
1665
1851
  };
1666
- // The body walk's own folder, emitting each finished event onward to `route`
1667
- // below. Declared here and bound after `route` exists.
1668
- /** @type {(event: any) => void} */
1669
- let fold;
1670
1852
  // A table is buffered whole before it is laid out: column widths come from
1671
1853
  // every cell in it, so the last row has to be in hand first.
1672
1854
  /** @type {any[]} */
1673
1855
  let collected = [];
1674
- // The opening event, kept for the caller: this band flow reserves the page
1675
- // bands off it, and the passes over the finished pages want the rest.
1676
- /** @type {any} */
1677
- let opening = null;
1856
+ // Hollowness is a property of the stream, not of what gets drawn: an
1857
+ // instance is hollow when nothing but hollow brackets arrived between its
1858
+ // own ends. Tracked at the handler layer, which every event reaches exactly
1859
+ // once `route` is re-entered by a region's replay, and a buffered instance
1860
+ // has to be judged before it is buffered, not after. `filling` is one flag
1861
+ // per open bracket; the verdict rides the closing event itself, so the
1862
+ // buffer and the replay both read the answer the stream gave.
1863
+ /** @type {boolean[]} */
1864
+ let filling = [];
1865
+ /** @type {WeakSet<any>} */
1866
+ let hollows = new WeakSet();
1867
+ // Whatever instance is open drew something. Called for an ordinary event and
1868
+ // for a child that turned out not to be hollow, because those are the same
1869
+ // claim: an instance draws through its content and through its children.
1870
+ let fill = () => {
1871
+ if (filling.length) filling[filling.length - 1] = true;
1872
+ };
1873
+ /** @type {(event: any) => void} */
1874
+ let track = (event) => {
1875
+ if (event.type === "group-start") filling.push(false);
1876
+ else if (event.type !== "group-end") fill();
1877
+ else if (filling.pop()) fill();
1878
+ else hollows.add(event);
1879
+ };
1678
1880
  // The body opens its own first page. Not through `turn`: a canvas that has
1679
1881
  // drawn nothing is fresh already, and would be left with no page at all.
1680
1882
  canvas.newPage();
@@ -1690,7 +1892,11 @@ let flow = (canvas) => {
1690
1892
  ? holdHeader(state, event, null, block)
1691
1893
  : placeItem(state, event, block);
1692
1894
 
1693
- /** @type {Record<string, (event: any, block: Block | null) => void>} */
1895
+ // What each event does once it is placed. The payload is typed loosely here
1896
+ // and nowhere else: this table is where the event type picks the handler,
1897
+ // so each one already knows whether it was handed a block or a measurement,
1898
+ // and no type can say that for the table as a whole.
1899
+ /** @type {Record<string, (event: any, payload: any) => void>} */
1694
1900
  let placed = {
1695
1901
  // A header is held rather than placed, and holding is bookkeeping the
1696
1902
  // replay has to see in its original order — which is why it routes like
@@ -1708,8 +1914,9 @@ let flow = (canvas) => {
1708
1914
  // default of its own -- its slots carry theirs.
1709
1915
  split: heldOrPlaced,
1710
1916
  "group-start": (event) => openGroup(state, event),
1711
- "group-end": () => {
1712
- flush(state, 0);
1917
+ "group-end": (event) => {
1918
+ if (hollows.has(event)) unopen(state);
1919
+ else flush(state, 0);
1713
1920
  state.open.pop();
1714
1921
  },
1715
1922
  // A table is collected as the events it arrived as, and shaped by `tableOf`
@@ -1718,13 +1925,15 @@ let flow = (canvas) => {
1718
1925
  "table-start": (event) => void (collected = [event]),
1719
1926
  row: (event) => void collected.push(event),
1720
1927
  "total-row": (event) => void collected.push(event),
1721
- "table-end": () => {
1722
- table(state, { ...tableOf(collected), opening: collected[0] });
1928
+ // The measurement rides the closing event when a region buffered this
1929
+ // table; anywhere else there is none and `table` takes its own.
1930
+ "table-end": (event, ready) => {
1931
+ table(state, collected, ready);
1723
1932
  collected = [];
1724
1933
  },
1725
1934
  };
1726
- /** @type {(event: any, block: Block | null) => void} */
1727
- let place = (event, block) => placed[event.type](event, block);
1935
+ /** @type {(event: any, payload: Block | Measured | null) => void} */
1936
+ let place = (event, payload) => placed[event.type](event, payload);
1728
1937
 
1729
1938
  // Which depth owes or holds the strips, whether or not any content has
1730
1939
  // opened them yet: an instance that emits nothing still closes with a
@@ -1759,18 +1968,61 @@ let flow = (canvas) => {
1759
1968
 
1760
1969
  // Route one event: the declaring node's own bands stay full-width, region
1761
1970
  // content opens the strips owed to it, and anything arriving while a region
1762
- // is still deciding is buffered rather than placed.
1763
- /** @type {(event: any, block?: Block | null) => void} */
1764
- let route = (event, block = null) => {
1971
+ // is still deciding is buffered rather than placed. The payload is the work
1972
+ // already done for this event a block off the walk, or, on the replay of
1973
+ // a buffered table, the measurement the buffer took. Which of the two it is
1974
+ // follows from the event's type, the same way the handler table dispatches.
1975
+ /** @type {(event: any, payload?: Block | Measured | null) => void} */
1976
+ let route = (event, payload = null) => {
1765
1977
  snapPin(event);
1766
- if (fullBand(event)) fullWidth(event, block);
1767
- else if (deciding()) buffer(state, event, block);
1978
+ if (fullBand(event)) fullWidth(event, payload);
1979
+ else if (deciding()) hold(event, payload);
1768
1980
  else if (opens(event)) begin(event);
1769
- else place(event, block);
1981
+ else place(event, payload);
1770
1982
  };
1771
1983
 
1772
- state.route = route;
1773
- fold = splitFolder(route);
1984
+ // The body walk's own folder, emitting each finished event onward to `route`.
1985
+ let fold = splitFolder(route);
1986
+
1987
+ // Hold one event back in the region's buffer, and lay that buffer out if this
1988
+ // is the event that filled it. Not `holdHeader`: that holds a group's header
1989
+ // for the content it introduces, this holds anything at all for a region
1990
+ // still deciding.
1991
+ /** @type {(event: any, payload: Block | Measured | null) => void} */
1992
+ let hold = (event, payload) => {
1993
+ if (buffer(state, event, payload, hollows.has(event))) replay();
1994
+ };
1995
+
1996
+ // Lay the buffer out: `commit` decides the strip height and hands the held
1997
+ // entries back in order, and this walks them through `route`. The walk lives
1998
+ // here rather than under the region section marker for the reason everything
1999
+ // there is module-level — that section is reachable without the router, and
2000
+ // a walk through `route` is not.
2001
+ let replay = () => {
2002
+ let region = /** @type {Region} */ (state.region);
2003
+ for (let entry of commit(state)) {
2004
+ if (breaksFor(state, region, entry.span)) advance(state);
2005
+ route(entry.event, entry.payload);
2006
+ }
2007
+ };
2008
+
2009
+ // Close the region: anything still held is laid out first, and the region
2010
+ // then lands where `landRegion` puts it.
2011
+ //
2012
+ // Nothing the replay routes can arrive back here, which is why the local
2013
+ // `region` survives the call. `route` asks `fullBand` before `deciding()`, so
2014
+ // a full band is never buffered; and a replayed event, asked `fullBand` a
2015
+ // second time, answers as it did the first, because `nesting()`'s two
2016
+ // branches walk the same brackets — `buffer` carries `region.nesting` through
2017
+ // them while nothing is placed, `state.open` through them once the replay is
2018
+ // placing. The other closer, `endOwned`, is wired above `route` at the
2019
+ // handler layer, where a replay does not reach.
2020
+ let closeRegion = () => {
2021
+ let region = state.region;
2022
+ if (!region) return;
2023
+ if (deciding()) replay();
2024
+ landRegion(state, region);
2025
+ };
1774
2026
 
1775
2027
  // Everything drawn across the page rather than down a strip: the declaring
1776
2028
  // node's own bands, its footer among them.
@@ -1790,7 +2042,7 @@ let flow = (canvas) => {
1790
2042
  /** @type {(depth: number) => void} */
1791
2043
  let endOwned = (depth) => {
1792
2044
  if (owner() !== depth) return;
1793
- if (state.region) closeRegion(state);
2045
+ if (state.region) closeRegion();
1794
2046
  else state.pending = null;
1795
2047
  };
1796
2048
 
@@ -1801,13 +2053,13 @@ let flow = (canvas) => {
1801
2053
  // owed survives a report header — that band arrives before the body it
1802
2054
  // columns — but not the owner's own footer, which is the end of what it
1803
2055
  // would have columned.
1804
- /** @type {(event: any, block: Block | null) => void} */
1805
- let fullWidth = (event, block) => {
2056
+ /** @type {(event: any, payload: Block | Measured | null) => void} */
2057
+ let fullWidth = (event, payload) => {
1806
2058
  // Asked before the region closes, because that is what it reads.
1807
2059
  let own = ownFooter(event);
1808
- closeRegion(state);
2060
+ closeRegion();
1809
2061
  if (own) state.pending = null;
1810
- place(event, block);
2062
+ place(event, payload);
1811
2063
  };
1812
2064
 
1813
2065
  // The first content of a columned node. The node's own header run is drawn
@@ -1821,29 +2073,7 @@ let flow = (canvas) => {
1821
2073
  let block = keepBlock(state, event, stripWidth(state, owed.count));
1822
2074
  flush(state, block ? keepWith(block) : 0);
1823
2075
  openRegion(state);
1824
- buffer(state, event, block);
1825
- };
1826
-
1827
- // The report default onto the canvas, before anything is measured. It is
1828
- // narrowed to `family` and `size`, and each replaces this target's own
1829
- // baseline outright: row heights and band gaps scale with the document's
1830
- // type rather than staying at a size nothing is set in, and text declaring
1831
- // no family is set in the document's. Lifting the pair here is the whole of
1832
- // this target's reading of docs/adr/0033 -- the default reaches a node as a
1833
- // fallback the canvas carries, never as a layer merged into its style, so a
1834
- // document-wide fact costs no allocation however many cells a report has.
1835
- //
1836
- // `sizeOf` and `familyName` are this target's one reading each of what a
1837
- // declared size and family amount to, so they read the default here too --
1838
- // both reach this unchecked from a computed style, and two spellings of that
1839
- // leniency would drift. Each falls back to what the canvas already carries,
1840
- // so a default declaring one of the pair leaves the other alone, and one
1841
- // whose value is unusable leaves this target's own baseline standing.
1842
- /** @type {(style: any) => void} */
1843
- let adoptDefault = (style) => {
1844
- if (!style) return;
1845
- canvas.base = sizeOf(style, canvas.base);
1846
- canvas.family = familyName(style) || canvas.family;
2076
+ hold(event, block);
1847
2077
  };
1848
2078
 
1849
2079
  /** @type {(event: any) => void} */
@@ -1857,13 +2087,17 @@ let flow = (canvas) => {
1857
2087
  };
1858
2088
 
1859
2089
  return {
1860
- handlers: {
2090
+ // Every handler is wrapped so `track` sees the stream once, in order, ahead
2091
+ // of the dispatch that may buffer the event for later. One wrap rather than
2092
+ // a call in each handler: a new event type must count towards the instance
2093
+ // it arrives in without anyone remembering to say so.
2094
+ handlers: tracked(track, {
1861
2095
  "report-start": (event) => {
1862
- opening = event;
1863
- canvas.locale = event.locale;
1864
- canvas.currency = event.currency;
1865
- canvas.timeZone = event.timeZone;
1866
- adoptDefault(event.style);
2096
+ // The document-wide settings were built complete from this same event
2097
+ // before the walk began (index.js peeks it), so this handler reads them
2098
+ // through `canvas.settings` and settles nothing — only the page bands,
2099
+ // whose height is not known until here, are taken off it below. The
2100
+ // entry peeked the event too, so nothing is kept for it here.
1867
2101
  if (event.columns) state.pending = { count: event.columns, owner: -1 };
1868
2102
  if (!event.page) {
1869
2103
  pinHeader(event);
@@ -1875,7 +2109,7 @@ let flow = (canvas) => {
1875
2109
  // one thing a measurement must not be able to do. What a band needs is
1876
2110
  // a property of the page box in any case, not of a content box
1877
2111
  // something may already have narrowed.
1878
- adopt(canvas, reserve(pageBox(canvas), canvas.fonts, event.page));
2112
+ adopt(canvas, reserve(unnarrowed(canvas), canvas.settings, event.page));
1879
2113
  pinHeader(event);
1880
2114
  },
1881
2115
  // Everything else routes: `placed` above says what each event does, and
@@ -1898,14 +2132,17 @@ let flow = (canvas) => {
1898
2132
  endOwned(event.depth);
1899
2133
  route(event);
1900
2134
  },
1901
- },
2135
+ }),
1902
2136
  finish: () => {
1903
2137
  snapPin({ type: "report-end" });
1904
2138
  // A root region reaches here undrained when a report declares `columns`
1905
2139
  // and no full-width footer followed its body.
1906
- closeRegion(state);
2140
+ closeRegion();
1907
2141
  flush(state, 0);
1908
- return { marks: state.marks, opening, pages: numbered(canvas.count, state.starts) };
2142
+ return {
2143
+ marks: state.marks,
2144
+ pages: numbered(canvas.count, state.starts),
2145
+ };
1909
2146
  },
1910
2147
  };
1911
2148
  };