@quario/pdf 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,11 +21,13 @@
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 { isReportBand, text } from "quario";
24
+ import { display, isReportBand, text } from "quario";
25
25
  import { balance } from "./balance.js";
26
26
  import { adopt, frame, measuring } from "./canvas.js";
27
+ import { familyName } from "./fonts.js";
27
28
  import { intrinsic } from "./image.js";
28
- import { BAND, GUTTER, LEAD, PADX, PADY, col, merge, roled, shift } from "./style.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";
29
31
  import { atoms, dress, heightOf, wrap } from "./text.js";
30
32
 
31
33
  /** @typedef {import('./balance.js').Unit} Unit */
@@ -46,8 +48,15 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
46
48
  * @typedef {{ canvas: Canvas, open: Group[], gap: number, marks: any[],
47
49
  * starts: number[], region: Region | null, pending: Pending | null,
48
50
  * measured: WeakMap<any, Measured>,
51
+ * pin: number | null, skipGap: boolean, inHeader: boolean,
49
52
  * route: (event: any, block?: Block | null) => void }} Flow
50
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
+ */
51
60
 
52
61
  // One flow unit and its presentation; `h` is the full flow height. A text
53
62
  // item's is its wrapped lines; an image item's is the picture, sized here so
@@ -57,31 +66,62 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
57
66
  * @typedef {{ bytes: Uint8Array, format: string, w: number, h: number }} Picture
58
67
  */
59
68
  /**
60
- * @typedef {{ lines: Line[], picture?: Picture, bg: string | null, align: any,
61
- * h: number }} Block
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
62
80
  */
63
81
 
64
82
  // How `fit` sizes a picture against the width it has: `natural` is the
65
83
  // image's own size in points, never wider than the content box; `width`
66
84
  // scales to the content box either way. The ratio is kept in both, so a
67
85
  // height is never anything but the width's consequence.
68
- /** @type {(canvas: Canvas, event: any, avail: number) => Picture} */
69
- let pictureOf = (canvas, event, avail) => {
86
+ /** @type {(event: any, avail: number) => Picture} */
87
+ let pictureOf = (event, avail) => {
70
88
  let { w, h } = intrinsic(event.bytes, event.format);
71
89
  let width = event.fit === "width" ? avail : Math.min(w, avail);
72
90
  return { bytes: event.bytes, format: event.format, w: width, h: (h * width) / w };
73
91
  };
74
92
 
75
- /** @type {(canvas: Canvas, event: any, avail: number) => Block} */
76
- let blockOf = (canvas, event, avail) => {
77
- let style = merge(null, event.style);
78
- let bg = col(style.background);
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);
79
103
  if (event.type === "image") {
80
- let picture = pictureOf(canvas, event, avail);
81
- return { lines: [], picture, bg, align: style.align, h: picture.h };
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
+ };
82
114
  }
83
- let lines = dress(wrap(canvas, atoms(canvas, event.tokens, style), Math.max(avail, 1)), style);
84
- return { lines, bg, align: style.align, h: heightOf(lines) };
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
+ };
85
125
  };
86
126
 
87
127
  // Scale a picture block down to the room it has. Best-effort, and only ever
@@ -89,9 +129,10 @@ let blockOf = (canvas, event, avail) => {
89
129
  /** @type {(block: Block, room: number) => void} */
90
130
  let shrink = (block, room) => {
91
131
  let picture = /** @type {Picture} */ (block.picture);
92
- picture.w *= room / picture.h;
93
- picture.h = room;
94
- block.h = room;
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;
95
136
  };
96
137
 
97
138
  // Draw one block whole at the cursor. A picture's background fills the
@@ -100,27 +141,123 @@ let shrink = (block, room) => {
100
141
  // is one box, so where it sits in the width it was given is worked out once.
101
142
  /** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
102
143
  let drawPicture = (canvas, block, x, avail) => {
144
+ let inset = block.inset;
103
145
  let { bytes, format, w, h } = /** @type {Picture} */ (block.picture);
104
- let at = x + shift(block.align, avail - w);
105
- if (block.bg) canvas.rect(block.bg, at, canvas.y - h, w, h);
106
- canvas.picture(bytes, format, at, canvas.y - h, w, h);
107
- canvas.y -= h;
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;
108
151
  canvas.fresh = false;
109
152
  };
110
153
 
111
154
  /** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
112
155
  let drawLines = (canvas, block, x, avail) => {
113
- if (block.bg) canvas.rect(block.bg, x, canvas.y - block.h, avail, block.h);
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);
114
160
  for (let line of block.lines) {
115
- canvas.drawLine(line, x, canvas.y, avail, block.align);
116
- canvas.y -= line.h;
161
+ canvas.drawLine(line, x + inset.l, y, inner, block.align);
162
+ y -= line.h;
117
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;
118
255
  canvas.fresh = false;
119
256
  };
120
257
 
121
258
  /** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
122
259
  let drawBlock = (canvas, block, x, avail) =>
123
- (block.picture ? drawPicture : drawLines)(canvas, block, x, avail);
260
+ (block.parts ? drawSplit : block.picture ? drawPicture : drawLines)(canvas, block, x, avail);
124
261
 
125
262
  // A region is a run of strips: what a columned node's content flows down
126
263
  // (CONTEXT.md "Strip"). `index` is the strip being filled, `top` the y every
@@ -169,7 +306,7 @@ let ceilOf = (state) => (state.region ? state.region.top : state.canvas.top);
169
306
  // leaving an empty page behind — so a strip claims no second meaning for it.
170
307
  // A block of no height would read fresh here and used there; none is reachable
171
308
  // today, since every item wraps to at least one line.
172
- /** @type {(state: Flow) => boolean} */
309
+ /** @type {(state: Cursor) => boolean} */
173
310
  let freshOf = (state) => (state.region ? state.canvas.y === state.region.top : state.canvas.fresh);
174
311
 
175
312
  /** @type {(canvas: Canvas, h: number, line: Line, floor: number) => boolean} */
@@ -229,17 +366,42 @@ let fitLines = (canvas, block, i, floor, fresh) => {
229
366
  /** @type {(canvas: Canvas, block: Block, floor: number) => boolean} */
230
367
  let whole = (canvas, block, floor) => canvas.y - block.h >= floor;
231
368
 
232
- // An image is never split: it moves whole to a fresh column. One that no
233
- // column can hold is scaled down to the one it lands on -- obeying the
234
- // guarantee literally would mean dropping it (SCHEMA.md, best-effort). It is
235
- // the unsplittable block, so a balanced floor never cuts it: only the page's
236
- // own floor can shrink it.
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.
237
399
  /** @type {(state: Flow, block: Block) => void} */
238
- let placePicture = (state, block) => {
400
+ let placeWhole = (state, block) => {
239
401
  advance(state);
240
402
  let canvas = state.canvas;
241
403
  let room = canvas.y - canvas.bottom;
242
- if (block.h > room) shrink(block, room);
404
+ if (block.picture && block.h > room) shrink(block, room);
243
405
  drawBlock(canvas, block, originOf(state), widthOf(state));
244
406
  };
245
407
 
@@ -285,12 +447,24 @@ let item = (state, event, measured = null) => {
285
447
  let canvas = state.canvas;
286
448
  let avail = widthOf(state);
287
449
  let block = measured || blockOf(canvas, event, avail);
288
- // Whole block fits where it stands: one piece, no slicing.
289
- if (whole(canvas, block, floorOf(state))) return drawBlock(canvas, block, originOf(state), avail);
290
- if (block.picture) return placePicture(state, block);
291
- slice(state, block);
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");
292
459
  };
293
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
+
294
468
  // The height a page turn carries over: the drawn headers of the groups still
295
469
  // open, which every page their instances continue onto replays. Computed rather
296
470
  // than kept — a stored total is a second copy of `open` that can drift from it.
@@ -434,8 +608,9 @@ let closeRegion = (state) => {
434
608
  // Two things this deliberately does not model, each stated because the cost of
435
609
  // getting them wrong is a strip that stops early rather than a wrong document.
436
610
  // A `total-row` is a row like any other, though it really keeps the last data
437
- // row with it (SCHEMA.md): pairing them costs the flat per-entry map this is,
438
- // to remove an error of one row at the end of a table. And a row taller than
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
439
614
  // any strip is sliced rather than moved (SCHEMA.md, "How a tall row slices");
440
615
  // called whole here, it makes every candidate height fail and the region
441
616
  // fills, which is the honest answer for a region holding one.
@@ -481,9 +656,16 @@ let UNITS = {
481
656
  },
482
657
  row: restated,
483
658
  "total-row": restated,
484
- image: (entry) => unsplit(entry.h, entry.h),
659
+ image: unbroken,
660
+ split: unbroken,
485
661
  };
486
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
+
487
669
  // A row opening a strip carries the column headings restated above it.
488
670
  /** @type {(entry: any, table: Restated) => Unit} */
489
671
  function restated(entry, table) {
@@ -580,11 +762,13 @@ let estimate = (state, event) => (WORTH[event.type] || (() => 0))(state.canvas);
580
762
  // What each kind of buffered event is worth. A module constant: `estimate` runs
581
763
  // once per buffered event, and rebuilding this table each time would allocate
582
764
  // a closure per table row.
765
+ /** @type {(canvas: Canvas) => number} */
766
+ let rowWorth = (canvas) => rowHeight(canvas) + CELL_PAD.t + CELL_PAD.b;
583
767
  /** @type {Record<string, (canvas: Canvas) => number>} */
584
768
  let WORTH = {
585
- row: (canvas) => rowHeight(canvas) + 2 * PADY,
586
- "total-row": (canvas) => rowHeight(canvas) + 2 * PADY,
587
- "table-start": (canvas) => rowHeight(canvas) + 2 * PADY,
769
+ row: rowWorth,
770
+ "total-row": rowWorth,
771
+ "table-start": rowWorth,
588
772
  "group-start": (canvas) => instanceGap(canvas),
589
773
  };
590
774
 
@@ -639,8 +823,8 @@ let remeasure = (state, region) => {
639
823
  // Reports what the correction moved the running total by.
640
824
  /** @type {(span: any[], measured: Measured) => number} */
641
825
  let retune = (span, measured) => {
642
- let heights = [measured.head.h, ...measured.laid.map((/** @type {any} */ laid) => laid.row.h)];
643
- if (measured.totalRow) heights.push(measured.totalRow.h);
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);
644
828
  let delta = 0;
645
829
  for (let [i, h] of heights.entries()) {
646
830
  delta += h - span[i].h;
@@ -664,7 +848,7 @@ let entryFor = (state, event, block, width) => {
664
848
  // decided anything. Events that are not blocks contribute an `estimate`.
665
849
  /** @type {(state: Flow, event: any, width: number) => Block | null} */
666
850
  let measureFor = (state, event, width) =>
667
- event.type === "item" || event.type === "image" ? blockOf(state.canvas, event, width) : null;
851
+ BLOCKS.has(event.type) ? blockOf(state.canvas, event, width) : null;
668
852
 
669
853
  // Give every outline mark still waiting a position: where its content begins.
670
854
  /** @type {(state: Flow) => void} */
@@ -684,7 +868,7 @@ let pending = (state) => state.open.filter((group) => group.held.length);
684
868
 
685
869
  // A held run, as blocks. A region measured its headers while buffering them,
686
870
  // at this same width, so a replayed run reuses those rather than wrapping the
687
- // same text a second time (ADR 0020).
871
+ // same text a second time (ADR 0027).
688
872
  /** @type {(canvas: Canvas, held: any[], avail: number) => Block[]} */
689
873
  let blocksOf = (canvas, held, avail) =>
690
874
  held.map((entry) => entry.block || blockOf(canvas, entry.event, avail));
@@ -761,7 +945,7 @@ let keepWith = (block) =>
761
945
  /** @type {(state: Flow, event: any, measured?: Block | null) => void} */
762
946
  let placeItem = (state, event, measured = null) => {
763
947
  let block = measured || keepBlock(state, event, widthOf(state));
764
- if (block && holding(state)) flush(state, keepWith(block));
948
+ if (block && holding(state)) flush(state, keepWith(block) + flowPad(state, event));
765
949
  item(state, event, block);
766
950
  };
767
951
 
@@ -790,7 +974,8 @@ let holding = (state) => state.open.some((group) => group.held.length) || state.
790
974
  let openGroup = (state, event) => {
791
975
  breakFor(state, event);
792
976
  if (event.columns) state.pending = { count: event.columns, owner: event.depth };
793
- state.gap = Math.max(state.gap, instanceGap(state.canvas));
977
+ state.gap = state.skipGap ? 0 : Math.max(state.gap, instanceGap(state.canvas));
978
+ state.skipGap = false;
794
979
  state.open.push({ held: [], blocks: [] });
795
980
  state.marks.push(markFor(event));
796
981
  };
@@ -805,7 +990,9 @@ let breakFor = (state, event) => {
805
990
 
806
991
  /** @type {(event: any) => any} */
807
992
  let markFor = (event) => ({
808
- title: event.name + ": " + String(event.key),
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),
809
996
  titled: false,
810
997
  depth: event.depth,
811
998
  page: -1,
@@ -848,16 +1035,22 @@ let headOf = (canvas, span) => span - instanceGap(canvas);
848
1035
  // gives flow spacing no meaning inside a table row, in either target.
849
1036
  /** @type {(canvas: Canvas, cell: any, rowStyle: any) => any} */
850
1037
  let cellOf = (canvas, cell, rowStyle) => {
851
- let style = merge(rowStyle, cell.style);
1038
+ let style = merge(unbox(rowStyle), unbox(cell.style));
1039
+ let inset = insetOf(cell.style, CELL_PAD);
852
1040
  let list = atoms(canvas, cell.tokens, style);
853
- let natural = wrap(canvas, list, Infinity).reduce((widest, line) => Math.max(widest, line.w), 0);
1041
+ let natural = wrap(canvas, list, Infinity, sizeOf(style, canvas.base)).reduce(
1042
+ (widest, line) => Math.max(widest, line.w),
1043
+ 0,
1044
+ );
854
1045
  return {
855
1046
  list,
856
- natural: natural + 2 * PADX,
1047
+ natural: natural + inset.l + inset.r,
857
1048
  align: style.align,
858
1049
  bg: col(style.background),
859
1050
  underline: !!style.underline,
860
1051
  strikethrough: !!style.strikethrough,
1052
+ inset,
1053
+ style: cell.style,
861
1054
  };
862
1055
  };
863
1056
 
@@ -865,9 +1058,14 @@ let cellOf = (canvas, cell, rowStyle) => {
865
1058
  let rowOf = (canvas, cells, widths) => {
866
1059
  let h = rowHeight(canvas);
867
1060
  let out = cells.map((cell, i) => {
868
- let inner = Math.max(widths[i] - 2 * PADX, 1);
869
- let lines = cell.list.length ? dress(wrap(canvas, cell.list, inner), cell) : [];
870
- let cellHeight = heightOf(lines) + 2 * PADY;
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;
871
1069
  if (cellHeight > h) h = cellHeight;
872
1070
  return { ...cell, lines };
873
1071
  });
@@ -881,8 +1079,9 @@ let fillCell = (canvas, cell, bg, x, y, w, h) => {
881
1079
 
882
1080
  /** @type {(canvas: Canvas, cell: any, lines: any[], x: number, y: number, w: number) => void} */
883
1081
  let writeCell = (canvas, cell, lines, x, y, w) => {
1082
+ let inset = cell.inset;
884
1083
  for (let line of lines) {
885
- canvas.drawLine(line, x + PADX, y, w - 2 * PADX, cell.align);
1084
+ canvas.drawLine(line, x + inset.l, y, w - inset.l - inset.r, cell.align);
886
1085
  y -= line.h;
887
1086
  }
888
1087
  };
@@ -896,9 +1095,10 @@ let writeCell = (canvas, cell, lines, x, y, w) => {
896
1095
  */
897
1096
  let paintCells = (canvas, cells, lines, h, padTop, xOffsets, widths, bg) => {
898
1097
  if (bg) canvas.rect(bg, xOffsets[0], canvas.y - h, sum(widths), h);
899
- let y0 = canvas.y - (padTop ? PADY : 0);
900
1098
  for (let [i, cell] of cells.entries()) {
901
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);
902
1102
  writeCell(canvas, cell, lines[i], xOffsets[i], y0, widths[i]);
903
1103
  }
904
1104
  canvas.y -= h;
@@ -906,10 +1106,11 @@ let paintCells = (canvas, cells, lines, h, padTop, xOffsets, widths, bg) => {
906
1106
  };
907
1107
 
908
1108
  /**
909
- * @type {(canvas: Canvas, row: { cells: any[], h: number }, xOffsets: number[],
1109
+ * @type {(canvas: Canvas, row: TableRow, xOffsets: number[],
910
1110
  * widths: number[], bg: string | null) => void}
911
1111
  */
912
- let drawRow = (canvas, row, xOffsets, widths, bg) =>
1112
+ let drawRow = (canvas, row, xOffsets, widths, bg) => {
1113
+ let yTop = canvas.y;
913
1114
  paintCells(
914
1115
  canvas,
915
1116
  row.cells,
@@ -920,14 +1121,17 @@ let drawRow = (canvas, row, xOffsets, widths, bg) =>
920
1121
  widths,
921
1122
  bg,
922
1123
  );
1124
+ paintBox(canvas, xOffsets[0], yTop, sum(widths), row.h, row.style, null);
1125
+ };
923
1126
 
924
- // The widest of each column's header, rows and total, padding included.
925
- /** @type {(cols: any[], header: any[], rows: any[], total: any[] | null) => number[]} */
926
- let naturalWidths = (cols, header, rows, total) =>
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) =>
927
1130
  cols.map((_, i) => {
928
1131
  let widest = header[i].natural;
929
1132
  for (let row of rows) widest = Math.max(widest, row.cells[i].natural);
930
- return total ? Math.max(widest, total[i].natural) : widest;
1133
+ for (let row of totals) widest = Math.max(widest, row.cells[i].natural);
1134
+ return widest;
931
1135
  });
932
1136
 
933
1137
  /** @type {(values: number[]) => number} */
@@ -945,7 +1149,7 @@ let sum = (values) => values.reduce((total, value) => total + value, 0);
945
1149
  // same fail-loud posture as the measuring canvas's `newPage`.
946
1150
  /** @type {(cols: any[], natural: number[], avail: number) => number[]} */
947
1151
  let columnWidths = (cols, natural, avail) => {
948
- let fixed = cols.map((col) => typeof col.width === "number" && Number.isFinite(col.width));
1152
+ let fixed = cols.map((column) => Number.isFinite(column.width));
949
1153
  if (!fixed.some(Boolean)) {
950
1154
  // Natural widths always carry the cell padding, so the sum is never zero.
951
1155
  let wanted = sum(natural);
@@ -963,14 +1167,10 @@ let columnWidths = (cols, natural, avail) => {
963
1167
  return widths;
964
1168
  };
965
1169
 
966
- // The rule under a table's header row and over its total: the only rule this
967
- // target draws, always the full content width at the cursor.
968
- /** @type {(canvas: Canvas, x: number, avail: number) => void} */
969
- let underline = (canvas, x, avail) => canvas.rule(x, x + avail, canvas.y);
970
-
971
1170
  // One table being emitted: what `carryOver`, `sliced` and `put` all need.
972
1171
  /**
973
- * @typedef {{ state: Flow, head: { cells: any[], h: number },
1172
+ * @typedef {{ cells: any[], h: number, style?: any }} TableRow
1173
+ * @typedef {{ state: Flow, head: TableRow,
974
1174
  * xOffsets: number[], widths: number[], x: number, avail: number }} Grid
975
1175
  */
976
1176
 
@@ -982,12 +1182,14 @@ let underline = (canvas, x, avail) => canvas.rule(x, x + avail, canvas.y);
982
1182
  // The headings replay at a strip head as well as a page head, unlike a group
983
1183
  // header (SCHEMA.md): a column of figures under nothing is unreadable, where a
984
1184
  // group header is merely absent.
1185
+ /** @type {(row: { style?: any }) => any} */
1186
+ let rowFill = (row) => col((row.style || {}).background);
1187
+
985
1188
  /** @type {(grid: Grid) => void} */
986
1189
  let carryOver = (grid) => {
987
1190
  advance(grid.state);
988
1191
  rebase(grid);
989
- drawRow(grid.state.canvas, grid.head, grid.xOffsets, grid.widths, null);
990
- underline(grid.state.canvas, grid.x, grid.avail);
1192
+ drawRow(grid.state.canvas, grid.head, grid.xOffsets, grid.widths, rowFill(grid.head));
991
1193
  };
992
1194
 
993
1195
  // Move the grid to the column the cursor is now in. A table's widths are its
@@ -1007,7 +1209,7 @@ let firstSlice = (canvas, row) =>
1007
1209
  rowHeight(canvas),
1008
1210
  ...row.cells
1009
1211
  .filter((/** @type {any} */ cell) => cell.lines.length)
1010
- .map((/** @type {any} */ cell) => PADY + cell.lines[0].h),
1212
+ .map((/** @type {any} */ cell) => cell.inset.t + cell.lines[0].h),
1011
1213
  );
1012
1214
 
1013
1215
  /** @type {(cell: any, next: number[], i: number, used: number, cap: number) => any[]} */
@@ -1027,10 +1229,17 @@ let takeLines = (cell, next, i, used, cap) => {
1027
1229
  let stillMore = (row, next) =>
1028
1230
  row.cells.some((/** @type {any} */ cell, /** @type {number} */ i) => next[i] < cell.lines.length);
1029
1231
 
1030
- /** @type {(taken: any[][], pad: number, more: boolean) => number} */
1031
- let sliceH = (taken, pad, more) => {
1032
- let h = Math.max(0, ...taken.map((lines) => pad + heightOf(lines)));
1033
- return more ? h : h + PADY;
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;
1034
1243
  };
1035
1244
 
1036
1245
  // A row no page can hold degrades to plain flow: each page takes what fits of
@@ -1042,13 +1251,11 @@ let sliced = (grid, row, bg) => {
1042
1251
  let first = true;
1043
1252
  for (;;) {
1044
1253
  let cap = canvas.y - canvas.bottom;
1045
- let pad = first ? PADY : 0;
1046
1254
  let taken = row.cells.map((/** @type {any} */ cell, /** @type {number} */ i) =>
1047
- takeLines(cell, next, i, pad, cap),
1255
+ takeLines(cell, next, i, first ? cell.inset.t : 0, cap),
1048
1256
  );
1049
1257
  let more = stillMore(row, next);
1050
- // The closing padding belongs to the last slice only.
1051
- let h = sliceH(taken, pad, more);
1258
+ let h = sliceH(taken, row.cells, first, more);
1052
1259
  paintCells(canvas, row.cells, taken, h, first, grid.xOffsets, grid.widths, bg);
1053
1260
  if (!more) return;
1054
1261
  carryOver(grid);
@@ -1074,21 +1281,13 @@ let shouldBreak = (state, row, keep, cap) => {
1074
1281
  return canvas.y - row.h - keep < floor && worthBreak(canvas, row, cap, floor) && !freshOf(state);
1075
1282
  };
1076
1283
 
1077
- /** @type {(grid: Grid, topRule: boolean, broke: boolean) => void} */
1078
- let openRow = (grid, topRule, broke) => {
1079
- if (broke) carryOver(grid);
1080
- if (topRule && !broke) underline(grid.state.canvas, grid.x, grid.avail);
1081
- };
1082
-
1083
1284
  // Place one row, breaking first when that helps. `keep` is a companion row it
1084
1285
  // must not be parted from — the total riding with the last data row.
1085
- // `topRule` is the total's opening rule, which after a break would double the
1086
- // replayed header's.
1087
1286
  /**
1088
- * @type {(grid: Grid, row: { cells: any[], h: number }, bg: string | null,
1089
- * keep?: number, topRule?: boolean) => void}
1287
+ * @type {(grid: Grid, row: TableRow, bg: string | null,
1288
+ * keep?: number) => void}
1090
1289
  */
1091
- let put = (grid, row, bg, keep = 0, topRule = false) => {
1290
+ let put = (grid, row, bg, keep = 0) => {
1092
1291
  let canvas = grid.state.canvas;
1093
1292
  // The tallest row a fresh column holds, after the replayed group headers and
1094
1293
  // the replayed column headings. A companion over that can never share a
@@ -1100,33 +1299,36 @@ let put = (grid, row, bg, keep = 0, topRule = false) => {
1100
1299
  // and leaves a headings-only column. Clamped to `cap`: a first slice no fresh
1101
1300
  // column could fit either is no reason to go looking for one.
1102
1301
  let broke = shouldBreak(grid.state, row, keep, cap);
1103
- openRow(grid, topRule, broke);
1302
+ if (broke) carryOver(grid);
1104
1303
  if (canvas.y - row.h < canvas.bottom) return sliced(grid, row, bg);
1105
1304
  drawRow(canvas, row, grid.xOffsets, grid.widths, bg);
1106
1305
  };
1107
1306
 
1108
- /** @type {(canvas: Canvas, row: any) => { cells: any[], bg: any }} */
1307
+ /** @type {(canvas: Canvas, row: any) => { cells: any[], style: any }} */
1109
1308
  let bodyRow = (canvas, row) => ({
1110
1309
  cells: row.cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, row.style)),
1111
- bg: col((row.style || {}).background),
1310
+ style: row.style,
1112
1311
  });
1113
1312
 
1114
1313
  /** @type {(canvas: Canvas, buffered: any) => any} */
1115
- let totalCells = (canvas, buffered) =>
1116
- buffered.total && buffered.total.map((/** @type {any} */ cell) => cellOf(canvas, cell, null));
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
+ }));
1117
1319
 
1118
- /** @type {(laid: any[], totalRow: any) => number} */
1119
- let firstH = (laid, totalRow) => (laid.length ? laid[0].row.h : totalRow ? totalRow.h : 0);
1320
+ /** @type {(laid: any[], totalRows: any[]) => number} */
1321
+ let firstH = (laid, totalRows) => (laid.length ? laid[0].h : totalRows.length ? totalRows[0].h : 0);
1120
1322
 
1121
- /** @type {(totalRow: any, i: number, last: number) => number} */
1122
- let keepTotal = (totalRow, i, last) => (totalRow && i === last ? totalRow.h : 0);
1323
+ /** @type {(totalRows: any[]) => number} */
1324
+ let totalsH = (totalRows) => totalRows.reduce((h, row) => h + row.h, 0);
1123
1325
 
1124
- /** @type {(grid: Grid, laid: any[], totalRow: any) => void} */
1125
- let emitRows = (grid, laid, totalRow) => {
1326
+ /** @type {(grid: Grid, laid: any[], totalRows: any[]) => void} */
1327
+ let emitRows = (grid, laid, totalRows) => {
1126
1328
  let last = laid.length - 1;
1127
- for (let [i, laidRow] of laid.entries())
1128
- put(grid, laidRow.row, laidRow.bg, keepTotal(totalRow, i, last));
1129
- if (totalRow) put(grid, totalRow, null, 0, true);
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);
1130
1332
  };
1131
1333
 
1132
1334
  // Lay out a buffered table: measure every cell, allocate the columns, then
@@ -1137,14 +1339,19 @@ let emitRows = (grid, laid, totalRow) => {
1137
1339
  // events as they arrive, and the region buffer, reading them back off what it
1138
1340
  // held — so a new table event kind cannot reach one and miss the other.
1139
1341
  /** @type {(events: any[]) => any} */
1140
- let tableOf = (events) => ({
1141
- columns: events[0].columns,
1142
- rows: events.filter((event) => event.type === "row"),
1143
- total: events.find((event) => event.type === "total-row")?.cells ?? null,
1144
- });
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
+ };
1145
1352
 
1146
1353
  /**
1147
- * @typedef {{ widths: number[], head: any, laid: any[], totalRow: any }} Measured
1354
+ * @typedef {{ widths: number[], head: any, laid: any[], totalRows: any[] }} Measured
1148
1355
  */
1149
1356
  // Measure a buffered table at a given width: the column widths every cell has
1150
1357
  // a say in, then each row laid out against them. Everything here is
@@ -1154,18 +1361,23 @@ let tableOf = (events) => ({
1154
1361
  /** @type {(canvas: Canvas, buffered: any, avail: number) => Measured} */
1155
1362
  let measureTable = (canvas, buffered, avail) => {
1156
1363
  let cols = /** @type {any[]} */ (buffered.columns);
1157
- let header = cols.map((/** @type {any} */ col) => cellOf(canvas, col.header, null));
1364
+ let header = cols.map((/** @type {any} */ col) =>
1365
+ cellOf(canvas, col.header, buffered.headerStyle),
1366
+ );
1158
1367
  let rows = buffered.rows.map((/** @type {any} */ row) => bodyRow(canvas, row));
1159
- let total = totalCells(canvas, buffered);
1160
- let widths = columnWidths(cols, naturalWidths(cols, header, rows, total), avail);
1368
+ let totals = totalRowsOf(canvas, buffered);
1369
+ let widths = columnWidths(cols, naturalWidths(cols, header, rows, totals), avail);
1161
1370
  return {
1162
1371
  widths,
1163
- head: rowOf(canvas, header, widths),
1372
+ head: { ...rowOf(canvas, header, widths), style: buffered.headerStyle },
1164
1373
  laid: rows.map((/** @type {any} */ row) => ({
1165
- row: rowOf(canvas, row.cells, widths),
1166
- bg: row.bg,
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,
1167
1380
  })),
1168
- totalRow: total && rowOf(canvas, total, widths),
1169
1381
  };
1170
1382
  };
1171
1383
 
@@ -1180,7 +1392,7 @@ let table = (state, buffered) => {
1180
1392
  // Measured already if a region buffered this table: the balance model needed
1181
1393
  // its real row heights, at this same width, and that is the same
1182
1394
  // measurement — so the replay draws it without wrapping every cell again.
1183
- let { widths, head, laid, totalRow } =
1395
+ let { widths, head, laid, totalRows } =
1184
1396
  state.measured.get(buffered.opening) || measureTable(canvas, buffered, avail);
1185
1397
  let xOffsets = widths.map((_, i) => x + sum(widths.slice(0, i)));
1186
1398
 
@@ -1188,16 +1400,15 @@ let table = (state, buffered) => {
1188
1400
  let grid = { state, head, xOffsets, widths, x, avail };
1189
1401
 
1190
1402
  // A pending group header keeps the table's header row and its first row.
1191
- flush(state, grid.head.h + firstH(laid, totalRow));
1403
+ flush(state, grid.head.h + firstH(laid, totalRows));
1192
1404
  if (canvas.y - grid.head.h < floorOf(state)) {
1193
1405
  advance(state);
1194
1406
  rebase(grid);
1195
1407
  }
1196
- drawRow(canvas, grid.head, grid.xOffsets, widths, null);
1197
- underline(canvas, grid.x, grid.avail);
1198
- // The last data row keeps the total row with it, so a total is never
1199
- // stranded alone at a page top.
1200
- emitRows(grid, laid, totalRow);
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);
1201
1412
  };
1202
1413
 
1203
1414
  // --- page bands -------------------------------------------------------------
@@ -1211,7 +1422,14 @@ let band = (canvas, items, yTop) => {
1211
1422
  fresh = canvas.fresh;
1212
1423
  canvas.y = yTop;
1213
1424
  canvas.fresh = false;
1214
- for (let event of items) drawBlock(canvas, blockOf(canvas, event, avail), canvas.margin, avail);
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);
1215
1433
  let h = yTop - canvas.y;
1216
1434
  canvas.y = saved;
1217
1435
  canvas.fresh = fresh;
@@ -1265,7 +1483,12 @@ let reserve = (geo, fonts, bands) => {
1265
1483
  // want it: reservation to measure against, the draw pass to hang the header
1266
1484
  // from.
1267
1485
  /** @type {(canvas: Canvas) => Frame} */
1268
- let pageBox = (canvas) => frame(canvas.width, canvas.height, canvas.margin, canvas.base);
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
+ });
1269
1492
 
1270
1493
  // One finished page's furniture, drawn in the strips `reserve` left for it:
1271
1494
  // the header hanging from the top margin, the footer resting on the bottom
@@ -1330,11 +1553,18 @@ let flow = (canvas) => {
1330
1553
  // the replay draws that table without wrapping every cell a second time.
1331
1554
  // Per flow, because the keys are one render's events.
1332
1555
  measured: new WeakMap(),
1556
+ pin: null,
1557
+ skipGap: false,
1558
+ inHeader: false,
1333
1559
  // Assigned below, once `route` exists: a buffered region replays through
1334
1560
  // it, and one entry point is what makes the replay take the path the
1335
1561
  // content would have taken had it never been held.
1336
1562
  route: () => {},
1337
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;
1338
1568
  // A table is buffered whole before it is laid out: column widths come from
1339
1569
  // every cell in it, so the last row has to be in hand first.
1340
1570
  /** @type {any[]} */
@@ -1350,6 +1580,14 @@ let flow = (canvas) => {
1350
1580
  // What each event does once it is placed for real. A buffered region replays
1351
1581
  // through this same table, so held content lays out by exactly the path it
1352
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
+
1353
1591
  /** @type {Record<string, (event: any, block: Block | null) => void>} */
1354
1592
  let placed = {
1355
1593
  // A header is held rather than placed, and holding is bookkeeping the
@@ -1362,10 +1600,11 @@ let flow = (canvas) => {
1362
1600
  // An image flows exactly as an item does, held back in a group header and
1363
1601
  // placed anywhere else -- the block it becomes carries a height like any
1364
1602
  // other. It wears no role default: those describe text.
1365
- image: (event, block) =>
1366
- event.role === "group-header"
1367
- ? holdHeader(state, event, null, block)
1368
- : placeItem(state, event, block),
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,
1369
1608
  "group-start": (event) => openGroup(state, event),
1370
1609
  "group-end": () => {
1371
1610
  flush(state, 0);
@@ -1393,11 +1632,28 @@ let flow = (canvas) => {
1393
1632
  /** @type {(event: any) => boolean} */
1394
1633
  let ownFooter = (event) => event.role === "group-footer" && owner() === state.open.length - 1;
1395
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
+
1396
1651
  // Route one event: the declaring node's own bands stay full-width, region
1397
1652
  // content opens the strips owed to it, and anything arriving while a region
1398
1653
  // is still deciding is buffered rather than placed.
1399
1654
  /** @type {(event: any, block?: Block | null) => void} */
1400
1655
  let route = (event, block = null) => {
1656
+ snapPin(event);
1401
1657
  if (fullBand(event)) fullWidth(event, block);
1402
1658
  else if (deciding()) buffer(state, event, block);
1403
1659
  else if (opens(event)) begin(event);
@@ -1405,6 +1661,7 @@ let flow = (canvas) => {
1405
1661
  };
1406
1662
 
1407
1663
  state.route = route;
1664
+ fold = splitFolder(route);
1408
1665
 
1409
1666
  // Everything drawn across the page rather than down a strip: the declaring
1410
1667
  // node's own bands, its footer among them.
@@ -1458,12 +1715,51 @@ let flow = (canvas) => {
1458
1715
  buffer(state, event, block);
1459
1716
  };
1460
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
+
1461
1750
  return {
1462
1751
  handlers: {
1463
1752
  "report-start": (event) => {
1464
1753
  opening = event;
1754
+ canvas.locale = event.locale;
1755
+ canvas.currency = event.currency;
1756
+ canvas.timeZone = event.timeZone;
1757
+ adoptDefault(event.style);
1465
1758
  if (event.columns) state.pending = { count: event.columns, owner: -1 };
1466
- if (!event.page) return;
1759
+ if (!event.page) {
1760
+ pinHeader(event);
1761
+ return;
1762
+ }
1467
1763
  // A frame of its own, never the canvas itself: `measuring` builds
1468
1764
  // from whatever it is handed, so handing it this canvas would spread
1469
1765
  // the page passes onto a measurement — and reaching a document is the
@@ -1471,22 +1767,31 @@ let flow = (canvas) => {
1471
1767
  // a property of the page box in any case, not of a content box
1472
1768
  // something may already have narrowed.
1473
1769
  adopt(canvas, reserve(pageBox(canvas), canvas.fonts, event.page));
1770
+ pinHeader(event);
1474
1771
  },
1475
1772
  // Everything else routes: `placed` above says what each event does, and
1476
- // one entry point is what lets a buffered region replay through it.
1477
- // Only the roled item needs a word of its own on the way in.
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.
1478
1776
  ...Object.fromEntries(
1479
- Object.keys(placed).map((type) => [
1480
- type,
1481
- type === "item" ? (event) => route(roled(event)) : (event) => route(event),
1482
- ]),
1777
+ Object.keys(placed)
1778
+ .filter((type) => !BLOCKS.has(type))
1779
+ .map((type) => [type, (/** @type {any} */ event) => route(event)]),
1483
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),
1484
1788
  "group-end": (event) => {
1485
1789
  endOwned(event.depth);
1486
1790
  route(event);
1487
1791
  },
1488
1792
  },
1489
1793
  finish: () => {
1794
+ snapPin({ type: "report-end" });
1490
1795
  // A root region reaches here undrained when a report declares `columns`
1491
1796
  // and no full-width footer followed its body.
1492
1797
  closeRegion(state);