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