@quario/pdf 0.2.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
@@ -24,8 +24,10 @@
24
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, roled, shift, sizeOf, stack } 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
@@ -60,7 +69,8 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
60
69
  * `align` is absent on a split, which has none of its own: alignment is a
61
70
  * slot's, reached through the style layering.
62
71
  * @typedef {{ lines: Line[], picture?: Picture, parts?: SlotPart[],
63
- * bg: string | null, align?: any, h: number }} Block
72
+ * bg: string | null, align?: any, h: number, style?: any,
73
+ * inset: { t: number, r: number, b: number, l: number } }} Block
64
74
  */
65
75
  /**
66
76
  * One slot of a split, as laid out: the block it drew to and the width it was
@@ -84,15 +94,34 @@ let pictureOf = (event, avail) => {
84
94
  // this is one of its slots. Null everywhere else, where nothing encloses.
85
95
  /** @type {(canvas: Canvas, event: any, avail: number, under?: any) => Block} */
86
96
  let blockOf = (canvas, event, avail, under = null) => {
87
- let style = stack(canvas, under, event.style);
88
- let bg = col(style.background);
89
- if (event.type === "split") return splitBlock(canvas, event, avail, style, bg);
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);
90
103
  if (event.type === "image") {
91
- let picture = pictureOf(event, avail);
92
- 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
+ };
93
114
  }
94
- let lines = dress(wrap(canvas, atoms(canvas, event.tokens, style), Math.max(avail, 1)), style);
95
- return { lines, bg, align: style.align, h: heightOf(lines) };
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
+ };
96
125
  };
97
126
 
98
127
  // Scale a picture block down to the room it has. Best-effort, and only ever
@@ -100,9 +129,10 @@ let blockOf = (canvas, event, avail, under = null) => {
100
129
  /** @type {(block: Block, room: number) => void} */
101
130
  let shrink = (block, room) => {
102
131
  let picture = /** @type {Picture} */ (block.picture);
103
- picture.w *= room / picture.h;
104
- picture.h = room;
105
- 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;
106
136
  };
107
137
 
108
138
  // Draw one block whole at the cursor. A picture's background fills the
@@ -111,21 +141,27 @@ let shrink = (block, room) => {
111
141
  // is one box, so where it sits in the width it was given is worked out once.
112
142
  /** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
113
143
  let drawPicture = (canvas, block, x, avail) => {
144
+ let inset = block.inset;
114
145
  let { bytes, format, w, h } = /** @type {Picture} */ (block.picture);
115
- let at = x + shift(block.align, avail - w);
116
- if (block.bg) canvas.rect(block.bg, at, canvas.y - h, w, h);
117
- canvas.picture(bytes, format, at, canvas.y - h, w, h);
118
- canvas.y -= h;
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;
119
151
  canvas.fresh = false;
120
152
  };
121
153
 
122
154
  /** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
123
155
  let drawLines = (canvas, block, x, avail) => {
124
- 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);
125
160
  for (let line of block.lines) {
126
- canvas.drawLine(line, x, canvas.y, avail, block.align);
127
- canvas.y -= line.h;
161
+ canvas.drawLine(line, x + inset.l, y, inner, block.align);
162
+ y -= line.h;
128
163
  }
164
+ canvas.y -= block.h;
129
165
  canvas.fresh = false;
130
166
  };
131
167
 
@@ -140,20 +176,20 @@ let drawLines = (canvas, block, x, avail) => {
140
176
  // inheritance in the HTML target and the `under` layer in XLSX. It has no
141
177
  // `align` of its own: alignment is a slot's, reached through that layering.
142
178
  /**
143
- * @type {(canvas: Canvas, event: any, avail: number, style: any,
144
- * bg: string | null) => Block}
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}
145
181
  */
146
- let splitBlock = (canvas, event, avail, style, bg) => {
147
- let widths = slotWidths(event.slots, avail);
182
+ let splitBlock = (canvas, event, inner, text, bg, box, inset) => {
183
+ let widths = slotWidths(event.slots, inner);
148
184
  /** @type {SlotPart[]} */
149
185
  let parts = [];
150
186
  let h = 0;
151
187
  for (let [i, slot] of event.items.entries()) {
152
- let block = blockOf(canvas, slot, Math.max(widths[i], 1), style);
188
+ let block = blockOf(canvas, slot, Math.max(widths[i], 1), text);
153
189
  if (block.h > h) h = block.h;
154
190
  parts.push({ block, w: widths[i] });
155
191
  }
156
- return { lines: [], parts, bg, h };
192
+ return { lines: [], parts, bg, h: h + inset.t + inset.b, style: box, inset };
157
193
  };
158
194
 
159
195
  /** @type {(slots: { width?: number }[], avail: number) => number[]} */
@@ -205,17 +241,17 @@ let splitFolder = (emit) => {
205
241
 
206
242
  /** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
207
243
  let drawSplit = (canvas, block, x, avail) => {
208
- if (block.bg) canvas.rect(block.bg, x, canvas.y - block.h, avail, block.h);
209
- let top = canvas.y;
210
- let at = x;
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;
211
249
  for (let part of block.parts || []) {
212
250
  canvas.y = top;
213
251
  drawBlock(canvas, part.block, at, part.w);
214
252
  at += part.w;
215
253
  }
216
- // The split takes its own height whatever the slots took, so a short slot
217
- // beside a wrapped one does not pull the band up.
218
- canvas.y = top - block.h;
254
+ canvas.y = yTop - block.h;
219
255
  canvas.fresh = false;
220
256
  };
221
257
 
@@ -270,7 +306,7 @@ let ceilOf = (state) => (state.region ? state.region.top : state.canvas.top);
270
306
  // leaving an empty page behind — so a strip claims no second meaning for it.
271
307
  // A block of no height would read fresh here and used there; none is reachable
272
308
  // today, since every item wraps to at least one line.
273
- /** @type {(state: Flow) => boolean} */
309
+ /** @type {(state: Cursor) => boolean} */
274
310
  let freshOf = (state) => (state.region ? state.canvas.y === state.region.top : state.canvas.fresh);
275
311
 
276
312
  /** @type {(canvas: Canvas, h: number, line: Line, floor: number) => boolean} */
@@ -330,6 +366,25 @@ let fitLines = (canvas, block, i, floor, fresh) => {
330
366
  /** @type {(canvas: Canvas, block: Block, floor: number) => boolean} */
331
367
  let whole = (canvas, block, floor) => canvas.y - block.h >= floor;
332
368
 
369
+ /** @type {(style: any, name: string) => number} */
370
+ let spaceOf = (style, name) => {
371
+ let n = style?.[name];
372
+ return Number.isFinite(n) && n > 0 ? n : 0;
373
+ };
374
+
375
+ /** @type {(event: any) => boolean} */
376
+ let pageBand = (event) => event.role === "page-header" || event.role === "page-footer";
377
+ /** @type {(state: Cursor, event: any) => boolean} */
378
+ let keepLead = (state, event) => pageBand(event) || !freshOf(state);
379
+ /** @type {(state: Cursor, event: any) => number} */
380
+ let leadOf = (state, event) => (keepLead(state, event) ? spaceOf(event.style, "spaceBefore") : 0);
381
+ /** @type {(state: Cursor, event: any, name: string) => void} */
382
+ let skipFlow = (state, event, name) => {
383
+ state.canvas.y -= name === "spaceBefore" ? leadOf(state, event) : spaceOf(event.style, name);
384
+ };
385
+ /** @type {(state: Cursor, event: any) => number} */
386
+ let flowPad = (state, event) => leadOf(state, event) + spaceOf(event.style, "spaceAfter");
387
+
333
388
  // The unsplittable blocks -- a picture and a split -- move whole to a fresh
334
389
  // column rather than breaking, so a balanced floor never cuts one: only the
335
390
  // page's own floor bears on it. A picture that no column can hold is scaled
@@ -392,9 +447,15 @@ let item = (state, event, measured = null) => {
392
447
  let canvas = state.canvas;
393
448
  let avail = widthOf(state);
394
449
  let block = measured || blockOf(canvas, event, avail);
395
- // Whole block fits where it stands: one piece, no slicing.
396
- if (whole(canvas, block, floorOf(state))) return drawBlock(canvas, block, originOf(state), avail);
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
+ }
397
457
  overflow(state, block);
458
+ skipFlow(state, event, "spaceAfter");
398
459
  };
399
460
 
400
461
  // What a block does when it does not fit where it stands. A picture and a
@@ -547,8 +608,9 @@ let closeRegion = (state) => {
547
608
  // Two things this deliberately does not model, each stated because the cost of
548
609
  // getting them wrong is a strip that stops early rather than a wrong document.
549
610
  // A `total-row` is a row like any other, though it really keeps the last data
550
- // row with it (SCHEMA.md): pairing them costs the flat per-entry map this is,
551
- // to remove an error of one row at the end of a table. And a row taller than
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
552
614
  // any strip is sliced rather than moved (SCHEMA.md, "How a tall row slices");
553
615
  // called whole here, it makes every candidate height fail and the region
554
616
  // fills, which is the honest answer for a region holding one.
@@ -700,11 +762,13 @@ let estimate = (state, event) => (WORTH[event.type] || (() => 0))(state.canvas);
700
762
  // What each kind of buffered event is worth. A module constant: `estimate` runs
701
763
  // once per buffered event, and rebuilding this table each time would allocate
702
764
  // a closure per table row.
765
+ /** @type {(canvas: Canvas) => number} */
766
+ let rowWorth = (canvas) => rowHeight(canvas) + CELL_PAD.t + CELL_PAD.b;
703
767
  /** @type {Record<string, (canvas: Canvas) => number>} */
704
768
  let WORTH = {
705
- row: (canvas) => rowHeight(canvas) + 2 * PADY,
706
- "total-row": (canvas) => rowHeight(canvas) + 2 * PADY,
707
- "table-start": (canvas) => rowHeight(canvas) + 2 * PADY,
769
+ row: rowWorth,
770
+ "total-row": rowWorth,
771
+ "table-start": rowWorth,
708
772
  "group-start": (canvas) => instanceGap(canvas),
709
773
  };
710
774
 
@@ -759,8 +823,8 @@ let remeasure = (state, region) => {
759
823
  // Reports what the correction moved the running total by.
760
824
  /** @type {(span: any[], measured: Measured) => number} */
761
825
  let retune = (span, measured) => {
762
- let heights = [measured.head.h, ...measured.laid.map((/** @type {any} */ laid) => laid.row.h)];
763
- if (measured.totalRow) heights.push(measured.totalRow.h);
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);
764
828
  let delta = 0;
765
829
  for (let [i, h] of heights.entries()) {
766
830
  delta += h - span[i].h;
@@ -881,7 +945,7 @@ let keepWith = (block) =>
881
945
  /** @type {(state: Flow, event: any, measured?: Block | null) => void} */
882
946
  let placeItem = (state, event, measured = null) => {
883
947
  let block = measured || keepBlock(state, event, widthOf(state));
884
- if (block && holding(state)) flush(state, keepWith(block));
948
+ if (block && holding(state)) flush(state, keepWith(block) + flowPad(state, event));
885
949
  item(state, event, block);
886
950
  };
887
951
 
@@ -910,7 +974,8 @@ let holding = (state) => state.open.some((group) => group.held.length) || state.
910
974
  let openGroup = (state, event) => {
911
975
  breakFor(state, event);
912
976
  if (event.columns) state.pending = { count: event.columns, owner: event.depth };
913
- 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;
914
979
  state.open.push({ held: [], blocks: [] });
915
980
  state.marks.push(markFor(event));
916
981
  };
@@ -970,16 +1035,22 @@ let headOf = (canvas, span) => span - instanceGap(canvas);
970
1035
  // gives flow spacing no meaning inside a table row, in either target.
971
1036
  /** @type {(canvas: Canvas, cell: any, rowStyle: any) => any} */
972
1037
  let cellOf = (canvas, cell, rowStyle) => {
973
- let style = stack(canvas, rowStyle, cell.style);
1038
+ let style = merge(unbox(rowStyle), unbox(cell.style));
1039
+ let inset = insetOf(cell.style, CELL_PAD);
974
1040
  let list = atoms(canvas, cell.tokens, style);
975
- 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
+ );
976
1045
  return {
977
1046
  list,
978
- natural: natural + 2 * PADX,
1047
+ natural: natural + inset.l + inset.r,
979
1048
  align: style.align,
980
1049
  bg: col(style.background),
981
1050
  underline: !!style.underline,
982
1051
  strikethrough: !!style.strikethrough,
1052
+ inset,
1053
+ style: cell.style,
983
1054
  };
984
1055
  };
985
1056
 
@@ -987,9 +1058,14 @@ let cellOf = (canvas, cell, rowStyle) => {
987
1058
  let rowOf = (canvas, cells, widths) => {
988
1059
  let h = rowHeight(canvas);
989
1060
  let out = cells.map((cell, i) => {
990
- let inner = Math.max(widths[i] - 2 * PADX, 1);
991
- let lines = cell.list.length ? dress(wrap(canvas, cell.list, inner), cell) : [];
992
- let cellHeight = heightOf(lines) + 2 * PADY;
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;
993
1069
  if (cellHeight > h) h = cellHeight;
994
1070
  return { ...cell, lines };
995
1071
  });
@@ -1003,8 +1079,9 @@ let fillCell = (canvas, cell, bg, x, y, w, h) => {
1003
1079
 
1004
1080
  /** @type {(canvas: Canvas, cell: any, lines: any[], x: number, y: number, w: number) => void} */
1005
1081
  let writeCell = (canvas, cell, lines, x, y, w) => {
1082
+ let inset = cell.inset;
1006
1083
  for (let line of lines) {
1007
- 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);
1008
1085
  y -= line.h;
1009
1086
  }
1010
1087
  };
@@ -1018,9 +1095,10 @@ let writeCell = (canvas, cell, lines, x, y, w) => {
1018
1095
  */
1019
1096
  let paintCells = (canvas, cells, lines, h, padTop, xOffsets, widths, bg) => {
1020
1097
  if (bg) canvas.rect(bg, xOffsets[0], canvas.y - h, sum(widths), h);
1021
- let y0 = canvas.y - (padTop ? PADY : 0);
1022
1098
  for (let [i, cell] of cells.entries()) {
1023
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);
1024
1102
  writeCell(canvas, cell, lines[i], xOffsets[i], y0, widths[i]);
1025
1103
  }
1026
1104
  canvas.y -= h;
@@ -1028,10 +1106,11 @@ let paintCells = (canvas, cells, lines, h, padTop, xOffsets, widths, bg) => {
1028
1106
  };
1029
1107
 
1030
1108
  /**
1031
- * @type {(canvas: Canvas, row: { cells: any[], h: number }, xOffsets: number[],
1109
+ * @type {(canvas: Canvas, row: TableRow, xOffsets: number[],
1032
1110
  * widths: number[], bg: string | null) => void}
1033
1111
  */
1034
- let drawRow = (canvas, row, xOffsets, widths, bg) =>
1112
+ let drawRow = (canvas, row, xOffsets, widths, bg) => {
1113
+ let yTop = canvas.y;
1035
1114
  paintCells(
1036
1115
  canvas,
1037
1116
  row.cells,
@@ -1042,14 +1121,17 @@ let drawRow = (canvas, row, xOffsets, widths, bg) =>
1042
1121
  widths,
1043
1122
  bg,
1044
1123
  );
1124
+ paintBox(canvas, xOffsets[0], yTop, sum(widths), row.h, row.style, null);
1125
+ };
1045
1126
 
1046
- // The widest of each column's header, rows and total, padding included.
1047
- /** @type {(cols: any[], header: any[], rows: any[], total: any[] | null) => number[]} */
1048
- let naturalWidths = (cols, header, rows, total) =>
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) =>
1049
1130
  cols.map((_, i) => {
1050
1131
  let widest = header[i].natural;
1051
1132
  for (let row of rows) widest = Math.max(widest, row.cells[i].natural);
1052
- 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;
1053
1135
  });
1054
1136
 
1055
1137
  /** @type {(values: number[]) => number} */
@@ -1085,14 +1167,10 @@ let columnWidths = (cols, natural, avail) => {
1085
1167
  return widths;
1086
1168
  };
1087
1169
 
1088
- // The rule under a table's header row and over its total: the only rule this
1089
- // target draws, always the full content width at the cursor.
1090
- /** @type {(canvas: Canvas, x: number, avail: number) => void} */
1091
- let underline = (canvas, x, avail) => canvas.rule(x, x + avail, canvas.y);
1092
-
1093
1170
  // One table being emitted: what `carryOver`, `sliced` and `put` all need.
1094
1171
  /**
1095
- * @typedef {{ state: Flow, head: { cells: any[], h: number },
1172
+ * @typedef {{ cells: any[], h: number, style?: any }} TableRow
1173
+ * @typedef {{ state: Flow, head: TableRow,
1096
1174
  * xOffsets: number[], widths: number[], x: number, avail: number }} Grid
1097
1175
  */
1098
1176
 
@@ -1104,12 +1182,14 @@ let underline = (canvas, x, avail) => canvas.rule(x, x + avail, canvas.y);
1104
1182
  // The headings replay at a strip head as well as a page head, unlike a group
1105
1183
  // header (SCHEMA.md): a column of figures under nothing is unreadable, where a
1106
1184
  // group header is merely absent.
1185
+ /** @type {(row: { style?: any }) => any} */
1186
+ let rowFill = (row) => col((row.style || {}).background);
1187
+
1107
1188
  /** @type {(grid: Grid) => void} */
1108
1189
  let carryOver = (grid) => {
1109
1190
  advance(grid.state);
1110
1191
  rebase(grid);
1111
- drawRow(grid.state.canvas, grid.head, grid.xOffsets, grid.widths, null);
1112
- underline(grid.state.canvas, grid.x, grid.avail);
1192
+ drawRow(grid.state.canvas, grid.head, grid.xOffsets, grid.widths, rowFill(grid.head));
1113
1193
  };
1114
1194
 
1115
1195
  // Move the grid to the column the cursor is now in. A table's widths are its
@@ -1129,7 +1209,7 @@ let firstSlice = (canvas, row) =>
1129
1209
  rowHeight(canvas),
1130
1210
  ...row.cells
1131
1211
  .filter((/** @type {any} */ cell) => cell.lines.length)
1132
- .map((/** @type {any} */ cell) => PADY + cell.lines[0].h),
1212
+ .map((/** @type {any} */ cell) => cell.inset.t + cell.lines[0].h),
1133
1213
  );
1134
1214
 
1135
1215
  /** @type {(cell: any, next: number[], i: number, used: number, cap: number) => any[]} */
@@ -1149,10 +1229,17 @@ let takeLines = (cell, next, i, used, cap) => {
1149
1229
  let stillMore = (row, next) =>
1150
1230
  row.cells.some((/** @type {any} */ cell, /** @type {number} */ i) => next[i] < cell.lines.length);
1151
1231
 
1152
- /** @type {(taken: any[][], pad: number, more: boolean) => number} */
1153
- let sliceH = (taken, pad, more) => {
1154
- let h = Math.max(0, ...taken.map((lines) => pad + heightOf(lines)));
1155
- return more ? h : h + PADY;
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;
1156
1243
  };
1157
1244
 
1158
1245
  // A row no page can hold degrades to plain flow: each page takes what fits of
@@ -1164,13 +1251,11 @@ let sliced = (grid, row, bg) => {
1164
1251
  let first = true;
1165
1252
  for (;;) {
1166
1253
  let cap = canvas.y - canvas.bottom;
1167
- let pad = first ? PADY : 0;
1168
1254
  let taken = row.cells.map((/** @type {any} */ cell, /** @type {number} */ i) =>
1169
- takeLines(cell, next, i, pad, cap),
1255
+ takeLines(cell, next, i, first ? cell.inset.t : 0, cap),
1170
1256
  );
1171
1257
  let more = stillMore(row, next);
1172
- // The closing padding belongs to the last slice only.
1173
- let h = sliceH(taken, pad, more);
1258
+ let h = sliceH(taken, row.cells, first, more);
1174
1259
  paintCells(canvas, row.cells, taken, h, first, grid.xOffsets, grid.widths, bg);
1175
1260
  if (!more) return;
1176
1261
  carryOver(grid);
@@ -1196,21 +1281,13 @@ let shouldBreak = (state, row, keep, cap) => {
1196
1281
  return canvas.y - row.h - keep < floor && worthBreak(canvas, row, cap, floor) && !freshOf(state);
1197
1282
  };
1198
1283
 
1199
- /** @type {(grid: Grid, topRule: boolean, broke: boolean) => void} */
1200
- let openRow = (grid, topRule, broke) => {
1201
- if (broke) carryOver(grid);
1202
- if (topRule && !broke) underline(grid.state.canvas, grid.x, grid.avail);
1203
- };
1204
-
1205
1284
  // Place one row, breaking first when that helps. `keep` is a companion row it
1206
1285
  // must not be parted from — the total riding with the last data row.
1207
- // `topRule` is the total's opening rule, which after a break would double the
1208
- // replayed header's.
1209
1286
  /**
1210
- * @type {(grid: Grid, row: { cells: any[], h: number }, bg: string | null,
1211
- * keep?: number, topRule?: boolean) => void}
1287
+ * @type {(grid: Grid, row: TableRow, bg: string | null,
1288
+ * keep?: number) => void}
1212
1289
  */
1213
- let put = (grid, row, bg, keep = 0, topRule = false) => {
1290
+ let put = (grid, row, bg, keep = 0) => {
1214
1291
  let canvas = grid.state.canvas;
1215
1292
  // The tallest row a fresh column holds, after the replayed group headers and
1216
1293
  // the replayed column headings. A companion over that can never share a
@@ -1222,33 +1299,36 @@ let put = (grid, row, bg, keep = 0, topRule = false) => {
1222
1299
  // and leaves a headings-only column. Clamped to `cap`: a first slice no fresh
1223
1300
  // column could fit either is no reason to go looking for one.
1224
1301
  let broke = shouldBreak(grid.state, row, keep, cap);
1225
- openRow(grid, topRule, broke);
1302
+ if (broke) carryOver(grid);
1226
1303
  if (canvas.y - row.h < canvas.bottom) return sliced(grid, row, bg);
1227
1304
  drawRow(canvas, row, grid.xOffsets, grid.widths, bg);
1228
1305
  };
1229
1306
 
1230
- /** @type {(canvas: Canvas, row: any) => { cells: any[], bg: any }} */
1307
+ /** @type {(canvas: Canvas, row: any) => { cells: any[], style: any }} */
1231
1308
  let bodyRow = (canvas, row) => ({
1232
1309
  cells: row.cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, row.style)),
1233
- bg: col((row.style || {}).background),
1310
+ style: row.style,
1234
1311
  });
1235
1312
 
1236
1313
  /** @type {(canvas: Canvas, buffered: any) => any} */
1237
- let totalCells = (canvas, buffered) =>
1238
- 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
+ }));
1239
1319
 
1240
- /** @type {(laid: any[], totalRow: any) => number} */
1241
- 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);
1242
1322
 
1243
- /** @type {(totalRow: any, i: number, last: number) => number} */
1244
- 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);
1245
1325
 
1246
- /** @type {(grid: Grid, laid: any[], totalRow: any) => void} */
1247
- let emitRows = (grid, laid, totalRow) => {
1326
+ /** @type {(grid: Grid, laid: any[], totalRows: any[]) => void} */
1327
+ let emitRows = (grid, laid, totalRows) => {
1248
1328
  let last = laid.length - 1;
1249
- for (let [i, laidRow] of laid.entries())
1250
- put(grid, laidRow.row, laidRow.bg, keepTotal(totalRow, i, last));
1251
- if (totalRow) put(grid, totalRow, null, 0, true);
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);
1252
1332
  };
1253
1333
 
1254
1334
  // Lay out a buffered table: measure every cell, allocate the columns, then
@@ -1259,14 +1339,19 @@ let emitRows = (grid, laid, totalRow) => {
1259
1339
  // events as they arrive, and the region buffer, reading them back off what it
1260
1340
  // held — so a new table event kind cannot reach one and miss the other.
1261
1341
  /** @type {(events: any[]) => any} */
1262
- let tableOf = (events) => ({
1263
- columns: events[0].columns,
1264
- rows: events.filter((event) => event.type === "row"),
1265
- total: events.find((event) => event.type === "total-row")?.cells ?? null,
1266
- });
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
+ };
1267
1352
 
1268
1353
  /**
1269
- * @typedef {{ widths: number[], head: any, laid: any[], totalRow: any }} Measured
1354
+ * @typedef {{ widths: number[], head: any, laid: any[], totalRows: any[] }} Measured
1270
1355
  */
1271
1356
  // Measure a buffered table at a given width: the column widths every cell has
1272
1357
  // a say in, then each row laid out against them. Everything here is
@@ -1276,18 +1361,23 @@ let tableOf = (events) => ({
1276
1361
  /** @type {(canvas: Canvas, buffered: any, avail: number) => Measured} */
1277
1362
  let measureTable = (canvas, buffered, avail) => {
1278
1363
  let cols = /** @type {any[]} */ (buffered.columns);
1279
- 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
+ );
1280
1367
  let rows = buffered.rows.map((/** @type {any} */ row) => bodyRow(canvas, row));
1281
- let total = totalCells(canvas, buffered);
1282
- let widths = columnWidths(cols, naturalWidths(cols, header, rows, total), avail);
1368
+ let totals = totalRowsOf(canvas, buffered);
1369
+ let widths = columnWidths(cols, naturalWidths(cols, header, rows, totals), avail);
1283
1370
  return {
1284
1371
  widths,
1285
- head: rowOf(canvas, header, widths),
1372
+ head: { ...rowOf(canvas, header, widths), style: buffered.headerStyle },
1286
1373
  laid: rows.map((/** @type {any} */ row) => ({
1287
- row: rowOf(canvas, row.cells, widths),
1288
- 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,
1289
1380
  })),
1290
- totalRow: total && rowOf(canvas, total, widths),
1291
1381
  };
1292
1382
  };
1293
1383
 
@@ -1302,7 +1392,7 @@ let table = (state, buffered) => {
1302
1392
  // Measured already if a region buffered this table: the balance model needed
1303
1393
  // its real row heights, at this same width, and that is the same
1304
1394
  // measurement — so the replay draws it without wrapping every cell again.
1305
- let { widths, head, laid, totalRow } =
1395
+ let { widths, head, laid, totalRows } =
1306
1396
  state.measured.get(buffered.opening) || measureTable(canvas, buffered, avail);
1307
1397
  let xOffsets = widths.map((_, i) => x + sum(widths.slice(0, i)));
1308
1398
 
@@ -1310,16 +1400,15 @@ let table = (state, buffered) => {
1310
1400
  let grid = { state, head, xOffsets, widths, x, avail };
1311
1401
 
1312
1402
  // A pending group header keeps the table's header row and its first row.
1313
- flush(state, grid.head.h + firstH(laid, totalRow));
1403
+ flush(state, grid.head.h + firstH(laid, totalRows));
1314
1404
  if (canvas.y - grid.head.h < floorOf(state)) {
1315
1405
  advance(state);
1316
1406
  rebase(grid);
1317
1407
  }
1318
- drawRow(canvas, grid.head, grid.xOffsets, widths, null);
1319
- underline(canvas, grid.x, grid.avail);
1320
- // The last data row keeps the total row with it, so a total is never
1321
- // stranded alone at a page top.
1322
- emitRows(grid, laid, totalRow);
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);
1323
1412
  };
1324
1413
 
1325
1414
  // --- page bands -------------------------------------------------------------
@@ -1335,9 +1424,11 @@ let band = (canvas, items, yTop) => {
1335
1424
  canvas.fresh = false;
1336
1425
  // Folded straight into the draw, the way the body walk folds straight into
1337
1426
  // `route`: the folder emits in order and nothing here needs the band whole.
1338
- let fold = splitFolder((event) =>
1339
- drawBlock(canvas, blockOf(canvas, event, avail), canvas.margin, avail),
1340
- );
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
+ });
1341
1432
  for (let event of items) fold(event);
1342
1433
  let h = yTop - canvas.y;
1343
1434
  canvas.y = saved;
@@ -1393,7 +1484,11 @@ let reserve = (geo, fonts, bands) => {
1393
1484
  // from.
1394
1485
  /** @type {(canvas: Canvas) => Frame} */
1395
1486
  let pageBox = (canvas) =>
1396
- frame(canvas.width, canvas.height, canvas.margin, canvas.base, canvas.style);
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
+ });
1397
1492
 
1398
1493
  // One finished page's furniture, drawn in the strips `reserve` left for it:
1399
1494
  // the header hanging from the top margin, the footer resting on the bottom
@@ -1458,6 +1553,9 @@ let flow = (canvas) => {
1458
1553
  // the replay draws that table without wrapping every cell a second time.
1459
1554
  // Per flow, because the keys are one render's events.
1460
1555
  measured: new WeakMap(),
1556
+ pin: null,
1557
+ skipGap: false,
1558
+ inHeader: false,
1461
1559
  // Assigned below, once `route` exists: a buffered region replays through
1462
1560
  // it, and one entry point is what makes the replay take the path the
1463
1561
  // content would have taken had it never been held.
@@ -1534,11 +1632,28 @@ let flow = (canvas) => {
1534
1632
  /** @type {(event: any) => boolean} */
1535
1633
  let ownFooter = (event) => event.role === "group-footer" && owner() === state.open.length - 1;
1536
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
+
1537
1651
  // Route one event: the declaring node's own bands stay full-width, region
1538
1652
  // content opens the strips owed to it, and anything arriving while a region
1539
1653
  // is still deciding is buffered rather than placed.
1540
1654
  /** @type {(event: any, block?: Block | null) => void} */
1541
1655
  let route = (event, block = null) => {
1656
+ snapPin(event);
1542
1657
  if (fullBand(event)) fullWidth(event, block);
1543
1658
  else if (deciding()) buffer(state, event, block);
1544
1659
  else if (opens(event)) begin(event);
@@ -1600,27 +1715,51 @@ let flow = (canvas) => {
1600
1715
  buffer(state, event, block);
1601
1716
  };
1602
1717
 
1603
- // The report default onto the canvas, before anything is measured. Its
1604
- // `size` replaces this target's baseline outright, so row heights and band
1605
- // gaps scale with the document's type rather than staying at a size nothing
1606
- // is set in; the block itself becomes the outermost layer `stack` composes
1607
- // (docs/adr/0033). `sizeOf` is what decides a size is usable anywhere in this
1608
- // target, so it decides it here too -- a computed `size` reaches this
1609
- // unchecked, and two spellings of that leniency would drift.
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.
1610
1733
  /** @type {(style: any) => void} */
1611
1734
  let adoptDefault = (style) => {
1612
1735
  if (!style) return;
1613
- canvas.style = style;
1614
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;
1615
1748
  };
1616
1749
 
1617
1750
  return {
1618
1751
  handlers: {
1619
1752
  "report-start": (event) => {
1620
1753
  opening = event;
1754
+ canvas.locale = event.locale;
1755
+ canvas.currency = event.currency;
1756
+ canvas.timeZone = event.timeZone;
1621
1757
  adoptDefault(event.style);
1622
1758
  if (event.columns) state.pending = { count: event.columns, owner: -1 };
1623
- if (!event.page) return;
1759
+ if (!event.page) {
1760
+ pinHeader(event);
1761
+ return;
1762
+ }
1624
1763
  // A frame of its own, never the canvas itself: `measuring` builds
1625
1764
  // from whatever it is handed, so handing it this canvas would spread
1626
1765
  // the page passes onto a measurement — and reaching a document is the
@@ -1628,6 +1767,7 @@ let flow = (canvas) => {
1628
1767
  // a property of the page box in any case, not of a content box
1629
1768
  // something may already have narrowed.
1630
1769
  adopt(canvas, reserve(pageBox(canvas), canvas.fonts, event.page));
1770
+ pinHeader(event);
1631
1771
  },
1632
1772
  // Everything else routes: `placed` above says what each event does, and
1633
1773
  // one entry point is what lets a buffered region replay through it. The
@@ -1651,6 +1791,7 @@ let flow = (canvas) => {
1651
1791
  },
1652
1792
  },
1653
1793
  finish: () => {
1794
+ snapPin({ type: "report-end" });
1654
1795
  // A root region reaches here undrained when a report declares `columns`
1655
1796
  // and no full-width footer followed its body.
1656
1797
  closeRegion(state);