@quario/pdf 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 +96 -0
- package/README.md +6 -4
- package/lib/embed.js +111 -0
- package/lib/index.d.ts +5 -14
- package/lib/index.js +35 -132
- package/lib/painter.js +205 -0
- package/package.json +6 -4
- package/lib/balance.js +0 -90
- package/lib/canvas.js +0 -376
- package/lib/fonts.js +0 -180
- package/lib/image.js +0 -58
- package/lib/layout.js +0 -1663
- package/lib/style.js +0 -106
- package/lib/text.js +0 -247
package/lib/layout.js
DELETED
|
@@ -1,1663 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Laying the event stream out on pages: items flow as wrapped blocks, tables
|
|
3
|
-
* are measured whole and emitted row by row with the header replayed after
|
|
4
|
-
* every break, and group headers wait for the content they introduce and then
|
|
5
|
-
* travel with it — a page a group instance continues onto opens with the same
|
|
6
|
-
* headers the page that introduced it did.
|
|
7
|
-
*
|
|
8
|
-
* Page state is the drawing surface's (`canvas.js`), so most of what follows
|
|
9
|
-
* takes a `Canvas` and nothing else. Pagination is the exception, and honestly
|
|
10
|
-
* so: `turn`, `item` and the table's `Grid` take the flow, because a page turn
|
|
11
|
-
* is now the moment the open groups get their headers back. What stays here is
|
|
12
|
-
* the band flow's own bookkeeping — the headers still waiting, the ones a turn
|
|
13
|
-
* replays, the space owed before the next group instance, and the outline marks
|
|
14
|
-
* it anchors as it goes. `flow` at the foot of the file is what owns that
|
|
15
|
-
* record: it hands out every handler a render walks the stream through, and a
|
|
16
|
-
* `finish` that flushes what is left.
|
|
17
|
-
* Nothing else can reach them, and no caller adds a handler of its own, so the
|
|
18
|
-
* order those handlers impose is not an order a caller can get wrong.
|
|
19
|
-
*
|
|
20
|
-
* Page furniture is here too, both halves of it: how much of the page a
|
|
21
|
-
* document's bands claim before anything is placed, and where they are drawn
|
|
22
|
-
* on each finished page once the count is known.
|
|
23
|
-
*/
|
|
24
|
-
import { display, isReportBand, text } from "quario";
|
|
25
|
-
import { balance } from "./balance.js";
|
|
26
|
-
import { adopt, frame, measuring } from "./canvas.js";
|
|
27
|
-
import { intrinsic } from "./image.js";
|
|
28
|
-
import { BAND, GUTTER, LEAD, PADX, PADY, col, roled, shift, sizeOf, stack } from "./style.js";
|
|
29
|
-
import { atoms, dress, heightOf, wrap } from "./text.js";
|
|
30
|
-
|
|
31
|
-
/** @typedef {import('./balance.js').Unit} Unit */
|
|
32
|
-
/** @typedef {import('./canvas.js').Canvas} Canvas */
|
|
33
|
-
/** @typedef {import('./canvas.js').Frame} Frame */
|
|
34
|
-
/** @typedef {import('./text.js').Line} Line */
|
|
35
|
-
|
|
36
|
-
// `open` is a stack, one entry per group instance still open: the header items
|
|
37
|
-
// it holds back until content arrives, and — once those are drawn — the blocks
|
|
38
|
-
// a page turn replays them from. `gap` is the space owed before the next group
|
|
39
|
-
// instance, `marks` the outline entries, which `anchor` positions from the
|
|
40
|
-
// cursor, and `starts` the page indexes where a `reset: "page"` sequence
|
|
41
|
-
// begins (the document itself is the sequence that starts at page 0).
|
|
42
|
-
/**
|
|
43
|
-
* @typedef {{ held: any[], blocks: Block[] }} Group
|
|
44
|
-
*/
|
|
45
|
-
/**
|
|
46
|
-
* @typedef {{ canvas: Canvas, open: Group[], gap: number, marks: any[],
|
|
47
|
-
* starts: number[], region: Region | null, pending: Pending | null,
|
|
48
|
-
* measured: WeakMap<any, Measured>,
|
|
49
|
-
* route: (event: any, block?: Block | null) => void }} Flow
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
// One flow unit and its presentation; `h` is the full flow height. A text
|
|
53
|
-
// item's is its wrapped lines; an image item's is the picture, sized here so
|
|
54
|
-
// everything downstream -- fitting, holding a group header back, replaying it
|
|
55
|
-
// on a continued page -- reads one height and never a second kind of unit.
|
|
56
|
-
/**
|
|
57
|
-
* @typedef {{ bytes: Uint8Array, format: string, w: number, h: number }} Picture
|
|
58
|
-
*/
|
|
59
|
-
/**
|
|
60
|
-
* `align` is absent on a split, which has none of its own: alignment is a
|
|
61
|
-
* slot's, reached through the style layering.
|
|
62
|
-
* @typedef {{ lines: Line[], picture?: Picture, parts?: SlotPart[],
|
|
63
|
-
* bg: string | null, align?: any, h: number }} Block
|
|
64
|
-
*/
|
|
65
|
-
/**
|
|
66
|
-
* One slot of a split, as laid out: the block it drew to and the width it was
|
|
67
|
-
* measured at. There is no offset — a slot's is the sum of the widths before
|
|
68
|
-
* it, which `drawSplit` accumulates as it walks them.
|
|
69
|
-
* @typedef {{ block: Block, w: number }} SlotPart
|
|
70
|
-
*/
|
|
71
|
-
|
|
72
|
-
// How `fit` sizes a picture against the width it has: `natural` is the
|
|
73
|
-
// image's own size in points, never wider than the content box; `width`
|
|
74
|
-
// scales to the content box either way. The ratio is kept in both, so a
|
|
75
|
-
// height is never anything but the width's consequence.
|
|
76
|
-
/** @type {(event: any, avail: number) => Picture} */
|
|
77
|
-
let pictureOf = (event, avail) => {
|
|
78
|
-
let { w, h } = intrinsic(event.bytes, event.format);
|
|
79
|
-
let width = event.fit === "width" ? avail : Math.min(w, avail);
|
|
80
|
-
return { bytes: event.bytes, format: event.format, w: width, h: (h * width) / w };
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// `under` is the enclosing style a slot's own layers over — a split's, when
|
|
84
|
-
// this is one of its slots. Null everywhere else, where nothing encloses.
|
|
85
|
-
/** @type {(canvas: Canvas, event: any, avail: number, under?: any) => Block} */
|
|
86
|
-
let blockOf = (canvas, event, avail, under = null) => {
|
|
87
|
-
let style = stack(canvas, under, event.style);
|
|
88
|
-
let bg = col(style.background);
|
|
89
|
-
if (event.type === "split") return splitBlock(canvas, event, avail, style, bg);
|
|
90
|
-
if (event.type === "image") {
|
|
91
|
-
let picture = pictureOf(event, avail);
|
|
92
|
-
return { lines: [], picture, bg, align: style.align, h: picture.h };
|
|
93
|
-
}
|
|
94
|
-
let lines = dress(wrap(canvas, atoms(canvas, event.tokens, style), Math.max(avail, 1)), style);
|
|
95
|
-
return { lines, bg, align: style.align, h: heightOf(lines) };
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
// Scale a picture block down to the room it has. Best-effort, and only ever
|
|
99
|
-
// reached by an image no page could hold whole (SCHEMA.md).
|
|
100
|
-
/** @type {(block: Block, room: number) => void} */
|
|
101
|
-
let shrink = (block, room) => {
|
|
102
|
-
let picture = /** @type {Picture} */ (block.picture);
|
|
103
|
-
picture.w *= room / picture.h;
|
|
104
|
-
picture.h = room;
|
|
105
|
-
block.h = room;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// Draw one block whole at the cursor. A picture's background fills the
|
|
109
|
-
// image's own box rather than the width of the flow -- there is no line for
|
|
110
|
-
// it to sit behind. Text is placed line by line as each is drawn; a picture
|
|
111
|
-
// is one box, so where it sits in the width it was given is worked out once.
|
|
112
|
-
/** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
|
|
113
|
-
let drawPicture = (canvas, block, x, avail) => {
|
|
114
|
-
let { bytes, format, w, h } = /** @type {Picture} */ (block.picture);
|
|
115
|
-
let at = x + shift(block.align, avail - w);
|
|
116
|
-
if (block.bg) canvas.rect(block.bg, at, canvas.y - h, w, h);
|
|
117
|
-
canvas.picture(bytes, format, at, canvas.y - h, w, h);
|
|
118
|
-
canvas.y -= h;
|
|
119
|
-
canvas.fresh = false;
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
/** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
|
|
123
|
-
let drawLines = (canvas, block, x, avail) => {
|
|
124
|
-
if (block.bg) canvas.rect(block.bg, x, canvas.y - block.h, avail, block.h);
|
|
125
|
-
for (let line of block.lines) {
|
|
126
|
-
canvas.drawLine(line, x, canvas.y, avail, block.align);
|
|
127
|
-
canvas.y -= line.h;
|
|
128
|
-
}
|
|
129
|
-
canvas.fresh = false;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// An authored share fixes its slot; the width-less ones divide what is left,
|
|
133
|
-
// evenly. Evenly rather than by natural width, because a split is a line of
|
|
134
|
-
// furniture whose geometry should read off the document — the same answer the
|
|
135
|
-
// HTML target's `flex:1` gives, so one definition places alike in both.
|
|
136
|
-
// Each slot is measured at its own share and drawn from a common top, so the
|
|
137
|
-
// split is as tall as its tallest slot and the line's geometry does not move
|
|
138
|
-
// with the data. The split's own declarations are the layer under each slot's,
|
|
139
|
-
// so a split styled `bold` reads the same here as it does through CSS
|
|
140
|
-
// inheritance in the HTML target and the `under` layer in XLSX. It has no
|
|
141
|
-
// `align` of its own: alignment is a slot's, reached through that layering.
|
|
142
|
-
/**
|
|
143
|
-
* @type {(canvas: Canvas, event: any, avail: number, style: any,
|
|
144
|
-
* bg: string | null) => Block}
|
|
145
|
-
*/
|
|
146
|
-
let splitBlock = (canvas, event, avail, style, bg) => {
|
|
147
|
-
let widths = slotWidths(event.slots, avail);
|
|
148
|
-
/** @type {SlotPart[]} */
|
|
149
|
-
let parts = [];
|
|
150
|
-
let h = 0;
|
|
151
|
-
for (let [i, slot] of event.items.entries()) {
|
|
152
|
-
let block = blockOf(canvas, slot, Math.max(widths[i], 1), style);
|
|
153
|
-
if (block.h > h) h = block.h;
|
|
154
|
-
parts.push({ block, w: widths[i] });
|
|
155
|
-
}
|
|
156
|
-
return { lines: [], parts, bg, h };
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
/** @type {(slots: { width?: number }[], avail: number) => number[]} */
|
|
160
|
-
let slotWidths = (slots, avail) => {
|
|
161
|
-
let autos = 0,
|
|
162
|
-
spent = 0;
|
|
163
|
-
/** @type {(number | null)[]} */
|
|
164
|
-
let fixed = slots.map(({ width }) => {
|
|
165
|
-
// Bound to a local first: `Number.isFinite` does not narrow a property.
|
|
166
|
-
if (width === undefined || !Number.isFinite(width)) return (autos++, null);
|
|
167
|
-
let share = (width / 100) * avail;
|
|
168
|
-
spent += share;
|
|
169
|
-
return share;
|
|
170
|
-
});
|
|
171
|
-
let each = autos ? Math.max(avail - spent, 0) / autos : 0;
|
|
172
|
-
return fixed.map((width) => width ?? each);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// A split reaches this target as a bracket around ordinary item and image
|
|
176
|
-
// events. Folding it into one event is what lets the rest of the target treat
|
|
177
|
-
// it as a single block -- measured, kept together, and paged by exactly the
|
|
178
|
-
// machinery every other block goes through. Stateful rather than a transform
|
|
179
|
-
// over an array, because the body walk is streaming; the page-band pass drives
|
|
180
|
-
// the same folder over its own list, so the two cannot drift.
|
|
181
|
-
// The event types that become a `Block`. They are also exactly the three the
|
|
182
|
-
// split folder owns: item and image reach `route` through it, so a slot's are
|
|
183
|
-
// collected rather than placed, and `split` is minted by the folder and never
|
|
184
|
-
// arrives from the stream at all.
|
|
185
|
-
let BLOCKS = new Set(["item", "image", "split"]);
|
|
186
|
-
|
|
187
|
-
/** @type {(emit: (event: any) => void) => (event: any) => void} */
|
|
188
|
-
let splitFolder = (emit) => {
|
|
189
|
-
/** @type {{ event: any, items: any[] } | null} */
|
|
190
|
-
let bracket = null;
|
|
191
|
-
// The engine emits the bracket as one array, so `split-end` always has its
|
|
192
|
-
// opening and this never reads a null.
|
|
193
|
-
let close = () => {
|
|
194
|
-
let { event, items } = /** @type {{ event: any, items: any[] }} */ (bracket);
|
|
195
|
-
bracket = null;
|
|
196
|
-
emit({ type: "split", role: event.role, style: event.style, slots: event.slots, items });
|
|
197
|
-
};
|
|
198
|
-
return (event) => {
|
|
199
|
-
if (event.type === "split-start") bracket = { event, items: [] };
|
|
200
|
-
else if (event.type === "split-end") close();
|
|
201
|
-
else if (bracket) bracket.items.push(event);
|
|
202
|
-
else emit(event);
|
|
203
|
-
};
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
/** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
|
|
207
|
-
let drawSplit = (canvas, block, x, avail) => {
|
|
208
|
-
if (block.bg) canvas.rect(block.bg, x, canvas.y - block.h, avail, block.h);
|
|
209
|
-
let top = canvas.y;
|
|
210
|
-
let at = x;
|
|
211
|
-
for (let part of block.parts || []) {
|
|
212
|
-
canvas.y = top;
|
|
213
|
-
drawBlock(canvas, part.block, at, part.w);
|
|
214
|
-
at += part.w;
|
|
215
|
-
}
|
|
216
|
-
// The split takes its own height whatever the slots took, so a short slot
|
|
217
|
-
// beside a wrapped one does not pull the band up.
|
|
218
|
-
canvas.y = top - block.h;
|
|
219
|
-
canvas.fresh = false;
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
/** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
|
|
223
|
-
let drawBlock = (canvas, block, x, avail) =>
|
|
224
|
-
(block.parts ? drawSplit : block.picture ? drawPicture : drawLines)(canvas, block, x, avail);
|
|
225
|
-
|
|
226
|
-
// A region is a run of strips: what a columned node's content flows down
|
|
227
|
-
// (CONTEXT.md "Strip"). `index` is the strip being filled, `top` the y every
|
|
228
|
-
// strip on this page starts from, `ends` where the finished ones stopped, and
|
|
229
|
-
// `floor` the bottom the current strip fills to — the page bottom while
|
|
230
|
-
// filling, a shorter target while balancing. `owner` is the depth of the group
|
|
231
|
-
// that declared the count, or -1 for the report root.
|
|
232
|
-
/**
|
|
233
|
-
* @typedef {{ width: number, count: number, index: number, top: number,
|
|
234
|
-
* height: number | null, ends: number[], owner: number,
|
|
235
|
-
* held: any[] | null, measured: number }} Region
|
|
236
|
-
*/
|
|
237
|
-
/**
|
|
238
|
-
* @typedef {{ count: number, owner: number }} Pending
|
|
239
|
-
*/
|
|
240
|
-
|
|
241
|
-
// Where the cursor's column starts and how wide it is: the strip's, inside a
|
|
242
|
-
// region, and the page's content box outside one. Every drawing call reads
|
|
243
|
-
// these rather than `canvas.margin`/`canvas.content` — the canvas keeps
|
|
244
|
-
// presenting the page, because `pageBox` re-derives page furniture from it.
|
|
245
|
-
/** @type {(state: Flow) => number} */
|
|
246
|
-
let originOf = (state) =>
|
|
247
|
-
state.canvas.margin + (state.region ? state.region.index * (state.region.width + GUTTER) : 0);
|
|
248
|
-
/** @type {(state: Flow) => number} */
|
|
249
|
-
let widthOf = (state) => (state.region ? state.region.width : state.canvas.content);
|
|
250
|
-
// The bottom the cursor fills to. A balanced strip's floor sits above the page
|
|
251
|
-
// bottom and is a target rather than a bound: a fresh strip still takes at
|
|
252
|
-
// least one line, so a block that fits the page is placed even where it
|
|
253
|
-
// overruns. The last strip answers to the page instead — a balanced share is
|
|
254
|
-
// rounded, so the strips before it may each stop a little short, and the
|
|
255
|
-
// remainder has to land somewhere.
|
|
256
|
-
/** @type {(state: Flow) => number} */
|
|
257
|
-
let floorOf = (state) => {
|
|
258
|
-
let region = state.region;
|
|
259
|
-
if (!region || region.height == null || region.index === region.count - 1)
|
|
260
|
-
return state.canvas.bottom;
|
|
261
|
-
return region.top - region.height;
|
|
262
|
-
};
|
|
263
|
-
// The y a fresh column starts from: a strip's shared top inside a region.
|
|
264
|
-
/** @type {(state: Flow) => number} */
|
|
265
|
-
let ceilOf = (state) => (state.region ? state.region.top : state.canvas.top);
|
|
266
|
-
// Has this column taken anything yet? Answered positionally, because the only
|
|
267
|
-
// thing that marks a page used is the cursor moving off its top, and the
|
|
268
|
-
// drawing primitives that move it take a `Canvas` rather than a `Flow`.
|
|
269
|
-
// `canvas.fresh` stays the *page*'s own answer — it is what keeps `turn` from
|
|
270
|
-
// leaving an empty page behind — so a strip claims no second meaning for it.
|
|
271
|
-
// A block of no height would read fresh here and used there; none is reachable
|
|
272
|
-
// today, since every item wraps to at least one line.
|
|
273
|
-
/** @type {(state: Flow) => boolean} */
|
|
274
|
-
let freshOf = (state) => (state.region ? state.canvas.y === state.region.top : state.canvas.fresh);
|
|
275
|
-
|
|
276
|
-
/** @type {(canvas: Canvas, h: number, line: Line, floor: number) => boolean} */
|
|
277
|
-
let hasRoom = (canvas, h, line, floor) => canvas.y - h - line.h >= floor;
|
|
278
|
-
|
|
279
|
-
// A fresh column always takes at least one line, or nothing would ever fit —
|
|
280
|
-
// which is also what makes a balanced floor a target rather than a bound.
|
|
281
|
-
/** @type {(fresh: boolean, j: number, i: number) => boolean} */
|
|
282
|
-
let takeLine = (fresh, j, i) => j === i && fresh;
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* @type {(canvas: Canvas, lines: Line[], i: number, floor: number,
|
|
286
|
-
* fresh: boolean) => { j: number, h: number }}
|
|
287
|
-
*/
|
|
288
|
-
let grow = (canvas, lines, i, floor, fresh) => {
|
|
289
|
-
let j = i,
|
|
290
|
-
h = 0,
|
|
291
|
-
n = lines.length;
|
|
292
|
-
while (j < n) {
|
|
293
|
-
if (!hasRoom(canvas, h, lines[j], floor) && !takeLine(fresh, j, i)) break;
|
|
294
|
-
h += lines[j++].h;
|
|
295
|
-
}
|
|
296
|
-
return { j, h };
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
/** @type {(n: number, j: number, i: number) => boolean} */
|
|
300
|
-
let orphan = (n, j, i) => n - j === 1 && j - i >= 2;
|
|
301
|
-
|
|
302
|
-
/** @type {(i: number, j: number, n: number, fresh: boolean) => boolean} */
|
|
303
|
-
let widow = (i, j, n, fresh) => i === 0 && j - i === 1 && n >= 2 && !fresh;
|
|
304
|
-
|
|
305
|
-
// Never leave a single line behind (an orphan) or take one across alone
|
|
306
|
-
// (a widow): give one back, or move the whole block down.
|
|
307
|
-
/**
|
|
308
|
-
* @type {(lines: Line[], i: number, j: number, h: number,
|
|
309
|
-
* fresh: boolean) => { j: number, h: number }}
|
|
310
|
-
*/
|
|
311
|
-
let mend = (lines, i, j, h, fresh) => {
|
|
312
|
-
let n = lines.length;
|
|
313
|
-
if (j >= n) return { j, h };
|
|
314
|
-
if (orphan(n, j, i)) h -= lines[--j].h;
|
|
315
|
-
if (widow(i, j, n, fresh)) j = i;
|
|
316
|
-
return { j, h };
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
// Lines `i..j` fit in this column, occupying height `h`; `j === i` means the
|
|
320
|
-
// column is spent.
|
|
321
|
-
/**
|
|
322
|
-
* @type {(canvas: Canvas, block: Block, i: number, floor: number,
|
|
323
|
-
* fresh: boolean) => { j: number, h: number }}
|
|
324
|
-
*/
|
|
325
|
-
let fitLines = (canvas, block, i, floor, fresh) => {
|
|
326
|
-
let { j, h } = grow(canvas, block.lines, i, floor, fresh);
|
|
327
|
-
return mend(block.lines, i, j, h, fresh);
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
/** @type {(canvas: Canvas, block: Block, floor: number) => boolean} */
|
|
331
|
-
let whole = (canvas, block, floor) => canvas.y - block.h >= floor;
|
|
332
|
-
|
|
333
|
-
// The unsplittable blocks -- a picture and a split -- move whole to a fresh
|
|
334
|
-
// column rather than breaking, so a balanced floor never cuts one: only the
|
|
335
|
-
// page's own floor bears on it. A picture that no column can hold is scaled
|
|
336
|
-
// down to the one it lands on, since obeying the guarantee literally would
|
|
337
|
-
// mean dropping it (SCHEMA.md, best-effort); a split has nothing to scale, so
|
|
338
|
-
// one taller than any column renders in full past the bottom margin, the
|
|
339
|
-
// posture a paragraph line taller than a page already has.
|
|
340
|
-
//
|
|
341
|
-
// A split also *cannot* be sliced: its own `lines` are empty -- the content
|
|
342
|
-
// lives in its slots -- so the slicing path would draw nothing and leave the
|
|
343
|
-
// cursor where it stood, taking everything after it down with it.
|
|
344
|
-
/** @type {(state: Flow, block: Block) => void} */
|
|
345
|
-
let placeWhole = (state, block) => {
|
|
346
|
-
advance(state);
|
|
347
|
-
let canvas = state.canvas;
|
|
348
|
-
let room = canvas.y - canvas.bottom;
|
|
349
|
-
if (block.picture && block.h > room) shrink(block, room);
|
|
350
|
-
drawBlock(canvas, block, originOf(state), widthOf(state));
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* @type {(canvas: Canvas, block: Block, x: number, avail: number, lines: Line[],
|
|
355
|
-
* i: number, j: number, h: number) => number}
|
|
356
|
-
*/
|
|
357
|
-
let fillSlice = (canvas, block, x, avail, lines, i, j, h) => {
|
|
358
|
-
if (block.bg) canvas.rect(block.bg, x, canvas.y - h, avail, h);
|
|
359
|
-
while (i < j) {
|
|
360
|
-
canvas.drawLine(lines[i], x, canvas.y, avail, block.align);
|
|
361
|
-
canvas.y -= lines[i++].h;
|
|
362
|
-
}
|
|
363
|
-
canvas.fresh = false;
|
|
364
|
-
return i;
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
/** @type {(state: Flow, block: Block, lines: Line[], i: number, n: number) => number} */
|
|
368
|
-
let paintSlice = (state, block, lines, i, n) => {
|
|
369
|
-
let canvas = state.canvas;
|
|
370
|
-
let { j, h } = fitLines(canvas, block, i, floorOf(state), freshOf(state));
|
|
371
|
-
if (j === i) {
|
|
372
|
-
advance(state);
|
|
373
|
-
return i;
|
|
374
|
-
}
|
|
375
|
-
i = fillSlice(canvas, block, originOf(state), widthOf(state), lines, i, j, h);
|
|
376
|
-
if (i < n) advance(state);
|
|
377
|
-
return i;
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
/** @type {(state: Flow, block: Block) => void} */
|
|
381
|
-
let slice = (state, block) => {
|
|
382
|
-
let lines = block.lines,
|
|
383
|
-
n = lines.length,
|
|
384
|
-
i = 0;
|
|
385
|
-
while (i < n) i = paintSlice(state, block, lines, i, n);
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
// Flow a block down the column, breaking where `fitLines` says. Each slice
|
|
389
|
-
// paints its background before its lines.
|
|
390
|
-
/** @type {(state: Flow, event: any, measured?: Block | null) => void} */
|
|
391
|
-
let item = (state, event, measured = null) => {
|
|
392
|
-
let canvas = state.canvas;
|
|
393
|
-
let avail = widthOf(state);
|
|
394
|
-
let block = measured || blockOf(canvas, event, avail);
|
|
395
|
-
// Whole block fits where it stands: one piece, no slicing.
|
|
396
|
-
if (whole(canvas, block, floorOf(state))) return drawBlock(canvas, block, originOf(state), avail);
|
|
397
|
-
overflow(state, block);
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
// What a block does when it does not fit where it stands. A picture and a
|
|
401
|
-
// split never break -- each moves whole to a fresh column; anything else flows
|
|
402
|
-
// down it, breaking where `fitLines` says.
|
|
403
|
-
/** @type {(state: Flow, block: Block) => void} */
|
|
404
|
-
let overflow = (state, block) =>
|
|
405
|
-
block.picture || block.parts ? placeWhole(state, block) : slice(state, block);
|
|
406
|
-
|
|
407
|
-
// The height a page turn carries over: the drawn headers of the groups still
|
|
408
|
-
// open, which every page their instances continue onto replays. Computed rather
|
|
409
|
-
// than kept — a stored total is a second copy of `open` that can drift from it.
|
|
410
|
-
// Never "running": CONTEXT.md spends that word on the accumulators under `run`.
|
|
411
|
-
//
|
|
412
|
-
// What a page has left after it is what `flush` measures a run against, so a
|
|
413
|
-
// repeat always leaves room for the content unit it introduces. That room is
|
|
414
|
-
// measured from the group's *first* unit, though: a later item with lines
|
|
415
|
-
// taller than what is left is drawn past the bottom margin all the same, as a
|
|
416
|
-
// line taller than a page always was. SCHEMA.md says so rather than guarding
|
|
417
|
-
// it — the alternative is dropping content.
|
|
418
|
-
/** @type {(state: Flow) => number} */
|
|
419
|
-
let carried = (state) =>
|
|
420
|
-
state.open.reduce(
|
|
421
|
-
(total, group) => total + group.blocks.reduce((height, block) => height + block.h, 0),
|
|
422
|
-
0,
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
// Break unless the page is still fresh, where a turn would only leave an empty
|
|
426
|
-
// page behind. Every break goes through here — the elective ones and the
|
|
427
|
-
// overflows alike — because a fresh page owes the open groups their headers
|
|
428
|
-
// back, and one owner for that is one place to get it right.
|
|
429
|
-
//
|
|
430
|
-
// The replayed run is not content, and `fresh` stays true across it: that is
|
|
431
|
-
// what keeps `fitLines` taking at least one line here (so a run of turns always
|
|
432
|
-
// advances), keeps `put` from breaking straight back out, and keeps a second
|
|
433
|
-
// turn a no-op. The blocks are the ones `flush` drew, repainted exactly as
|
|
434
|
-
// `carryOver` repaints a table's header row — `adopt` moves only the top and
|
|
435
|
-
// bottom, so the content width is fixed for the render and a block measured on
|
|
436
|
-
// page one is valid on page nine.
|
|
437
|
-
/** @type {(state: Flow) => void} */
|
|
438
|
-
let turn = (state) => {
|
|
439
|
-
let canvas = state.canvas;
|
|
440
|
-
if (canvas.fresh) return;
|
|
441
|
-
canvas.newPage();
|
|
442
|
-
replayOpen(state);
|
|
443
|
-
canvas.fresh = true;
|
|
444
|
-
if (state.region) restrip(state.region, canvas.y);
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
// What a fresh page owes the instances still open: their drawn headers again.
|
|
448
|
-
// Under a region the replay goes into the first strip, at the width the blocks
|
|
449
|
-
// were measured at — they are the region's content continuing, not page
|
|
450
|
-
// furniture, and drawing them across the page would straddle every strip below
|
|
451
|
-
// them. The strips then restart under the run.
|
|
452
|
-
/** @type {(state: Flow) => void} */
|
|
453
|
-
let replayOpen = (state) => {
|
|
454
|
-
if (state.region) state.region.index = 0;
|
|
455
|
-
let x = originOf(state),
|
|
456
|
-
avail = widthOf(state);
|
|
457
|
-
for (let group of state.open)
|
|
458
|
-
for (let block of group.blocks) drawBlock(state.canvas, block, x, avail);
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
// Every strip on a page shares a top and, while balancing, a floor. Reset
|
|
462
|
-
// together so a page turn and a region opening say the same thing once.
|
|
463
|
-
/** @type {(region: Region, top: number) => void} */
|
|
464
|
-
let restrip = (region, top) => {
|
|
465
|
-
region.index = 0;
|
|
466
|
-
region.top = top;
|
|
467
|
-
region.ends = [];
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
// The column break: move to the next strip, and only turn the page once the
|
|
471
|
-
// last one is spent. Outside a region this is a page turn and nothing else,
|
|
472
|
-
// which is why every break in this file goes through here.
|
|
473
|
-
/** @type {(state: Flow) => void} */
|
|
474
|
-
let advance = (state) => {
|
|
475
|
-
let region = state.region;
|
|
476
|
-
if (!region) return turn(state);
|
|
477
|
-
// Nothing in this column yet: advancing would leave it empty and the next
|
|
478
|
-
// one would refuse the same block, so the caller must place it here.
|
|
479
|
-
if (freshOf(state)) return;
|
|
480
|
-
region.ends.push(state.canvas.y);
|
|
481
|
-
if (region.index + 1 < region.count) {
|
|
482
|
-
region.index++;
|
|
483
|
-
state.canvas.y = region.top;
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
// Spent: a real page, which restrips through `turn`.
|
|
487
|
-
turn(state);
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
// --- regions ----------------------------------------------------------------
|
|
491
|
-
|
|
492
|
-
// One strip's width: the content box less the gutters, shared out. Needed
|
|
493
|
-
// before a region exists, because a keep-with block is measured against the
|
|
494
|
-
// width the item is about to land in.
|
|
495
|
-
/** @type {(state: Flow, count: number) => number} */
|
|
496
|
-
let stripWidth = (state, count) => (state.canvas.content - GUTTER * (count - 1)) / count;
|
|
497
|
-
|
|
498
|
-
// Open the region a `columns` count owed, at the first content that belongs in
|
|
499
|
-
// it. Deferred to here rather than opened where the count arrived, because the
|
|
500
|
-
// declaring node's own header run is full-width and has to be drawn first —
|
|
501
|
-
// `flush` does that immediately above every call site.
|
|
502
|
-
//
|
|
503
|
-
// It opens buffering: what the strips do with the content depends on how much
|
|
504
|
-
// of it there is, and that is not known until the region ends or outgrows a
|
|
505
|
-
// page. `held` is that buffer; `null` means the region has committed.
|
|
506
|
-
/** @type {(state: Flow) => void} */
|
|
507
|
-
let openRegion = (state) => {
|
|
508
|
-
let owed = state.pending;
|
|
509
|
-
if (!owed || state.region) return;
|
|
510
|
-
state.pending = null;
|
|
511
|
-
let canvas = state.canvas;
|
|
512
|
-
state.region = {
|
|
513
|
-
width: stripWidth(state, owed.count),
|
|
514
|
-
count: owed.count,
|
|
515
|
-
index: 0,
|
|
516
|
-
top: canvas.y,
|
|
517
|
-
height: null,
|
|
518
|
-
ends: [],
|
|
519
|
-
owner: owed.owner,
|
|
520
|
-
held: [],
|
|
521
|
-
measured: 0,
|
|
522
|
-
};
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
// Close the region: the cursor lands at whichever strip reached lowest, and
|
|
526
|
-
// full-width content resumes under it on the same page (SCHEMA.md).
|
|
527
|
-
/** @type {(state: Flow) => void} */
|
|
528
|
-
let closeRegion = (state) => {
|
|
529
|
-
let region = state.region;
|
|
530
|
-
if (!region) return;
|
|
531
|
-
// Committing replays the buffer, and a replayed footer closes the region on
|
|
532
|
-
// its own way through — so re-read rather than trusting the local.
|
|
533
|
-
if (region.held) commit(state);
|
|
534
|
-
if (!state.region) return;
|
|
535
|
-
let canvas = state.canvas;
|
|
536
|
-
canvas.y = Math.min(canvas.y, ...region.ends);
|
|
537
|
-
state.region = null;
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
// The region's own entries as the things a strip places (`balance.js`). Only
|
|
541
|
-
// text flows; a group instance, a table's headings row, a table row and an
|
|
542
|
-
// image all move whole, and `head` is where each says what it costs when it is
|
|
543
|
-
// the first thing in a strip: an instance drops its opening gap there, a row
|
|
544
|
-
// pays for the column headings the table restates above it, and the other two
|
|
545
|
-
// cost the same wherever they land.
|
|
546
|
-
//
|
|
547
|
-
// Two things this deliberately does not model, each stated because the cost of
|
|
548
|
-
// getting them wrong is a strip that stops early rather than a wrong document.
|
|
549
|
-
// A `total-row` is a row like any other, though it really keeps the last data
|
|
550
|
-
// row with it (SCHEMA.md): pairing them costs the flat per-entry map this is,
|
|
551
|
-
// to remove an error of one row at the end of a table. And a row taller than
|
|
552
|
-
// any strip is sliced rather than moved (SCHEMA.md, "How a tall row slices");
|
|
553
|
-
// called whole here, it makes every candidate height fail and the region
|
|
554
|
-
// fills, which is the honest answer for a region holding one.
|
|
555
|
-
//
|
|
556
|
-
// Row heights themselves are real, not the buffer's one-line estimate —
|
|
557
|
-
// `remeasure` corrects them when the table closes.
|
|
558
|
-
/** @type {(state: Flow, own: any[]) => Unit[]} */
|
|
559
|
-
let unitsOf = (state, own) => {
|
|
560
|
-
let table = { headings: 0 };
|
|
561
|
-
return own.map((entry) => unitFor(state.canvas, entry, table));
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
// What the open table costs a strip that has to restate it, carried between
|
|
565
|
-
// entries because a row is what pays for the headings and a row cannot see
|
|
566
|
-
// them.
|
|
567
|
-
/**
|
|
568
|
-
* @typedef {{ headings: number }} Restated
|
|
569
|
-
*/
|
|
570
|
-
// One entry's cost, by what it is.
|
|
571
|
-
/** @type {(canvas: Canvas, entry: any, table: Restated) => Unit} */
|
|
572
|
-
let unitFor = (canvas, entry, table) => {
|
|
573
|
-
if (entry.span != null) return unsplit(entry.span, headOf(canvas, entry.span));
|
|
574
|
-
return (UNITS[entry.event.type] || flows)(entry, table);
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
/** @type {(h: number, head: number) => Unit} */
|
|
578
|
-
let unsplit = (h, head) => ({ whole: true, h, head });
|
|
579
|
-
/** @type {(entry: any) => Unit} */
|
|
580
|
-
let flows = (entry) => ({ whole: false, h: entry.h, head: entry.h });
|
|
581
|
-
|
|
582
|
-
// What each kind of entry that does not flow costs. A module constant for the
|
|
583
|
-
// reason `WORTH` is one: this runs once per buffered entry. It is not `WORTH`
|
|
584
|
-
// with more names — that answers what an event adds to the running total when
|
|
585
|
-
// it carries no block, so it holds `group-start` (whose gap nothing else
|
|
586
|
-
// counts) and no `image` (which always carries one).
|
|
587
|
-
/** @type {Record<string, (entry: any, table: Restated) => Unit>} */
|
|
588
|
-
let UNITS = {
|
|
589
|
-
// The headings row is a unit itself, and opening the table is where what a
|
|
590
|
-
// later strip owes for restating it is recorded.
|
|
591
|
-
"table-start": (entry, table) => {
|
|
592
|
-
table.headings = entry.h;
|
|
593
|
-
return unsplit(entry.h, entry.h);
|
|
594
|
-
},
|
|
595
|
-
row: restated,
|
|
596
|
-
"total-row": restated,
|
|
597
|
-
image: unbroken,
|
|
598
|
-
split: unbroken,
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
// An unsplittable block owes a strip nothing but its own height.
|
|
602
|
-
/** @type {(entry: any) => Unit} */
|
|
603
|
-
function unbroken(entry) {
|
|
604
|
-
return unsplit(entry.h, entry.h);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// A row opening a strip carries the column headings restated above it.
|
|
608
|
-
/** @type {(entry: any, table: Restated) => Unit} */
|
|
609
|
-
function restated(entry, table) {
|
|
610
|
-
return unsplit(entry.h, entry.h + table.headings);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// The one number the buffer decides. A region that fits a page balances — the
|
|
614
|
-
// shortest strip height that still holds its units in `count` strips — and a
|
|
615
|
-
// longer one, or one no such height exists for, fills each strip to the page
|
|
616
|
-
// bottom. Filling is the taller floor of the two, so committing to it is
|
|
617
|
-
// committing to nothing: the strips simply take what they take.
|
|
618
|
-
/** @type {(state: Flow, region: Region, own: any[]) => number | null} */
|
|
619
|
-
let stripHeight = (state, region, own) =>
|
|
620
|
-
outgrown(state, region)
|
|
621
|
-
? null
|
|
622
|
-
: balance(unitsOf(state, own), region.count, roomOf(state, region), rowHeight(state.canvas));
|
|
623
|
-
|
|
624
|
-
/** @type {(state: Flow) => void} */
|
|
625
|
-
let commit = (state) => {
|
|
626
|
-
let region = /** @type {Region} */ (state.region);
|
|
627
|
-
let held = /** @type {any[]} */ (region.held);
|
|
628
|
-
// Marking the spans is also what picks out the region's own children, and
|
|
629
|
-
// the height is decided from those — so it runs before the replay, not with
|
|
630
|
-
// it.
|
|
631
|
-
region.height = stripHeight(state, region, spans(held));
|
|
632
|
-
region.held = null;
|
|
633
|
-
restrip(region, region.top);
|
|
634
|
-
for (let entry of held) {
|
|
635
|
-
if (breaksFor(state, region, entry.span)) advance(state);
|
|
636
|
-
state.route(entry.event, entry.block);
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
// A group instance is the atomic unit inside a region: one that would cross a
|
|
641
|
-
// strip boundary, and would fit a strip of its own, opens the next one
|
|
642
|
-
// instead. An instance no strip can hold answers false and continues, the way
|
|
643
|
-
// anything too tall does (SCHEMA.md). What a fresh strip has to hold is the
|
|
644
|
-
// span less the opening gap, which `settle` drops at a strip head — the same
|
|
645
|
-
// unit `balance.js` packs. Only a buffered `group-start` carries a span;
|
|
646
|
-
// everything else flows.
|
|
647
|
-
/** @type {(state: Flow, region: Region, span: number | undefined) => boolean} */
|
|
648
|
-
let breaksFor = (state, region, span) =>
|
|
649
|
-
span != null &&
|
|
650
|
-
!freshOf(state) &&
|
|
651
|
-
state.canvas.y - span < floorOf(state) &&
|
|
652
|
-
headOf(state.canvas, span) <= roomOf(state, region);
|
|
653
|
-
|
|
654
|
-
// How much one strip of this region can hold, and whether the buffer has more
|
|
655
|
-
// than every strip together could take. The balance-or-fill decision and the
|
|
656
|
-
// early commit that gives up on balancing are the same question, so they are
|
|
657
|
-
// the same predicate.
|
|
658
|
-
/** @type {(state: Flow, region: Region) => number} */
|
|
659
|
-
let roomOf = (state, region) => region.top - state.canvas.bottom;
|
|
660
|
-
/** @type {(state: Flow, region: Region) => boolean} */
|
|
661
|
-
let outgrown = (state, region) => region.measured > roomOf(state, region) * region.count;
|
|
662
|
-
|
|
663
|
-
// Mark every buffered `group-start` with the height of the instance it opens,
|
|
664
|
-
// so the replay can keep it whole. Only the region's own children carry one:
|
|
665
|
-
// an inner instance is held by the outer one it belongs to.
|
|
666
|
-
//
|
|
667
|
-
// The empty stack is also what says an entry is the region's own child, so the
|
|
668
|
-
// same walk hands those back — one traversal answers both questions rather
|
|
669
|
-
// than two tracking the same nesting.
|
|
670
|
-
/** @type {(held: any[]) => any[]} */
|
|
671
|
-
let spans = (held) => {
|
|
672
|
-
/** @type {number[]} */
|
|
673
|
-
let stack = [];
|
|
674
|
-
/** @type {any[]} */
|
|
675
|
-
let own = [];
|
|
676
|
-
for (let [i, entry] of held.entries()) {
|
|
677
|
-
if (!stack.length) own.push(entry);
|
|
678
|
-
nest(held, stack, entry, i);
|
|
679
|
-
if (stack.length) held[stack[0]].span += entry.h;
|
|
680
|
-
}
|
|
681
|
-
return own;
|
|
682
|
-
};
|
|
683
|
-
|
|
684
|
-
// Track the instance nesting one entry at a time. Only the outermost one open
|
|
685
|
-
// carries a span, which is what makes it the region's own child.
|
|
686
|
-
/** @type {(held: any[], stack: number[], entry: any, i: number) => void} */
|
|
687
|
-
let nest = (held, stack, entry, i) => {
|
|
688
|
-
if (entry.event.type === "group-end") return void stack.pop();
|
|
689
|
-
if (entry.event.type !== "group-start") return;
|
|
690
|
-
if (!stack.length) held[i].span = 0;
|
|
691
|
-
stack.push(i);
|
|
692
|
-
};
|
|
693
|
-
|
|
694
|
-
// What a buffered event is worth to the running total when it carries no block
|
|
695
|
-
// of its own: a table row and a group's opening gap are the two that matter,
|
|
696
|
-
// and both are known without laying anything out.
|
|
697
|
-
/** @type {(state: Flow, event: any) => number} */
|
|
698
|
-
let estimate = (state, event) => (WORTH[event.type] || (() => 0))(state.canvas);
|
|
699
|
-
|
|
700
|
-
// What each kind of buffered event is worth. A module constant: `estimate` runs
|
|
701
|
-
// once per buffered event, and rebuilding this table each time would allocate
|
|
702
|
-
// a closure per table row.
|
|
703
|
-
/** @type {Record<string, (canvas: Canvas) => number>} */
|
|
704
|
-
let WORTH = {
|
|
705
|
-
row: (canvas) => rowHeight(canvas) + 2 * PADY,
|
|
706
|
-
"total-row": (canvas) => rowHeight(canvas) + 2 * PADY,
|
|
707
|
-
"table-start": (canvas) => rowHeight(canvas) + 2 * PADY,
|
|
708
|
-
"group-start": (canvas) => instanceGap(canvas),
|
|
709
|
-
};
|
|
710
|
-
|
|
711
|
-
// Buffer one event while the region is still deciding, keeping a running total
|
|
712
|
-
// of what it holds. A measured block rides along so the replay never wraps the
|
|
713
|
-
// same text twice; a held header counts towards the total like anything else,
|
|
714
|
-
// but the flush that draws it measures from the event, so only its height is
|
|
715
|
-
// kept. A buffer that outgrows what the strips could hold has answered the
|
|
716
|
-
// question early: it commits to filling, and everything after it streams.
|
|
717
|
-
/** @type {(state: Flow, event: any, block: Block | null) => void} */
|
|
718
|
-
let buffer = (state, event, block) => {
|
|
719
|
-
let region = /** @type {Region} */ (state.region);
|
|
720
|
-
let entry = entryFor(state, event, block, region.width);
|
|
721
|
-
/** @type {any[]} */ (region.held).push(entry);
|
|
722
|
-
region.measured += entry.h;
|
|
723
|
-
if (event.type === "table-end") region.measured += remeasure(state, region);
|
|
724
|
-
if (outgrown(state, region)) commit(state);
|
|
725
|
-
};
|
|
726
|
-
|
|
727
|
-
// A table's events reach the buffer one at a time, and each is worth a bare
|
|
728
|
-
// estimate — one line — until the table closes: a row's height is what its
|
|
729
|
-
// cells wrap to, that needs the column widths, and those need every cell in
|
|
730
|
-
// the table. The last event is where the table is finally in hand, so it is
|
|
731
|
-
// measured at the strip width and its entries are corrected. Both readers of
|
|
732
|
-
// those heights want the real ones: the balance model, and `outgrown`.
|
|
733
|
-
//
|
|
734
|
-
// The replay measures the table again when it draws it. That is the price of
|
|
735
|
-
// keeping `table` the one place a table is laid out, and it is paid once per
|
|
736
|
-
// table per render.
|
|
737
|
-
// The measurement is kept, keyed by the opening event, so the replay draws
|
|
738
|
-
// this table without wrapping every cell a second time. Held per flow rather
|
|
739
|
-
// than per module: the keys are one render's events.
|
|
740
|
-
/** @type {(state: Flow, region: Region) => number} */
|
|
741
|
-
let remeasure = (state, region) => {
|
|
742
|
-
let held = /** @type {any[]} */ (region.held);
|
|
743
|
-
let at = held.findLastIndex((entry) => entry.event.type === "table-start");
|
|
744
|
-
if (at < 0) return 0;
|
|
745
|
-
let span = held.slice(at);
|
|
746
|
-
let measured = measureTable(
|
|
747
|
-
state.canvas,
|
|
748
|
-
tableOf(span.map((entry) => entry.event)),
|
|
749
|
-
region.width,
|
|
750
|
-
);
|
|
751
|
-
state.measured.set(span[0].event, measured);
|
|
752
|
-
return retune(span, measured);
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
// Write the measured heights back over the estimates. `span` is the table's
|
|
756
|
-
// events in the order they were buffered — its opening, its rows, an optional
|
|
757
|
-
// total, then its end — and the measurement comes out in that same order, so
|
|
758
|
-
// the two line up by position and the end simply runs off the shorter list.
|
|
759
|
-
// Reports what the correction moved the running total by.
|
|
760
|
-
/** @type {(span: any[], measured: Measured) => number} */
|
|
761
|
-
let retune = (span, measured) => {
|
|
762
|
-
let heights = [measured.head.h, ...measured.laid.map((/** @type {any} */ laid) => laid.row.h)];
|
|
763
|
-
if (measured.totalRow) heights.push(measured.totalRow.h);
|
|
764
|
-
let delta = 0;
|
|
765
|
-
for (let [i, h] of heights.entries()) {
|
|
766
|
-
delta += h - span[i].h;
|
|
767
|
-
span[i].h = h;
|
|
768
|
-
}
|
|
769
|
-
return delta;
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
// One buffered entry: the event, the block the replay will draw it from, and
|
|
773
|
-
// what it is worth to the running total. A header keeps its block like
|
|
774
|
-
// anything else — the flush that eventually draws it takes the block from the
|
|
775
|
-
// hold rather than wrapping the text again.
|
|
776
|
-
/** @type {(state: Flow, event: any, block: Block | null, width: number) => any} */
|
|
777
|
-
let entryFor = (state, event, block, width) => {
|
|
778
|
-
let measured = block || measureFor(state, event, width);
|
|
779
|
-
return { event, block: measured, h: measured ? measured.h : estimate(state, event) };
|
|
780
|
-
};
|
|
781
|
-
|
|
782
|
-
// The block a buffered event will be drawn from, measured once at the strip
|
|
783
|
-
// width — already known, which is why this can happen before the region has
|
|
784
|
-
// decided anything. Events that are not blocks contribute an `estimate`.
|
|
785
|
-
/** @type {(state: Flow, event: any, width: number) => Block | null} */
|
|
786
|
-
let measureFor = (state, event, width) =>
|
|
787
|
-
BLOCKS.has(event.type) ? blockOf(state.canvas, event, width) : null;
|
|
788
|
-
|
|
789
|
-
// Give every outline mark still waiting a position: where its content begins.
|
|
790
|
-
/** @type {(state: Flow) => void} */
|
|
791
|
-
let anchor = (state) => {
|
|
792
|
-
for (let i = state.marks.length - 1; i >= 0 && state.marks[i].page < 0; i--) {
|
|
793
|
-
state.marks[i].page = state.canvas.count - 1;
|
|
794
|
-
state.marks[i].y = state.canvas.y;
|
|
795
|
-
// The strip too: two instances in two strips share a y, so a mark that
|
|
796
|
-
// recorded only the page and the cursor would send both bookmarks to the
|
|
797
|
-
// same place.
|
|
798
|
-
state.marks[i].x = originOf(state);
|
|
799
|
-
}
|
|
800
|
-
};
|
|
801
|
-
|
|
802
|
-
/** @type {(state: Flow) => Group[]} */
|
|
803
|
-
let pending = (state) => state.open.filter((group) => group.held.length);
|
|
804
|
-
|
|
805
|
-
// A held run, as blocks. A region measured its headers while buffering them,
|
|
806
|
-
// at this same width, so a replayed run reuses those rather than wrapping the
|
|
807
|
-
// same text a second time (ADR 0027).
|
|
808
|
-
/** @type {(canvas: Canvas, held: any[], avail: number) => Block[]} */
|
|
809
|
-
let blocksOf = (canvas, held, avail) =>
|
|
810
|
-
held.map((entry) => entry.block || blockOf(canvas, entry.event, avail));
|
|
811
|
-
|
|
812
|
-
/** @type {(state: Flow, extra: number, runs: Block[][]) => number} */
|
|
813
|
-
let needed = (state, extra, runs) =>
|
|
814
|
-
state.gap + runs.flat().reduce((total, block) => total + block.h, 0) + extra;
|
|
815
|
-
|
|
816
|
-
/** @type {(state: Flow, need: number) => boolean} */
|
|
817
|
-
let tooTall = (state, need) => need > ceilOf(state) - carried(state) - floorOf(state);
|
|
818
|
-
|
|
819
|
-
// Degenerate: taller than a page — keep nothing, paginate normally, and
|
|
820
|
-
// repeat nothing afterwards. That last part is what bounds the replayed run
|
|
821
|
-
// for good: every other path drew its headers inside the room a page still
|
|
822
|
-
// had, so a run always leaves space for the content it introduces.
|
|
823
|
-
/** @type {(state: Flow, holding: Group[]) => void} */
|
|
824
|
-
let dropHeld = (state, holding) => {
|
|
825
|
-
let held = holding.flatMap((group) => group.held);
|
|
826
|
-
for (let group of holding) group.held = [];
|
|
827
|
-
state.gap = 0;
|
|
828
|
-
anchor(state);
|
|
829
|
-
for (let entry of held) item(state, entry.event, entry.block);
|
|
830
|
-
};
|
|
831
|
-
|
|
832
|
-
/** @type {(state: Flow, need: number) => void} */
|
|
833
|
-
let settle = (state, need) => {
|
|
834
|
-
if (state.canvas.y - need < floorOf(state)) advance(state);
|
|
835
|
-
// The half-line gap is dropped at the head of a column, exactly as it is at
|
|
836
|
-
// the head of a page: three strips of labels start level or they read as a
|
|
837
|
-
// ragged grid.
|
|
838
|
-
if (!freshOf(state)) state.canvas.y -= state.gap;
|
|
839
|
-
state.gap = 0;
|
|
840
|
-
anchor(state);
|
|
841
|
-
};
|
|
842
|
-
|
|
843
|
-
// Drawn and handed back one instance at a time. Nothing in here turns a page
|
|
844
|
-
// — the turn above already did — so an instance taking its blocks the moment
|
|
845
|
-
// they are drawn can never have them replayed on top of themselves.
|
|
846
|
-
/** @type {(state: Flow, holding: Group[], runs: Block[][], avail: number) => void} */
|
|
847
|
-
let drawHeld = (state, holding, runs, avail) => {
|
|
848
|
-
let x = originOf(state);
|
|
849
|
-
for (let [i, group] of holding.entries()) {
|
|
850
|
-
for (let block of runs[i]) drawBlock(state.canvas, block, x, avail);
|
|
851
|
-
group.blocks.push(...runs[i]);
|
|
852
|
-
group.held = [];
|
|
853
|
-
}
|
|
854
|
-
};
|
|
855
|
-
|
|
856
|
-
/** @type {(state: Flow, extra: number) => void} */
|
|
857
|
-
let flush = (state, extra) => {
|
|
858
|
-
let runsFor = pending(state);
|
|
859
|
-
if (!holding(state)) return;
|
|
860
|
-
let canvas = state.canvas;
|
|
861
|
-
let avail = widthOf(state);
|
|
862
|
-
// Measured per instance, so each one takes back exactly the blocks its own
|
|
863
|
-
// headers were drawn from: no flat list to slice, no order to keep in step.
|
|
864
|
-
let runs = runsFor.map((group) => blocksOf(canvas, group.held, avail));
|
|
865
|
-
let need = needed(state, extra, runs);
|
|
866
|
-
// Against what a fresh column has left once the page has replayed what it
|
|
867
|
-
// carries. Inside a region that is a strip, and the replay sits above it.
|
|
868
|
-
if (tooTall(state, need)) return dropHeld(state, runsFor);
|
|
869
|
-
settle(state, need);
|
|
870
|
-
drawHeld(state, runsFor, runs, avail);
|
|
871
|
-
};
|
|
872
|
-
|
|
873
|
-
// Enough of an item that its group header is never left introducing nothing.
|
|
874
|
-
let KEEP_LINES = 2;
|
|
875
|
-
/** @type {(block: Block) => number} */
|
|
876
|
-
let keepWith = (block) =>
|
|
877
|
-
block.picture ? block.h : Math.min(block.h, heightOf(block.lines.slice(0, KEEP_LINES)));
|
|
878
|
-
|
|
879
|
-
// Flush any pending header with this item's opening, then place the item. The
|
|
880
|
-
// block is measured once here and handed on rather than wrapped twice.
|
|
881
|
-
/** @type {(state: Flow, event: any, measured?: Block | null) => void} */
|
|
882
|
-
let placeItem = (state, event, measured = null) => {
|
|
883
|
-
let block = measured || keepBlock(state, event, widthOf(state));
|
|
884
|
-
if (block && holding(state)) flush(state, keepWith(block));
|
|
885
|
-
item(state, event, block);
|
|
886
|
-
};
|
|
887
|
-
|
|
888
|
-
// The block a held header run must keep company with: null when nothing is
|
|
889
|
-
// waiting on it, or when this event is a header itself rather than the content
|
|
890
|
-
// one introduces. The width is the caller's, because a region measures against
|
|
891
|
-
// the strip its content is about to land in rather than the page.
|
|
892
|
-
/** @type {(state: Flow, event: any, width: number) => Block | null} */
|
|
893
|
-
let keepBlock = (state, event, width) =>
|
|
894
|
-
holding(state) && event.role !== "group-header" ? measureFor(state, event, width) : null;
|
|
895
|
-
|
|
896
|
-
// Is anything waiting on this item's arrival — a header run, or the gap an
|
|
897
|
-
// instance opens with?
|
|
898
|
-
/** @type {(state: Flow) => boolean} */
|
|
899
|
-
let holding = (state) => state.open.some((group) => group.held.length) || state.gap > 0;
|
|
900
|
-
|
|
901
|
-
// A half-line gap before each instance (dropped at a page top by `flush`)
|
|
902
|
-
// keeps groups reading as blocks. Every instance becomes an outline entry.
|
|
903
|
-
// `break: "page"` turns the page first — pending ancestor headers stay
|
|
904
|
-
// buffered across it, so they open the fresh page with their content and
|
|
905
|
-
// their own marks anchor there. `reset: "page"` turns the same way, then
|
|
906
|
-
// starts a new `page.number` / `page.total` sequence on the page it opens.
|
|
907
|
-
// A duplicate start on a page that already began a sequence is a zero-length
|
|
908
|
-
// range; `numbered` skips it.
|
|
909
|
-
/** @type {(state: Flow, event: any) => void} */
|
|
910
|
-
let openGroup = (state, event) => {
|
|
911
|
-
breakFor(state, event);
|
|
912
|
-
if (event.columns) state.pending = { count: event.columns, owner: event.depth };
|
|
913
|
-
state.gap = Math.max(state.gap, instanceGap(state.canvas));
|
|
914
|
-
state.open.push({ held: [], blocks: [] });
|
|
915
|
-
state.marks.push(markFor(event));
|
|
916
|
-
};
|
|
917
|
-
|
|
918
|
-
// A real page, inside a region as much as outside one: ADR 0013 declined a
|
|
919
|
-
// `break: "column"`, so this never means the next strip. `turn` restrips.
|
|
920
|
-
/** @type {(state: Flow, event: any) => void} */
|
|
921
|
-
let breakFor = (state, event) => {
|
|
922
|
-
if (event.break === "page" || event.reset === "page") turn(state);
|
|
923
|
-
if (event.reset === "page") state.starts.push(state.canvas.count - 1);
|
|
924
|
-
};
|
|
925
|
-
|
|
926
|
-
/** @type {(event: any) => any} */
|
|
927
|
-
let markFor = (event) => ({
|
|
928
|
-
// display(), not String(): a Date group key must title its outline bookmark
|
|
929
|
-
// with the same ISO 8601 UTC text its cells render, on every machine.
|
|
930
|
-
title: event.name + ": " + display(event.key),
|
|
931
|
-
titled: false,
|
|
932
|
-
depth: event.depth,
|
|
933
|
-
page: -1,
|
|
934
|
-
y: 0,
|
|
935
|
-
});
|
|
936
|
-
|
|
937
|
-
// Headers wait for the content they introduce, so one is never stranded at a
|
|
938
|
-
// page bottom. The instance's first header titles its outline entry.
|
|
939
|
-
/** @type {(state: Flow, event: any, title: string | null, block?: Block | null) => void} */
|
|
940
|
-
let holdHeader = (state, event, title, block = null) => {
|
|
941
|
-
let mark = state.marks[state.marks.length - 1];
|
|
942
|
-
// A picture has no words to title an outline entry with, so it holds its
|
|
943
|
-
// place in the run and leaves the title to the next header that has some.
|
|
944
|
-
if (mark && !mark.titled && title != null) {
|
|
945
|
-
mark.titled = true;
|
|
946
|
-
mark.title = title;
|
|
947
|
-
}
|
|
948
|
-
state.open[state.open.length - 1].held.push({ event, block });
|
|
949
|
-
};
|
|
950
|
-
|
|
951
|
-
// A table row's floor height and the gap a group instance opens with: named
|
|
952
|
-
// because the region's estimator reads both, and a constant with two readers
|
|
953
|
-
// and no name drifts between them.
|
|
954
|
-
/** @type {(canvas: Canvas) => number} */
|
|
955
|
-
let rowHeight = (canvas) => LEAD * canvas.base;
|
|
956
|
-
/** @type {(canvas: Canvas) => number} */
|
|
957
|
-
let instanceGap = (canvas) => 0.5 * LEAD * canvas.base;
|
|
958
|
-
|
|
959
|
-
// What a fresh strip has to hold to take an instance: the span less the
|
|
960
|
-
// opening gap, which `settle` drops at a strip head. This is the one fact the
|
|
961
|
-
// balance model and the placement rule have to agree on, so both read it here
|
|
962
|
-
// rather than each subtracting the gap for itself.
|
|
963
|
-
/** @type {(canvas: Canvas, span: number) => number} */
|
|
964
|
-
let headOf = (canvas, span) => span - instanceGap(canvas);
|
|
965
|
-
|
|
966
|
-
// --- tables -----------------------------------------------------------------
|
|
967
|
-
|
|
968
|
-
// Pre-measure one cell: wrapped lines per column width come later; natural
|
|
969
|
-
// width first (no wrapping except hard breaks). `mt`/`mb` go unread: SCHEMA.md
|
|
970
|
-
// gives flow spacing no meaning inside a table row, in either target.
|
|
971
|
-
/** @type {(canvas: Canvas, cell: any, rowStyle: any) => any} */
|
|
972
|
-
let cellOf = (canvas, cell, rowStyle) => {
|
|
973
|
-
let style = stack(canvas, rowStyle, cell.style);
|
|
974
|
-
let list = atoms(canvas, cell.tokens, style);
|
|
975
|
-
let natural = wrap(canvas, list, Infinity).reduce((widest, line) => Math.max(widest, line.w), 0);
|
|
976
|
-
return {
|
|
977
|
-
list,
|
|
978
|
-
natural: natural + 2 * PADX,
|
|
979
|
-
align: style.align,
|
|
980
|
-
bg: col(style.background),
|
|
981
|
-
underline: !!style.underline,
|
|
982
|
-
strikethrough: !!style.strikethrough,
|
|
983
|
-
};
|
|
984
|
-
};
|
|
985
|
-
|
|
986
|
-
/** @type {(canvas: Canvas, cells: any[], widths: number[]) => { cells: any[], h: number }} */
|
|
987
|
-
let rowOf = (canvas, cells, widths) => {
|
|
988
|
-
let h = rowHeight(canvas);
|
|
989
|
-
let out = cells.map((cell, i) => {
|
|
990
|
-
let inner = Math.max(widths[i] - 2 * PADX, 1);
|
|
991
|
-
let lines = cell.list.length ? dress(wrap(canvas, cell.list, inner), cell) : [];
|
|
992
|
-
let cellHeight = heightOf(lines) + 2 * PADY;
|
|
993
|
-
if (cellHeight > h) h = cellHeight;
|
|
994
|
-
return { ...cell, lines };
|
|
995
|
-
});
|
|
996
|
-
return { cells: out, h };
|
|
997
|
-
};
|
|
998
|
-
|
|
999
|
-
/** @type {(canvas: Canvas, cell: any, bg: string | null, x: number, y: number, w: number, h: number) => void} */
|
|
1000
|
-
let fillCell = (canvas, cell, bg, x, y, w, h) => {
|
|
1001
|
-
if (cell.bg && cell.bg !== bg) canvas.rect(cell.bg, x, y, w, h);
|
|
1002
|
-
};
|
|
1003
|
-
|
|
1004
|
-
/** @type {(canvas: Canvas, cell: any, lines: any[], x: number, y: number, w: number) => void} */
|
|
1005
|
-
let writeCell = (canvas, cell, lines, x, y, w) => {
|
|
1006
|
-
for (let line of lines) {
|
|
1007
|
-
canvas.drawLine(line, x + PADX, y, w - 2 * PADX, cell.align);
|
|
1008
|
-
y -= line.h;
|
|
1009
|
-
}
|
|
1010
|
-
};
|
|
1011
|
-
|
|
1012
|
-
// Paint a band of cells `h` tall and advance past it. `lines[i]` is what
|
|
1013
|
-
// column `i` draws here: its whole cell, or one slice of a row too tall to
|
|
1014
|
-
// fit — which is why `padTop` is a parameter, a continuation starting flush.
|
|
1015
|
-
/**
|
|
1016
|
-
* @type {(canvas: Canvas, cells: any[], lines: any[][], h: number, padTop: boolean,
|
|
1017
|
-
* xOffsets: number[], widths: number[], bg: string | null) => void}
|
|
1018
|
-
*/
|
|
1019
|
-
let paintCells = (canvas, cells, lines, h, padTop, xOffsets, widths, bg) => {
|
|
1020
|
-
if (bg) canvas.rect(bg, xOffsets[0], canvas.y - h, sum(widths), h);
|
|
1021
|
-
let y0 = canvas.y - (padTop ? PADY : 0);
|
|
1022
|
-
for (let [i, cell] of cells.entries()) {
|
|
1023
|
-
fillCell(canvas, cell, bg, xOffsets[i], canvas.y - h, widths[i], h);
|
|
1024
|
-
writeCell(canvas, cell, lines[i], xOffsets[i], y0, widths[i]);
|
|
1025
|
-
}
|
|
1026
|
-
canvas.y -= h;
|
|
1027
|
-
canvas.fresh = false;
|
|
1028
|
-
};
|
|
1029
|
-
|
|
1030
|
-
/**
|
|
1031
|
-
* @type {(canvas: Canvas, row: { cells: any[], h: number }, xOffsets: number[],
|
|
1032
|
-
* widths: number[], bg: string | null) => void}
|
|
1033
|
-
*/
|
|
1034
|
-
let drawRow = (canvas, row, xOffsets, widths, bg) =>
|
|
1035
|
-
paintCells(
|
|
1036
|
-
canvas,
|
|
1037
|
-
row.cells,
|
|
1038
|
-
row.cells.map((cell) => cell.lines),
|
|
1039
|
-
row.h,
|
|
1040
|
-
true,
|
|
1041
|
-
xOffsets,
|
|
1042
|
-
widths,
|
|
1043
|
-
bg,
|
|
1044
|
-
);
|
|
1045
|
-
|
|
1046
|
-
// The widest of each column's header, rows and total, padding included.
|
|
1047
|
-
/** @type {(cols: any[], header: any[], rows: any[], total: any[] | null) => number[]} */
|
|
1048
|
-
let naturalWidths = (cols, header, rows, total) =>
|
|
1049
|
-
cols.map((_, i) => {
|
|
1050
|
-
let widest = header[i].natural;
|
|
1051
|
-
for (let row of rows) widest = Math.max(widest, row.cells[i].natural);
|
|
1052
|
-
return total ? Math.max(widest, total[i].natural) : widest;
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
/** @type {(values: number[]) => number} */
|
|
1056
|
-
let sum = (values) => values.reduce((total, value) => total + value, 0);
|
|
1057
|
-
|
|
1058
|
-
// An authored `width` percentage fixes its column; the rest share what is left
|
|
1059
|
-
// in proportion to their natural widths. Three cases, one return each — the
|
|
1060
|
-
// fall-through is all-authored within budget, honoured exactly.
|
|
1061
|
-
//
|
|
1062
|
-
// There is no over-commitment case: the engine rejects a document whose fixed
|
|
1063
|
-
// shares leave the width-less columns nothing, so `left` is positive whenever
|
|
1064
|
-
// an auto column exists and the fixed shares never exceed the content width.
|
|
1065
|
-
// That invariant is asserted rather than trusted — falling through with auto
|
|
1066
|
-
// columns unplaced would draw them at zero width, an invisible failure — the
|
|
1067
|
-
// same fail-loud posture as the measuring canvas's `newPage`.
|
|
1068
|
-
/** @type {(cols: any[], natural: number[], avail: number) => number[]} */
|
|
1069
|
-
let columnWidths = (cols, natural, avail) => {
|
|
1070
|
-
let fixed = cols.map((column) => Number.isFinite(column.width));
|
|
1071
|
-
if (!fixed.some(Boolean)) {
|
|
1072
|
-
// Natural widths always carry the cell padding, so the sum is never zero.
|
|
1073
|
-
let wanted = sum(natural);
|
|
1074
|
-
return natural.map((width) => (width * avail) / wanted);
|
|
1075
|
-
}
|
|
1076
|
-
let widths = cols.map((col, i) => (fixed[i] ? (col.width / 100) * avail : 0));
|
|
1077
|
-
let auto = natural.map((width, i) => (fixed[i] ? 0 : width));
|
|
1078
|
-
let left = avail - sum(widths);
|
|
1079
|
-
let wanted = sum(auto);
|
|
1080
|
-
// Room for the auto columns: they share what the fixed ones left.
|
|
1081
|
-
if (wanted > 0) {
|
|
1082
|
-
if (left <= 0) throw new Error("over-committed column widths reached the pdf target");
|
|
1083
|
-
return widths.map((width, i) => (fixed[i] ? width : (auto[i] * left) / wanted));
|
|
1084
|
-
}
|
|
1085
|
-
return widths;
|
|
1086
|
-
};
|
|
1087
|
-
|
|
1088
|
-
// The rule under a table's header row and over its total: the only rule this
|
|
1089
|
-
// target draws, always the full content width at the cursor.
|
|
1090
|
-
/** @type {(canvas: Canvas, x: number, avail: number) => void} */
|
|
1091
|
-
let underline = (canvas, x, avail) => canvas.rule(x, x + avail, canvas.y);
|
|
1092
|
-
|
|
1093
|
-
// One table being emitted: what `carryOver`, `sliced` and `put` all need.
|
|
1094
|
-
/**
|
|
1095
|
-
* @typedef {{ state: Flow, head: { cells: any[], h: number },
|
|
1096
|
-
* xOffsets: number[], widths: number[], x: number, avail: number }} Grid
|
|
1097
|
-
*/
|
|
1098
|
-
|
|
1099
|
-
// Continue in a fresh column: a table that spans columns or pages restates its
|
|
1100
|
-
// headings, under whatever headers the open groups replay above them. Both
|
|
1101
|
-
// callers reach here on a column something is already drawn in, so the break
|
|
1102
|
-
// always breaks.
|
|
1103
|
-
//
|
|
1104
|
-
// The headings replay at a strip head as well as a page head, unlike a group
|
|
1105
|
-
// header (SCHEMA.md): a column of figures under nothing is unreadable, where a
|
|
1106
|
-
// group header is merely absent.
|
|
1107
|
-
/** @type {(grid: Grid) => void} */
|
|
1108
|
-
let carryOver = (grid) => {
|
|
1109
|
-
advance(grid.state);
|
|
1110
|
-
rebase(grid);
|
|
1111
|
-
drawRow(grid.state.canvas, grid.head, grid.xOffsets, grid.widths, null);
|
|
1112
|
-
underline(grid.state.canvas, grid.x, grid.avail);
|
|
1113
|
-
};
|
|
1114
|
-
|
|
1115
|
-
// Move the grid to the column the cursor is now in. A table's widths are its
|
|
1116
|
-
// own for its whole life -- every strip is the same width -- but where its
|
|
1117
|
-
// columns start is the strip's, so a break across strips rebases them.
|
|
1118
|
-
/** @type {(grid: Grid) => void} */
|
|
1119
|
-
let rebase = (grid) => {
|
|
1120
|
-
grid.x = originOf(grid.state);
|
|
1121
|
-
grid.avail = widthOf(grid.state);
|
|
1122
|
-
grid.xOffsets = grid.widths.map((_, i) => grid.x + sum(grid.widths.slice(0, i)));
|
|
1123
|
-
};
|
|
1124
|
-
|
|
1125
|
-
// The least of a row that can go on a page, so a slice always advances.
|
|
1126
|
-
/** @type {(canvas: Canvas, row: { cells: any[] }) => number} */
|
|
1127
|
-
let firstSlice = (canvas, row) =>
|
|
1128
|
-
Math.max(
|
|
1129
|
-
rowHeight(canvas),
|
|
1130
|
-
...row.cells
|
|
1131
|
-
.filter((/** @type {any} */ cell) => cell.lines.length)
|
|
1132
|
-
.map((/** @type {any} */ cell) => PADY + cell.lines[0].h),
|
|
1133
|
-
);
|
|
1134
|
-
|
|
1135
|
-
/** @type {(cell: any, next: number[], i: number, used: number, cap: number) => any[]} */
|
|
1136
|
-
let takeLines = (cell, next, i, used, cap) => {
|
|
1137
|
-
/** @type {any[]} */
|
|
1138
|
-
let out = [];
|
|
1139
|
-
while (next[i] < cell.lines.length) {
|
|
1140
|
-
let h = cell.lines[next[i]].h;
|
|
1141
|
-
if (out.length && used + h > cap) break;
|
|
1142
|
-
used += h;
|
|
1143
|
-
out.push(cell.lines[next[i]++]);
|
|
1144
|
-
}
|
|
1145
|
-
return out;
|
|
1146
|
-
};
|
|
1147
|
-
|
|
1148
|
-
/** @type {(row: { cells: any[] }, next: number[]) => boolean} */
|
|
1149
|
-
let stillMore = (row, next) =>
|
|
1150
|
-
row.cells.some((/** @type {any} */ cell, /** @type {number} */ i) => next[i] < cell.lines.length);
|
|
1151
|
-
|
|
1152
|
-
/** @type {(taken: any[][], pad: number, more: boolean) => number} */
|
|
1153
|
-
let sliceH = (taken, pad, more) => {
|
|
1154
|
-
let h = Math.max(0, ...taken.map((lines) => pad + heightOf(lines)));
|
|
1155
|
-
return more ? h : h + PADY;
|
|
1156
|
-
};
|
|
1157
|
-
|
|
1158
|
-
// A row no page can hold degrades to plain flow: each page takes what fits of
|
|
1159
|
-
// each column, always at least one line, with the header replayed between.
|
|
1160
|
-
/** @type {(grid: Grid, row: { cells: any[] }, bg: string | null) => void} */
|
|
1161
|
-
let sliced = (grid, row, bg) => {
|
|
1162
|
-
let canvas = grid.state.canvas;
|
|
1163
|
-
let next = row.cells.map(() => 0); // first line of each column not yet placed
|
|
1164
|
-
let first = true;
|
|
1165
|
-
for (;;) {
|
|
1166
|
-
let cap = canvas.y - canvas.bottom;
|
|
1167
|
-
let pad = first ? PADY : 0;
|
|
1168
|
-
let taken = row.cells.map((/** @type {any} */ cell, /** @type {number} */ i) =>
|
|
1169
|
-
takeLines(cell, next, i, pad, cap),
|
|
1170
|
-
);
|
|
1171
|
-
let more = stillMore(row, next);
|
|
1172
|
-
// The closing padding belongs to the last slice only.
|
|
1173
|
-
let h = sliceH(taken, pad, more);
|
|
1174
|
-
paintCells(canvas, row.cells, taken, h, first, grid.xOffsets, grid.widths, bg);
|
|
1175
|
-
if (!more) return;
|
|
1176
|
-
carryOver(grid);
|
|
1177
|
-
first = false;
|
|
1178
|
-
}
|
|
1179
|
-
};
|
|
1180
|
-
|
|
1181
|
-
/** @type {(grid: Grid) => number} */
|
|
1182
|
-
let pageCap = (grid) =>
|
|
1183
|
-
ceilOf(grid.state) - carried(grid.state) - floorOf(grid.state) - grid.head.h;
|
|
1184
|
-
|
|
1185
|
-
/** @type {(row: { h: number }, keep: number, cap: number) => number} */
|
|
1186
|
-
let companion = (row, keep, cap) => (row.h + keep > cap ? 0 : keep);
|
|
1187
|
-
|
|
1188
|
-
/** @type {(canvas: Canvas, row: any, cap: number, floor: number) => boolean} */
|
|
1189
|
-
let worthBreak = (canvas, row, cap, floor) =>
|
|
1190
|
-
row.h <= cap || canvas.y - floor < Math.min(firstSlice(canvas, row), cap);
|
|
1191
|
-
|
|
1192
|
-
/** @type {(state: Flow, row: any, keep: number, cap: number) => boolean} */
|
|
1193
|
-
let shouldBreak = (state, row, keep, cap) => {
|
|
1194
|
-
let canvas = state.canvas,
|
|
1195
|
-
floor = floorOf(state);
|
|
1196
|
-
return canvas.y - row.h - keep < floor && worthBreak(canvas, row, cap, floor) && !freshOf(state);
|
|
1197
|
-
};
|
|
1198
|
-
|
|
1199
|
-
/** @type {(grid: Grid, topRule: boolean, broke: boolean) => void} */
|
|
1200
|
-
let openRow = (grid, topRule, broke) => {
|
|
1201
|
-
if (broke) carryOver(grid);
|
|
1202
|
-
if (topRule && !broke) underline(grid.state.canvas, grid.x, grid.avail);
|
|
1203
|
-
};
|
|
1204
|
-
|
|
1205
|
-
// Place one row, breaking first when that helps. `keep` is a companion row it
|
|
1206
|
-
// must not be parted from — the total riding with the last data row.
|
|
1207
|
-
// `topRule` is the total's opening rule, which after a break would double the
|
|
1208
|
-
// replayed header's.
|
|
1209
|
-
/**
|
|
1210
|
-
* @type {(grid: Grid, row: { cells: any[], h: number }, bg: string | null,
|
|
1211
|
-
* keep?: number, topRule?: boolean) => void}
|
|
1212
|
-
*/
|
|
1213
|
-
let put = (grid, row, bg, keep = 0, topRule = false) => {
|
|
1214
|
-
let canvas = grid.state.canvas;
|
|
1215
|
-
// The tallest row a fresh column holds, after the replayed group headers and
|
|
1216
|
-
// the replayed column headings. A companion over that can never share a
|
|
1217
|
-
// column, so it stops being a reason to break.
|
|
1218
|
-
let cap = pageCap(grid);
|
|
1219
|
-
keep = companion(row, keep, cap);
|
|
1220
|
-
// A row that will be sliced anyway is only worth breaking for while this
|
|
1221
|
-
// column cannot take even its first slice — beyond that a break buys nothing
|
|
1222
|
-
// and leaves a headings-only column. Clamped to `cap`: a first slice no fresh
|
|
1223
|
-
// column could fit either is no reason to go looking for one.
|
|
1224
|
-
let broke = shouldBreak(grid.state, row, keep, cap);
|
|
1225
|
-
openRow(grid, topRule, broke);
|
|
1226
|
-
if (canvas.y - row.h < canvas.bottom) return sliced(grid, row, bg);
|
|
1227
|
-
drawRow(canvas, row, grid.xOffsets, grid.widths, bg);
|
|
1228
|
-
};
|
|
1229
|
-
|
|
1230
|
-
/** @type {(canvas: Canvas, row: any) => { cells: any[], bg: any }} */
|
|
1231
|
-
let bodyRow = (canvas, row) => ({
|
|
1232
|
-
cells: row.cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, row.style)),
|
|
1233
|
-
bg: col((row.style || {}).background),
|
|
1234
|
-
});
|
|
1235
|
-
|
|
1236
|
-
/** @type {(canvas: Canvas, buffered: any) => any} */
|
|
1237
|
-
let totalCells = (canvas, buffered) =>
|
|
1238
|
-
buffered.total && buffered.total.map((/** @type {any} */ cell) => cellOf(canvas, cell, null));
|
|
1239
|
-
|
|
1240
|
-
/** @type {(laid: any[], totalRow: any) => number} */
|
|
1241
|
-
let firstH = (laid, totalRow) => (laid.length ? laid[0].row.h : totalRow ? totalRow.h : 0);
|
|
1242
|
-
|
|
1243
|
-
/** @type {(totalRow: any, i: number, last: number) => number} */
|
|
1244
|
-
let keepTotal = (totalRow, i, last) => (totalRow && i === last ? totalRow.h : 0);
|
|
1245
|
-
|
|
1246
|
-
/** @type {(grid: Grid, laid: any[], totalRow: any) => void} */
|
|
1247
|
-
let emitRows = (grid, laid, totalRow) => {
|
|
1248
|
-
let last = laid.length - 1;
|
|
1249
|
-
for (let [i, laidRow] of laid.entries())
|
|
1250
|
-
put(grid, laidRow.row, laidRow.bg, keepTotal(totalRow, i, last));
|
|
1251
|
-
if (totalRow) put(grid, totalRow, null, 0, true);
|
|
1252
|
-
};
|
|
1253
|
-
|
|
1254
|
-
// Lay out a buffered table: measure every cell, allocate the columns, then
|
|
1255
|
-
// emit the header, the rows and the total, breaking pages as needed.
|
|
1256
|
-
// A buffered table, in the one shape `table` lays out: its columns come from
|
|
1257
|
-
// the opening event, its rows are the row events themselves, and its total is
|
|
1258
|
-
// the total row's cells. Both readers build it here — the replay, collecting
|
|
1259
|
-
// events as they arrive, and the region buffer, reading them back off what it
|
|
1260
|
-
// held — so a new table event kind cannot reach one and miss the other.
|
|
1261
|
-
/** @type {(events: any[]) => any} */
|
|
1262
|
-
let tableOf = (events) => ({
|
|
1263
|
-
columns: events[0].columns,
|
|
1264
|
-
rows: events.filter((event) => event.type === "row"),
|
|
1265
|
-
total: events.find((event) => event.type === "total-row")?.cells ?? null,
|
|
1266
|
-
});
|
|
1267
|
-
|
|
1268
|
-
/**
|
|
1269
|
-
* @typedef {{ widths: number[], head: any, laid: any[], totalRow: any }} Measured
|
|
1270
|
-
*/
|
|
1271
|
-
// Measure a buffered table at a given width: the column widths every cell has
|
|
1272
|
-
// a say in, then each row laid out against them. Everything here is
|
|
1273
|
-
// arithmetic over the cells, so the region's buffer can ask for it before
|
|
1274
|
-
// anything is drawn — which is the only way a row's real height is known
|
|
1275
|
-
// early enough to balance on (bead `quario-cgk`).
|
|
1276
|
-
/** @type {(canvas: Canvas, buffered: any, avail: number) => Measured} */
|
|
1277
|
-
let measureTable = (canvas, buffered, avail) => {
|
|
1278
|
-
let cols = /** @type {any[]} */ (buffered.columns);
|
|
1279
|
-
let header = cols.map((/** @type {any} */ col) => cellOf(canvas, col.header, null));
|
|
1280
|
-
let rows = buffered.rows.map((/** @type {any} */ row) => bodyRow(canvas, row));
|
|
1281
|
-
let total = totalCells(canvas, buffered);
|
|
1282
|
-
let widths = columnWidths(cols, naturalWidths(cols, header, rows, total), avail);
|
|
1283
|
-
return {
|
|
1284
|
-
widths,
|
|
1285
|
-
head: rowOf(canvas, header, widths),
|
|
1286
|
-
laid: rows.map((/** @type {any} */ row) => ({
|
|
1287
|
-
row: rowOf(canvas, row.cells, widths),
|
|
1288
|
-
bg: row.bg,
|
|
1289
|
-
})),
|
|
1290
|
-
totalRow: total && rowOf(canvas, total, widths),
|
|
1291
|
-
};
|
|
1292
|
-
};
|
|
1293
|
-
|
|
1294
|
-
/** @type {(state: Flow, buffered: any) => void} */
|
|
1295
|
-
let table = (state, buffered) => {
|
|
1296
|
-
let canvas = state.canvas;
|
|
1297
|
-
// A table inside a region is measured against the strip, not the page: an
|
|
1298
|
-
// authored `width` percentage is a share of the column it lands in
|
|
1299
|
-
// (SCHEMA.md, "Page columns").
|
|
1300
|
-
let avail = widthOf(state);
|
|
1301
|
-
let x = originOf(state);
|
|
1302
|
-
// Measured already if a region buffered this table: the balance model needed
|
|
1303
|
-
// its real row heights, at this same width, and that is the same
|
|
1304
|
-
// measurement — so the replay draws it without wrapping every cell again.
|
|
1305
|
-
let { widths, head, laid, totalRow } =
|
|
1306
|
-
state.measured.get(buffered.opening) || measureTable(canvas, buffered, avail);
|
|
1307
|
-
let xOffsets = widths.map((_, i) => x + sum(widths.slice(0, i)));
|
|
1308
|
-
|
|
1309
|
-
/** @type {Grid} */
|
|
1310
|
-
let grid = { state, head, xOffsets, widths, x, avail };
|
|
1311
|
-
|
|
1312
|
-
// A pending group header keeps the table's header row and its first row.
|
|
1313
|
-
flush(state, grid.head.h + firstH(laid, totalRow));
|
|
1314
|
-
if (canvas.y - grid.head.h < floorOf(state)) {
|
|
1315
|
-
advance(state);
|
|
1316
|
-
rebase(grid);
|
|
1317
|
-
}
|
|
1318
|
-
drawRow(canvas, grid.head, grid.xOffsets, widths, null);
|
|
1319
|
-
underline(canvas, grid.x, grid.avail);
|
|
1320
|
-
// The last data row keeps the total row with it, so a total is never
|
|
1321
|
-
// stranded alone at a page top.
|
|
1322
|
-
emitRows(grid, laid, totalRow);
|
|
1323
|
-
};
|
|
1324
|
-
|
|
1325
|
-
// --- page bands -------------------------------------------------------------
|
|
1326
|
-
|
|
1327
|
-
// Lay page band items out at a fixed spot (no page breaks): used per page
|
|
1328
|
-
// with the real `{ number, total }`, and once as a probe to reserve height.
|
|
1329
|
-
/** @type {(canvas: Canvas, items: any[], yTop: number) => number} */
|
|
1330
|
-
let band = (canvas, items, yTop) => {
|
|
1331
|
-
let avail = canvas.content;
|
|
1332
|
-
let saved = canvas.y,
|
|
1333
|
-
fresh = canvas.fresh;
|
|
1334
|
-
canvas.y = yTop;
|
|
1335
|
-
canvas.fresh = false;
|
|
1336
|
-
// Folded straight into the draw, the way the body walk folds straight into
|
|
1337
|
-
// `route`: the folder emits in order and nothing here needs the band whole.
|
|
1338
|
-
let fold = splitFolder((event) =>
|
|
1339
|
-
drawBlock(canvas, blockOf(canvas, event, avail), canvas.margin, avail),
|
|
1340
|
-
);
|
|
1341
|
-
for (let event of items) fold(event);
|
|
1342
|
-
let h = yTop - canvas.y;
|
|
1343
|
-
canvas.y = saved;
|
|
1344
|
-
canvas.fresh = fresh;
|
|
1345
|
-
return h;
|
|
1346
|
-
};
|
|
1347
|
-
|
|
1348
|
-
// Measure a band without drawing it, to reserve its height. The trial render
|
|
1349
|
-
// goes onto a measuring canvas of its own, which can draw nothing and holds
|
|
1350
|
-
// nothing through which a document could be reached — so it runs while a live
|
|
1351
|
-
// canvas is open on one, with no live state for it to disturb.
|
|
1352
|
-
/** @type {(geo: Frame, fonts: any, items: any[]) => number} */
|
|
1353
|
-
let probe = (geo, fonts, items) => band(measuring(geo, fonts), items, 0);
|
|
1354
|
-
|
|
1355
|
-
// The pages every band is probed on, and the whole of what reservation assumes
|
|
1356
|
-
// about where a band varies. Two anchors cover each visibility edge — a band
|
|
1357
|
-
// hidden on the cover (`visible: '=page.number > 1'`) and a cover-only
|
|
1358
|
-
// masthead alike reserve their true height — and the total of two is a
|
|
1359
|
-
// stand-in for a count no one knows yet, which SCHEMA.md's cover-page note is
|
|
1360
|
-
// about: a band that varies with the real page count, in visibility or in
|
|
1361
|
-
// height, is measured as though the document were two pages long.
|
|
1362
|
-
let PROBES = [
|
|
1363
|
-
{ number: 1, total: 2 },
|
|
1364
|
-
{ number: 2, total: 2 },
|
|
1365
|
-
];
|
|
1366
|
-
|
|
1367
|
-
/**
|
|
1368
|
-
* The content box a render's body flows through: the page box less what its
|
|
1369
|
-
* page bands need, each band's height plus the gap that separates it from the
|
|
1370
|
-
* body. A declared band costs that gap whether or not it measured anything —
|
|
1371
|
-
* the gap belongs to the declaration, and a band drawing nothing on either
|
|
1372
|
-
* probed page may still draw on page three.
|
|
1373
|
-
*
|
|
1374
|
-
* Pure, and measured against the page box rather than a live canvas: what a
|
|
1375
|
-
* band needs is a property of the page.
|
|
1376
|
-
*
|
|
1377
|
-
* @type {(geo: Frame, fonts: any, bands: any) => Frame}
|
|
1378
|
-
*/
|
|
1379
|
-
let reserve = (geo, fonts, bands) => {
|
|
1380
|
-
let height = (/** @type {(page: any) => any[]} */ items) =>
|
|
1381
|
-
Math.max(...PROBES.map((page) => probe(geo, fonts, items(page))));
|
|
1382
|
-
return {
|
|
1383
|
-
...geo,
|
|
1384
|
-
top: geo.top - (bands.header ? height(bands.header) + BAND : 0),
|
|
1385
|
-
bottom: geo.bottom + (bands.footer ? height(bands.footer) + BAND : 0),
|
|
1386
|
-
};
|
|
1387
|
-
};
|
|
1388
|
-
|
|
1389
|
-
// The page box a canvas presents, before any band narrowed it. `frame` owns how
|
|
1390
|
-
// `top`/`bottom` fall out of a page and a margin — "so no caller and no suite
|
|
1391
|
-
// has to restate it", as it says next door — and both halves of page furniture
|
|
1392
|
-
// want it: reservation to measure against, the draw pass to hang the header
|
|
1393
|
-
// from.
|
|
1394
|
-
/** @type {(canvas: Canvas) => Frame} */
|
|
1395
|
-
let pageBox = (canvas) =>
|
|
1396
|
-
frame(canvas.width, canvas.height, canvas.margin, canvas.base, canvas.style);
|
|
1397
|
-
|
|
1398
|
-
// One finished page's furniture, drawn in the strips `reserve` left for it:
|
|
1399
|
-
// the header hanging from the top margin, the footer resting on the bottom
|
|
1400
|
-
// one, a gap between each and the body either way. The other half of that
|
|
1401
|
-
// constant lives directly above, which is the point of the two sitting here
|
|
1402
|
-
// together — the pass that draws the bands cannot drift from the reservation
|
|
1403
|
-
// that made room for them.
|
|
1404
|
-
/** @type {(canvas: Canvas, bands: any, anchor: any) => void} */
|
|
1405
|
-
let furniture = (canvas, bands, anchor) => {
|
|
1406
|
-
if (bands.header) band(canvas, bands.header(anchor), pageBox(canvas).top);
|
|
1407
|
-
if (bands.footer) band(canvas, bands.footer(anchor), canvas.bottom - BAND);
|
|
1408
|
-
};
|
|
1409
|
-
|
|
1410
|
-
// One `{ number, total }` per finished page, from the sequence starts the
|
|
1411
|
-
// band flow recorded. A document with no `reset` is one sequence; each
|
|
1412
|
-
// resetting instance starts another at the page it opened.
|
|
1413
|
-
/** @type {(count: number, starts: number[]) => { number: number, total: number }[]} */
|
|
1414
|
-
let numbered = (count, starts) => {
|
|
1415
|
-
let pages = Array(count);
|
|
1416
|
-
let ends = starts.concat(count);
|
|
1417
|
-
for (let s = 0; s < starts.length; s++) {
|
|
1418
|
-
let lo = ends[s];
|
|
1419
|
-
let total = ends[s + 1] - lo;
|
|
1420
|
-
for (let i = lo; i < lo + total; i++) pages[i] = { number: i - lo + 1, total };
|
|
1421
|
-
}
|
|
1422
|
-
return pages;
|
|
1423
|
-
};
|
|
1424
|
-
|
|
1425
|
-
// --- the band flow ----------------------------------------------------------
|
|
1426
|
-
|
|
1427
|
-
/**
|
|
1428
|
-
* The band flow (CONTEXT.md): one render's consumer of the banded walk. It owns
|
|
1429
|
-
* the placement state — what is held back, what a fresh page owes the groups
|
|
1430
|
-
* still open, what is owed before the next instance, what is not yet anchored —
|
|
1431
|
-
* and hands out the handlers the walk driver dispatches to, the opening event's
|
|
1432
|
-
* among them: the page bands come off the content box there,
|
|
1433
|
-
* while the driver still guarantees no second event has been pulled and this
|
|
1434
|
-
* band flow's own first page is open and empty.
|
|
1435
|
-
*
|
|
1436
|
-
* `finish` flushes whatever the last event left pending and returns what the
|
|
1437
|
-
* passes over the finished pages still need: the outline marks — the only way
|
|
1438
|
-
* to reach them, so an outline read off a band flow that still owes a flush is
|
|
1439
|
-
* not a mistake this seam leaves open — the opening event it settled,
|
|
1440
|
-
* handed back whole rather than read into, and the per-page `{ number, total }`
|
|
1441
|
-
* anchors the furniture pass draws with.
|
|
1442
|
-
*
|
|
1443
|
-
* @type {(canvas: Canvas) => { handlers: Record<string, (event: any) => void>,
|
|
1444
|
-
* finish: () => { marks: any[], opening: any,
|
|
1445
|
-
* pages: { number: number, total: number }[] } }}
|
|
1446
|
-
*/
|
|
1447
|
-
let flow = (canvas) => {
|
|
1448
|
-
/** @type {Flow} */
|
|
1449
|
-
let state = {
|
|
1450
|
-
canvas,
|
|
1451
|
-
open: [],
|
|
1452
|
-
gap: 0,
|
|
1453
|
-
marks: [],
|
|
1454
|
-
starts: [0],
|
|
1455
|
-
region: null,
|
|
1456
|
-
pending: null,
|
|
1457
|
-
// What a region already measured, keyed by the table's opening event, so
|
|
1458
|
-
// the replay draws that table without wrapping every cell a second time.
|
|
1459
|
-
// Per flow, because the keys are one render's events.
|
|
1460
|
-
measured: new WeakMap(),
|
|
1461
|
-
// Assigned below, once `route` exists: a buffered region replays through
|
|
1462
|
-
// it, and one entry point is what makes the replay take the path the
|
|
1463
|
-
// content would have taken had it never been held.
|
|
1464
|
-
route: () => {},
|
|
1465
|
-
};
|
|
1466
|
-
// The body walk's own folder, emitting each finished event onward to `route`
|
|
1467
|
-
// below. Declared here and bound after `route` exists.
|
|
1468
|
-
/** @type {(event: any) => void} */
|
|
1469
|
-
let fold;
|
|
1470
|
-
// A table is buffered whole before it is laid out: column widths come from
|
|
1471
|
-
// every cell in it, so the last row has to be in hand first.
|
|
1472
|
-
/** @type {any[]} */
|
|
1473
|
-
let collected = [];
|
|
1474
|
-
// The opening event, kept for the caller: this band flow reserves the page
|
|
1475
|
-
// bands off it, and the passes over the finished pages want the rest.
|
|
1476
|
-
/** @type {any} */
|
|
1477
|
-
let opening = null;
|
|
1478
|
-
// The body opens its own first page. Not through `turn`: a canvas that has
|
|
1479
|
-
// drawn nothing is fresh already, and would be left with no page at all.
|
|
1480
|
-
canvas.newPage();
|
|
1481
|
-
|
|
1482
|
-
// What each event does once it is placed for real. A buffered region replays
|
|
1483
|
-
// through this same table, so held content lays out by exactly the path it
|
|
1484
|
-
// would have taken had it never been held.
|
|
1485
|
-
// A block with no title of its own: held back under a group header, placed
|
|
1486
|
-
// anywhere else. Both the image and the split flow this way.
|
|
1487
|
-
/** @type {(event: any, block: Block | null) => void} */
|
|
1488
|
-
let heldOrPlaced = (event, block) =>
|
|
1489
|
-
event.role === "group-header"
|
|
1490
|
-
? holdHeader(state, event, null, block)
|
|
1491
|
-
: placeItem(state, event, block);
|
|
1492
|
-
|
|
1493
|
-
/** @type {Record<string, (event: any, block: Block | null) => void>} */
|
|
1494
|
-
let placed = {
|
|
1495
|
-
// A header is held rather than placed, and holding is bookkeeping the
|
|
1496
|
-
// replay has to see in its original order — which is why it routes like
|
|
1497
|
-
// everything else instead of reaching the open stack straight away.
|
|
1498
|
-
item: (event, block) =>
|
|
1499
|
-
event.role === "group-header"
|
|
1500
|
-
? holdHeader(state, event, text(event.tokens), block)
|
|
1501
|
-
: placeItem(state, event, block),
|
|
1502
|
-
// An image flows exactly as an item does, held back in a group header and
|
|
1503
|
-
// placed anywhere else -- the block it becomes carries a height like any
|
|
1504
|
-
// other. It wears no role default: those describe text.
|
|
1505
|
-
image: heldOrPlaced,
|
|
1506
|
-
// A split flows exactly as an image does, for the same reason: one block,
|
|
1507
|
-
// held back in a group header and placed anywhere else. It wears no role
|
|
1508
|
-
// default of its own -- its slots carry theirs.
|
|
1509
|
-
split: heldOrPlaced,
|
|
1510
|
-
"group-start": (event) => openGroup(state, event),
|
|
1511
|
-
"group-end": () => {
|
|
1512
|
-
flush(state, 0);
|
|
1513
|
-
state.open.pop();
|
|
1514
|
-
},
|
|
1515
|
-
// A table is collected as the events it arrived as, and shaped by `tableOf`
|
|
1516
|
-
// once its last one lands — the same constructor the region buffer reads
|
|
1517
|
-
// its own held events back through.
|
|
1518
|
-
"table-start": (event) => void (collected = [event]),
|
|
1519
|
-
row: (event) => void collected.push(event),
|
|
1520
|
-
"total-row": (event) => void collected.push(event),
|
|
1521
|
-
"table-end": () => {
|
|
1522
|
-
table(state, { ...tableOf(collected), opening: collected[0] });
|
|
1523
|
-
collected = [];
|
|
1524
|
-
},
|
|
1525
|
-
};
|
|
1526
|
-
/** @type {(event: any, block: Block | null) => void} */
|
|
1527
|
-
let place = (event, block) => placed[event.type](event, block);
|
|
1528
|
-
|
|
1529
|
-
// Which depth owes or holds the strips, whether or not any content has
|
|
1530
|
-
// opened them yet: an instance that emits nothing still closes with a
|
|
1531
|
-
// full-width footer, and measuring that at a strip width would wrap it to a
|
|
1532
|
-
// column it is not drawn in.
|
|
1533
|
-
let owner = () => (state.region ? state.region.owner : state.pending ? state.pending.owner : -2);
|
|
1534
|
-
/** @type {(event: any) => boolean} */
|
|
1535
|
-
let ownFooter = (event) => event.role === "group-footer" && owner() === state.open.length - 1;
|
|
1536
|
-
|
|
1537
|
-
// Route one event: the declaring node's own bands stay full-width, region
|
|
1538
|
-
// content opens the strips owed to it, and anything arriving while a region
|
|
1539
|
-
// is still deciding is buffered rather than placed.
|
|
1540
|
-
/** @type {(event: any, block?: Block | null) => void} */
|
|
1541
|
-
let route = (event, block = null) => {
|
|
1542
|
-
if (fullBand(event)) fullWidth(event, block);
|
|
1543
|
-
else if (deciding()) buffer(state, event, block);
|
|
1544
|
-
else if (opens(event)) begin(event);
|
|
1545
|
-
else place(event, block);
|
|
1546
|
-
};
|
|
1547
|
-
|
|
1548
|
-
state.route = route;
|
|
1549
|
-
fold = splitFolder(route);
|
|
1550
|
-
|
|
1551
|
-
// Everything drawn across the page rather than down a strip: the declaring
|
|
1552
|
-
// node's own bands, its footer among them.
|
|
1553
|
-
/** @type {(event: any) => boolean} */
|
|
1554
|
-
let fullBand = (event) => ownFooter(event) || isReportBand(event.role);
|
|
1555
|
-
|
|
1556
|
-
// Does this event open the strips a `columns` count owes? A header does not:
|
|
1557
|
-
// it is held, never placed, so the run it belongs to is drawn by the flush
|
|
1558
|
-
// its own content triggers — which is what leaves a columned node's header
|
|
1559
|
-
// above the strips.
|
|
1560
|
-
/** @type {(event: any) => boolean} */
|
|
1561
|
-
let opens = (event) => Boolean(state.pending) && event.role !== "group-header";
|
|
1562
|
-
|
|
1563
|
-
// The instance that owns the strips is the one ending here, so they close
|
|
1564
|
-
// before anything resumes full-width under them. A count the instance never
|
|
1565
|
-
// reached any content for is simply dropped.
|
|
1566
|
-
/** @type {(depth: number) => void} */
|
|
1567
|
-
let endOwned = (depth) => {
|
|
1568
|
-
if (owner() !== depth) return;
|
|
1569
|
-
if (state.region) closeRegion(state);
|
|
1570
|
-
else state.pending = null;
|
|
1571
|
-
};
|
|
1572
|
-
|
|
1573
|
-
// Is the open region still buffering, rather than laying anything out?
|
|
1574
|
-
let deciding = () => Boolean(state.region && state.region.held);
|
|
1575
|
-
|
|
1576
|
-
// The declaring node's own bands, above and below its strips. A count still
|
|
1577
|
-
// owed survives a report header — that band arrives before the body it
|
|
1578
|
-
// columns — but not the owner's own footer, which is the end of what it
|
|
1579
|
-
// would have columned.
|
|
1580
|
-
/** @type {(event: any, block: Block | null) => void} */
|
|
1581
|
-
let fullWidth = (event, block) => {
|
|
1582
|
-
// Asked before the region closes, because that is what it reads.
|
|
1583
|
-
let own = ownFooter(event);
|
|
1584
|
-
closeRegion(state);
|
|
1585
|
-
if (own) state.pending = null;
|
|
1586
|
-
place(event, block);
|
|
1587
|
-
};
|
|
1588
|
-
|
|
1589
|
-
// The first content of a columned node. The node's own header run is drawn
|
|
1590
|
-
// full-width here, before the region exists, which is what keeps it above
|
|
1591
|
-
// the strips; the region then opens at the cursor that left behind. The run
|
|
1592
|
-
// still keeps company with the content it introduces, measured at the strip
|
|
1593
|
-
// width because that is where the content is about to land.
|
|
1594
|
-
/** @type {(event: any) => void} */
|
|
1595
|
-
let begin = (event) => {
|
|
1596
|
-
let owed = /** @type {Pending} */ (state.pending);
|
|
1597
|
-
let block = keepBlock(state, event, stripWidth(state, owed.count));
|
|
1598
|
-
flush(state, block ? keepWith(block) : 0);
|
|
1599
|
-
openRegion(state);
|
|
1600
|
-
buffer(state, event, block);
|
|
1601
|
-
};
|
|
1602
|
-
|
|
1603
|
-
// The report default onto the canvas, before anything is measured. Its
|
|
1604
|
-
// `size` replaces this target's baseline outright, so row heights and band
|
|
1605
|
-
// gaps scale with the document's type rather than staying at a size nothing
|
|
1606
|
-
// is set in; the block itself becomes the outermost layer `stack` composes
|
|
1607
|
-
// (docs/adr/0033). `sizeOf` is what decides a size is usable anywhere in this
|
|
1608
|
-
// target, so it decides it here too -- a computed `size` reaches this
|
|
1609
|
-
// unchecked, and two spellings of that leniency would drift.
|
|
1610
|
-
/** @type {(style: any) => void} */
|
|
1611
|
-
let adoptDefault = (style) => {
|
|
1612
|
-
if (!style) return;
|
|
1613
|
-
canvas.style = style;
|
|
1614
|
-
canvas.base = sizeOf(style, canvas.base);
|
|
1615
|
-
};
|
|
1616
|
-
|
|
1617
|
-
return {
|
|
1618
|
-
handlers: {
|
|
1619
|
-
"report-start": (event) => {
|
|
1620
|
-
opening = event;
|
|
1621
|
-
adoptDefault(event.style);
|
|
1622
|
-
if (event.columns) state.pending = { count: event.columns, owner: -1 };
|
|
1623
|
-
if (!event.page) return;
|
|
1624
|
-
// A frame of its own, never the canvas itself: `measuring` builds
|
|
1625
|
-
// from whatever it is handed, so handing it this canvas would spread
|
|
1626
|
-
// the page passes onto a measurement — and reaching a document is the
|
|
1627
|
-
// one thing a measurement must not be able to do. What a band needs is
|
|
1628
|
-
// a property of the page box in any case, not of a content box
|
|
1629
|
-
// something may already have narrowed.
|
|
1630
|
-
adopt(canvas, reserve(pageBox(canvas), canvas.fonts, event.page));
|
|
1631
|
-
},
|
|
1632
|
-
// Everything else routes: `placed` above says what each event does, and
|
|
1633
|
-
// one entry point is what lets a buffered region replay through it. The
|
|
1634
|
-
// three the folder owns are left out — `split` is not a stream event at
|
|
1635
|
-
// all (the folder mints it) and item and image reach `route` through it.
|
|
1636
|
-
...Object.fromEntries(
|
|
1637
|
-
Object.keys(placed)
|
|
1638
|
-
.filter((type) => !BLOCKS.has(type))
|
|
1639
|
-
.map((type) => [type, (/** @type {any} */ event) => route(event)]),
|
|
1640
|
-
),
|
|
1641
|
-
// Item, image and both bracket events all go through the folder, which
|
|
1642
|
-
// routes what is not inside a split straight onward. The item is the one
|
|
1643
|
-
// that wears a band-role default on the way in.
|
|
1644
|
-
"split-start": (event) => fold(event),
|
|
1645
|
-
"split-end": (event) => fold(event),
|
|
1646
|
-
item: (event) => fold(roled(event)),
|
|
1647
|
-
image: (event) => fold(event),
|
|
1648
|
-
"group-end": (event) => {
|
|
1649
|
-
endOwned(event.depth);
|
|
1650
|
-
route(event);
|
|
1651
|
-
},
|
|
1652
|
-
},
|
|
1653
|
-
finish: () => {
|
|
1654
|
-
// A root region reaches here undrained when a report declares `columns`
|
|
1655
|
-
// and no full-width footer followed its body.
|
|
1656
|
-
closeRegion(state);
|
|
1657
|
-
flush(state, 0);
|
|
1658
|
-
return { marks: state.marks, opening, pages: numbered(canvas.count, state.starts) };
|
|
1659
|
-
},
|
|
1660
|
-
};
|
|
1661
|
-
};
|
|
1662
|
-
|
|
1663
|
-
export { flow, furniture };
|