@quario/layout 0.1.0 → 0.2.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
@@ -23,16 +23,16 @@
23
23
  */
24
24
  import { display, 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, adoptSettings, frame, measuring } from "./canvas.js";
28
27
  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";
28
+ import { CELL_PAD, NO_PAD, WHOLE, insetOf, isWhole, paintBox, sliceInset, unbox } from "./box.js";
29
+ import { BAND, col, GUTTER, LEAD, merge, roled, shift, sizeOf, vshift } from "./style.js";
31
30
  import { atoms, dress, heightOf, wrap } from "./text.js";
32
31
 
33
32
  /** @typedef {import('./balance.js').Unit} Unit */
34
33
  /** @typedef {import('./canvas.js').Canvas} Canvas */
35
34
  /** @typedef {import('./canvas.js').Frame} Frame */
35
+ /** @typedef {import('./canvas.js').Settings} Settings */
36
36
  /** @typedef {import('./text.js').Line} Line */
37
37
 
38
38
  // `open` is a stack, one entry per group instance still open: the header items
@@ -47,9 +47,8 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
47
47
  /**
48
48
  * @typedef {{ canvas: Canvas, open: Group[], gap: number, marks: any[],
49
49
  * starts: number[], region: Region | null, pending: Pending | null,
50
- * measured: WeakMap<any, Measured>,
51
50
  * pin: number | null, skipGap: boolean, inHeader: boolean,
52
- * route: (event: any, block?: Block | null) => void }} Flow
51
+ * route: (event: any, payload?: Block | Measured | null) => void }} Flow
53
52
  */
54
53
  /**
55
54
  * What flow spacing reads of the page: the cursor and whether a strip is open.
@@ -69,13 +68,16 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
69
68
  * @typedef {{ bytes: Uint8Array, format: string, w: number, h: number }} Picture
70
69
  */
71
70
  /**
72
- * `align` is absent on a split, which has none of its own: alignment is a
73
- * slot's, reached through the style layering.
71
+ * `align` and `valign` are absent on a split, which has none of its own:
72
+ * alignment is a slot's, reached through the style layering.
73
+ * `drop` is how far below the top of the box the content starts: zero for a
74
+ * box that is its content's height, and for a slot the slack its `valign`
75
+ * reads once `splitBlock` has handed the split's height down.
74
76
  * `path` names the schema node the block draws, when the event carried one:
75
77
  * the box the canvas records for the hit-test.
76
78
  * @typedef {{ lines: Line[], picture?: Picture, parts?: SlotPart[],
77
- * bg: import('./style.js').Color | null, align?: any, h: number, style?: any,
78
- * path?: string,
79
+ * bg: import('./style.js').Color | null, align?: any, valign?: any,
80
+ * drop: number, h: number, style?: any, path?: string,
79
81
  * inset: { t: number, r: number, b: number, l: number } }} Block
80
82
  */
81
83
  /**
@@ -113,18 +115,22 @@ let blockOf = (canvas, event, avail, under = null) => {
113
115
  picture,
114
116
  bg,
115
117
  align: text.align,
118
+ valign: text.valign,
119
+ drop: 0,
116
120
  h: picture.h + inset.t + inset.b,
117
121
  style: box,
118
122
  path: event.path,
119
123
  inset,
120
124
  };
121
125
  }
122
- let size = sizeOf(text, canvas.base);
123
- let lines = dress(wrap(canvas, atoms(canvas, event.tokens, text), inner, size), text);
126
+ let size = sizeOf(text, canvas.settings.base);
127
+ let lines = dress(wrap(atoms(canvas.settings, event.tokens, text), inner, size), text);
124
128
  return {
125
129
  lines,
126
130
  bg,
127
131
  align: text.align,
132
+ valign: text.valign,
133
+ drop: 0,
128
134
  h: heightOf(lines) + inset.t + inset.b,
129
135
  style: box,
130
136
  path: event.path,
@@ -155,7 +161,7 @@ let drawPicture = (canvas, block, x, avail) => {
155
161
  let at = x + shift(block.align, avail - outer);
156
162
  paintBox(canvas, at, canvas.y, outer, block.h, block.style, block.bg);
157
163
  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);
164
+ canvas.picture(bytes, format, at + inset.l, canvas.y - inset.t - block.drop - h, w, h);
159
165
  canvas.y -= block.h;
160
166
  canvas.fresh = false;
161
167
  };
@@ -175,7 +181,7 @@ let drawLines = (canvas, block, x, avail, slice = WHOLE) => {
175
181
  let inset = sliceInset(block.inset, slice);
176
182
  paintBox(canvas, x, canvas.y, avail, block.h, block.style, block.bg, slice);
177
183
  canvas.box(block.path, x, canvas.y, avail, block.h);
178
- let y = canvas.y - inset.t;
184
+ let y = canvas.y - inset.t - block.drop;
179
185
  let inner = Math.max(avail - inset.l - inset.r, 1);
180
186
  for (let line of block.lines) {
181
187
  canvas.drawLine(line, x + inset.l, y, inner, block.align);
@@ -196,12 +202,13 @@ let drawLines = (canvas, block, x, avail, slice = WHOLE) => {
196
202
  // table cell already follows, a cell box being painted at the row's height.
197
203
  // A shorter slot keeping its own height left daylight under every bordered
198
204
  // 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.
205
+ // down: a slot's lines start at the top of the box unless its `valign` reads
206
+ // the slack, and a picture is never scaled, `fit` having no vertical mode --
207
+ // `valign` moves it within the box, it does not fill it.
201
208
  // The split's own declarations are the layer under each slot's, so a split
202
209
  // 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.
210
+ // HTML target and the `under` layer in XLSX. It has no `align` or `valign` of
211
+ // its own: alignment is a slot's, reached through that layering.
205
212
  /**
206
213
  * @type {(canvas: Canvas, event: any, inner: number, text: any,
207
214
  * bg: any, box: any, inset: { t: number, r: number, b: number, l: number }) => Block}
@@ -216,8 +223,20 @@ let splitBlock = (canvas, event, inner, text, bg, box, inset) => {
216
223
  if (block.h > h) h = block.h;
217
224
  parts.push({ block, w: widths[i] });
218
225
  }
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 };
226
+ for (let part of parts) {
227
+ part.block.drop = vshift(part.block.valign, h - part.block.h);
228
+ part.block.h = h;
229
+ }
230
+ return {
231
+ lines: [],
232
+ parts,
233
+ bg,
234
+ drop: 0,
235
+ h: h + inset.t + inset.b,
236
+ style: box,
237
+ path: event.path,
238
+ inset,
239
+ };
221
240
  };
222
241
 
223
242
  /** @type {(slots: { width?: number }[], avail: number) => number[]} */
@@ -314,7 +333,7 @@ let drawBlock = (canvas, block, x, avail) =>
314
333
  // Where the cursor's column starts and how wide it is: the strip's, inside a
315
334
  // region, and the page's content box outside one. Every drawing call reads
316
335
  // these rather than `canvas.margin`/`canvas.content` — the canvas keeps
317
- // presenting the page, because `pageBox` re-derives page furniture from it.
336
+ // presenting the page, because `pageFrame` re-derives page furniture from it.
318
337
  /** @type {(state: Flow) => number} */
319
338
  let originOf = (state) =>
320
339
  state.canvas.margin + (state.region ? state.region.index * (state.region.width + GUTTER) : 0);
@@ -346,9 +365,6 @@ let ceilOf = (state) => (state.region ? state.region.top : state.canvas.top);
346
365
  /** @type {(state: Cursor) => boolean} */
347
366
  let freshOf = (state) => (state.region ? state.canvas.y === state.region.top : state.canvas.fresh);
348
367
 
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
368
  // A fresh column always takes at least one line, or nothing would ever fit —
353
369
  // which is also what makes a balanced floor a target rather than a bound.
354
370
  /** @type {(fresh: boolean, j: number, i: number) => boolean} */
@@ -363,7 +379,7 @@ let grow = (canvas, lines, i, floor, fresh) => {
363
379
  h = 0,
364
380
  n = lines.length;
365
381
  while (j < n) {
366
- if (!hasRoom(canvas, h, lines[j], floor) && !takeLine(fresh, j, i)) break;
382
+ if (!(canvas.y - h - lines[j].h >= floor) && !takeLine(fresh, j, i)) break;
367
383
  h += lines[j++].h;
368
384
  }
369
385
  return { j, h };
@@ -762,7 +778,7 @@ let commit = (state) => {
762
778
  restrip(region, region.top);
763
779
  for (let entry of held) {
764
780
  if (breaksFor(state, region, entry.span)) advance(state);
765
- state.route(entry.event, entry.block);
781
+ state.route(entry.event, entry.payload);
766
782
  }
767
783
  };
768
784
 
@@ -850,17 +866,17 @@ let NESTING = { "group-start": 1, "group-end": -1 };
850
866
  // but the flush that draws it measures from the event, so only its height is
851
867
  // kept. A buffer that outgrows what the strips could hold has answered the
852
868
  // 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) => {
869
+ /** @type {(state: Flow, event: any, payload: Block | Measured | null) => void} */
870
+ let buffer = (state, event, payload) => {
855
871
  let region = /** @type {Region} */ (state.region);
856
872
  // The one door into the held list, so it is where the buffer's own walk is
857
873
  // kept. A bracket buffered here is a bracket the open stack will not see
858
874
  // until the replay places it.
859
875
  region.nesting += NESTING[event.type] ?? 0;
860
- let entry = entryFor(state, event, block, region.width);
876
+ let entry = entryFor(state, event, payload, region.width);
861
877
  /** @type {any[]} */ (region.held).push(entry);
862
878
  region.measured += entry.h;
863
- if (event.type === "table-end") region.measured += remeasure(state, region);
879
+ if (event.type === "table-end") region.measured += remeasure(state, region, entry);
864
880
  if (outgrown(state, region)) commit(state);
865
881
  };
866
882
 
@@ -871,52 +887,59 @@ let buffer = (state, event, block) => {
871
887
  // measured at the strip width and its entries are corrected. Both readers of
872
888
  // those heights want the real ones: the balance model, and `outgrown`.
873
889
  //
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) => {
890
+ // The measurement then rides the entry that closes the table, so the replay
891
+ // lays it out without wrapping every cell a second time. Nothing outside the
892
+ // buffer holds on to it: `commit` hands the entry to the replay and the
893
+ // replay is the last reader.
894
+ /** @type {(state: Flow, region: Region, closing: any) => number} */
895
+ let remeasure = (state, region, closing) => {
882
896
  let held = /** @type {any[]} */ (region.held);
883
897
  let at = held.findLastIndex((entry) => entry.event.type === "table-start");
884
898
  if (at < 0) return 0;
885
899
  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.
900
+ let buffered = tableOf(span.map((entry) => entry.event));
901
+ let measured = measureTable(state.canvas, buffered, region.width);
902
+ closing.payload = measured;
903
+ return retune(span, buffered, measured);
904
+ };
905
+
906
+ // Every measured height filed under the event it was measured from: the
907
+ // opening event pays for the headings row, each row and each total for
908
+ // itself. What has no height of its own is simply absent, which is what keeps
909
+ // a new kind of table event landing between a row and a total costless.
910
+ /** @type {(span: any[], buffered: any, measured: Measured) => Map<any, number>} */
911
+ let heightsOf = (span, buffered, measured) => {
912
+ let by = new Map([[span[0].event, measured.head.h]]);
913
+ for (let [i, row] of buffered.rows.entries()) by.set(row, measured.laid[i].h);
914
+ for (let [i, row] of buffered.totals.entries()) by.set(row, measured.totalRows[i].h);
915
+ return by;
916
+ };
917
+
918
+ // Write those heights back over the estimates. The correction cannot slide:
919
+ // an entry the measurement filed nothing under keeps the estimate it had.
899
920
  // 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);
921
+ /** @type {(span: any[], buffered: any, measured: Measured) => number} */
922
+ let retune = (span, buffered, measured) => {
923
+ let by = heightsOf(span, buffered, measured);
904
924
  let delta = 0;
905
- for (let [i, h] of heights.entries()) {
906
- delta += h - span[i].h;
907
- span[i].h = h;
925
+ for (let entry of span) {
926
+ let h = by.get(entry.event) ?? entry.h;
927
+ delta += h - entry.h;
928
+ entry.h = h;
908
929
  }
909
930
  return delta;
910
931
  };
911
932
 
912
- // One buffered entry: the event, the block the replay will draw it from, and
933
+ // One buffered entry: the event, what the replay is handed along with it, and
913
934
  // what it is worth to the running total. A header keeps its block like
914
935
  // 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) };
936
+ // hold rather than wrapping the text again. Only a table's closing entry is
937
+ // ever given a measurement, and `remeasure` puts it there afterwards, so what
938
+ // reaches here is a block or nothing.
939
+ /** @type {(state: Flow, event: any, payload: Block | Measured | null, width: number) => any} */
940
+ let entryFor = (state, event, payload, width) => {
941
+ let block = /** @type {Block | null} */ (payload) || measureFor(state, event, width);
942
+ return { event, payload: block, h: block ? block.h : estimate(state, event) };
920
943
  };
921
944
 
922
945
  // The block a buffered event will be drawn from, measured once at the strip
@@ -942,20 +965,10 @@ let anchor = (state) => {
942
965
  /** @type {(state: Flow) => Group[]} */
943
966
  let pending = (state) => state.open.filter((group) => group.held.length);
944
967
 
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
968
  /** @type {(state: Flow, extra: number, runs: Block[][]) => number} */
953
969
  let needed = (state, extra, runs) =>
954
970
  state.gap + runs.flat().reduce((total, block) => total + block.h, 0) + extra;
955
971
 
956
- /** @type {(state: Flow, need: number) => boolean} */
957
- let tooTall = (state, need) => need > ceilOf(state) - carried(state) - floorOf(state);
958
-
959
972
  // Degenerate: taller than a page — keep nothing, paginate normally, and
960
973
  // repeat nothing afterwards. That last part is what bounds the replayed run
961
974
  // for good: every other path drew its headers inside the room a page still
@@ -1001,20 +1014,31 @@ let flush = (state, extra) => {
1001
1014
  let avail = widthOf(state);
1002
1015
  // Measured per instance, so each one takes back exactly the blocks its own
1003
1016
  // 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));
1017
+ // A held run, as blocks. A region measured its headers while buffering them,
1018
+ // at this same width, so a replayed run reuses those rather than wrapping
1019
+ // the same text a second time (ADR 0027).
1020
+ let runs = runsFor.map((group) =>
1021
+ group.held.map((entry) => entry.block || blockOf(canvas, entry.event, avail)),
1022
+ );
1005
1023
  let need = needed(state, extra, runs);
1006
1024
  // Against what a fresh column has left once the page has replayed what it
1007
1025
  // carries. Inside a region that is a strip, and the replay sits above it.
1008
- if (tooTall(state, need)) return dropHeld(state, runsFor);
1026
+ if (need > ceilOf(state) - carried(state) - floorOf(state)) return dropHeld(state, runsFor);
1009
1027
  settle(state, need);
1010
1028
  drawHeld(state, runsFor, runs, avail);
1011
1029
  };
1012
1030
 
1013
1031
  // Enough of an item that its group header is never left introducing nothing.
1032
+ // The unsplittable blocks are kept whole -- the same `picture || parts` pair
1033
+ // `overflow` moves rather than slices. There is no first part of one to keep
1034
+ // company with: a split's own `lines` are empty, so clipping to `KEEP_LINES`
1035
+ // would reserve nothing at all and strand the run it was called to protect.
1014
1036
  let KEEP_LINES = 2;
1015
1037
  /** @type {(block: Block) => number} */
1016
1038
  let keepWith = (block) =>
1017
- block.picture ? block.h : Math.min(block.h, heightOf(block.lines.slice(0, KEEP_LINES)));
1039
+ block.picture || block.parts
1040
+ ? block.h
1041
+ : Math.min(block.h, heightOf(block.lines.slice(0, KEEP_LINES)));
1018
1042
 
1019
1043
  // Flush any pending header with this item's opening, then place the item. The
1020
1044
  // block is measured once here and handed on rather than wrapped twice.
@@ -1093,9 +1117,9 @@ let holdHeader = (state, event, title, block = null) => {
1093
1117
  // because the region's estimator reads both, and a constant with two readers
1094
1118
  // and no name drifts between them.
1095
1119
  /** @type {(canvas: Canvas) => number} */
1096
- let rowHeight = (canvas) => LEAD * canvas.base;
1120
+ let rowHeight = (canvas) => LEAD * canvas.settings.base;
1097
1121
  /** @type {(canvas: Canvas) => number} */
1098
- let instanceGap = (canvas) => 0.5 * LEAD * canvas.base;
1122
+ let instanceGap = (canvas) => 0.5 * LEAD * canvas.settings.base;
1099
1123
 
1100
1124
  // What a fresh strip has to hold to take an instance: the span less the
1101
1125
  // opening gap, which `settle` drops at a strip head. This is the one fact the
@@ -1111,10 +1135,14 @@ let headOf = (canvas, span) => span - instanceGap(canvas);
1111
1135
  // gives flow spacing no meaning inside a table row, in either target.
1112
1136
  /** @type {(canvas: Canvas, cell: any, rowStyle: any) => any} */
1113
1137
  let cellOf = (canvas, cell, rowStyle) => {
1114
- let style = merge(unbox(rowStyle), unbox(cell.style));
1138
+ // A row's block reaches its cells with no box in it: the engine resolved
1139
+ // that half onto the cells themselves before the event was emitted
1140
+ // (SCHEMA.md, "Style declarations"), which is why the inset and the ink
1141
+ // both read the cell and a row is never asked for a box it cannot have.
1142
+ let style = merge(rowStyle, unbox(cell.style));
1115
1143
  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(
1144
+ let list = atoms(canvas.settings, cell.tokens, style);
1145
+ let natural = wrap(list, Infinity, sizeOf(style, canvas.settings.base)).reduce(
1118
1146
  (widest, line) => Math.max(widest, line.w),
1119
1147
  0,
1120
1148
  );
@@ -1122,6 +1150,7 @@ let cellOf = (canvas, cell, rowStyle) => {
1122
1150
  list,
1123
1151
  natural: natural + inset.l + inset.r,
1124
1152
  align: style.align,
1153
+ valign: style.valign,
1125
1154
  bg: col(style.background),
1126
1155
  underline: !!style.underline,
1127
1156
  strikethrough: !!style.strikethrough,
@@ -1134,18 +1163,24 @@ let cellOf = (canvas, cell, rowStyle) => {
1134
1163
  /** @type {(canvas: Canvas, cells: any[], widths: number[]) => { cells: any[], h: number }} */
1135
1164
  let rowOf = (canvas, cells, widths) => {
1136
1165
  let h = rowHeight(canvas);
1166
+ /** @type {number[]} */
1167
+ let owns = [];
1137
1168
  let out = cells.map((cell, i) => {
1138
1169
  let inner = Math.max(widths[i] - cell.inset.l - cell.inset.r, 1);
1139
1170
  // A glyph or a hard break occupies; spaces alone are an empty cell.
1140
1171
  let lines = cell.list.some(
1141
1172
  (/** @type {{ hard: boolean, space: boolean }} */ atom) => atom.hard || !atom.space,
1142
1173
  )
1143
- ? dress(wrap(canvas, cell.list, inner, cell.list[0].size), cell)
1174
+ ? dress(wrap(cell.list, inner, cell.list[0].size), cell)
1144
1175
  : [];
1145
- let cellHeight = heightOf(lines) + cell.inset.t + cell.inset.b;
1146
- if (cellHeight > h) h = cellHeight;
1176
+ let own = heightOf(lines) + cell.inset.t + cell.inset.b;
1177
+ if (own > h) h = own;
1178
+ owns.push(own);
1147
1179
  return { ...cell, lines };
1148
1180
  });
1181
+ // The row's height is known only now, so the slack each cell's `valign`
1182
+ // reads is measured here, as `splitBlock` measures a slot's.
1183
+ for (let [i, cell] of out.entries()) cell.drop = vshift(cell.valign, h - owns[i]);
1149
1184
  return { cells: out, h };
1150
1185
  };
1151
1186
 
@@ -1169,6 +1204,10 @@ let writeCell = (canvas, cell, lines, x, y, w) => {
1169
1204
  // with, so the ink a slice lays down and the height it was given come from
1170
1205
  // one answer: a continuation starts flush, and only the slice that owns a
1171
1206
  // side strokes it.
1207
+ // A row sliced across pages has no one box for `valign` to read the slack
1208
+ // against, so its cells paint from the top (SCHEMA.md, the pdf Layout contract).
1209
+ /** @type {(cell: any, slice: import('./box.js').Slice) => number} */
1210
+ let dropOf = (cell, slice) => (isWhole(slice) ? cell.drop : 0);
1172
1211
  /**
1173
1212
  * @type {(canvas: Canvas, cells: any[], lines: any[][], h: number,
1174
1213
  * slice: import('./box.js').Slice, xOffsets: number[], widths: number[],
@@ -1180,7 +1219,7 @@ let paintCells = (canvas, cells, lines, h, slice, xOffsets, widths, bg) => {
1180
1219
  fillCell(canvas, cell, bg, xOffsets[i], canvas.y - h, widths[i], h);
1181
1220
  paintBox(canvas, xOffsets[i], canvas.y, widths[i], h, cell.style, null, slice);
1182
1221
  canvas.box(cell.path, xOffsets[i], canvas.y, widths[i], h);
1183
- let y0 = canvas.y - (slice.first ? cell.inset.t : 0);
1222
+ let y0 = canvas.y - (slice.first ? cell.inset.t : 0) - dropOf(cell, slice);
1184
1223
  writeCell(canvas, cell, lines[i], xOffsets[i], y0, widths[i]);
1185
1224
  }
1186
1225
  canvas.y -= h;
@@ -1192,7 +1231,6 @@ let paintCells = (canvas, cells, lines, h, slice, xOffsets, widths, bg) => {
1192
1231
  * widths: number[], bg: any) => void}
1193
1232
  */
1194
1233
  let drawRow = (canvas, row, xOffsets, widths, bg) => {
1195
- let yTop = canvas.y;
1196
1234
  paintCells(
1197
1235
  canvas,
1198
1236
  row.cells,
@@ -1203,18 +1241,69 @@ let drawRow = (canvas, row, xOffsets, widths, bg) => {
1203
1241
  widths,
1204
1242
  bg,
1205
1243
  );
1206
- paintBox(canvas, xOffsets[0], yTop, sum(widths), row.h, row.style, null);
1207
1244
  };
1208
1245
 
1246
+ // The column geometry one row sees. A cell covering several columns starts
1247
+ // where the first of them starts and is as wide as all of them together; a row
1248
+ // that spans nothing sees the columns themselves, and pays nothing for the
1249
+ // feature. `null` spans is that row -- every data row, and every total row
1250
+ // whose cells each cover one column.
1251
+ /** @type {(cells: any[]) => number[] | null} */
1252
+ let spansOf = (cells) =>
1253
+ cells.some((cell) => cell.span > 1) ? cells.map((cell) => cell.span || 1) : null;
1254
+ // One value per cell, walking the columns each of them covers. Reached only
1255
+ // for a row that spans, so the closure it takes costs nothing per data row.
1256
+ /** @type {(spans: number[], pick: (at: number, span: number) => number) => number[]} */
1257
+ let overSpans = (spans, pick) => {
1258
+ /** @type {number[]} */
1259
+ let out = [];
1260
+ let at = 0;
1261
+ for (let span of spans) {
1262
+ out.push(pick(at, span));
1263
+ at += span;
1264
+ }
1265
+ return out;
1266
+ };
1267
+ /** @type {(widths: number[], spans: number[] | null) => number[]} */
1268
+ let spanWidths = (widths, spans) =>
1269
+ spans ? overSpans(spans, (at, span) => sum(widths.slice(at, at + span))) : widths;
1270
+ /** @type {(xOffsets: number[], spans: number[] | null) => number[]} */
1271
+ let spanOffsets = (xOffsets, spans) => (spans ? overSpans(spans, (at) => xOffsets[at]) : xOffsets);
1272
+
1209
1273
  // 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
- });
1274
+ //
1275
+ // A cell covering more than one column has no say in their widths (SCHEMA.md):
1276
+ // what a span states is which columns a cell reaches across, never how wide
1277
+ // they are. So a column can end up with no voter at all -- every cell above it
1278
+ // spans over it -- which only an empty table reaches, since a data row never
1279
+ // spans. It opens at the padding floor rather than at nothing, so an empty
1280
+ // table still shows the geometry it promises.
1281
+ /**
1282
+ * @typedef {{ cells: any[], spans: number[] | null }} Voting
1283
+ */
1284
+ /** @type {(cols: any[], header: Voting, rows: Voting[], totals: Voting[]) => number[]} */
1285
+ let naturalWidths = (cols, header, rows, totals) => {
1286
+ /** @type {(number | null)[]} */
1287
+ let widest = cols.map(() => null);
1288
+ /** @type {(at: number, natural: number) => void} */
1289
+ let widen = (at, natural) => {
1290
+ let held = widest[at];
1291
+ if (held === null || natural > held) widest[at] = natural;
1292
+ };
1293
+ /** @type {(row: Voting) => void} */
1294
+ let vote = (row) => {
1295
+ let at = 0;
1296
+ for (let [i, cell] of row.cells.entries()) {
1297
+ let span = row.spans ? row.spans[i] : 1;
1298
+ if (span === 1) widen(at, cell.natural);
1299
+ at += span;
1300
+ }
1301
+ };
1302
+ vote(header);
1303
+ for (let row of rows) vote(row);
1304
+ for (let row of totals) vote(row);
1305
+ return widest.map((width) => (width === null ? CELL_PAD.l + CELL_PAD.r : width));
1306
+ };
1218
1307
 
1219
1308
  /** @type {(values: number[]) => number} */
1220
1309
  let sum = (values) => values.reduce((total, value) => total + value, 0);
@@ -1251,7 +1340,7 @@ let columnWidths = (cols, natural, avail) => {
1251
1340
 
1252
1341
  // One table being emitted: what `carryOver`, `sliced` and `put` all need.
1253
1342
  /**
1254
- * @typedef {{ cells: any[], h: number, style?: any }} TableRow
1343
+ * @typedef {{ cells: any[], h: number, style?: any, spans?: number[] | null }} TableRow
1255
1344
  * @typedef {{ state: Flow, head: TableRow,
1256
1345
  * xOffsets: number[], widths: number[], x: number, avail: number }} Grid
1257
1346
  */
@@ -1267,11 +1356,24 @@ let columnWidths = (cols, natural, avail) => {
1267
1356
  /** @type {(row: { style?: any }) => any} */
1268
1357
  let rowFill = (row) => col((row.style || {}).background);
1269
1358
 
1359
+ // What this row draws against: the grid's own columns, or the merged geometry
1360
+ // a spanning row sees. Read at draw time rather than kept on the row, because
1361
+ // `rebase` moves the offsets under it every time the table crosses a strip.
1362
+ /** @type {(grid: Grid, row: TableRow) => { xOffsets: number[], widths: number[] }} */
1363
+ let gridOf = (grid, row) =>
1364
+ row.spans
1365
+ ? {
1366
+ xOffsets: spanOffsets(grid.xOffsets, row.spans),
1367
+ widths: spanWidths(grid.widths, row.spans),
1368
+ }
1369
+ : grid;
1370
+
1270
1371
  /** @type {(grid: Grid) => void} */
1271
1372
  let carryOver = (grid) => {
1272
1373
  advance(grid.state);
1273
1374
  rebase(grid);
1274
- drawRow(grid.state.canvas, grid.head, grid.xOffsets, grid.widths, rowFill(grid.head));
1375
+ let geo = gridOf(grid, grid.head);
1376
+ drawRow(grid.state.canvas, grid.head, geo.xOffsets, geo.widths, rowFill(grid.head));
1275
1377
  };
1276
1378
 
1277
1379
  // Move the grid to the column the cursor is now in. A table's widths are its
@@ -1335,34 +1437,29 @@ let sliceH = (taken, cells, slice) => {
1335
1437
  // Each column reserves its own bottom inset out of the room, the same trade
1336
1438
  // `sliceFloor` makes for an item: a slice that stops short leaves whitespace,
1337
1439
  // 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
1440
  /** @type {(grid: Grid, row: TableRow, bg: any) => void} */
1342
1441
  let sliced = (grid, row, bg) => {
1343
1442
  let canvas = grid.state.canvas;
1344
1443
  let next = row.cells.map(() => 0); // first line of each column not yet placed
1345
1444
  let first = true;
1346
1445
  for (;;) {
1446
+ // Inside the loop, not above it: `carryOver` rebases the grid at the foot
1447
+ // of every slice, and a merged geometry read once would keep painting the
1448
+ // continuations at the strip this row started in.
1449
+ let geo = gridOf(grid, row);
1347
1450
  let cap = canvas.y - canvas.bottom;
1348
1451
  let taken = row.cells.map((/** @type {any} */ cell, /** @type {number} */ i) =>
1349
1452
  takeLines(cell, next, i, first ? cell.inset.t : 0, cap - cell.inset.b),
1350
1453
  );
1351
1454
  let slice = { first, more: stillMore(row, next) };
1352
1455
  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);
1456
+ paintCells(canvas, row.cells, taken, h, slice, geo.xOffsets, geo.widths, bg);
1356
1457
  if (!slice.more) return;
1357
1458
  carryOver(grid);
1358
1459
  first = false;
1359
1460
  }
1360
1461
  };
1361
1462
 
1362
- /** @type {(grid: Grid) => number} */
1363
- let pageCap = (grid) =>
1364
- ceilOf(grid.state) - carried(grid.state) - floorOf(grid.state) - grid.head.h;
1365
-
1366
1463
  /** @type {(row: { h: number }, keep: number, cap: number) => number} */
1367
1464
  let companion = (row, keep, cap) => (row.h + keep > cap ? 0 : keep);
1368
1465
 
@@ -1388,7 +1485,7 @@ let put = (grid, row, bg, keep = 0) => {
1388
1485
  // The tallest row a fresh column holds, after the replayed group headers and
1389
1486
  // the replayed column headings. A companion over that can never share a
1390
1487
  // column, so it stops being a reason to break.
1391
- let cap = pageCap(grid);
1488
+ let cap = ceilOf(grid.state) - carried(grid.state) - floorOf(grid.state) - grid.head.h;
1392
1489
  keep = companion(row, keep, cap);
1393
1490
  // A row that will be sliced anyway is only worth breaking for while this
1394
1491
  // column cannot take even its first slice — beyond that a break buys nothing
@@ -1397,38 +1494,21 @@ let put = (grid, row, bg, keep = 0) => {
1397
1494
  let broke = shouldBreak(grid.state, row, keep, cap);
1398
1495
  if (broke) carryOver(grid);
1399
1496
  if (canvas.y - row.h < canvas.bottom) return sliced(grid, row, bg);
1400
- drawRow(canvas, row, grid.xOffsets, grid.widths, bg);
1497
+ let geo = gridOf(grid, row);
1498
+ drawRow(canvas, row, geo.xOffsets, geo.widths, bg);
1401
1499
  };
1402
1500
 
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
1501
  /** @type {(laid: any[], totalRows: any[]) => number} */
1417
1502
  let firstH = (laid, totalRows) => (laid.length ? laid[0].h : totalRows.length ? totalRows[0].h : 0);
1418
1503
 
1419
- /** @type {(totalRows: any[]) => number} */
1420
- let totalsH = (totalRows) => totalRows.reduce((h, row) => h + row.h, 0);
1421
-
1422
1504
  /** @type {(grid: Grid, laid: any[], totalRows: any[]) => void} */
1423
1505
  let emitRows = (grid, laid, totalRows) => {
1424
1506
  let last = laid.length - 1;
1425
- let keep = totalsH(totalRows);
1507
+ let keep = totalRows.reduce((h, row) => h + row.h, 0);
1426
1508
  for (let [i, row] of laid.entries()) put(grid, row, rowFill(row), i === last ? keep : 0);
1427
1509
  for (let row of totalRows) put(grid, row, rowFill(row), 0);
1428
1510
  };
1429
1511
 
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
1512
  // A buffered table, in the one shape `table` lays out: its columns come from
1433
1513
  // the opening event, its rows are the row events themselves, and its total is
1434
1514
  // the total row's cells. Both readers build it here — the replay, collecting
@@ -1437,18 +1517,21 @@ let emitRows = (grid, laid, totalRows) => {
1437
1517
  /** @type {(events: any[]) => any} */
1438
1518
  let tableOf = (events) => {
1439
1519
  let opening = events[0];
1440
- let totals = events.filter((event) => event.type === "total-row");
1441
1520
  return {
1521
+ columns: opening.columns.map((/** @type {any} */ col) => ({ ...col })),
1442
1522
  // A header cell arrives without a path of its own; the column's is what
1443
1523
  // 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
- })),
1524
+ // assemble the table. A column a neighbour's span covers declares no
1525
+ // header, so this is the header *row* -- as many cells as the columns
1526
+ // wrote, which is fewer than the columns whenever one spans.
1527
+ header: opening.columns
1528
+ .filter((/** @type {any} */ col) => col.header)
1529
+ .map((/** @type {any} */ col) => ({ ...col.header, path: col.path })),
1449
1530
  headerStyle: opening.style,
1450
1531
  rows: events.filter((event) => event.type === "row"),
1451
- totals: totals.map((row) => ({ cells: row.cells, style: row.style })),
1532
+ // The events themselves, as `rows` are: a total row is a row, and the
1533
+ // buffer files each corrected height under the event it was measured from.
1534
+ totals: events.filter((event) => event.type === "total-row"),
1452
1535
  };
1453
1536
  };
1454
1537
 
@@ -1463,39 +1546,52 @@ let tableOf = (events) => {
1463
1546
  /** @type {(canvas: Canvas, buffered: any, avail: number) => Measured} */
1464
1547
  let measureTable = (canvas, buffered, avail) => {
1465
1548
  let cols = /** @type {any[]} */ (buffered.columns);
1466
- let header = cols.map((/** @type {any} */ col) =>
1467
- cellOf(canvas, col.header, buffered.headerStyle),
1549
+ /** @type {(cells: any[], style: any, spans: number[] | null) => Voting & { style: any }} */
1550
+ let measured = (cells, style, spans) => ({
1551
+ cells: cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, style)),
1552
+ spans,
1553
+ style,
1554
+ });
1555
+ let header = measured(buffered.header, buffered.headerStyle, spansOf(buffered.header));
1556
+ // A data row's cells are the columns' own, so nothing in one can span
1557
+ // (SCHEMA.md, "Span"). Asking each row anyway would scan every cell of the
1558
+ // table to rediscover what the schema already guarantees.
1559
+ let rows = buffered.rows.map((/** @type {any} */ row) => measured(row.cells, row.style, null));
1560
+ let totals = buffered.totals.map((/** @type {any} */ row) =>
1561
+ measured(row.cells, row.style, spansOf(row.cells)),
1468
1562
  );
1469
- let rows = buffered.rows.map((/** @type {any} */ row) => bodyRow(canvas, row));
1470
- let totals = totalRowsOf(canvas, buffered);
1471
1563
  let widths = columnWidths(cols, naturalWidths(cols, header, rows, totals), avail);
1564
+ // Each row wraps against the geometry it sees, which is the columns' own
1565
+ // unless one of its cells spans.
1566
+ /** @type {(row: Voting & { style: any }) => any} */
1567
+ let laidOut = (row) => ({
1568
+ ...rowOf(canvas, row.cells, spanWidths(widths, row.spans)),
1569
+ spans: row.spans,
1570
+ style: row.style,
1571
+ });
1472
1572
  return {
1473
1573
  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
- })),
1574
+ head: laidOut(header),
1575
+ laid: rows.map(laidOut),
1576
+ totalRows: totals.map(laidOut),
1483
1577
  };
1484
1578
  };
1485
1579
 
1486
- /** @type {(state: Flow, buffered: any) => void} */
1487
- let table = (state, buffered) => {
1580
+ // Lay out one table from the events it arrived as: measure every cell,
1581
+ // allocate the columns, then emit the header, the rows and the total,
1582
+ // breaking pages as needed.
1583
+ /** @type {(state: Flow, events: any[], ready: Measured | null) => void} */
1584
+ let table = (state, events, ready) => {
1488
1585
  let canvas = state.canvas;
1489
1586
  // A table inside a region is measured against the strip, not the page: an
1490
1587
  // authored `width` percentage is a share of the column it lands in
1491
1588
  // (SCHEMA.md, "Page columns").
1492
1589
  let avail = widthOf(state);
1493
1590
  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);
1591
+ // Handed a measurement if a region buffered this table: the balance model
1592
+ // needed its real row heights, at this same width, and that is the same
1593
+ // measurement — so the replay neither shapes the table nor wraps a cell.
1594
+ let { widths, head, laid, totalRows } = ready || measureTable(canvas, tableOf(events), avail);
1499
1595
  let xOffsets = widths.map((_, i) => x + sum(widths.slice(0, i)));
1500
1596
 
1501
1597
  /** @type {Grid} */
@@ -1507,7 +1603,8 @@ let table = (state, buffered) => {
1507
1603
  advance(state);
1508
1604
  rebase(grid);
1509
1605
  }
1510
- drawRow(canvas, grid.head, grid.xOffsets, widths, rowFill(grid.head));
1606
+ let headGeo = gridOf(grid, grid.head);
1607
+ drawRow(canvas, grid.head, headGeo.xOffsets, headGeo.widths, rowFill(grid.head));
1511
1608
  // The last data row keeps the whole emitted total block with it, so a
1512
1609
  // total is never stranded alone at a page top.
1513
1610
  emitRows(grid, laid, totalRows);
@@ -1541,9 +1638,11 @@ let band = (canvas, items, yTop) => {
1541
1638
  // Measure a band without drawing it, to reserve its height. The trial render
1542
1639
  // goes onto a measuring canvas of its own, which can draw nothing and holds
1543
1640
  // 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);
1641
+ // canvas is open on one, with no live state for it to disturb. The settings it
1642
+ // measures against are the render's own object, not a copy `Settings` in
1643
+ // `canvas.js` says what that buys.
1644
+ /** @type {(geo: Frame, settings: Settings, items: any[]) => number} */
1645
+ let probe = (geo, settings, items) => band(measuring(geo, settings), items, 0);
1547
1646
 
1548
1647
  // The pages every band is probed on, and the whole of what reservation assumes
1549
1648
  // about where a band varies. Two anchors cover each visibility edge — a band
@@ -1567,11 +1666,11 @@ let PROBES = [
1567
1666
  * Pure, and measured against the page box rather than a live canvas: what a
1568
1667
  * band needs is a property of the page.
1569
1668
  *
1570
- * @type {(geo: Frame, fonts: any, bands: any) => Frame}
1669
+ * @type {(geo: Frame, settings: Settings, bands: any) => Frame}
1571
1670
  */
1572
- let reserve = (geo, fonts, bands) => {
1671
+ let reserve = (geo, settings, bands) => {
1573
1672
  let height = (/** @type {(page: any) => any[]} */ items) =>
1574
- Math.max(...PROBES.map((page) => probe(geo, fonts, items(page))));
1673
+ Math.max(...PROBES.map((page) => probe(geo, settings, items(page))));
1575
1674
  return {
1576
1675
  ...geo,
1577
1676
  top: geo.top - (bands.header ? height(bands.header) + BAND : 0),
@@ -1579,18 +1678,14 @@ let reserve = (geo, fonts, bands) => {
1579
1678
  };
1580
1679
  };
1581
1680
 
1582
- // The page box a canvas presents, before any band narrowed it. `frame` owns how
1583
- // `top`/`bottom` fall out of a page and a margin — "so no caller and no suite
1584
- // has to restate it", as it says next door — and both halves of page furniture
1585
- // want it: reservation to measure against, the draw pass to hang the header
1586
- // from.
1681
+ // The page frame a canvas presents, before any band narrowed it. `frame` owns
1682
+ // how `top`/`bottom` fall out of a page and a margin — "so no caller and no
1683
+ // suite has to restate it", as it says next door — and both halves of page
1684
+ // furniture want it: reservation to measure against, the draw pass to hang the
1685
+ // header from. Named apart from `page.js`'s exported `pageBox`, which validates
1686
+ // a host's `page` option and is a different thing entirely.
1587
1687
  /** @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
- });
1688
+ let pageFrame = (canvas) => frame(canvas.width, canvas.height, canvas.margin);
1594
1689
 
1595
1690
  // One finished page's furniture, drawn in the strips `reserve` left for it:
1596
1691
  // the header hanging from the top margin, the footer resting on the bottom
@@ -1600,7 +1695,7 @@ let pageBox = (canvas) =>
1600
1695
  // that made room for them.
1601
1696
  /** @type {(canvas: Canvas, bands: any, anchor: any) => void} */
1602
1697
  let furniture = (canvas, bands, anchor) => {
1603
- if (bands.header) band(canvas, bands.header(anchor), pageBox(canvas).top);
1698
+ if (bands.header) band(canvas, bands.header(anchor), pageFrame(canvas).top);
1604
1699
  if (bands.footer) band(canvas, bands.footer(anchor), canvas.bottom - BAND);
1605
1700
  };
1606
1701
 
@@ -1651,10 +1746,6 @@ let flow = (canvas) => {
1651
1746
  starts: [0],
1652
1747
  region: null,
1653
1748
  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
1749
  pin: null,
1659
1750
  skipGap: false,
1660
1751
  inHeader: false,
@@ -1690,7 +1781,11 @@ let flow = (canvas) => {
1690
1781
  ? holdHeader(state, event, null, block)
1691
1782
  : placeItem(state, event, block);
1692
1783
 
1693
- /** @type {Record<string, (event: any, block: Block | null) => void>} */
1784
+ // What each event does once it is placed. The payload is typed loosely here
1785
+ // and nowhere else: this table is where the event type picks the handler,
1786
+ // so each one already knows whether it was handed a block or a measurement,
1787
+ // and no type can say that for the table as a whole.
1788
+ /** @type {Record<string, (event: any, payload: any) => void>} */
1694
1789
  let placed = {
1695
1790
  // A header is held rather than placed, and holding is bookkeeping the
1696
1791
  // replay has to see in its original order — which is why it routes like
@@ -1718,13 +1813,15 @@ let flow = (canvas) => {
1718
1813
  "table-start": (event) => void (collected = [event]),
1719
1814
  row: (event) => void collected.push(event),
1720
1815
  "total-row": (event) => void collected.push(event),
1721
- "table-end": () => {
1722
- table(state, { ...tableOf(collected), opening: collected[0] });
1816
+ // The measurement rides the closing event when a region buffered this
1817
+ // table; anywhere else there is none and `table` takes its own.
1818
+ "table-end": (event, ready) => {
1819
+ table(state, collected, ready);
1723
1820
  collected = [];
1724
1821
  },
1725
1822
  };
1726
- /** @type {(event: any, block: Block | null) => void} */
1727
- let place = (event, block) => placed[event.type](event, block);
1823
+ /** @type {(event: any, payload: Block | Measured | null) => void} */
1824
+ let place = (event, payload) => placed[event.type](event, payload);
1728
1825
 
1729
1826
  // Which depth owes or holds the strips, whether or not any content has
1730
1827
  // opened them yet: an instance that emits nothing still closes with a
@@ -1759,14 +1856,17 @@ let flow = (canvas) => {
1759
1856
 
1760
1857
  // Route one event: the declaring node's own bands stay full-width, region
1761
1858
  // 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) => {
1859
+ // is still deciding is buffered rather than placed. The payload is the work
1860
+ // already done for this event a block off the walk, or, on the replay of
1861
+ // a buffered table, the measurement the buffer took. Which of the two it is
1862
+ // follows from the event's type, the same way the handler table dispatches.
1863
+ /** @type {(event: any, payload?: Block | Measured | null) => void} */
1864
+ let route = (event, payload = null) => {
1765
1865
  snapPin(event);
1766
- if (fullBand(event)) fullWidth(event, block);
1767
- else if (deciding()) buffer(state, event, block);
1866
+ if (fullBand(event)) fullWidth(event, payload);
1867
+ else if (deciding()) buffer(state, event, payload);
1768
1868
  else if (opens(event)) begin(event);
1769
- else place(event, block);
1869
+ else place(event, payload);
1770
1870
  };
1771
1871
 
1772
1872
  state.route = route;
@@ -1801,13 +1901,13 @@ let flow = (canvas) => {
1801
1901
  // owed survives a report header — that band arrives before the body it
1802
1902
  // columns — but not the owner's own footer, which is the end of what it
1803
1903
  // would have columned.
1804
- /** @type {(event: any, block: Block | null) => void} */
1805
- let fullWidth = (event, block) => {
1904
+ /** @type {(event: any, payload: Block | Measured | null) => void} */
1905
+ let fullWidth = (event, payload) => {
1806
1906
  // Asked before the region closes, because that is what it reads.
1807
1907
  let own = ownFooter(event);
1808
1908
  closeRegion(state);
1809
1909
  if (own) state.pending = null;
1810
- place(event, block);
1910
+ place(event, payload);
1811
1911
  };
1812
1912
 
1813
1913
  // The first content of a columned node. The node's own header run is drawn
@@ -1824,28 +1924,6 @@ let flow = (canvas) => {
1824
1924
  buffer(state, event, block);
1825
1925
  };
1826
1926
 
1827
- // The report default onto the canvas, before anything is measured. It is
1828
- // narrowed to `family` and `size`, and each replaces this target's own
1829
- // baseline outright: row heights and band gaps scale with the document's
1830
- // type rather than staying at a size nothing is set in, and text declaring
1831
- // no family is set in the document's. Lifting the pair here is the whole of
1832
- // this target's reading of docs/adr/0033 -- the default reaches a node as a
1833
- // fallback the canvas carries, never as a layer merged into its style, so a
1834
- // document-wide fact costs no allocation however many cells a report has.
1835
- //
1836
- // `sizeOf` and `familyName` are this target's one reading each of what a
1837
- // declared size and family amount to, so they read the default here too --
1838
- // both reach this unchecked from a computed style, and two spellings of that
1839
- // leniency would drift. Each falls back to what the canvas already carries,
1840
- // so a default declaring one of the pair leaves the other alone, and one
1841
- // whose value is unusable leaves this target's own baseline standing.
1842
- /** @type {(style: any) => void} */
1843
- let adoptDefault = (style) => {
1844
- if (!style) return;
1845
- canvas.base = sizeOf(style, canvas.base);
1846
- canvas.family = familyName(style) || canvas.family;
1847
- };
1848
-
1849
1927
  /** @type {(event: any) => void} */
1850
1928
  let pinHeader = (event) => {
1851
1929
  if (event.headerHeight == null) return;
@@ -1860,10 +1938,9 @@ let flow = (canvas) => {
1860
1938
  handlers: {
1861
1939
  "report-start": (event) => {
1862
1940
  opening = event;
1863
- canvas.locale = event.locale;
1864
- canvas.currency = event.currency;
1865
- canvas.timeZone = event.timeZone;
1866
- adoptDefault(event.style);
1941
+ // Every document-wide fact this target reads, settled in one place and
1942
+ // once: `canvas.js` owns what they are and what taking them means.
1943
+ adoptSettings(canvas.settings, event);
1867
1944
  if (event.columns) state.pending = { count: event.columns, owner: -1 };
1868
1945
  if (!event.page) {
1869
1946
  pinHeader(event);
@@ -1875,7 +1952,7 @@ let flow = (canvas) => {
1875
1952
  // one thing a measurement must not be able to do. What a band needs is
1876
1953
  // a property of the page box in any case, not of a content box
1877
1954
  // something may already have narrowed.
1878
- adopt(canvas, reserve(pageBox(canvas), canvas.fonts, event.page));
1955
+ adopt(canvas, reserve(pageFrame(canvas), canvas.settings, event.page));
1879
1956
  pinHeader(event);
1880
1957
  },
1881
1958
  // Everything else routes: `placed` above says what each event does, and