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