@quario/layout 0.2.0 → 0.4.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 +70 -0
- package/lib/canvas.js +63 -115
- package/lib/fonts.js +19 -9
- package/lib/image.js +1 -1
- package/lib/index.d.ts +20 -2
- package/lib/index.js +19 -12
- package/lib/layout.js +321 -369
- package/lib/measure.js +263 -0
- package/lib/page.js +40 -2
- package/lib/paint.js +30 -7
- package/lib/settings.js +88 -0
- package/lib/style.js +29 -13
- package/lib/text.js +90 -51
- package/package.json +3 -3
package/lib/layout.js
CHANGED
|
@@ -21,39 +21,64 @@
|
|
|
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
|
-
|
|
26
|
+
/** `Measured` is the measured table's own type, and the flow passes one
|
|
27
|
+
* through: the region buffer measures early and the replay hands it back.
|
|
28
|
+
* @import { Measured, TableRow } from './measure.js' */
|
|
29
|
+
|
|
30
|
+
/** One table being emitted: what `carryOver`, `sliced` and `put` all need. It
|
|
31
|
+
* holds the flow, so it is the band flow's own — the measured half never sees
|
|
32
|
+
* a cursor.
|
|
33
|
+
* @typedef {{ state: Flow, head: TableRow,
|
|
34
|
+
* xOffsets: number[], widths: number[], x: number, avail: number }} Grid */
|
|
35
|
+
import { gridOf, measureTable, sum, tableOf } from "./measure.js";
|
|
36
|
+
import { adopt, measuring } from "./canvas.js";
|
|
37
|
+
import { frame } from "./page.js";
|
|
27
38
|
import { intrinsic } from "./image.js";
|
|
28
39
|
import { CELL_PAD, NO_PAD, WHOLE, insetOf, isWhole, paintBox, sliceInset, unbox } from "./box.js";
|
|
29
|
-
import {
|
|
30
|
-
|
|
40
|
+
import {
|
|
41
|
+
BAND,
|
|
42
|
+
col,
|
|
43
|
+
GUTTER,
|
|
44
|
+
instanceGap,
|
|
45
|
+
merge,
|
|
46
|
+
roled,
|
|
47
|
+
rowHeight,
|
|
48
|
+
shift,
|
|
49
|
+
sizeOf,
|
|
50
|
+
vshift,
|
|
51
|
+
} from "./style.js";
|
|
52
|
+
import { atoms, heightOf, wrap } from "./text.js";
|
|
31
53
|
|
|
32
54
|
/** @typedef {import('./balance.js').Unit} Unit */
|
|
33
55
|
/** @typedef {import('./canvas.js').Canvas} Canvas */
|
|
34
|
-
/** @typedef {import('./
|
|
35
|
-
/** @typedef {import('./
|
|
56
|
+
/** @typedef {import('./page.js').Frame} Frame */
|
|
57
|
+
/** @typedef {import('./settings.js').Settings} Settings */
|
|
36
58
|
/** @typedef {import('./text.js').Line} Line */
|
|
37
59
|
|
|
38
60
|
// `open` is a stack, one entry per group instance still open: the header items
|
|
39
61
|
// it holds back until content arrives, and — once those are drawn — the blocks
|
|
40
|
-
// a page turn replays them from.
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
62
|
+
// a page turn replays them from. Each entry also carries the `gap` and `skip`
|
|
63
|
+
// that stood before it opened, which is what a hollow instance puts back.
|
|
64
|
+
// `gap` is the space owed before the next group instance, `marks` the outline
|
|
65
|
+
// entries, which `anchor` positions from the cursor, and `starts` the page
|
|
66
|
+
// indexes where a `reset: "page"` sequence begins (the document itself is the
|
|
67
|
+
// sequence that starts at page 0).
|
|
44
68
|
/**
|
|
45
|
-
* @typedef {{
|
|
69
|
+
* @typedef {{ pending: any[], blocks: Block[], gap: number, skip: boolean, titled: boolean }} Group
|
|
46
70
|
*/
|
|
47
71
|
/**
|
|
48
72
|
* @typedef {{ canvas: Canvas, open: Group[], gap: number, marks: any[],
|
|
49
73
|
* starts: number[], region: Region | null, pending: Pending | null,
|
|
50
|
-
* pin: number | null, skipGap: boolean, inHeader: boolean
|
|
51
|
-
* route: (event: any, payload?: Block | Measured | null) => void }} Flow
|
|
74
|
+
* pin: number | null, skipGap: boolean, inHeader: boolean }} Flow
|
|
52
75
|
*/
|
|
53
76
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
77
|
+
* Where the cursor is and whether a strip is open — the whole of what
|
|
78
|
+
* page-vs-strip geometry depends on, and less than the whole flow. The five
|
|
79
|
+
* accessors under "the cursor" below take this, and so do the flow-spacing
|
|
80
|
+
* helpers beside them, which is how page furniture drives spacing off a canvas
|
|
81
|
+
* and no region at all.
|
|
57
82
|
* @typedef {{ canvas: Canvas, region: Region | null }} Cursor
|
|
58
83
|
*/
|
|
59
84
|
|
|
@@ -87,15 +112,36 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
|
|
|
87
112
|
* @typedef {{ block: Block, w: number }} SlotPart
|
|
88
113
|
*/
|
|
89
114
|
|
|
115
|
+
// A block no page break may pass through, and the negation of `slice`. The
|
|
116
|
+
// region balancer answers the same question its own way, keyed on the event
|
|
117
|
+
// rather than the block (`UNITS`), so a new kind of one belongs there too.
|
|
118
|
+
/** @type {(block: Block) => boolean} */
|
|
119
|
+
let unsliceable = (block) => Boolean(block.picture || block.parts);
|
|
120
|
+
|
|
90
121
|
// How `fit` sizes a picture against the width it has: `natural` is the
|
|
91
122
|
// image's own size in points, never wider than the content box; `width`
|
|
92
123
|
// scales to the content box either way. The ratio is kept in both, so a
|
|
93
124
|
// height is never anything but the width's consequence.
|
|
94
125
|
/** @type {(event: any, avail: number) => Picture} */
|
|
95
126
|
let pictureOf = (event, avail) => {
|
|
96
|
-
|
|
127
|
+
// Located here rather than in `image.js`, which is handed bytes and a
|
|
128
|
+
// format and has never heard of a schema: this is the innermost place that
|
|
129
|
+
// knows both what went wrong and which item asked for it. SCHEMA.md calls
|
|
130
|
+
// this failure a render error, and a render error names its node.
|
|
131
|
+
let sized;
|
|
132
|
+
try {
|
|
133
|
+
sized = intrinsic(event.bytes, event.format);
|
|
134
|
+
} catch (cause) {
|
|
135
|
+
throw imageError(event.path, /** @type {Error} */ (cause).message, cause);
|
|
136
|
+
}
|
|
137
|
+
let { w, h } = sized;
|
|
97
138
|
let width = event.fit === "width" ? avail : Math.min(w, avail);
|
|
98
|
-
return {
|
|
139
|
+
return {
|
|
140
|
+
bytes: event.bytes,
|
|
141
|
+
format: event.format,
|
|
142
|
+
w: width,
|
|
143
|
+
h: (h * width) / w,
|
|
144
|
+
};
|
|
99
145
|
};
|
|
100
146
|
|
|
101
147
|
// `under` is the enclosing style a slot's own layers over — a split's, when
|
|
@@ -124,7 +170,7 @@ let blockOf = (canvas, event, avail, under = null) => {
|
|
|
124
170
|
};
|
|
125
171
|
}
|
|
126
172
|
let size = sizeOf(text, canvas.settings.base);
|
|
127
|
-
let lines =
|
|
173
|
+
let lines = wrap(atoms(canvas.settings, event.tokens, text), inner, size);
|
|
128
174
|
return {
|
|
129
175
|
lines,
|
|
130
176
|
bg,
|
|
@@ -161,7 +207,15 @@ let drawPicture = (canvas, block, x, avail) => {
|
|
|
161
207
|
let at = x + shift(block.align, avail - outer);
|
|
162
208
|
paintBox(canvas, at, canvas.y, outer, block.h, block.style, block.bg);
|
|
163
209
|
canvas.box(block.path, at, canvas.y, outer, block.h);
|
|
164
|
-
canvas.picture(
|
|
210
|
+
canvas.picture(
|
|
211
|
+
block.path,
|
|
212
|
+
bytes,
|
|
213
|
+
format,
|
|
214
|
+
at + inset.l,
|
|
215
|
+
canvas.y - inset.t - block.drop - h,
|
|
216
|
+
w,
|
|
217
|
+
h,
|
|
218
|
+
);
|
|
165
219
|
canvas.y -= block.h;
|
|
166
220
|
canvas.fresh = false;
|
|
167
221
|
};
|
|
@@ -330,14 +384,24 @@ let drawBlock = (canvas, block, x, avail) =>
|
|
|
330
384
|
* @typedef {{ count: number, owner: number }} Pending
|
|
331
385
|
*/
|
|
332
386
|
|
|
387
|
+
// --- the cursor -------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
// The five below are the whole of how anything reaches page-vs-strip geometry,
|
|
390
|
+
// and they take a `Cursor` — a canvas and the region open on it, if one is —
|
|
391
|
+
// because the region is the only thing that changes any of their answers. The
|
|
392
|
+
// drawing primitives take a bare `Canvas` and read it flat, and that is not an
|
|
393
|
+
// exception to this: a primitive draws where it is told and does not choose a
|
|
394
|
+
// column, so it has nothing to ask. Page furniture reads flat for the same
|
|
395
|
+
// reason — `band` places at the page's own margin and never inside a region.
|
|
396
|
+
// The flow-spacing helpers below take a `Cursor` too: `keepLead` asks
|
|
397
|
+
// `freshOf`, so the spacing and the geometry are one seam, not two.
|
|
398
|
+
|
|
333
399
|
// Where the cursor's column starts and how wide it is: the strip's, inside a
|
|
334
|
-
// region, and the page's content box outside one.
|
|
335
|
-
|
|
336
|
-
// presenting the page, because `pageFrame` re-derives page furniture from it.
|
|
337
|
-
/** @type {(state: Flow) => number} */
|
|
400
|
+
// region, and the page's content box outside one.
|
|
401
|
+
/** @type {(state: Cursor) => number} */
|
|
338
402
|
let originOf = (state) =>
|
|
339
403
|
state.canvas.margin + (state.region ? state.region.index * (state.region.width + GUTTER) : 0);
|
|
340
|
-
/** @type {(state:
|
|
404
|
+
/** @type {(state: Cursor) => number} */
|
|
341
405
|
let widthOf = (state) => (state.region ? state.region.width : state.canvas.content);
|
|
342
406
|
// The bottom the cursor fills to. A balanced strip's floor sits above the page
|
|
343
407
|
// bottom and is a target rather than a bound: a fresh strip still takes at
|
|
@@ -345,7 +409,7 @@ let widthOf = (state) => (state.region ? state.region.width : state.canvas.conte
|
|
|
345
409
|
// overruns. The last strip answers to the page instead — a balanced share is
|
|
346
410
|
// rounded, so the strips before it may each stop a little short, and the
|
|
347
411
|
// remainder has to land somewhere.
|
|
348
|
-
/** @type {(state:
|
|
412
|
+
/** @type {(state: Cursor) => number} */
|
|
349
413
|
let floorOf = (state) => {
|
|
350
414
|
let region = state.region;
|
|
351
415
|
if (!region || region.height == null || region.index === region.count - 1)
|
|
@@ -353,11 +417,11 @@ let floorOf = (state) => {
|
|
|
353
417
|
return region.top - region.height;
|
|
354
418
|
};
|
|
355
419
|
// The y a fresh column starts from: a strip's shared top inside a region.
|
|
356
|
-
/** @type {(state:
|
|
420
|
+
/** @type {(state: Cursor) => number} */
|
|
357
421
|
let ceilOf = (state) => (state.region ? state.region.top : state.canvas.top);
|
|
358
422
|
// Has this column taken anything yet? Answered positionally, because the only
|
|
359
423
|
// thing that marks a page used is the cursor moving off its top, and the
|
|
360
|
-
// drawing primitives that move it take a `Canvas` rather than a `
|
|
424
|
+
// drawing primitives that move it take a `Canvas` rather than a `Cursor`.
|
|
361
425
|
// `canvas.fresh` stays the *page*'s own answer — it is what keeps `turn` from
|
|
362
426
|
// leaving an empty page behind — so a strip claims no second meaning for it.
|
|
363
427
|
// A block of no height would read fresh here and used there; none is reachable
|
|
@@ -438,7 +502,7 @@ let skipFlow = (state, event, name) => {
|
|
|
438
502
|
/** @type {(state: Cursor, event: any) => number} */
|
|
439
503
|
let flowPad = (state, event) => leadOf(state, event) + spaceOf(event.style, "spaceAfter");
|
|
440
504
|
|
|
441
|
-
// The
|
|
505
|
+
// The unsliceable blocks -- a picture and a split -- move whole to a fresh
|
|
442
506
|
// column rather than breaking, so a balanced floor never cuts one: only the
|
|
443
507
|
// page's own floor bears on it. A picture that no column can hold is scaled
|
|
444
508
|
// down to the one it lands on, since obeying the guarantee literally would
|
|
@@ -538,12 +602,12 @@ let item = (state, event, measured = null) => {
|
|
|
538
602
|
skipFlow(state, event, "spaceAfter");
|
|
539
603
|
};
|
|
540
604
|
|
|
541
|
-
// What a block does when it does not fit where it stands.
|
|
542
|
-
//
|
|
543
|
-
//
|
|
605
|
+
// What a block does when it does not fit where it stands. An unsliceable block
|
|
606
|
+
// moves whole to a fresh column; anything else flows down it, breaking where
|
|
607
|
+
// `fitLines` says.
|
|
544
608
|
/** @type {(state: Flow, block: Block) => void} */
|
|
545
609
|
let overflow = (state, block) =>
|
|
546
|
-
block
|
|
610
|
+
unsliceable(block) ? placeWhole(state, block) : slice(state, block);
|
|
547
611
|
|
|
548
612
|
// The height a page turn carries over: the drawn headers of the groups still
|
|
549
613
|
// open, which every page their instances continue onto replays. Computed rather
|
|
@@ -643,7 +707,7 @@ let stripWidth = (state, count) => (state.canvas.content - GUTTER * (count - 1))
|
|
|
643
707
|
//
|
|
644
708
|
// It opens buffering: what the strips do with the content depends on how much
|
|
645
709
|
// of it there is, and that is not known until the region ends or outgrows a
|
|
646
|
-
// page. `held` is that buffer; `null` means the region has
|
|
710
|
+
// page. `held` is that buffer; `null` means the region has decided.
|
|
647
711
|
/** @type {(state: Flow) => void} */
|
|
648
712
|
let openRegion = (state) => {
|
|
649
713
|
let owed = state.pending;
|
|
@@ -666,18 +730,14 @@ let openRegion = (state) => {
|
|
|
666
730
|
};
|
|
667
731
|
};
|
|
668
732
|
|
|
669
|
-
//
|
|
670
|
-
// full-width content resumes under it on the same page (SCHEMA.md).
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (region.held) commit(state);
|
|
678
|
-
if (!state.region) return;
|
|
679
|
-
let canvas = state.canvas;
|
|
680
|
-
canvas.y = Math.min(canvas.y, ...region.ends);
|
|
733
|
+
// Land the region: the cursor drops to whichever strip reached lowest, and
|
|
734
|
+
// full-width content resumes under it on the same page (SCHEMA.md). The last
|
|
735
|
+
// reader of `ends`, which `restrip` empties and `advance` fills — closing is
|
|
736
|
+
// the flow's, because a region still holding has to be laid out first, but
|
|
737
|
+
// where the region lands is geometry and belongs here.
|
|
738
|
+
/** @type {(state: Flow, region: Region) => void} */
|
|
739
|
+
let landRegion = (state, region) => {
|
|
740
|
+
state.canvas.y = Math.min(state.canvas.y, ...region.ends);
|
|
681
741
|
state.region = null;
|
|
682
742
|
};
|
|
683
743
|
|
|
@@ -739,11 +799,14 @@ let UNITS = {
|
|
|
739
799
|
},
|
|
740
800
|
row: restated,
|
|
741
801
|
"total-row": restated,
|
|
802
|
+
// The kinds `unsliceable` names, restated because a strip is costed from the
|
|
803
|
+
// event rather than the block -- this table's `whole` is the balancer's
|
|
804
|
+
// model, not the pagination truth. A third such kind belongs in both places.
|
|
742
805
|
image: unbroken,
|
|
743
806
|
split: unbroken,
|
|
744
807
|
};
|
|
745
808
|
|
|
746
|
-
// An
|
|
809
|
+
// An entry no strip may cut owes it nothing but its own height.
|
|
747
810
|
/** @type {(entry: any) => Unit} */
|
|
748
811
|
function unbroken(entry) {
|
|
749
812
|
return unsplit(entry.h, entry.h);
|
|
@@ -766,20 +829,21 @@ let stripHeight = (state, region, own) =>
|
|
|
766
829
|
? null
|
|
767
830
|
: balance(unitsOf(state, own), region.count, roomOf(state, region), rowHeight(state.canvas));
|
|
768
831
|
|
|
769
|
-
|
|
770
|
-
|
|
832
|
+
// Decide the region: settle the strip height, stop holding, and hand the held
|
|
833
|
+
// entries back in the order they arrived, for `flow`'s `replay` to walk.
|
|
834
|
+
// Not `commit`: the editor's session already spends that word on a gesture
|
|
835
|
+
// ending, and git spends it on a commit.
|
|
836
|
+
//
|
|
837
|
+
// Marking the spans is also what picks out the region's own children, and the
|
|
838
|
+
// height is decided from those — so it runs before the replay, not with it.
|
|
839
|
+
/** @type {(state: Flow) => any[]} */
|
|
840
|
+
let decide = (state) => {
|
|
771
841
|
let region = /** @type {Region} */ (state.region);
|
|
772
842
|
let held = /** @type {any[]} */ (region.held);
|
|
773
|
-
// Marking the spans is also what picks out the region's own children, and
|
|
774
|
-
// the height is decided from those — so it runs before the replay, not with
|
|
775
|
-
// it.
|
|
776
843
|
region.height = stripHeight(state, region, spans(held));
|
|
777
844
|
region.held = null;
|
|
778
845
|
restrip(region, region.top);
|
|
779
|
-
|
|
780
|
-
if (breaksFor(state, region, entry.span)) advance(state);
|
|
781
|
-
state.route(entry.event, entry.payload);
|
|
782
|
-
}
|
|
846
|
+
return held;
|
|
783
847
|
};
|
|
784
848
|
|
|
785
849
|
// A group instance is the atomic unit inside a region: one that would cross a
|
|
@@ -864,10 +928,11 @@ let NESTING = { "group-start": 1, "group-end": -1 };
|
|
|
864
928
|
// of what it holds. A measured block rides along so the replay never wraps the
|
|
865
929
|
// same text twice; a held header counts towards the total like anything else,
|
|
866
930
|
// but the flush that draws it measures from the event, so only its height is
|
|
867
|
-
// kept.
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
|
|
931
|
+
// kept. Answers whether that event filled it: a buffer that outgrows what the
|
|
932
|
+
// strips could hold has answered the balancing question early, and everything
|
|
933
|
+
// after it streams.
|
|
934
|
+
/** @type {(state: Flow, event: any, payload: Block | Measured | null, hollow: boolean) => boolean} */
|
|
935
|
+
let buffer = (state, event, payload, hollow) => {
|
|
871
936
|
let region = /** @type {Region} */ (state.region);
|
|
872
937
|
// The one door into the held list, so it is where the buffer's own walk is
|
|
873
938
|
// kept. A bracket buffered here is a bracket the open stack will not see
|
|
@@ -877,7 +942,33 @@ let buffer = (state, event, payload) => {
|
|
|
877
942
|
/** @type {any[]} */ (region.held).push(entry);
|
|
878
943
|
region.measured += entry.h;
|
|
879
944
|
if (event.type === "table-end") region.measured += remeasure(state, region, entry);
|
|
880
|
-
if (
|
|
945
|
+
if (hollow) region.measured -= refund(/** @type {any[]} */ (region.held));
|
|
946
|
+
return outgrown(state, region);
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// Give back what `WORTH` charged the opening of an instance that turned out
|
|
950
|
+
// hollow, and zero the entry so the replay's `spans` walk agrees. Taken as the
|
|
951
|
+
// close lands rather than at commit because `outgrown` is tested on every
|
|
952
|
+
// buffered event: a region carrying a phantom height can give up on balancing
|
|
953
|
+
// before it ever reaches the commit that would have corrected it.
|
|
954
|
+
//
|
|
955
|
+
// The walk back is over brackets only, which is safe precisely because the
|
|
956
|
+
// caller has already established the instance is hollow — there is nothing
|
|
957
|
+
// else between the two ends to walk over. Nested hollow instances refunded
|
|
958
|
+
// themselves on the way in, so each charge is given back exactly once.
|
|
959
|
+
/** @type {(held: any[]) => number} */
|
|
960
|
+
let refund = (held) => {
|
|
961
|
+
let depth = 0;
|
|
962
|
+
for (let i = held.length - 2; i >= 0; i--) {
|
|
963
|
+
if (held[i].event.type === "group-end") depth++;
|
|
964
|
+
else if (depth) depth--;
|
|
965
|
+
else {
|
|
966
|
+
let charged = held[i].h;
|
|
967
|
+
held[i].h = 0;
|
|
968
|
+
return charged;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return 0;
|
|
881
972
|
};
|
|
882
973
|
|
|
883
974
|
// A table's events reach the buffer one at a time, and each is worth a bare
|
|
@@ -963,7 +1054,7 @@ let anchor = (state) => {
|
|
|
963
1054
|
};
|
|
964
1055
|
|
|
965
1056
|
/** @type {(state: Flow) => Group[]} */
|
|
966
|
-
let pending = (state) => state.open.filter((group) => group.
|
|
1057
|
+
let pending = (state) => state.open.filter((group) => group.pending.length);
|
|
967
1058
|
|
|
968
1059
|
/** @type {(state: Flow, extra: number, runs: Block[][]) => number} */
|
|
969
1060
|
let needed = (state, extra, runs) =>
|
|
@@ -974,12 +1065,12 @@ let needed = (state, extra, runs) =>
|
|
|
974
1065
|
// for good: every other path drew its headers inside the room a page still
|
|
975
1066
|
// had, so a run always leaves space for the content it introduces.
|
|
976
1067
|
/** @type {(state: Flow, holding: Group[]) => void} */
|
|
977
|
-
let
|
|
978
|
-
let
|
|
979
|
-
for (let group of holding) group.
|
|
1068
|
+
let dropPending = (state, holding) => {
|
|
1069
|
+
let waiting = holding.flatMap((group) => group.pending);
|
|
1070
|
+
for (let group of holding) group.pending = [];
|
|
980
1071
|
state.gap = 0;
|
|
981
1072
|
anchor(state);
|
|
982
|
-
for (let entry of
|
|
1073
|
+
for (let entry of waiting) item(state, entry.event, entry.block);
|
|
983
1074
|
};
|
|
984
1075
|
|
|
985
1076
|
/** @type {(state: Flow, need: number) => void} */
|
|
@@ -997,12 +1088,12 @@ let settle = (state, need) => {
|
|
|
997
1088
|
// — the turn above already did — so an instance taking its blocks the moment
|
|
998
1089
|
// they are drawn can never have them replayed on top of themselves.
|
|
999
1090
|
/** @type {(state: Flow, holding: Group[], runs: Block[][], avail: number) => void} */
|
|
1000
|
-
let
|
|
1091
|
+
let drawPending = (state, holding, runs, avail) => {
|
|
1001
1092
|
let x = originOf(state);
|
|
1002
1093
|
for (let [i, group] of holding.entries()) {
|
|
1003
1094
|
for (let block of runs[i]) drawBlock(state.canvas, block, x, avail);
|
|
1004
1095
|
group.blocks.push(...runs[i]);
|
|
1005
|
-
group.
|
|
1096
|
+
group.pending = [];
|
|
1006
1097
|
}
|
|
1007
1098
|
};
|
|
1008
1099
|
|
|
@@ -1018,27 +1109,25 @@ let flush = (state, extra) => {
|
|
|
1018
1109
|
// at this same width, so a replayed run reuses those rather than wrapping
|
|
1019
1110
|
// the same text a second time (ADR 0027).
|
|
1020
1111
|
let runs = runsFor.map((group) =>
|
|
1021
|
-
group.
|
|
1112
|
+
group.pending.map((entry) => entry.block || blockOf(canvas, entry.event, avail)),
|
|
1022
1113
|
);
|
|
1023
1114
|
let need = needed(state, extra, runs);
|
|
1024
1115
|
// Against what a fresh column has left once the page has replayed what it
|
|
1025
1116
|
// carries. Inside a region that is a strip, and the replay sits above it.
|
|
1026
|
-
if (need > ceilOf(state) - carried(state) - floorOf(state)) return
|
|
1117
|
+
if (need > ceilOf(state) - carried(state) - floorOf(state)) return dropPending(state, runsFor);
|
|
1027
1118
|
settle(state, need);
|
|
1028
|
-
|
|
1119
|
+
drawPending(state, runsFor, runs, avail);
|
|
1029
1120
|
};
|
|
1030
1121
|
|
|
1031
1122
|
// Enough of an item that its group header is never left introducing nothing.
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1123
|
+
// An unsliceable block is kept whole -- the same blocks `overflow` moves
|
|
1124
|
+
// rather than slices. There is no first part of one to keep company with: a
|
|
1125
|
+
// split's own `lines` are empty, so clipping to `KEEP_LINES` would reserve
|
|
1126
|
+
// nothing at all and strand the run it was called to protect.
|
|
1036
1127
|
let KEEP_LINES = 2;
|
|
1037
1128
|
/** @type {(block: Block) => number} */
|
|
1038
1129
|
let keepWith = (block) =>
|
|
1039
|
-
block.
|
|
1040
|
-
? block.h
|
|
1041
|
-
: Math.min(block.h, heightOf(block.lines.slice(0, KEEP_LINES)));
|
|
1130
|
+
unsliceable(block) ? block.h : Math.min(block.h, heightOf(block.lines.slice(0, KEEP_LINES)));
|
|
1042
1131
|
|
|
1043
1132
|
// Flush any pending header with this item's opening, then place the item. The
|
|
1044
1133
|
// block is measured once here and handed on rather than wrapped twice.
|
|
@@ -1060,7 +1149,7 @@ let keepBlock = (state, event, width) =>
|
|
|
1060
1149
|
// Is anything waiting on this item's arrival — a header run, or the gap an
|
|
1061
1150
|
// instance opens with?
|
|
1062
1151
|
/** @type {(state: Flow) => boolean} */
|
|
1063
|
-
let holding = (state) => state.open.some((group) => group.
|
|
1152
|
+
let holding = (state) => state.open.some((group) => group.pending.length) || state.gap > 0;
|
|
1064
1153
|
|
|
1065
1154
|
// A half-line gap before each instance (dropped at a page top by `flush`)
|
|
1066
1155
|
// keeps groups reading as blocks. Every instance becomes an outline entry.
|
|
@@ -1070,16 +1159,52 @@ let holding = (state) => state.open.some((group) => group.held.length) || state.
|
|
|
1070
1159
|
// starts a new `page.number` / `page.total` sequence on the page it opens.
|
|
1071
1160
|
// A duplicate start on a page that already began a sequence is a zero-length
|
|
1072
1161
|
// range; `numbered` skips it.
|
|
1162
|
+
// Run `seen` on every event before the handler that acts on it. The stream is
|
|
1163
|
+
// the only place hollowness can be read in order: past this point a region may
|
|
1164
|
+
// hold an event back for a commit that lands after its own instance has closed.
|
|
1165
|
+
/** @type {(seen: (event: any) => void, handlers: Record<string, (event: any) => void>) => Record<string, (event: any) => void>} */
|
|
1166
|
+
let tracked = (seen, handlers) =>
|
|
1167
|
+
Object.fromEntries(
|
|
1168
|
+
Object.entries(handlers).map(([type, handle]) => [
|
|
1169
|
+
type,
|
|
1170
|
+
(/** @type {any} */ event) => {
|
|
1171
|
+
seen(event);
|
|
1172
|
+
handle(event);
|
|
1173
|
+
},
|
|
1174
|
+
]),
|
|
1175
|
+
);
|
|
1176
|
+
|
|
1073
1177
|
/** @type {(state: Flow, event: any) => void} */
|
|
1074
1178
|
let openGroup = (state, event) => {
|
|
1075
1179
|
breakFor(state, event);
|
|
1076
1180
|
if (event.columns) state.pending = { count: event.columns, owner: event.depth };
|
|
1077
|
-
|
|
1181
|
+
let gap = state.gap;
|
|
1182
|
+
let skip = state.skipGap;
|
|
1183
|
+
state.gap = skip ? 0 : Math.max(state.gap, instanceGap(state.canvas));
|
|
1078
1184
|
state.skipGap = false;
|
|
1079
|
-
state.open.push({
|
|
1185
|
+
state.open.push({ pending: [], blocks: [], gap, skip, titled: false });
|
|
1080
1186
|
state.marks.push(markFor(event));
|
|
1081
1187
|
};
|
|
1082
1188
|
|
|
1189
|
+
// Put back everything opening this instance displaced, so a hollow one leaves
|
|
1190
|
+
// the flow exactly as it found it. Restoring rather than zeroing is the whole
|
|
1191
|
+
// point: `Math.max` folded any ancestor's gap into this one, and a close that
|
|
1192
|
+
// zeroed would rob the parent of a gap its own opening earned. The pinned
|
|
1193
|
+
// header's `skip` travels the same way — a hollow instance is not the band the
|
|
1194
|
+
// pin was suppressing the gap for, so the suppression passes through it to
|
|
1195
|
+
// whatever lands next. The mark goes too: the outline is what a reader opens,
|
|
1196
|
+
// and a bookmark onto content this instance did not draw is a worse answer
|
|
1197
|
+
// than no bookmark. It is this instance's mark for certain — a non-hollow
|
|
1198
|
+
// child would have made this instance non-hollow, and a hollow one already
|
|
1199
|
+
// took its own back.
|
|
1200
|
+
/** @type {(state: Flow) => void} */
|
|
1201
|
+
let unopen = (state) => {
|
|
1202
|
+
let frame = state.open[state.open.length - 1];
|
|
1203
|
+
state.gap = frame.gap;
|
|
1204
|
+
state.skipGap = frame.skip;
|
|
1205
|
+
state.marks.pop();
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1083
1208
|
// A real page, inside a region as much as outside one: ADR 0013 declined a
|
|
1084
1209
|
// `break: "column"`, so this never means the next strip. `turn` restrips.
|
|
1085
1210
|
/** @type {(state: Flow, event: any) => void} */
|
|
@@ -1093,7 +1218,6 @@ let markFor = (event) => ({
|
|
|
1093
1218
|
// display(), not String(): a Date group key must title its outline bookmark
|
|
1094
1219
|
// with the same ISO 8601 UTC text its cells render, on every machine.
|
|
1095
1220
|
title: event.name + ": " + display(event.key),
|
|
1096
|
-
titled: false,
|
|
1097
1221
|
depth: event.depth,
|
|
1098
1222
|
page: -1,
|
|
1099
1223
|
y: 0,
|
|
@@ -1103,24 +1227,24 @@ let markFor = (event) => ({
|
|
|
1103
1227
|
// page bottom. The instance's first header titles its outline entry.
|
|
1104
1228
|
/** @type {(state: Flow, event: any, title: string | null, block?: Block | null) => void} */
|
|
1105
1229
|
let holdHeader = (state, event, title, block = null) => {
|
|
1230
|
+
let group = state.open[state.open.length - 1];
|
|
1106
1231
|
let mark = state.marks[state.marks.length - 1];
|
|
1232
|
+
// Whether a header has claimed this entry is the open instance's own fact,
|
|
1233
|
+
// not the outline entry's: a Mark is what a consumer receives, and it
|
|
1234
|
+
// carries only what `index.d.ts` declares. The two records correspond here
|
|
1235
|
+
// because `openGroup` pushes them together and this only ever runs inside
|
|
1236
|
+
// that instance's own header band, before any nested group opens -- groups
|
|
1237
|
+
// pop from `open` while their marks stay, so nowhere else may assume it.
|
|
1238
|
+
//
|
|
1107
1239
|
// A picture has no words to title an outline entry with, so it holds its
|
|
1108
1240
|
// place in the run and leaves the title to the next header that has some.
|
|
1109
|
-
if (mark && !
|
|
1110
|
-
|
|
1241
|
+
if (mark && !group.titled && title != null) {
|
|
1242
|
+
group.titled = true;
|
|
1111
1243
|
mark.title = title;
|
|
1112
1244
|
}
|
|
1113
|
-
|
|
1245
|
+
group.pending.push({ event, block });
|
|
1114
1246
|
};
|
|
1115
1247
|
|
|
1116
|
-
// A table row's floor height and the gap a group instance opens with: named
|
|
1117
|
-
// because the region's estimator reads both, and a constant with two readers
|
|
1118
|
-
// and no name drifts between them.
|
|
1119
|
-
/** @type {(canvas: Canvas) => number} */
|
|
1120
|
-
let rowHeight = (canvas) => LEAD * canvas.settings.base;
|
|
1121
|
-
/** @type {(canvas: Canvas) => number} */
|
|
1122
|
-
let instanceGap = (canvas) => 0.5 * LEAD * canvas.settings.base;
|
|
1123
|
-
|
|
1124
1248
|
// What a fresh strip has to hold to take an instance: the span less the
|
|
1125
1249
|
// opening gap, which `settle` drops at a strip head. This is the one fact the
|
|
1126
1250
|
// balance model and the placement rule have to agree on, so both read it here
|
|
@@ -1130,60 +1254,6 @@ let headOf = (canvas, span) => span - instanceGap(canvas);
|
|
|
1130
1254
|
|
|
1131
1255
|
// --- tables -----------------------------------------------------------------
|
|
1132
1256
|
|
|
1133
|
-
// Pre-measure one cell: wrapped lines per column width come later; natural
|
|
1134
|
-
// width first (no wrapping except hard breaks). `mt`/`mb` go unread: SCHEMA.md
|
|
1135
|
-
// gives flow spacing no meaning inside a table row, in either target.
|
|
1136
|
-
/** @type {(canvas: Canvas, cell: any, rowStyle: any) => any} */
|
|
1137
|
-
let cellOf = (canvas, cell, rowStyle) => {
|
|
1138
|
-
// A row's block reaches its cells with no box in it: the engine resolved
|
|
1139
|
-
// that half onto the cells themselves before the event was emitted
|
|
1140
|
-
// (SCHEMA.md, "Style declarations"), which is why the inset and the ink
|
|
1141
|
-
// both read the cell and a row is never asked for a box it cannot have.
|
|
1142
|
-
let style = merge(rowStyle, unbox(cell.style));
|
|
1143
|
-
let inset = insetOf(cell.style, CELL_PAD);
|
|
1144
|
-
let list = atoms(canvas.settings, cell.tokens, style);
|
|
1145
|
-
let natural = wrap(list, Infinity, sizeOf(style, canvas.settings.base)).reduce(
|
|
1146
|
-
(widest, line) => Math.max(widest, line.w),
|
|
1147
|
-
0,
|
|
1148
|
-
);
|
|
1149
|
-
return {
|
|
1150
|
-
list,
|
|
1151
|
-
natural: natural + inset.l + inset.r,
|
|
1152
|
-
align: style.align,
|
|
1153
|
-
valign: style.valign,
|
|
1154
|
-
bg: col(style.background),
|
|
1155
|
-
underline: !!style.underline,
|
|
1156
|
-
strikethrough: !!style.strikethrough,
|
|
1157
|
-
inset,
|
|
1158
|
-
style: cell.style,
|
|
1159
|
-
path: cell.path,
|
|
1160
|
-
};
|
|
1161
|
-
};
|
|
1162
|
-
|
|
1163
|
-
/** @type {(canvas: Canvas, cells: any[], widths: number[]) => { cells: any[], h: number }} */
|
|
1164
|
-
let rowOf = (canvas, cells, widths) => {
|
|
1165
|
-
let h = rowHeight(canvas);
|
|
1166
|
-
/** @type {number[]} */
|
|
1167
|
-
let owns = [];
|
|
1168
|
-
let out = cells.map((cell, i) => {
|
|
1169
|
-
let inner = Math.max(widths[i] - cell.inset.l - cell.inset.r, 1);
|
|
1170
|
-
// A glyph or a hard break occupies; spaces alone are an empty cell.
|
|
1171
|
-
let lines = cell.list.some(
|
|
1172
|
-
(/** @type {{ hard: boolean, space: boolean }} */ atom) => atom.hard || !atom.space,
|
|
1173
|
-
)
|
|
1174
|
-
? dress(wrap(cell.list, inner, cell.list[0].size), cell)
|
|
1175
|
-
: [];
|
|
1176
|
-
let own = heightOf(lines) + cell.inset.t + cell.inset.b;
|
|
1177
|
-
if (own > h) h = own;
|
|
1178
|
-
owns.push(own);
|
|
1179
|
-
return { ...cell, lines };
|
|
1180
|
-
});
|
|
1181
|
-
// The row's height is known only now, so the slack each cell's `valign`
|
|
1182
|
-
// reads is measured here, as `splitBlock` measures a slot's.
|
|
1183
|
-
for (let [i, cell] of out.entries()) cell.drop = vshift(cell.valign, h - owns[i]);
|
|
1184
|
-
return { cells: out, h };
|
|
1185
|
-
};
|
|
1186
|
-
|
|
1187
1257
|
/** @type {(canvas: Canvas, cell: any, bg: any, x: number, y: number, w: number, h: number) => void} */
|
|
1188
1258
|
let fillCell = (canvas, cell, bg, x, y, w, h) => {
|
|
1189
1259
|
if (cell.bg && cell.bg !== bg) canvas.rect(cell.bg, x, y, w, h);
|
|
@@ -1243,107 +1313,7 @@ let drawRow = (canvas, row, xOffsets, widths, bg) => {
|
|
|
1243
1313
|
);
|
|
1244
1314
|
};
|
|
1245
1315
|
|
|
1246
|
-
// The column geometry one row sees. A cell covering several columns starts
|
|
1247
|
-
// where the first of them starts and is as wide as all of them together; a row
|
|
1248
|
-
// that spans nothing sees the columns themselves, and pays nothing for the
|
|
1249
|
-
// feature. `null` spans is that row -- every data row, and every total row
|
|
1250
|
-
// whose cells each cover one column.
|
|
1251
|
-
/** @type {(cells: any[]) => number[] | null} */
|
|
1252
|
-
let spansOf = (cells) =>
|
|
1253
|
-
cells.some((cell) => cell.span > 1) ? cells.map((cell) => cell.span || 1) : null;
|
|
1254
|
-
// One value per cell, walking the columns each of them covers. Reached only
|
|
1255
|
-
// for a row that spans, so the closure it takes costs nothing per data row.
|
|
1256
|
-
/** @type {(spans: number[], pick: (at: number, span: number) => number) => number[]} */
|
|
1257
|
-
let overSpans = (spans, pick) => {
|
|
1258
|
-
/** @type {number[]} */
|
|
1259
|
-
let out = [];
|
|
1260
|
-
let at = 0;
|
|
1261
|
-
for (let span of spans) {
|
|
1262
|
-
out.push(pick(at, span));
|
|
1263
|
-
at += span;
|
|
1264
|
-
}
|
|
1265
|
-
return out;
|
|
1266
|
-
};
|
|
1267
|
-
/** @type {(widths: number[], spans: number[] | null) => number[]} */
|
|
1268
|
-
let spanWidths = (widths, spans) =>
|
|
1269
|
-
spans ? overSpans(spans, (at, span) => sum(widths.slice(at, at + span))) : widths;
|
|
1270
1316
|
/** @type {(xOffsets: number[], spans: number[] | null) => number[]} */
|
|
1271
|
-
let spanOffsets = (xOffsets, spans) => (spans ? overSpans(spans, (at) => xOffsets[at]) : xOffsets);
|
|
1272
|
-
|
|
1273
|
-
// The widest of each column's header, rows and totals, padding included.
|
|
1274
|
-
//
|
|
1275
|
-
// A cell covering more than one column has no say in their widths (SCHEMA.md):
|
|
1276
|
-
// what a span states is which columns a cell reaches across, never how wide
|
|
1277
|
-
// they are. So a column can end up with no voter at all -- every cell above it
|
|
1278
|
-
// spans over it -- which only an empty table reaches, since a data row never
|
|
1279
|
-
// spans. It opens at the padding floor rather than at nothing, so an empty
|
|
1280
|
-
// table still shows the geometry it promises.
|
|
1281
|
-
/**
|
|
1282
|
-
* @typedef {{ cells: any[], spans: number[] | null }} Voting
|
|
1283
|
-
*/
|
|
1284
|
-
/** @type {(cols: any[], header: Voting, rows: Voting[], totals: Voting[]) => number[]} */
|
|
1285
|
-
let naturalWidths = (cols, header, rows, totals) => {
|
|
1286
|
-
/** @type {(number | null)[]} */
|
|
1287
|
-
let widest = cols.map(() => null);
|
|
1288
|
-
/** @type {(at: number, natural: number) => void} */
|
|
1289
|
-
let widen = (at, natural) => {
|
|
1290
|
-
let held = widest[at];
|
|
1291
|
-
if (held === null || natural > held) widest[at] = natural;
|
|
1292
|
-
};
|
|
1293
|
-
/** @type {(row: Voting) => void} */
|
|
1294
|
-
let vote = (row) => {
|
|
1295
|
-
let at = 0;
|
|
1296
|
-
for (let [i, cell] of row.cells.entries()) {
|
|
1297
|
-
let span = row.spans ? row.spans[i] : 1;
|
|
1298
|
-
if (span === 1) widen(at, cell.natural);
|
|
1299
|
-
at += span;
|
|
1300
|
-
}
|
|
1301
|
-
};
|
|
1302
|
-
vote(header);
|
|
1303
|
-
for (let row of rows) vote(row);
|
|
1304
|
-
for (let row of totals) vote(row);
|
|
1305
|
-
return widest.map((width) => (width === null ? CELL_PAD.l + CELL_PAD.r : width));
|
|
1306
|
-
};
|
|
1307
|
-
|
|
1308
|
-
/** @type {(values: number[]) => number} */
|
|
1309
|
-
let sum = (values) => values.reduce((total, value) => total + value, 0);
|
|
1310
|
-
|
|
1311
|
-
// An authored `width` percentage fixes its column; the rest share what is left
|
|
1312
|
-
// in proportion to their natural widths. Three cases, one return each — the
|
|
1313
|
-
// fall-through is all-authored within budget, honoured exactly.
|
|
1314
|
-
//
|
|
1315
|
-
// There is no over-commitment case: the engine rejects a document whose fixed
|
|
1316
|
-
// shares leave the width-less columns nothing, so `left` is positive whenever
|
|
1317
|
-
// an auto column exists and the fixed shares never exceed the content width.
|
|
1318
|
-
// That invariant is asserted rather than trusted — falling through with auto
|
|
1319
|
-
// columns unplaced would draw them at zero width, an invisible failure — the
|
|
1320
|
-
// same fail-loud posture as the measuring canvas's `newPage`.
|
|
1321
|
-
/** @type {(cols: any[], natural: number[], avail: number) => number[]} */
|
|
1322
|
-
let columnWidths = (cols, natural, avail) => {
|
|
1323
|
-
let fixed = cols.map((column) => Number.isFinite(column.width));
|
|
1324
|
-
if (!fixed.some(Boolean)) {
|
|
1325
|
-
// Natural widths always carry the cell padding, so the sum is never zero.
|
|
1326
|
-
let wanted = sum(natural);
|
|
1327
|
-
return natural.map((width) => (width * avail) / wanted);
|
|
1328
|
-
}
|
|
1329
|
-
let widths = cols.map((col, i) => (fixed[i] ? (col.width / 100) * avail : 0));
|
|
1330
|
-
let auto = natural.map((width, i) => (fixed[i] ? 0 : width));
|
|
1331
|
-
let left = avail - sum(widths);
|
|
1332
|
-
let wanted = sum(auto);
|
|
1333
|
-
// Room for the auto columns: they share what the fixed ones left.
|
|
1334
|
-
if (wanted > 0) {
|
|
1335
|
-
if (left <= 0) throw new Error("over-committed column widths reached the layout");
|
|
1336
|
-
return widths.map((width, i) => (fixed[i] ? width : (auto[i] * left) / wanted));
|
|
1337
|
-
}
|
|
1338
|
-
return widths;
|
|
1339
|
-
};
|
|
1340
|
-
|
|
1341
|
-
// One table being emitted: what `carryOver`, `sliced` and `put` all need.
|
|
1342
|
-
/**
|
|
1343
|
-
* @typedef {{ cells: any[], h: number, style?: any, spans?: number[] | null }} TableRow
|
|
1344
|
-
* @typedef {{ state: Flow, head: TableRow,
|
|
1345
|
-
* xOffsets: number[], widths: number[], x: number, avail: number }} Grid
|
|
1346
|
-
*/
|
|
1347
1317
|
|
|
1348
1318
|
// Continue in a fresh column: a table that spans columns or pages restates its
|
|
1349
1319
|
// headings, under whatever headers the open groups replay above them. Both
|
|
@@ -1356,18 +1326,6 @@ let columnWidths = (cols, natural, avail) => {
|
|
|
1356
1326
|
/** @type {(row: { style?: any }) => any} */
|
|
1357
1327
|
let rowFill = (row) => col((row.style || {}).background);
|
|
1358
1328
|
|
|
1359
|
-
// What this row draws against: the grid's own columns, or the merged geometry
|
|
1360
|
-
// a spanning row sees. Read at draw time rather than kept on the row, because
|
|
1361
|
-
// `rebase` moves the offsets under it every time the table crosses a strip.
|
|
1362
|
-
/** @type {(grid: Grid, row: TableRow) => { xOffsets: number[], widths: number[] }} */
|
|
1363
|
-
let gridOf = (grid, row) =>
|
|
1364
|
-
row.spans
|
|
1365
|
-
? {
|
|
1366
|
-
xOffsets: spanOffsets(grid.xOffsets, row.spans),
|
|
1367
|
-
widths: spanWidths(grid.widths, row.spans),
|
|
1368
|
-
}
|
|
1369
|
-
: grid;
|
|
1370
|
-
|
|
1371
1329
|
/** @type {(grid: Grid) => void} */
|
|
1372
1330
|
let carryOver = (grid) => {
|
|
1373
1331
|
advance(grid.state);
|
|
@@ -1509,74 +1467,6 @@ let emitRows = (grid, laid, totalRows) => {
|
|
|
1509
1467
|
for (let row of totalRows) put(grid, row, rowFill(row), 0);
|
|
1510
1468
|
};
|
|
1511
1469
|
|
|
1512
|
-
// A buffered table, in the one shape `table` lays out: its columns come from
|
|
1513
|
-
// the opening event, its rows are the row events themselves, and its total is
|
|
1514
|
-
// the total row's cells. Both readers build it here — the replay, collecting
|
|
1515
|
-
// events as they arrive, and the region buffer, reading them back off what it
|
|
1516
|
-
// held — so a new table event kind cannot reach one and miss the other.
|
|
1517
|
-
/** @type {(events: any[]) => any} */
|
|
1518
|
-
let tableOf = (events) => {
|
|
1519
|
-
let opening = events[0];
|
|
1520
|
-
return {
|
|
1521
|
-
columns: opening.columns.map((/** @type {any} */ col) => ({ ...col })),
|
|
1522
|
-
// A header cell arrives without a path of its own; the column's is what
|
|
1523
|
-
// its box names, so it rides on the cell from here, where both readers
|
|
1524
|
-
// assemble the table. A column a neighbour's span covers declares no
|
|
1525
|
-
// header, so this is the header *row* -- as many cells as the columns
|
|
1526
|
-
// wrote, which is fewer than the columns whenever one spans.
|
|
1527
|
-
header: opening.columns
|
|
1528
|
-
.filter((/** @type {any} */ col) => col.header)
|
|
1529
|
-
.map((/** @type {any} */ col) => ({ ...col.header, path: col.path })),
|
|
1530
|
-
headerStyle: opening.style,
|
|
1531
|
-
rows: events.filter((event) => event.type === "row"),
|
|
1532
|
-
// The events themselves, as `rows` are: a total row is a row, and the
|
|
1533
|
-
// buffer files each corrected height under the event it was measured from.
|
|
1534
|
-
totals: events.filter((event) => event.type === "total-row"),
|
|
1535
|
-
};
|
|
1536
|
-
};
|
|
1537
|
-
|
|
1538
|
-
/**
|
|
1539
|
-
* @typedef {{ widths: number[], head: any, laid: any[], totalRows: any[] }} Measured
|
|
1540
|
-
*/
|
|
1541
|
-
// Measure a buffered table at a given width: the column widths every cell has
|
|
1542
|
-
// a say in, then each row laid out against them. Everything here is
|
|
1543
|
-
// arithmetic over the cells, so the region's buffer can ask for it before
|
|
1544
|
-
// anything is drawn — which is the only way a row's real height is known
|
|
1545
|
-
// early enough to balance on (bead `quario-cgk`).
|
|
1546
|
-
/** @type {(canvas: Canvas, buffered: any, avail: number) => Measured} */
|
|
1547
|
-
let measureTable = (canvas, buffered, avail) => {
|
|
1548
|
-
let cols = /** @type {any[]} */ (buffered.columns);
|
|
1549
|
-
/** @type {(cells: any[], style: any, spans: number[] | null) => Voting & { style: any }} */
|
|
1550
|
-
let measured = (cells, style, spans) => ({
|
|
1551
|
-
cells: cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, style)),
|
|
1552
|
-
spans,
|
|
1553
|
-
style,
|
|
1554
|
-
});
|
|
1555
|
-
let header = measured(buffered.header, buffered.headerStyle, spansOf(buffered.header));
|
|
1556
|
-
// A data row's cells are the columns' own, so nothing in one can span
|
|
1557
|
-
// (SCHEMA.md, "Span"). Asking each row anyway would scan every cell of the
|
|
1558
|
-
// table to rediscover what the schema already guarantees.
|
|
1559
|
-
let rows = buffered.rows.map((/** @type {any} */ row) => measured(row.cells, row.style, null));
|
|
1560
|
-
let totals = buffered.totals.map((/** @type {any} */ row) =>
|
|
1561
|
-
measured(row.cells, row.style, spansOf(row.cells)),
|
|
1562
|
-
);
|
|
1563
|
-
let widths = columnWidths(cols, naturalWidths(cols, header, rows, totals), avail);
|
|
1564
|
-
// Each row wraps against the geometry it sees, which is the columns' own
|
|
1565
|
-
// unless one of its cells spans.
|
|
1566
|
-
/** @type {(row: Voting & { style: any }) => any} */
|
|
1567
|
-
let laidOut = (row) => ({
|
|
1568
|
-
...rowOf(canvas, row.cells, spanWidths(widths, row.spans)),
|
|
1569
|
-
spans: row.spans,
|
|
1570
|
-
style: row.style,
|
|
1571
|
-
});
|
|
1572
|
-
return {
|
|
1573
|
-
widths,
|
|
1574
|
-
head: laidOut(header),
|
|
1575
|
-
laid: rows.map(laidOut),
|
|
1576
|
-
totalRows: totals.map(laidOut),
|
|
1577
|
-
};
|
|
1578
|
-
};
|
|
1579
|
-
|
|
1580
1470
|
// Lay out one table from the events it arrived as: measure every cell,
|
|
1581
1471
|
// allocate the columns, then emit the header, the rows and the total,
|
|
1582
1472
|
// breaking pages as needed.
|
|
@@ -1678,14 +1568,15 @@ let reserve = (geo, settings, bands) => {
|
|
|
1678
1568
|
};
|
|
1679
1569
|
};
|
|
1680
1570
|
|
|
1681
|
-
// The page frame a canvas presents,
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1686
|
-
//
|
|
1571
|
+
// The page frame a canvas presents, recovered from the canvas after `adopt`
|
|
1572
|
+
// narrowed it. Only `top`/`bottom` were overwritten there; `width`, `height`
|
|
1573
|
+
// and `margin` are still the page's own, so this re-derives the two that went
|
|
1574
|
+
// through the same `frame` every other consumer reads — a derivation from live
|
|
1575
|
+
// fields, never a copy that could drift from one. Both halves of page furniture
|
|
1576
|
+
// want it: reservation to measure against, the draw pass to hang the header
|
|
1577
|
+
// from.
|
|
1687
1578
|
/** @type {(canvas: Canvas) => Frame} */
|
|
1688
|
-
let
|
|
1579
|
+
let unnarrowed = (canvas) => frame(canvas.width, canvas.height, canvas.margin);
|
|
1689
1580
|
|
|
1690
1581
|
// One finished page's furniture, drawn in the strips `reserve` left for it:
|
|
1691
1582
|
// the header hanging from the top margin, the footer resting on the bottom
|
|
@@ -1695,7 +1586,7 @@ let pageFrame = (canvas) => frame(canvas.width, canvas.height, canvas.margin);
|
|
|
1695
1586
|
// that made room for them.
|
|
1696
1587
|
/** @type {(canvas: Canvas, bands: any, anchor: any) => void} */
|
|
1697
1588
|
let furniture = (canvas, bands, anchor) => {
|
|
1698
|
-
if (bands.header) band(canvas, bands.header(anchor),
|
|
1589
|
+
if (bands.header) band(canvas, bands.header(anchor), unnarrowed(canvas).top);
|
|
1699
1590
|
if (bands.footer) band(canvas, bands.footer(anchor), canvas.bottom - BAND);
|
|
1700
1591
|
};
|
|
1701
1592
|
|
|
@@ -1728,12 +1619,12 @@ let numbered = (count, starts) => {
|
|
|
1728
1619
|
* `finish` flushes whatever the last event left pending and returns what the
|
|
1729
1620
|
* passes over the finished pages still need: the outline marks — the only way
|
|
1730
1621
|
* to reach them, so an outline read off a band flow that still owes a flush is
|
|
1731
|
-
* not a mistake this seam leaves open — the
|
|
1732
|
-
*
|
|
1733
|
-
*
|
|
1622
|
+
* not a mistake this seam leaves open — and the per-page `{ number, total }`
|
|
1623
|
+
* anchors the furniture pass draws with. The opening event itself the entry
|
|
1624
|
+
* peeked for both halves it is built from, so this seam keeps none of it.
|
|
1734
1625
|
*
|
|
1735
1626
|
* @type {(canvas: Canvas) => { handlers: Record<string, (event: any) => void>,
|
|
1736
|
-
* finish: () => { marks: any[],
|
|
1627
|
+
* finish: () => { marks: any[],
|
|
1737
1628
|
* pages: { number: number, total: number }[] } }}
|
|
1738
1629
|
*/
|
|
1739
1630
|
let flow = (canvas) => {
|
|
@@ -1749,23 +1640,35 @@ let flow = (canvas) => {
|
|
|
1749
1640
|
pin: null,
|
|
1750
1641
|
skipGap: false,
|
|
1751
1642
|
inHeader: false,
|
|
1752
|
-
// Assigned below, once `route` exists: a buffered region replays through
|
|
1753
|
-
// it, and one entry point is what makes the replay take the path the
|
|
1754
|
-
// content would have taken had it never been held.
|
|
1755
|
-
route: () => {},
|
|
1756
1643
|
};
|
|
1757
|
-
// The body walk's own folder, emitting each finished event onward to `route`
|
|
1758
|
-
// below. Declared here and bound after `route` exists.
|
|
1759
|
-
/** @type {(event: any) => void} */
|
|
1760
|
-
let fold;
|
|
1761
1644
|
// A table is buffered whole before it is laid out: column widths come from
|
|
1762
1645
|
// every cell in it, so the last row has to be in hand first.
|
|
1763
1646
|
/** @type {any[]} */
|
|
1764
1647
|
let collected = [];
|
|
1765
|
-
//
|
|
1766
|
-
//
|
|
1767
|
-
|
|
1768
|
-
|
|
1648
|
+
// Hollowness is a property of the stream, not of what gets drawn: an
|
|
1649
|
+
// instance is hollow when nothing but hollow brackets arrived between its
|
|
1650
|
+
// own ends. Tracked at the handler layer, which every event reaches exactly
|
|
1651
|
+
// once — `route` is re-entered by a region's replay, and a buffered instance
|
|
1652
|
+
// has to be judged before it is buffered, not after. `filling` is one flag
|
|
1653
|
+
// per open bracket; the verdict rides the closing event itself, so the
|
|
1654
|
+
// buffer and the replay both read the answer the stream gave.
|
|
1655
|
+
/** @type {boolean[]} */
|
|
1656
|
+
let filling = [];
|
|
1657
|
+
/** @type {WeakSet<any>} */
|
|
1658
|
+
let hollows = new WeakSet();
|
|
1659
|
+
// Whatever instance is open drew something. Called for an ordinary event and
|
|
1660
|
+
// for a child that turned out not to be hollow, because those are the same
|
|
1661
|
+
// claim: an instance draws through its content and through its children.
|
|
1662
|
+
let fill = () => {
|
|
1663
|
+
if (filling.length) filling[filling.length - 1] = true;
|
|
1664
|
+
};
|
|
1665
|
+
/** @type {(event: any) => void} */
|
|
1666
|
+
let track = (event) => {
|
|
1667
|
+
if (event.type === "group-start") filling.push(false);
|
|
1668
|
+
else if (event.type !== "group-end") fill();
|
|
1669
|
+
else if (filling.pop()) fill();
|
|
1670
|
+
else hollows.add(event);
|
|
1671
|
+
};
|
|
1769
1672
|
// The body opens its own first page. Not through `turn`: a canvas that has
|
|
1770
1673
|
// drawn nothing is fresh already, and would be left with no page at all.
|
|
1771
1674
|
canvas.newPage();
|
|
@@ -1803,8 +1706,9 @@ let flow = (canvas) => {
|
|
|
1803
1706
|
// default of its own -- its slots carry theirs.
|
|
1804
1707
|
split: heldOrPlaced,
|
|
1805
1708
|
"group-start": (event) => openGroup(state, event),
|
|
1806
|
-
"group-end": () => {
|
|
1807
|
-
|
|
1709
|
+
"group-end": (event) => {
|
|
1710
|
+
if (hollows.has(event)) unopen(state);
|
|
1711
|
+
else flush(state, 0);
|
|
1808
1712
|
state.open.pop();
|
|
1809
1713
|
},
|
|
1810
1714
|
// A table is collected as the events it arrived as, and shaped by `tableOf`
|
|
@@ -1831,7 +1735,7 @@ let flow = (canvas) => {
|
|
|
1831
1735
|
// The nesting the router is at. While the buffer holds, routing runs ahead of
|
|
1832
1736
|
// placement: nothing has been placed, so `state.open` is frozen at the depth
|
|
1833
1737
|
// the region opened at and only the buffer knows where the walk really is.
|
|
1834
|
-
// Once `
|
|
1738
|
+
// Once `decide` nulls `held`, the replay *is* the placement timeline and the
|
|
1835
1739
|
// open stack is exact again.
|
|
1836
1740
|
let nesting = () =>
|
|
1837
1741
|
state.region && state.region.held ? state.region.nesting : state.open.length - 1;
|
|
@@ -1864,13 +1768,53 @@ let flow = (canvas) => {
|
|
|
1864
1768
|
let route = (event, payload = null) => {
|
|
1865
1769
|
snapPin(event);
|
|
1866
1770
|
if (fullBand(event)) fullWidth(event, payload);
|
|
1867
|
-
else if (deciding())
|
|
1771
|
+
else if (deciding()) hold(event, payload);
|
|
1868
1772
|
else if (opens(event)) begin(event);
|
|
1869
1773
|
else place(event, payload);
|
|
1870
1774
|
};
|
|
1871
1775
|
|
|
1872
|
-
|
|
1873
|
-
fold = splitFolder(route);
|
|
1776
|
+
// The body walk's own folder, emitting each finished event onward to `route`.
|
|
1777
|
+
let fold = splitFolder(route);
|
|
1778
|
+
|
|
1779
|
+
// Hold one event back in the region's buffer, and lay that buffer out if this
|
|
1780
|
+
// is the event that filled it. Not `holdHeader`: that holds a group's header
|
|
1781
|
+
// for the content it introduces, this holds anything at all for a region
|
|
1782
|
+
// still deciding.
|
|
1783
|
+
/** @type {(event: any, payload: Block | Measured | null) => void} */
|
|
1784
|
+
let hold = (event, payload) => {
|
|
1785
|
+
if (buffer(state, event, payload, hollows.has(event))) replay();
|
|
1786
|
+
};
|
|
1787
|
+
|
|
1788
|
+
// Lay the buffer out: `decide` settles the strip height and hands the held
|
|
1789
|
+
// entries back in order, and this walks them through `route`. The walk lives
|
|
1790
|
+
// here rather than under the region section marker for the reason everything
|
|
1791
|
+
// there is module-level — that section is reachable without the router, and
|
|
1792
|
+
// a walk through `route` is not.
|
|
1793
|
+
let replay = () => {
|
|
1794
|
+
let region = /** @type {Region} */ (state.region);
|
|
1795
|
+
for (let entry of decide(state)) {
|
|
1796
|
+
if (breaksFor(state, region, entry.span)) advance(state);
|
|
1797
|
+
route(entry.event, entry.payload);
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
// Close the region: anything still held is laid out first, and the region
|
|
1802
|
+
// then lands where `landRegion` puts it.
|
|
1803
|
+
//
|
|
1804
|
+
// Nothing the replay routes can arrive back here, which is why the local
|
|
1805
|
+
// `region` survives the call. `route` asks `fullBand` before `deciding()`, so
|
|
1806
|
+
// a full band is never buffered; and a replayed event, asked `fullBand` a
|
|
1807
|
+
// second time, answers as it did the first, because `nesting()`'s two
|
|
1808
|
+
// branches walk the same brackets — `buffer` carries `region.nesting` through
|
|
1809
|
+
// them while nothing is placed, `state.open` through them once the replay is
|
|
1810
|
+
// placing. The other closer, `endOwned`, is wired above `route` at the
|
|
1811
|
+
// handler layer, where a replay does not reach.
|
|
1812
|
+
let closeRegion = () => {
|
|
1813
|
+
let region = state.region;
|
|
1814
|
+
if (!region) return;
|
|
1815
|
+
if (deciding()) replay();
|
|
1816
|
+
landRegion(state, region);
|
|
1817
|
+
};
|
|
1874
1818
|
|
|
1875
1819
|
// Everything drawn across the page rather than down a strip: the declaring
|
|
1876
1820
|
// node's own bands, its footer among them.
|
|
@@ -1890,7 +1834,7 @@ let flow = (canvas) => {
|
|
|
1890
1834
|
/** @type {(depth: number) => void} */
|
|
1891
1835
|
let endOwned = (depth) => {
|
|
1892
1836
|
if (owner() !== depth) return;
|
|
1893
|
-
if (state.region) closeRegion(
|
|
1837
|
+
if (state.region) closeRegion();
|
|
1894
1838
|
else state.pending = null;
|
|
1895
1839
|
};
|
|
1896
1840
|
|
|
@@ -1905,7 +1849,7 @@ let flow = (canvas) => {
|
|
|
1905
1849
|
let fullWidth = (event, payload) => {
|
|
1906
1850
|
// Asked before the region closes, because that is what it reads.
|
|
1907
1851
|
let own = ownFooter(event);
|
|
1908
|
-
closeRegion(
|
|
1852
|
+
closeRegion();
|
|
1909
1853
|
if (own) state.pending = null;
|
|
1910
1854
|
place(event, payload);
|
|
1911
1855
|
};
|
|
@@ -1921,7 +1865,7 @@ let flow = (canvas) => {
|
|
|
1921
1865
|
let block = keepBlock(state, event, stripWidth(state, owed.count));
|
|
1922
1866
|
flush(state, block ? keepWith(block) : 0);
|
|
1923
1867
|
openRegion(state);
|
|
1924
|
-
|
|
1868
|
+
hold(event, block);
|
|
1925
1869
|
};
|
|
1926
1870
|
|
|
1927
1871
|
/** @type {(event: any) => void} */
|
|
@@ -1935,12 +1879,17 @@ let flow = (canvas) => {
|
|
|
1935
1879
|
};
|
|
1936
1880
|
|
|
1937
1881
|
return {
|
|
1938
|
-
|
|
1882
|
+
// Every handler is wrapped so `track` sees the stream once, in order, ahead
|
|
1883
|
+
// of the dispatch that may buffer the event for later. One wrap rather than
|
|
1884
|
+
// a call in each handler: a new event type must count towards the instance
|
|
1885
|
+
// it arrives in without anyone remembering to say so.
|
|
1886
|
+
handlers: tracked(track, {
|
|
1939
1887
|
"report-start": (event) => {
|
|
1940
|
-
|
|
1941
|
-
//
|
|
1942
|
-
//
|
|
1943
|
-
|
|
1888
|
+
// The document-wide settings were built complete from this same event
|
|
1889
|
+
// before the walk began (index.js peeks it), so this handler reads them
|
|
1890
|
+
// through `canvas.settings` and settles nothing — only the page bands,
|
|
1891
|
+
// whose height is not known until here, are taken off it below. The
|
|
1892
|
+
// entry peeked the event too, so nothing is kept for it here.
|
|
1944
1893
|
if (event.columns) state.pending = { count: event.columns, owner: -1 };
|
|
1945
1894
|
if (!event.page) {
|
|
1946
1895
|
pinHeader(event);
|
|
@@ -1952,7 +1901,7 @@ let flow = (canvas) => {
|
|
|
1952
1901
|
// one thing a measurement must not be able to do. What a band needs is
|
|
1953
1902
|
// a property of the page box in any case, not of a content box
|
|
1954
1903
|
// something may already have narrowed.
|
|
1955
|
-
adopt(canvas, reserve(
|
|
1904
|
+
adopt(canvas, reserve(unnarrowed(canvas), canvas.settings, event.page));
|
|
1956
1905
|
pinHeader(event);
|
|
1957
1906
|
},
|
|
1958
1907
|
// Everything else routes: `placed` above says what each event does, and
|
|
@@ -1975,14 +1924,17 @@ let flow = (canvas) => {
|
|
|
1975
1924
|
endOwned(event.depth);
|
|
1976
1925
|
route(event);
|
|
1977
1926
|
},
|
|
1978
|
-
},
|
|
1927
|
+
}),
|
|
1979
1928
|
finish: () => {
|
|
1980
1929
|
snapPin({ type: "report-end" });
|
|
1981
1930
|
// A root region reaches here undrained when a report declares `columns`
|
|
1982
1931
|
// and no full-width footer followed its body.
|
|
1983
|
-
closeRegion(
|
|
1932
|
+
closeRegion();
|
|
1984
1933
|
flush(state, 0);
|
|
1985
|
-
return {
|
|
1934
|
+
return {
|
|
1935
|
+
marks: state.marks,
|
|
1936
|
+
pages: numbered(canvas.count, state.starts),
|
|
1937
|
+
};
|
|
1986
1938
|
},
|
|
1987
1939
|
};
|
|
1988
1940
|
};
|