@quario/pdf 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/README.md +6 -5
- package/lib/balance.js +1 -1
- package/lib/box.js +111 -0
- package/lib/canvas.js +38 -24
- package/lib/fonts.js +71 -29
- package/lib/image.js +2 -7
- package/lib/index.d.ts +0 -2
- package/lib/index.js +60 -62
- package/lib/layout.js +445 -140
- package/lib/outline.js +18 -22
- package/lib/style.js +25 -12
- package/lib/text.js +39 -22
- package/package.json +4 -4
package/lib/layout.js
CHANGED
|
@@ -21,11 +21,13 @@
|
|
|
21
21
|
* document's bands claim before anything is placed, and where they are drawn
|
|
22
22
|
* on each finished page once the count is known.
|
|
23
23
|
*/
|
|
24
|
-
import { isReportBand, text } from "quario";
|
|
24
|
+
import { display, isReportBand, text } from "quario";
|
|
25
25
|
import { balance } from "./balance.js";
|
|
26
26
|
import { adopt, frame, measuring } from "./canvas.js";
|
|
27
|
+
import { familyName } from "./fonts.js";
|
|
27
28
|
import { intrinsic } from "./image.js";
|
|
28
|
-
import {
|
|
29
|
+
import { CELL_PAD, NO_PAD, insetOf, paintBox, unbox } from "./box.js";
|
|
30
|
+
import { BAND, GUTTER, LEAD, col, merge, roled, shift, sizeOf } from "./style.js";
|
|
29
31
|
import { atoms, dress, heightOf, wrap } from "./text.js";
|
|
30
32
|
|
|
31
33
|
/** @typedef {import('./balance.js').Unit} Unit */
|
|
@@ -46,8 +48,15 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
|
|
|
46
48
|
* @typedef {{ canvas: Canvas, open: Group[], gap: number, marks: any[],
|
|
47
49
|
* starts: number[], region: Region | null, pending: Pending | null,
|
|
48
50
|
* measured: WeakMap<any, Measured>,
|
|
51
|
+
* pin: number | null, skipGap: boolean, inHeader: boolean,
|
|
49
52
|
* route: (event: any, block?: Block | null) => void }} Flow
|
|
50
53
|
*/
|
|
54
|
+
/**
|
|
55
|
+
* What flow spacing reads of the page: the cursor and whether a strip is open.
|
|
56
|
+
* Page furniture has a canvas and no region, so the skip helpers take this
|
|
57
|
+
* rather than the whole flow.
|
|
58
|
+
* @typedef {{ canvas: Canvas, region: Region | null }} Cursor
|
|
59
|
+
*/
|
|
51
60
|
|
|
52
61
|
// One flow unit and its presentation; `h` is the full flow height. A text
|
|
53
62
|
// item's is its wrapped lines; an image item's is the picture, sized here so
|
|
@@ -57,31 +66,62 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
|
|
|
57
66
|
* @typedef {{ bytes: Uint8Array, format: string, w: number, h: number }} Picture
|
|
58
67
|
*/
|
|
59
68
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
69
|
+
* `align` is absent on a split, which has none of its own: alignment is a
|
|
70
|
+
* slot's, reached through the style layering.
|
|
71
|
+
* @typedef {{ lines: Line[], picture?: Picture, parts?: SlotPart[],
|
|
72
|
+
* bg: string | null, align?: any, h: number, style?: any,
|
|
73
|
+
* inset: { t: number, r: number, b: number, l: number } }} Block
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* One slot of a split, as laid out: the block it drew to and the width it was
|
|
77
|
+
* measured at. There is no offset — a slot's is the sum of the widths before
|
|
78
|
+
* it, which `drawSplit` accumulates as it walks them.
|
|
79
|
+
* @typedef {{ block: Block, w: number }} SlotPart
|
|
62
80
|
*/
|
|
63
81
|
|
|
64
82
|
// How `fit` sizes a picture against the width it has: `natural` is the
|
|
65
83
|
// image's own size in points, never wider than the content box; `width`
|
|
66
84
|
// scales to the content box either way. The ratio is kept in both, so a
|
|
67
85
|
// height is never anything but the width's consequence.
|
|
68
|
-
/** @type {(
|
|
69
|
-
let pictureOf = (
|
|
86
|
+
/** @type {(event: any, avail: number) => Picture} */
|
|
87
|
+
let pictureOf = (event, avail) => {
|
|
70
88
|
let { w, h } = intrinsic(event.bytes, event.format);
|
|
71
89
|
let width = event.fit === "width" ? avail : Math.min(w, avail);
|
|
72
90
|
return { bytes: event.bytes, format: event.format, w: width, h: (h * width) / w };
|
|
73
91
|
};
|
|
74
92
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
// `under` is the enclosing style a slot's own layers over — a split's, when
|
|
94
|
+
// this is one of its slots. Null everywhere else, where nothing encloses.
|
|
95
|
+
/** @type {(canvas: Canvas, event: any, avail: number, under?: any) => Block} */
|
|
96
|
+
let blockOf = (canvas, event, avail, under = null) => {
|
|
97
|
+
let text = merge(unbox(under), unbox(event.style));
|
|
98
|
+
let box = event.style;
|
|
99
|
+
let bg = col((box || {}).background);
|
|
100
|
+
let inset = insetOf(box, NO_PAD);
|
|
101
|
+
let inner = Math.max(avail - inset.l - inset.r, 1);
|
|
102
|
+
if (event.type === "split") return splitBlock(canvas, event, inner, text, bg, box, inset);
|
|
79
103
|
if (event.type === "image") {
|
|
80
|
-
let picture = pictureOf(
|
|
81
|
-
return {
|
|
104
|
+
let picture = pictureOf(event, inner);
|
|
105
|
+
return {
|
|
106
|
+
lines: [],
|
|
107
|
+
picture,
|
|
108
|
+
bg,
|
|
109
|
+
align: text.align,
|
|
110
|
+
h: picture.h + inset.t + inset.b,
|
|
111
|
+
style: box,
|
|
112
|
+
inset,
|
|
113
|
+
};
|
|
82
114
|
}
|
|
83
|
-
let
|
|
84
|
-
|
|
115
|
+
let size = sizeOf(text, canvas.base);
|
|
116
|
+
let lines = dress(wrap(canvas, atoms(canvas, event.tokens, text), inner, size), text);
|
|
117
|
+
return {
|
|
118
|
+
lines,
|
|
119
|
+
bg,
|
|
120
|
+
align: text.align,
|
|
121
|
+
h: heightOf(lines) + inset.t + inset.b,
|
|
122
|
+
style: box,
|
|
123
|
+
inset,
|
|
124
|
+
};
|
|
85
125
|
};
|
|
86
126
|
|
|
87
127
|
// Scale a picture block down to the room it has. Best-effort, and only ever
|
|
@@ -89,9 +129,10 @@ let blockOf = (canvas, event, avail) => {
|
|
|
89
129
|
/** @type {(block: Block, room: number) => void} */
|
|
90
130
|
let shrink = (block, room) => {
|
|
91
131
|
let picture = /** @type {Picture} */ (block.picture);
|
|
92
|
-
|
|
93
|
-
picture.
|
|
94
|
-
|
|
132
|
+
let inner = Math.max(room - block.inset.t - block.inset.b, 1);
|
|
133
|
+
picture.w *= inner / picture.h;
|
|
134
|
+
picture.h = inner;
|
|
135
|
+
block.h = inner + block.inset.t + block.inset.b;
|
|
95
136
|
};
|
|
96
137
|
|
|
97
138
|
// Draw one block whole at the cursor. A picture's background fills the
|
|
@@ -100,27 +141,123 @@ let shrink = (block, room) => {
|
|
|
100
141
|
// is one box, so where it sits in the width it was given is worked out once.
|
|
101
142
|
/** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
|
|
102
143
|
let drawPicture = (canvas, block, x, avail) => {
|
|
144
|
+
let inset = block.inset;
|
|
103
145
|
let { bytes, format, w, h } = /** @type {Picture} */ (block.picture);
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
canvas
|
|
107
|
-
canvas.y
|
|
146
|
+
let outer = w + inset.l + inset.r;
|
|
147
|
+
let at = x + shift(block.align, avail - outer);
|
|
148
|
+
paintBox(canvas, at, canvas.y, outer, block.h, block.style, block.bg);
|
|
149
|
+
canvas.picture(bytes, format, at + inset.l, canvas.y - inset.t - h, w, h);
|
|
150
|
+
canvas.y -= block.h;
|
|
108
151
|
canvas.fresh = false;
|
|
109
152
|
};
|
|
110
153
|
|
|
111
154
|
/** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
|
|
112
155
|
let drawLines = (canvas, block, x, avail) => {
|
|
113
|
-
|
|
156
|
+
let inset = block.inset;
|
|
157
|
+
paintBox(canvas, x, canvas.y, avail, block.h, block.style, block.bg);
|
|
158
|
+
let y = canvas.y - inset.t;
|
|
159
|
+
let inner = Math.max(avail - inset.l - inset.r, 1);
|
|
114
160
|
for (let line of block.lines) {
|
|
115
|
-
canvas.drawLine(line, x,
|
|
116
|
-
|
|
161
|
+
canvas.drawLine(line, x + inset.l, y, inner, block.align);
|
|
162
|
+
y -= line.h;
|
|
117
163
|
}
|
|
164
|
+
canvas.y -= block.h;
|
|
165
|
+
canvas.fresh = false;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// An authored share fixes its slot; the width-less ones divide what is left,
|
|
169
|
+
// evenly. Evenly rather than by natural width, because a split is a line of
|
|
170
|
+
// furniture whose geometry should read off the document — the same answer the
|
|
171
|
+
// HTML target's `flex:1` gives, so one definition places alike in both.
|
|
172
|
+
// Each slot is measured at its own share and drawn from a common top, so the
|
|
173
|
+
// split is as tall as its tallest slot and the line's geometry does not move
|
|
174
|
+
// with the data. The split's own declarations are the layer under each slot's,
|
|
175
|
+
// so a split styled `bold` reads the same here as it does through CSS
|
|
176
|
+
// inheritance in the HTML target and the `under` layer in XLSX. It has no
|
|
177
|
+
// `align` of its own: alignment is a slot's, reached through that layering.
|
|
178
|
+
/**
|
|
179
|
+
* @type {(canvas: Canvas, event: any, inner: number, text: any,
|
|
180
|
+
* bg: string | null, box: any, inset: { t: number, r: number, b: number, l: number }) => Block}
|
|
181
|
+
*/
|
|
182
|
+
let splitBlock = (canvas, event, inner, text, bg, box, inset) => {
|
|
183
|
+
let widths = slotWidths(event.slots, inner);
|
|
184
|
+
/** @type {SlotPart[]} */
|
|
185
|
+
let parts = [];
|
|
186
|
+
let h = 0;
|
|
187
|
+
for (let [i, slot] of event.items.entries()) {
|
|
188
|
+
let block = blockOf(canvas, slot, Math.max(widths[i], 1), text);
|
|
189
|
+
if (block.h > h) h = block.h;
|
|
190
|
+
parts.push({ block, w: widths[i] });
|
|
191
|
+
}
|
|
192
|
+
return { lines: [], parts, bg, h: h + inset.t + inset.b, style: box, inset };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** @type {(slots: { width?: number }[], avail: number) => number[]} */
|
|
196
|
+
let slotWidths = (slots, avail) => {
|
|
197
|
+
let autos = 0,
|
|
198
|
+
spent = 0;
|
|
199
|
+
/** @type {(number | null)[]} */
|
|
200
|
+
let fixed = slots.map(({ width }) => {
|
|
201
|
+
// Bound to a local first: `Number.isFinite` does not narrow a property.
|
|
202
|
+
if (width === undefined || !Number.isFinite(width)) return (autos++, null);
|
|
203
|
+
let share = (width / 100) * avail;
|
|
204
|
+
spent += share;
|
|
205
|
+
return share;
|
|
206
|
+
});
|
|
207
|
+
let each = autos ? Math.max(avail - spent, 0) / autos : 0;
|
|
208
|
+
return fixed.map((width) => width ?? each);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// A split reaches this target as a bracket around ordinary item and image
|
|
212
|
+
// events. Folding it into one event is what lets the rest of the target treat
|
|
213
|
+
// it as a single block -- measured, kept together, and paged by exactly the
|
|
214
|
+
// machinery every other block goes through. Stateful rather than a transform
|
|
215
|
+
// over an array, because the body walk is streaming; the page-band pass drives
|
|
216
|
+
// the same folder over its own list, so the two cannot drift.
|
|
217
|
+
// The event types that become a `Block`. They are also exactly the three the
|
|
218
|
+
// split folder owns: item and image reach `route` through it, so a slot's are
|
|
219
|
+
// collected rather than placed, and `split` is minted by the folder and never
|
|
220
|
+
// arrives from the stream at all.
|
|
221
|
+
let BLOCKS = new Set(["item", "image", "split"]);
|
|
222
|
+
|
|
223
|
+
/** @type {(emit: (event: any) => void) => (event: any) => void} */
|
|
224
|
+
let splitFolder = (emit) => {
|
|
225
|
+
/** @type {{ event: any, items: any[] } | null} */
|
|
226
|
+
let bracket = null;
|
|
227
|
+
// The engine emits the bracket as one array, so `split-end` always has its
|
|
228
|
+
// opening and this never reads a null.
|
|
229
|
+
let close = () => {
|
|
230
|
+
let { event, items } = /** @type {{ event: any, items: any[] }} */ (bracket);
|
|
231
|
+
bracket = null;
|
|
232
|
+
emit({ type: "split", role: event.role, style: event.style, slots: event.slots, items });
|
|
233
|
+
};
|
|
234
|
+
return (event) => {
|
|
235
|
+
if (event.type === "split-start") bracket = { event, items: [] };
|
|
236
|
+
else if (event.type === "split-end") close();
|
|
237
|
+
else if (bracket) bracket.items.push(event);
|
|
238
|
+
else emit(event);
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
|
|
243
|
+
let drawSplit = (canvas, block, x, avail) => {
|
|
244
|
+
let inset = block.inset;
|
|
245
|
+
let yTop = canvas.y;
|
|
246
|
+
paintBox(canvas, x, yTop, avail, block.h, block.style, block.bg);
|
|
247
|
+
let top = yTop - inset.t;
|
|
248
|
+
let at = x + inset.l;
|
|
249
|
+
for (let part of block.parts || []) {
|
|
250
|
+
canvas.y = top;
|
|
251
|
+
drawBlock(canvas, part.block, at, part.w);
|
|
252
|
+
at += part.w;
|
|
253
|
+
}
|
|
254
|
+
canvas.y = yTop - block.h;
|
|
118
255
|
canvas.fresh = false;
|
|
119
256
|
};
|
|
120
257
|
|
|
121
258
|
/** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
|
|
122
259
|
let drawBlock = (canvas, block, x, avail) =>
|
|
123
|
-
(block.picture ? drawPicture : drawLines)(canvas, block, x, avail);
|
|
260
|
+
(block.parts ? drawSplit : block.picture ? drawPicture : drawLines)(canvas, block, x, avail);
|
|
124
261
|
|
|
125
262
|
// A region is a run of strips: what a columned node's content flows down
|
|
126
263
|
// (CONTEXT.md "Strip"). `index` is the strip being filled, `top` the y every
|
|
@@ -169,7 +306,7 @@ let ceilOf = (state) => (state.region ? state.region.top : state.canvas.top);
|
|
|
169
306
|
// leaving an empty page behind — so a strip claims no second meaning for it.
|
|
170
307
|
// A block of no height would read fresh here and used there; none is reachable
|
|
171
308
|
// today, since every item wraps to at least one line.
|
|
172
|
-
/** @type {(state:
|
|
309
|
+
/** @type {(state: Cursor) => boolean} */
|
|
173
310
|
let freshOf = (state) => (state.region ? state.canvas.y === state.region.top : state.canvas.fresh);
|
|
174
311
|
|
|
175
312
|
/** @type {(canvas: Canvas, h: number, line: Line, floor: number) => boolean} */
|
|
@@ -229,17 +366,42 @@ let fitLines = (canvas, block, i, floor, fresh) => {
|
|
|
229
366
|
/** @type {(canvas: Canvas, block: Block, floor: number) => boolean} */
|
|
230
367
|
let whole = (canvas, block, floor) => canvas.y - block.h >= floor;
|
|
231
368
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
369
|
+
/** @type {(style: any, name: string) => number} */
|
|
370
|
+
let spaceOf = (style, name) => {
|
|
371
|
+
let n = style?.[name];
|
|
372
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/** @type {(event: any) => boolean} */
|
|
376
|
+
let pageBand = (event) => event.role === "page-header" || event.role === "page-footer";
|
|
377
|
+
/** @type {(state: Cursor, event: any) => boolean} */
|
|
378
|
+
let keepLead = (state, event) => pageBand(event) || !freshOf(state);
|
|
379
|
+
/** @type {(state: Cursor, event: any) => number} */
|
|
380
|
+
let leadOf = (state, event) => (keepLead(state, event) ? spaceOf(event.style, "spaceBefore") : 0);
|
|
381
|
+
/** @type {(state: Cursor, event: any, name: string) => void} */
|
|
382
|
+
let skipFlow = (state, event, name) => {
|
|
383
|
+
state.canvas.y -= name === "spaceBefore" ? leadOf(state, event) : spaceOf(event.style, name);
|
|
384
|
+
};
|
|
385
|
+
/** @type {(state: Cursor, event: any) => number} */
|
|
386
|
+
let flowPad = (state, event) => leadOf(state, event) + spaceOf(event.style, "spaceAfter");
|
|
387
|
+
|
|
388
|
+
// The unsplittable blocks -- a picture and a split -- move whole to a fresh
|
|
389
|
+
// column rather than breaking, so a balanced floor never cuts one: only the
|
|
390
|
+
// page's own floor bears on it. A picture that no column can hold is scaled
|
|
391
|
+
// down to the one it lands on, since obeying the guarantee literally would
|
|
392
|
+
// mean dropping it (SCHEMA.md, best-effort); a split has nothing to scale, so
|
|
393
|
+
// one taller than any column renders in full past the bottom margin, the
|
|
394
|
+
// posture a paragraph line taller than a page already has.
|
|
395
|
+
//
|
|
396
|
+
// A split also *cannot* be sliced: its own `lines` are empty -- the content
|
|
397
|
+
// lives in its slots -- so the slicing path would draw nothing and leave the
|
|
398
|
+
// cursor where it stood, taking everything after it down with it.
|
|
237
399
|
/** @type {(state: Flow, block: Block) => void} */
|
|
238
|
-
let
|
|
400
|
+
let placeWhole = (state, block) => {
|
|
239
401
|
advance(state);
|
|
240
402
|
let canvas = state.canvas;
|
|
241
403
|
let room = canvas.y - canvas.bottom;
|
|
242
|
-
if (block.h > room) shrink(block, room);
|
|
404
|
+
if (block.picture && block.h > room) shrink(block, room);
|
|
243
405
|
drawBlock(canvas, block, originOf(state), widthOf(state));
|
|
244
406
|
};
|
|
245
407
|
|
|
@@ -285,12 +447,24 @@ let item = (state, event, measured = null) => {
|
|
|
285
447
|
let canvas = state.canvas;
|
|
286
448
|
let avail = widthOf(state);
|
|
287
449
|
let block = measured || blockOf(canvas, event, avail);
|
|
288
|
-
|
|
289
|
-
if (whole(canvas,
|
|
290
|
-
|
|
291
|
-
|
|
450
|
+
let padded = { ...block, h: block.h + flowPad(state, event) };
|
|
451
|
+
if (whole(canvas, padded, floorOf(state))) {
|
|
452
|
+
skipFlow(state, event, "spaceBefore");
|
|
453
|
+
drawBlock(canvas, block, originOf(state), avail);
|
|
454
|
+
skipFlow(state, event, "spaceAfter");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
overflow(state, block);
|
|
458
|
+
skipFlow(state, event, "spaceAfter");
|
|
292
459
|
};
|
|
293
460
|
|
|
461
|
+
// What a block does when it does not fit where it stands. A picture and a
|
|
462
|
+
// split never break -- each moves whole to a fresh column; anything else flows
|
|
463
|
+
// down it, breaking where `fitLines` says.
|
|
464
|
+
/** @type {(state: Flow, block: Block) => void} */
|
|
465
|
+
let overflow = (state, block) =>
|
|
466
|
+
block.picture || block.parts ? placeWhole(state, block) : slice(state, block);
|
|
467
|
+
|
|
294
468
|
// The height a page turn carries over: the drawn headers of the groups still
|
|
295
469
|
// open, which every page their instances continue onto replays. Computed rather
|
|
296
470
|
// than kept — a stored total is a second copy of `open` that can drift from it.
|
|
@@ -434,8 +608,9 @@ let closeRegion = (state) => {
|
|
|
434
608
|
// Two things this deliberately does not model, each stated because the cost of
|
|
435
609
|
// getting them wrong is a strip that stops early rather than a wrong document.
|
|
436
610
|
// A `total-row` is a row like any other, though it really keeps the last data
|
|
437
|
-
// row with
|
|
438
|
-
// to remove an error of one row at the end of a
|
|
611
|
+
// row with the whole emitted total block (SCHEMA.md): pairing them costs the
|
|
612
|
+
// flat per-entry map this is, to remove an error of one row at the end of a
|
|
613
|
+
// table. And a row taller than
|
|
439
614
|
// any strip is sliced rather than moved (SCHEMA.md, "How a tall row slices");
|
|
440
615
|
// called whole here, it makes every candidate height fail and the region
|
|
441
616
|
// fills, which is the honest answer for a region holding one.
|
|
@@ -481,9 +656,16 @@ let UNITS = {
|
|
|
481
656
|
},
|
|
482
657
|
row: restated,
|
|
483
658
|
"total-row": restated,
|
|
484
|
-
image:
|
|
659
|
+
image: unbroken,
|
|
660
|
+
split: unbroken,
|
|
485
661
|
};
|
|
486
662
|
|
|
663
|
+
// An unsplittable block owes a strip nothing but its own height.
|
|
664
|
+
/** @type {(entry: any) => Unit} */
|
|
665
|
+
function unbroken(entry) {
|
|
666
|
+
return unsplit(entry.h, entry.h);
|
|
667
|
+
}
|
|
668
|
+
|
|
487
669
|
// A row opening a strip carries the column headings restated above it.
|
|
488
670
|
/** @type {(entry: any, table: Restated) => Unit} */
|
|
489
671
|
function restated(entry, table) {
|
|
@@ -580,11 +762,13 @@ let estimate = (state, event) => (WORTH[event.type] || (() => 0))(state.canvas);
|
|
|
580
762
|
// What each kind of buffered event is worth. A module constant: `estimate` runs
|
|
581
763
|
// once per buffered event, and rebuilding this table each time would allocate
|
|
582
764
|
// a closure per table row.
|
|
765
|
+
/** @type {(canvas: Canvas) => number} */
|
|
766
|
+
let rowWorth = (canvas) => rowHeight(canvas) + CELL_PAD.t + CELL_PAD.b;
|
|
583
767
|
/** @type {Record<string, (canvas: Canvas) => number>} */
|
|
584
768
|
let WORTH = {
|
|
585
|
-
row:
|
|
586
|
-
"total-row":
|
|
587
|
-
"table-start":
|
|
769
|
+
row: rowWorth,
|
|
770
|
+
"total-row": rowWorth,
|
|
771
|
+
"table-start": rowWorth,
|
|
588
772
|
"group-start": (canvas) => instanceGap(canvas),
|
|
589
773
|
};
|
|
590
774
|
|
|
@@ -639,8 +823,8 @@ let remeasure = (state, region) => {
|
|
|
639
823
|
// Reports what the correction moved the running total by.
|
|
640
824
|
/** @type {(span: any[], measured: Measured) => number} */
|
|
641
825
|
let retune = (span, measured) => {
|
|
642
|
-
let heights = [measured.head.h, ...measured.laid.map((/** @type {any} */ laid) => laid.
|
|
643
|
-
if (measured.
|
|
826
|
+
let heights = [measured.head.h, ...measured.laid.map((/** @type {any} */ laid) => laid.h)];
|
|
827
|
+
if (measured.totalRows) for (let row of measured.totalRows) heights.push(row.h);
|
|
644
828
|
let delta = 0;
|
|
645
829
|
for (let [i, h] of heights.entries()) {
|
|
646
830
|
delta += h - span[i].h;
|
|
@@ -664,7 +848,7 @@ let entryFor = (state, event, block, width) => {
|
|
|
664
848
|
// decided anything. Events that are not blocks contribute an `estimate`.
|
|
665
849
|
/** @type {(state: Flow, event: any, width: number) => Block | null} */
|
|
666
850
|
let measureFor = (state, event, width) =>
|
|
667
|
-
|
|
851
|
+
BLOCKS.has(event.type) ? blockOf(state.canvas, event, width) : null;
|
|
668
852
|
|
|
669
853
|
// Give every outline mark still waiting a position: where its content begins.
|
|
670
854
|
/** @type {(state: Flow) => void} */
|
|
@@ -684,7 +868,7 @@ let pending = (state) => state.open.filter((group) => group.held.length);
|
|
|
684
868
|
|
|
685
869
|
// A held run, as blocks. A region measured its headers while buffering them,
|
|
686
870
|
// at this same width, so a replayed run reuses those rather than wrapping the
|
|
687
|
-
// same text a second time (ADR
|
|
871
|
+
// same text a second time (ADR 0027).
|
|
688
872
|
/** @type {(canvas: Canvas, held: any[], avail: number) => Block[]} */
|
|
689
873
|
let blocksOf = (canvas, held, avail) =>
|
|
690
874
|
held.map((entry) => entry.block || blockOf(canvas, entry.event, avail));
|
|
@@ -761,7 +945,7 @@ let keepWith = (block) =>
|
|
|
761
945
|
/** @type {(state: Flow, event: any, measured?: Block | null) => void} */
|
|
762
946
|
let placeItem = (state, event, measured = null) => {
|
|
763
947
|
let block = measured || keepBlock(state, event, widthOf(state));
|
|
764
|
-
if (block && holding(state)) flush(state, keepWith(block));
|
|
948
|
+
if (block && holding(state)) flush(state, keepWith(block) + flowPad(state, event));
|
|
765
949
|
item(state, event, block);
|
|
766
950
|
};
|
|
767
951
|
|
|
@@ -790,7 +974,8 @@ let holding = (state) => state.open.some((group) => group.held.length) || state.
|
|
|
790
974
|
let openGroup = (state, event) => {
|
|
791
975
|
breakFor(state, event);
|
|
792
976
|
if (event.columns) state.pending = { count: event.columns, owner: event.depth };
|
|
793
|
-
state.gap = Math.max(state.gap, instanceGap(state.canvas));
|
|
977
|
+
state.gap = state.skipGap ? 0 : Math.max(state.gap, instanceGap(state.canvas));
|
|
978
|
+
state.skipGap = false;
|
|
794
979
|
state.open.push({ held: [], blocks: [] });
|
|
795
980
|
state.marks.push(markFor(event));
|
|
796
981
|
};
|
|
@@ -805,7 +990,9 @@ let breakFor = (state, event) => {
|
|
|
805
990
|
|
|
806
991
|
/** @type {(event: any) => any} */
|
|
807
992
|
let markFor = (event) => ({
|
|
808
|
-
|
|
993
|
+
// display(), not String(): a Date group key must title its outline bookmark
|
|
994
|
+
// with the same ISO 8601 UTC text its cells render, on every machine.
|
|
995
|
+
title: event.name + ": " + display(event.key),
|
|
809
996
|
titled: false,
|
|
810
997
|
depth: event.depth,
|
|
811
998
|
page: -1,
|
|
@@ -848,16 +1035,22 @@ let headOf = (canvas, span) => span - instanceGap(canvas);
|
|
|
848
1035
|
// gives flow spacing no meaning inside a table row, in either target.
|
|
849
1036
|
/** @type {(canvas: Canvas, cell: any, rowStyle: any) => any} */
|
|
850
1037
|
let cellOf = (canvas, cell, rowStyle) => {
|
|
851
|
-
let style = merge(rowStyle, cell.style);
|
|
1038
|
+
let style = merge(unbox(rowStyle), unbox(cell.style));
|
|
1039
|
+
let inset = insetOf(cell.style, CELL_PAD);
|
|
852
1040
|
let list = atoms(canvas, cell.tokens, style);
|
|
853
|
-
let natural = wrap(canvas, list, Infinity
|
|
1041
|
+
let natural = wrap(canvas, list, Infinity, sizeOf(style, canvas.base)).reduce(
|
|
1042
|
+
(widest, line) => Math.max(widest, line.w),
|
|
1043
|
+
0,
|
|
1044
|
+
);
|
|
854
1045
|
return {
|
|
855
1046
|
list,
|
|
856
|
-
natural: natural +
|
|
1047
|
+
natural: natural + inset.l + inset.r,
|
|
857
1048
|
align: style.align,
|
|
858
1049
|
bg: col(style.background),
|
|
859
1050
|
underline: !!style.underline,
|
|
860
1051
|
strikethrough: !!style.strikethrough,
|
|
1052
|
+
inset,
|
|
1053
|
+
style: cell.style,
|
|
861
1054
|
};
|
|
862
1055
|
};
|
|
863
1056
|
|
|
@@ -865,9 +1058,14 @@ let cellOf = (canvas, cell, rowStyle) => {
|
|
|
865
1058
|
let rowOf = (canvas, cells, widths) => {
|
|
866
1059
|
let h = rowHeight(canvas);
|
|
867
1060
|
let out = cells.map((cell, i) => {
|
|
868
|
-
let inner = Math.max(widths[i] -
|
|
869
|
-
|
|
870
|
-
let
|
|
1061
|
+
let inner = Math.max(widths[i] - cell.inset.l - cell.inset.r, 1);
|
|
1062
|
+
// A glyph or a hard break occupies; spaces alone are an empty cell.
|
|
1063
|
+
let lines = cell.list.some(
|
|
1064
|
+
(/** @type {{ hard: boolean, space: boolean }} */ atom) => atom.hard || !atom.space,
|
|
1065
|
+
)
|
|
1066
|
+
? dress(wrap(canvas, cell.list, inner, cell.list[0].size), cell)
|
|
1067
|
+
: [];
|
|
1068
|
+
let cellHeight = heightOf(lines) + cell.inset.t + cell.inset.b;
|
|
871
1069
|
if (cellHeight > h) h = cellHeight;
|
|
872
1070
|
return { ...cell, lines };
|
|
873
1071
|
});
|
|
@@ -881,8 +1079,9 @@ let fillCell = (canvas, cell, bg, x, y, w, h) => {
|
|
|
881
1079
|
|
|
882
1080
|
/** @type {(canvas: Canvas, cell: any, lines: any[], x: number, y: number, w: number) => void} */
|
|
883
1081
|
let writeCell = (canvas, cell, lines, x, y, w) => {
|
|
1082
|
+
let inset = cell.inset;
|
|
884
1083
|
for (let line of lines) {
|
|
885
|
-
canvas.drawLine(line, x +
|
|
1084
|
+
canvas.drawLine(line, x + inset.l, y, w - inset.l - inset.r, cell.align);
|
|
886
1085
|
y -= line.h;
|
|
887
1086
|
}
|
|
888
1087
|
};
|
|
@@ -896,9 +1095,10 @@ let writeCell = (canvas, cell, lines, x, y, w) => {
|
|
|
896
1095
|
*/
|
|
897
1096
|
let paintCells = (canvas, cells, lines, h, padTop, xOffsets, widths, bg) => {
|
|
898
1097
|
if (bg) canvas.rect(bg, xOffsets[0], canvas.y - h, sum(widths), h);
|
|
899
|
-
let y0 = canvas.y - (padTop ? PADY : 0);
|
|
900
1098
|
for (let [i, cell] of cells.entries()) {
|
|
901
1099
|
fillCell(canvas, cell, bg, xOffsets[i], canvas.y - h, widths[i], h);
|
|
1100
|
+
paintBox(canvas, xOffsets[i], canvas.y, widths[i], h, cell.style, null);
|
|
1101
|
+
let y0 = canvas.y - (padTop ? cell.inset.t : 0);
|
|
902
1102
|
writeCell(canvas, cell, lines[i], xOffsets[i], y0, widths[i]);
|
|
903
1103
|
}
|
|
904
1104
|
canvas.y -= h;
|
|
@@ -906,10 +1106,11 @@ let paintCells = (canvas, cells, lines, h, padTop, xOffsets, widths, bg) => {
|
|
|
906
1106
|
};
|
|
907
1107
|
|
|
908
1108
|
/**
|
|
909
|
-
* @type {(canvas: Canvas, row:
|
|
1109
|
+
* @type {(canvas: Canvas, row: TableRow, xOffsets: number[],
|
|
910
1110
|
* widths: number[], bg: string | null) => void}
|
|
911
1111
|
*/
|
|
912
|
-
let drawRow = (canvas, row, xOffsets, widths, bg) =>
|
|
1112
|
+
let drawRow = (canvas, row, xOffsets, widths, bg) => {
|
|
1113
|
+
let yTop = canvas.y;
|
|
913
1114
|
paintCells(
|
|
914
1115
|
canvas,
|
|
915
1116
|
row.cells,
|
|
@@ -920,14 +1121,17 @@ let drawRow = (canvas, row, xOffsets, widths, bg) =>
|
|
|
920
1121
|
widths,
|
|
921
1122
|
bg,
|
|
922
1123
|
);
|
|
1124
|
+
paintBox(canvas, xOffsets[0], yTop, sum(widths), row.h, row.style, null);
|
|
1125
|
+
};
|
|
923
1126
|
|
|
924
|
-
// The widest of each column's header, rows and
|
|
925
|
-
/** @type {(cols: any[], header: any[], rows: any[],
|
|
926
|
-
let naturalWidths = (cols, header, rows,
|
|
1127
|
+
// The widest of each column's header, rows and totals, padding included.
|
|
1128
|
+
/** @type {(cols: any[], header: any[], rows: any[], totals: { cells: any[] }[]) => number[]} */
|
|
1129
|
+
let naturalWidths = (cols, header, rows, totals) =>
|
|
927
1130
|
cols.map((_, i) => {
|
|
928
1131
|
let widest = header[i].natural;
|
|
929
1132
|
for (let row of rows) widest = Math.max(widest, row.cells[i].natural);
|
|
930
|
-
|
|
1133
|
+
for (let row of totals) widest = Math.max(widest, row.cells[i].natural);
|
|
1134
|
+
return widest;
|
|
931
1135
|
});
|
|
932
1136
|
|
|
933
1137
|
/** @type {(values: number[]) => number} */
|
|
@@ -945,7 +1149,7 @@ let sum = (values) => values.reduce((total, value) => total + value, 0);
|
|
|
945
1149
|
// same fail-loud posture as the measuring canvas's `newPage`.
|
|
946
1150
|
/** @type {(cols: any[], natural: number[], avail: number) => number[]} */
|
|
947
1151
|
let columnWidths = (cols, natural, avail) => {
|
|
948
|
-
let fixed = cols.map((
|
|
1152
|
+
let fixed = cols.map((column) => Number.isFinite(column.width));
|
|
949
1153
|
if (!fixed.some(Boolean)) {
|
|
950
1154
|
// Natural widths always carry the cell padding, so the sum is never zero.
|
|
951
1155
|
let wanted = sum(natural);
|
|
@@ -963,14 +1167,10 @@ let columnWidths = (cols, natural, avail) => {
|
|
|
963
1167
|
return widths;
|
|
964
1168
|
};
|
|
965
1169
|
|
|
966
|
-
// The rule under a table's header row and over its total: the only rule this
|
|
967
|
-
// target draws, always the full content width at the cursor.
|
|
968
|
-
/** @type {(canvas: Canvas, x: number, avail: number) => void} */
|
|
969
|
-
let underline = (canvas, x, avail) => canvas.rule(x, x + avail, canvas.y);
|
|
970
|
-
|
|
971
1170
|
// One table being emitted: what `carryOver`, `sliced` and `put` all need.
|
|
972
1171
|
/**
|
|
973
|
-
* @typedef {{
|
|
1172
|
+
* @typedef {{ cells: any[], h: number, style?: any }} TableRow
|
|
1173
|
+
* @typedef {{ state: Flow, head: TableRow,
|
|
974
1174
|
* xOffsets: number[], widths: number[], x: number, avail: number }} Grid
|
|
975
1175
|
*/
|
|
976
1176
|
|
|
@@ -982,12 +1182,14 @@ let underline = (canvas, x, avail) => canvas.rule(x, x + avail, canvas.y);
|
|
|
982
1182
|
// The headings replay at a strip head as well as a page head, unlike a group
|
|
983
1183
|
// header (SCHEMA.md): a column of figures under nothing is unreadable, where a
|
|
984
1184
|
// group header is merely absent.
|
|
1185
|
+
/** @type {(row: { style?: any }) => any} */
|
|
1186
|
+
let rowFill = (row) => col((row.style || {}).background);
|
|
1187
|
+
|
|
985
1188
|
/** @type {(grid: Grid) => void} */
|
|
986
1189
|
let carryOver = (grid) => {
|
|
987
1190
|
advance(grid.state);
|
|
988
1191
|
rebase(grid);
|
|
989
|
-
drawRow(grid.state.canvas, grid.head, grid.xOffsets, grid.widths,
|
|
990
|
-
underline(grid.state.canvas, grid.x, grid.avail);
|
|
1192
|
+
drawRow(grid.state.canvas, grid.head, grid.xOffsets, grid.widths, rowFill(grid.head));
|
|
991
1193
|
};
|
|
992
1194
|
|
|
993
1195
|
// Move the grid to the column the cursor is now in. A table's widths are its
|
|
@@ -1007,7 +1209,7 @@ let firstSlice = (canvas, row) =>
|
|
|
1007
1209
|
rowHeight(canvas),
|
|
1008
1210
|
...row.cells
|
|
1009
1211
|
.filter((/** @type {any} */ cell) => cell.lines.length)
|
|
1010
|
-
.map((/** @type {any} */ cell) =>
|
|
1212
|
+
.map((/** @type {any} */ cell) => cell.inset.t + cell.lines[0].h),
|
|
1011
1213
|
);
|
|
1012
1214
|
|
|
1013
1215
|
/** @type {(cell: any, next: number[], i: number, used: number, cap: number) => any[]} */
|
|
@@ -1027,10 +1229,17 @@ let takeLines = (cell, next, i, used, cap) => {
|
|
|
1027
1229
|
let stillMore = (row, next) =>
|
|
1028
1230
|
row.cells.some((/** @type {any} */ cell, /** @type {number} */ i) => next[i] < cell.lines.length);
|
|
1029
1231
|
|
|
1030
|
-
/** @type {(
|
|
1031
|
-
let
|
|
1032
|
-
|
|
1033
|
-
|
|
1232
|
+
/** @type {(cell: any, first: boolean, more: boolean) => number} */
|
|
1233
|
+
let vPad = (cell, first, more) => (first ? cell.inset.t : 0) + (more ? 0 : cell.inset.b);
|
|
1234
|
+
|
|
1235
|
+
/** @type {(taken: any[][], cells: any[], first: boolean, more: boolean) => number} */
|
|
1236
|
+
let sliceH = (taken, cells, first, more) => {
|
|
1237
|
+
let h = 0;
|
|
1238
|
+
for (let [i, lines] of taken.entries()) {
|
|
1239
|
+
let cellH = vPad(cells[i], first, more) + heightOf(lines);
|
|
1240
|
+
if (cellH > h) h = cellH;
|
|
1241
|
+
}
|
|
1242
|
+
return h;
|
|
1034
1243
|
};
|
|
1035
1244
|
|
|
1036
1245
|
// A row no page can hold degrades to plain flow: each page takes what fits of
|
|
@@ -1042,13 +1251,11 @@ let sliced = (grid, row, bg) => {
|
|
|
1042
1251
|
let first = true;
|
|
1043
1252
|
for (;;) {
|
|
1044
1253
|
let cap = canvas.y - canvas.bottom;
|
|
1045
|
-
let pad = first ? PADY : 0;
|
|
1046
1254
|
let taken = row.cells.map((/** @type {any} */ cell, /** @type {number} */ i) =>
|
|
1047
|
-
takeLines(cell, next, i,
|
|
1255
|
+
takeLines(cell, next, i, first ? cell.inset.t : 0, cap),
|
|
1048
1256
|
);
|
|
1049
1257
|
let more = stillMore(row, next);
|
|
1050
|
-
|
|
1051
|
-
let h = sliceH(taken, pad, more);
|
|
1258
|
+
let h = sliceH(taken, row.cells, first, more);
|
|
1052
1259
|
paintCells(canvas, row.cells, taken, h, first, grid.xOffsets, grid.widths, bg);
|
|
1053
1260
|
if (!more) return;
|
|
1054
1261
|
carryOver(grid);
|
|
@@ -1074,21 +1281,13 @@ let shouldBreak = (state, row, keep, cap) => {
|
|
|
1074
1281
|
return canvas.y - row.h - keep < floor && worthBreak(canvas, row, cap, floor) && !freshOf(state);
|
|
1075
1282
|
};
|
|
1076
1283
|
|
|
1077
|
-
/** @type {(grid: Grid, topRule: boolean, broke: boolean) => void} */
|
|
1078
|
-
let openRow = (grid, topRule, broke) => {
|
|
1079
|
-
if (broke) carryOver(grid);
|
|
1080
|
-
if (topRule && !broke) underline(grid.state.canvas, grid.x, grid.avail);
|
|
1081
|
-
};
|
|
1082
|
-
|
|
1083
1284
|
// Place one row, breaking first when that helps. `keep` is a companion row it
|
|
1084
1285
|
// must not be parted from — the total riding with the last data row.
|
|
1085
|
-
// `topRule` is the total's opening rule, which after a break would double the
|
|
1086
|
-
// replayed header's.
|
|
1087
1286
|
/**
|
|
1088
|
-
* @type {(grid: Grid, row:
|
|
1089
|
-
* keep?: number
|
|
1287
|
+
* @type {(grid: Grid, row: TableRow, bg: string | null,
|
|
1288
|
+
* keep?: number) => void}
|
|
1090
1289
|
*/
|
|
1091
|
-
let put = (grid, row, bg, keep = 0
|
|
1290
|
+
let put = (grid, row, bg, keep = 0) => {
|
|
1092
1291
|
let canvas = grid.state.canvas;
|
|
1093
1292
|
// The tallest row a fresh column holds, after the replayed group headers and
|
|
1094
1293
|
// the replayed column headings. A companion over that can never share a
|
|
@@ -1100,33 +1299,36 @@ let put = (grid, row, bg, keep = 0, topRule = false) => {
|
|
|
1100
1299
|
// and leaves a headings-only column. Clamped to `cap`: a first slice no fresh
|
|
1101
1300
|
// column could fit either is no reason to go looking for one.
|
|
1102
1301
|
let broke = shouldBreak(grid.state, row, keep, cap);
|
|
1103
|
-
|
|
1302
|
+
if (broke) carryOver(grid);
|
|
1104
1303
|
if (canvas.y - row.h < canvas.bottom) return sliced(grid, row, bg);
|
|
1105
1304
|
drawRow(canvas, row, grid.xOffsets, grid.widths, bg);
|
|
1106
1305
|
};
|
|
1107
1306
|
|
|
1108
|
-
/** @type {(canvas: Canvas, row: any) => { cells: any[],
|
|
1307
|
+
/** @type {(canvas: Canvas, row: any) => { cells: any[], style: any }} */
|
|
1109
1308
|
let bodyRow = (canvas, row) => ({
|
|
1110
1309
|
cells: row.cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, row.style)),
|
|
1111
|
-
|
|
1310
|
+
style: row.style,
|
|
1112
1311
|
});
|
|
1113
1312
|
|
|
1114
1313
|
/** @type {(canvas: Canvas, buffered: any) => any} */
|
|
1115
|
-
let
|
|
1116
|
-
buffered.
|
|
1314
|
+
let totalRowsOf = (canvas, buffered) =>
|
|
1315
|
+
buffered.totals.map((/** @type {any} */ row) => ({
|
|
1316
|
+
cells: row.cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, row.style)),
|
|
1317
|
+
style: row.style,
|
|
1318
|
+
}));
|
|
1117
1319
|
|
|
1118
|
-
/** @type {(laid: any[],
|
|
1119
|
-
let firstH = (laid,
|
|
1320
|
+
/** @type {(laid: any[], totalRows: any[]) => number} */
|
|
1321
|
+
let firstH = (laid, totalRows) => (laid.length ? laid[0].h : totalRows.length ? totalRows[0].h : 0);
|
|
1120
1322
|
|
|
1121
|
-
/** @type {(
|
|
1122
|
-
let
|
|
1323
|
+
/** @type {(totalRows: any[]) => number} */
|
|
1324
|
+
let totalsH = (totalRows) => totalRows.reduce((h, row) => h + row.h, 0);
|
|
1123
1325
|
|
|
1124
|
-
/** @type {(grid: Grid, laid: any[],
|
|
1125
|
-
let emitRows = (grid, laid,
|
|
1326
|
+
/** @type {(grid: Grid, laid: any[], totalRows: any[]) => void} */
|
|
1327
|
+
let emitRows = (grid, laid, totalRows) => {
|
|
1126
1328
|
let last = laid.length - 1;
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1329
|
+
let keep = totalsH(totalRows);
|
|
1330
|
+
for (let [i, row] of laid.entries()) put(grid, row, rowFill(row), i === last ? keep : 0);
|
|
1331
|
+
for (let row of totalRows) put(grid, row, rowFill(row), 0);
|
|
1130
1332
|
};
|
|
1131
1333
|
|
|
1132
1334
|
// Lay out a buffered table: measure every cell, allocate the columns, then
|
|
@@ -1137,14 +1339,19 @@ let emitRows = (grid, laid, totalRow) => {
|
|
|
1137
1339
|
// events as they arrive, and the region buffer, reading them back off what it
|
|
1138
1340
|
// held — so a new table event kind cannot reach one and miss the other.
|
|
1139
1341
|
/** @type {(events: any[]) => any} */
|
|
1140
|
-
let tableOf = (events) =>
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1342
|
+
let tableOf = (events) => {
|
|
1343
|
+
let opening = events[0];
|
|
1344
|
+
let totals = events.filter((event) => event.type === "total-row");
|
|
1345
|
+
return {
|
|
1346
|
+
columns: opening.columns,
|
|
1347
|
+
headerStyle: opening.style,
|
|
1348
|
+
rows: events.filter((event) => event.type === "row"),
|
|
1349
|
+
totals: totals.map((row) => ({ cells: row.cells, style: row.style })),
|
|
1350
|
+
};
|
|
1351
|
+
};
|
|
1145
1352
|
|
|
1146
1353
|
/**
|
|
1147
|
-
* @typedef {{ widths: number[], head: any, laid: any[],
|
|
1354
|
+
* @typedef {{ widths: number[], head: any, laid: any[], totalRows: any[] }} Measured
|
|
1148
1355
|
*/
|
|
1149
1356
|
// Measure a buffered table at a given width: the column widths every cell has
|
|
1150
1357
|
// a say in, then each row laid out against them. Everything here is
|
|
@@ -1154,18 +1361,23 @@ let tableOf = (events) => ({
|
|
|
1154
1361
|
/** @type {(canvas: Canvas, buffered: any, avail: number) => Measured} */
|
|
1155
1362
|
let measureTable = (canvas, buffered, avail) => {
|
|
1156
1363
|
let cols = /** @type {any[]} */ (buffered.columns);
|
|
1157
|
-
let header = cols.map((/** @type {any} */ col) =>
|
|
1364
|
+
let header = cols.map((/** @type {any} */ col) =>
|
|
1365
|
+
cellOf(canvas, col.header, buffered.headerStyle),
|
|
1366
|
+
);
|
|
1158
1367
|
let rows = buffered.rows.map((/** @type {any} */ row) => bodyRow(canvas, row));
|
|
1159
|
-
let
|
|
1160
|
-
let widths = columnWidths(cols, naturalWidths(cols, header, rows,
|
|
1368
|
+
let totals = totalRowsOf(canvas, buffered);
|
|
1369
|
+
let widths = columnWidths(cols, naturalWidths(cols, header, rows, totals), avail);
|
|
1161
1370
|
return {
|
|
1162
1371
|
widths,
|
|
1163
|
-
head: rowOf(canvas, header, widths),
|
|
1372
|
+
head: { ...rowOf(canvas, header, widths), style: buffered.headerStyle },
|
|
1164
1373
|
laid: rows.map((/** @type {any} */ row) => ({
|
|
1165
|
-
|
|
1166
|
-
|
|
1374
|
+
...rowOf(canvas, row.cells, widths),
|
|
1375
|
+
style: row.style,
|
|
1376
|
+
})),
|
|
1377
|
+
totalRows: totals.map((/** @type {any} */ row) => ({
|
|
1378
|
+
...rowOf(canvas, row.cells, widths),
|
|
1379
|
+
style: row.style,
|
|
1167
1380
|
})),
|
|
1168
|
-
totalRow: total && rowOf(canvas, total, widths),
|
|
1169
1381
|
};
|
|
1170
1382
|
};
|
|
1171
1383
|
|
|
@@ -1180,7 +1392,7 @@ let table = (state, buffered) => {
|
|
|
1180
1392
|
// Measured already if a region buffered this table: the balance model needed
|
|
1181
1393
|
// its real row heights, at this same width, and that is the same
|
|
1182
1394
|
// measurement — so the replay draws it without wrapping every cell again.
|
|
1183
|
-
let { widths, head, laid,
|
|
1395
|
+
let { widths, head, laid, totalRows } =
|
|
1184
1396
|
state.measured.get(buffered.opening) || measureTable(canvas, buffered, avail);
|
|
1185
1397
|
let xOffsets = widths.map((_, i) => x + sum(widths.slice(0, i)));
|
|
1186
1398
|
|
|
@@ -1188,16 +1400,15 @@ let table = (state, buffered) => {
|
|
|
1188
1400
|
let grid = { state, head, xOffsets, widths, x, avail };
|
|
1189
1401
|
|
|
1190
1402
|
// A pending group header keeps the table's header row and its first row.
|
|
1191
|
-
flush(state, grid.head.h + firstH(laid,
|
|
1403
|
+
flush(state, grid.head.h + firstH(laid, totalRows));
|
|
1192
1404
|
if (canvas.y - grid.head.h < floorOf(state)) {
|
|
1193
1405
|
advance(state);
|
|
1194
1406
|
rebase(grid);
|
|
1195
1407
|
}
|
|
1196
|
-
drawRow(canvas, grid.head, grid.xOffsets, widths,
|
|
1197
|
-
|
|
1198
|
-
//
|
|
1199
|
-
|
|
1200
|
-
emitRows(grid, laid, totalRow);
|
|
1408
|
+
drawRow(canvas, grid.head, grid.xOffsets, widths, rowFill(grid.head));
|
|
1409
|
+
// The last data row keeps the whole emitted total block with it, so a
|
|
1410
|
+
// total is never stranded alone at a page top.
|
|
1411
|
+
emitRows(grid, laid, totalRows);
|
|
1201
1412
|
};
|
|
1202
1413
|
|
|
1203
1414
|
// --- page bands -------------------------------------------------------------
|
|
@@ -1211,7 +1422,14 @@ let band = (canvas, items, yTop) => {
|
|
|
1211
1422
|
fresh = canvas.fresh;
|
|
1212
1423
|
canvas.y = yTop;
|
|
1213
1424
|
canvas.fresh = false;
|
|
1214
|
-
|
|
1425
|
+
// Folded straight into the draw, the way the body walk folds straight into
|
|
1426
|
+
// `route`: the folder emits in order and nothing here needs the band whole.
|
|
1427
|
+
let fold = splitFolder((event) => {
|
|
1428
|
+
skipFlow({ canvas, region: null }, event, "spaceBefore");
|
|
1429
|
+
drawBlock(canvas, blockOf(canvas, event, avail), canvas.margin, avail);
|
|
1430
|
+
skipFlow({ canvas, region: null }, event, "spaceAfter");
|
|
1431
|
+
});
|
|
1432
|
+
for (let event of items) fold(event);
|
|
1215
1433
|
let h = yTop - canvas.y;
|
|
1216
1434
|
canvas.y = saved;
|
|
1217
1435
|
canvas.fresh = fresh;
|
|
@@ -1265,7 +1483,12 @@ let reserve = (geo, fonts, bands) => {
|
|
|
1265
1483
|
// want it: reservation to measure against, the draw pass to hang the header
|
|
1266
1484
|
// from.
|
|
1267
1485
|
/** @type {(canvas: Canvas) => Frame} */
|
|
1268
|
-
let pageBox = (canvas) =>
|
|
1486
|
+
let pageBox = (canvas) =>
|
|
1487
|
+
Object.assign(frame(canvas.width, canvas.height, canvas.margin, canvas.base, canvas.family), {
|
|
1488
|
+
locale: canvas.locale,
|
|
1489
|
+
currency: canvas.currency,
|
|
1490
|
+
timeZone: canvas.timeZone,
|
|
1491
|
+
});
|
|
1269
1492
|
|
|
1270
1493
|
// One finished page's furniture, drawn in the strips `reserve` left for it:
|
|
1271
1494
|
// the header hanging from the top margin, the footer resting on the bottom
|
|
@@ -1330,11 +1553,18 @@ let flow = (canvas) => {
|
|
|
1330
1553
|
// the replay draws that table without wrapping every cell a second time.
|
|
1331
1554
|
// Per flow, because the keys are one render's events.
|
|
1332
1555
|
measured: new WeakMap(),
|
|
1556
|
+
pin: null,
|
|
1557
|
+
skipGap: false,
|
|
1558
|
+
inHeader: false,
|
|
1333
1559
|
// Assigned below, once `route` exists: a buffered region replays through
|
|
1334
1560
|
// it, and one entry point is what makes the replay take the path the
|
|
1335
1561
|
// content would have taken had it never been held.
|
|
1336
1562
|
route: () => {},
|
|
1337
1563
|
};
|
|
1564
|
+
// The body walk's own folder, emitting each finished event onward to `route`
|
|
1565
|
+
// below. Declared here and bound after `route` exists.
|
|
1566
|
+
/** @type {(event: any) => void} */
|
|
1567
|
+
let fold;
|
|
1338
1568
|
// A table is buffered whole before it is laid out: column widths come from
|
|
1339
1569
|
// every cell in it, so the last row has to be in hand first.
|
|
1340
1570
|
/** @type {any[]} */
|
|
@@ -1350,6 +1580,14 @@ let flow = (canvas) => {
|
|
|
1350
1580
|
// What each event does once it is placed for real. A buffered region replays
|
|
1351
1581
|
// through this same table, so held content lays out by exactly the path it
|
|
1352
1582
|
// would have taken had it never been held.
|
|
1583
|
+
// A block with no title of its own: held back under a group header, placed
|
|
1584
|
+
// anywhere else. Both the image and the split flow this way.
|
|
1585
|
+
/** @type {(event: any, block: Block | null) => void} */
|
|
1586
|
+
let heldOrPlaced = (event, block) =>
|
|
1587
|
+
event.role === "group-header"
|
|
1588
|
+
? holdHeader(state, event, null, block)
|
|
1589
|
+
: placeItem(state, event, block);
|
|
1590
|
+
|
|
1353
1591
|
/** @type {Record<string, (event: any, block: Block | null) => void>} */
|
|
1354
1592
|
let placed = {
|
|
1355
1593
|
// A header is held rather than placed, and holding is bookkeeping the
|
|
@@ -1362,10 +1600,11 @@ let flow = (canvas) => {
|
|
|
1362
1600
|
// An image flows exactly as an item does, held back in a group header and
|
|
1363
1601
|
// placed anywhere else -- the block it becomes carries a height like any
|
|
1364
1602
|
// other. It wears no role default: those describe text.
|
|
1365
|
-
image:
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1603
|
+
image: heldOrPlaced,
|
|
1604
|
+
// A split flows exactly as an image does, for the same reason: one block,
|
|
1605
|
+
// held back in a group header and placed anywhere else. It wears no role
|
|
1606
|
+
// default of its own -- its slots carry theirs.
|
|
1607
|
+
split: heldOrPlaced,
|
|
1369
1608
|
"group-start": (event) => openGroup(state, event),
|
|
1370
1609
|
"group-end": () => {
|
|
1371
1610
|
flush(state, 0);
|
|
@@ -1393,11 +1632,28 @@ let flow = (canvas) => {
|
|
|
1393
1632
|
/** @type {(event: any) => boolean} */
|
|
1394
1633
|
let ownFooter = (event) => event.role === "group-footer" && owner() === state.open.length - 1;
|
|
1395
1634
|
|
|
1635
|
+
/** @type {(event: any) => boolean} */
|
|
1636
|
+
let stillHeader = (event) =>
|
|
1637
|
+
event.role === "report-header" || (event.type === "split-end" && state.inHeader);
|
|
1638
|
+
/** @type {(event: any) => void} */
|
|
1639
|
+
let snapPin = (event) => {
|
|
1640
|
+
if (state.pin == null) return;
|
|
1641
|
+
if (stillHeader(event)) {
|
|
1642
|
+
state.inHeader = true;
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
if (state.canvas.y < state.pin) throw Error("header: content taller than height");
|
|
1646
|
+
state.canvas.y = state.pin;
|
|
1647
|
+
state.pin = null;
|
|
1648
|
+
state.inHeader = false;
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1396
1651
|
// Route one event: the declaring node's own bands stay full-width, region
|
|
1397
1652
|
// content opens the strips owed to it, and anything arriving while a region
|
|
1398
1653
|
// is still deciding is buffered rather than placed.
|
|
1399
1654
|
/** @type {(event: any, block?: Block | null) => void} */
|
|
1400
1655
|
let route = (event, block = null) => {
|
|
1656
|
+
snapPin(event);
|
|
1401
1657
|
if (fullBand(event)) fullWidth(event, block);
|
|
1402
1658
|
else if (deciding()) buffer(state, event, block);
|
|
1403
1659
|
else if (opens(event)) begin(event);
|
|
@@ -1405,6 +1661,7 @@ let flow = (canvas) => {
|
|
|
1405
1661
|
};
|
|
1406
1662
|
|
|
1407
1663
|
state.route = route;
|
|
1664
|
+
fold = splitFolder(route);
|
|
1408
1665
|
|
|
1409
1666
|
// Everything drawn across the page rather than down a strip: the declaring
|
|
1410
1667
|
// node's own bands, its footer among them.
|
|
@@ -1458,12 +1715,51 @@ let flow = (canvas) => {
|
|
|
1458
1715
|
buffer(state, event, block);
|
|
1459
1716
|
};
|
|
1460
1717
|
|
|
1718
|
+
// The report default onto the canvas, before anything is measured. It is
|
|
1719
|
+
// narrowed to `family` and `size`, and each replaces this target's own
|
|
1720
|
+
// baseline outright: row heights and band gaps scale with the document's
|
|
1721
|
+
// type rather than staying at a size nothing is set in, and text declaring
|
|
1722
|
+
// no family is set in the document's. Lifting the pair here is the whole of
|
|
1723
|
+
// this target's reading of docs/adr/0033 -- the default reaches a node as a
|
|
1724
|
+
// fallback the canvas carries, never as a layer merged into its style, so a
|
|
1725
|
+
// document-wide fact costs no allocation however many cells a report has.
|
|
1726
|
+
//
|
|
1727
|
+
// `sizeOf` and `familyName` are this target's one reading each of what a
|
|
1728
|
+
// declared size and family amount to, so they read the default here too --
|
|
1729
|
+
// both reach this unchecked from a computed style, and two spellings of that
|
|
1730
|
+
// leniency would drift. Each falls back to what the canvas already carries,
|
|
1731
|
+
// so a default declaring one of the pair leaves the other alone, and one
|
|
1732
|
+
// whose value is unusable leaves this target's own baseline standing.
|
|
1733
|
+
/** @type {(style: any) => void} */
|
|
1734
|
+
let adoptDefault = (style) => {
|
|
1735
|
+
if (!style) return;
|
|
1736
|
+
canvas.base = sizeOf(style, canvas.base);
|
|
1737
|
+
canvas.family = familyName(style) || canvas.family;
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
/** @type {(event: any) => void} */
|
|
1741
|
+
let pinHeader = (event) => {
|
|
1742
|
+
if (event.headerHeight == null) return;
|
|
1743
|
+
let pin = canvas.y - event.headerHeight;
|
|
1744
|
+
if (pin < canvas.bottom) throw Error("header: height exceeds the first page's body");
|
|
1745
|
+
state.pin = pin;
|
|
1746
|
+
state.skipGap = true;
|
|
1747
|
+
state.inHeader = true;
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1461
1750
|
return {
|
|
1462
1751
|
handlers: {
|
|
1463
1752
|
"report-start": (event) => {
|
|
1464
1753
|
opening = event;
|
|
1754
|
+
canvas.locale = event.locale;
|
|
1755
|
+
canvas.currency = event.currency;
|
|
1756
|
+
canvas.timeZone = event.timeZone;
|
|
1757
|
+
adoptDefault(event.style);
|
|
1465
1758
|
if (event.columns) state.pending = { count: event.columns, owner: -1 };
|
|
1466
|
-
if (!event.page)
|
|
1759
|
+
if (!event.page) {
|
|
1760
|
+
pinHeader(event);
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1467
1763
|
// A frame of its own, never the canvas itself: `measuring` builds
|
|
1468
1764
|
// from whatever it is handed, so handing it this canvas would spread
|
|
1469
1765
|
// the page passes onto a measurement — and reaching a document is the
|
|
@@ -1471,22 +1767,31 @@ let flow = (canvas) => {
|
|
|
1471
1767
|
// a property of the page box in any case, not of a content box
|
|
1472
1768
|
// something may already have narrowed.
|
|
1473
1769
|
adopt(canvas, reserve(pageBox(canvas), canvas.fonts, event.page));
|
|
1770
|
+
pinHeader(event);
|
|
1474
1771
|
},
|
|
1475
1772
|
// Everything else routes: `placed` above says what each event does, and
|
|
1476
|
-
// one entry point is what lets a buffered region replay through it.
|
|
1477
|
-
//
|
|
1773
|
+
// one entry point is what lets a buffered region replay through it. The
|
|
1774
|
+
// three the folder owns are left out — `split` is not a stream event at
|
|
1775
|
+
// all (the folder mints it) and item and image reach `route` through it.
|
|
1478
1776
|
...Object.fromEntries(
|
|
1479
|
-
Object.keys(placed)
|
|
1480
|
-
type
|
|
1481
|
-
type
|
|
1482
|
-
]),
|
|
1777
|
+
Object.keys(placed)
|
|
1778
|
+
.filter((type) => !BLOCKS.has(type))
|
|
1779
|
+
.map((type) => [type, (/** @type {any} */ event) => route(event)]),
|
|
1483
1780
|
),
|
|
1781
|
+
// Item, image and both bracket events all go through the folder, which
|
|
1782
|
+
// routes what is not inside a split straight onward. The item is the one
|
|
1783
|
+
// that wears a band-role default on the way in.
|
|
1784
|
+
"split-start": (event) => fold(event),
|
|
1785
|
+
"split-end": (event) => fold(event),
|
|
1786
|
+
item: (event) => fold(roled(event)),
|
|
1787
|
+
image: (event) => fold(event),
|
|
1484
1788
|
"group-end": (event) => {
|
|
1485
1789
|
endOwned(event.depth);
|
|
1486
1790
|
route(event);
|
|
1487
1791
|
},
|
|
1488
1792
|
},
|
|
1489
1793
|
finish: () => {
|
|
1794
|
+
snapPin({ type: "report-end" });
|
|
1490
1795
|
// A root region reaches here undrained when a report declares `columns`
|
|
1491
1796
|
// and no full-width footer followed its body.
|
|
1492
1797
|
closeRegion(state);
|