@quario/layout 0.3.0 → 0.5.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 +91 -43
- package/README.md +16 -6
- package/lib/canvas.js +49 -17
- package/lib/fonts.js +19 -9
- package/lib/index.d.ts +38 -9
- package/lib/index.js +1 -0
- package/lib/layout.js +81 -286
- package/lib/measure.js +263 -0
- package/lib/page.js +1 -0
- package/lib/paint.js +18 -9
- package/lib/style.js +29 -11
- package/lib/text.js +85 -46
- package/package.json +3 -3
- package/lib/image.js +0 -58
package/lib/layout.js
CHANGED
|
@@ -21,14 +21,34 @@
|
|
|
21
21
|
* document's bands claim before anything is placed, and where they are drawn
|
|
22
22
|
* on each finished page once the count is known.
|
|
23
23
|
*/
|
|
24
|
-
import { display,
|
|
24
|
+
import { display, isReportBand, text } from "quario";
|
|
25
25
|
import { balance } from "./balance.js";
|
|
26
|
+
/** `Measured` is the measured table's own type, and the flow passes one
|
|
27
|
+
* through: the region buffer measures early and the replay hands it back.
|
|
28
|
+
* @import { Measured, TableRow } from './measure.js' */
|
|
29
|
+
|
|
30
|
+
/** One table being emitted: what `carryOver`, `sliced` and `put` all need. It
|
|
31
|
+
* holds the flow, so it is the band flow's own — the measured half never sees
|
|
32
|
+
* a cursor.
|
|
33
|
+
* @typedef {{ state: Flow, head: TableRow,
|
|
34
|
+
* xOffsets: number[], widths: number[], x: number, avail: number }} Grid */
|
|
35
|
+
import { gridOf, measureTable, sum, tableOf } from "./measure.js";
|
|
26
36
|
import { adopt, measuring } from "./canvas.js";
|
|
27
37
|
import { frame } from "./page.js";
|
|
28
|
-
import { intrinsic } from "./image.js";
|
|
29
38
|
import { CELL_PAD, NO_PAD, WHOLE, insetOf, isWhole, paintBox, sliceInset, unbox } from "./box.js";
|
|
30
|
-
import {
|
|
31
|
-
|
|
39
|
+
import {
|
|
40
|
+
BAND,
|
|
41
|
+
col,
|
|
42
|
+
GUTTER,
|
|
43
|
+
instanceGap,
|
|
44
|
+
merge,
|
|
45
|
+
roled,
|
|
46
|
+
rowHeight,
|
|
47
|
+
shift,
|
|
48
|
+
sizeOf,
|
|
49
|
+
vshift,
|
|
50
|
+
} from "./style.js";
|
|
51
|
+
import { atoms, heightOf, wrap } from "./text.js";
|
|
32
52
|
|
|
33
53
|
/** @typedef {import('./balance.js').Unit} Unit */
|
|
34
54
|
/** @typedef {import('./canvas.js').Canvas} Canvas */
|
|
@@ -45,7 +65,7 @@ import { atoms, dress, heightOf, wrap } from "./text.js";
|
|
|
45
65
|
// indexes where a `reset: "page"` sequence begins (the document itself is the
|
|
46
66
|
// sequence that starts at page 0).
|
|
47
67
|
/**
|
|
48
|
-
* @typedef {{
|
|
68
|
+
* @typedef {{ pending: any[], blocks: Block[], gap: number, skip: boolean, titled: boolean }} Group
|
|
49
69
|
*/
|
|
50
70
|
/**
|
|
51
71
|
* @typedef {{ canvas: Canvas, open: Group[], gap: number, marks: any[],
|
|
@@ -101,19 +121,22 @@ let unsliceable = (block) => Boolean(block.picture || block.parts);
|
|
|
101
121
|
// image's own size in points, never wider than the content box; `width`
|
|
102
122
|
// scales to the content box either way. The ratio is kept in both, so a
|
|
103
123
|
// height is never anything but the width's consequence.
|
|
124
|
+
// An image pixel as a page point, at the conventional 96 dpi the schema names
|
|
125
|
+
// for `fit: "natural"`. Deliberately not `PX_PER_POINT`, which is the same
|
|
126
|
+
// number answering a different question -- how a point becomes a length on
|
|
127
|
+
// screen, where a zoom is a factor on top of it. An image's resolution and a
|
|
128
|
+
// viewer's scale share a convention today and are free to stop.
|
|
129
|
+
let PER_PX = 72 / 96;
|
|
130
|
+
|
|
104
131
|
/** @type {(event: any, avail: number) => Picture} */
|
|
105
132
|
let pictureOf = (event, avail) => {
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
} catch (cause) {
|
|
114
|
-
throw imageError(event.path, /** @type {Error} */ (cause).message, cause);
|
|
115
|
-
}
|
|
116
|
-
let { w, h } = sized;
|
|
133
|
+
// The event states the size in pixels; a page is measured in points. The
|
|
134
|
+
// conversion is the first step of the same calculation the rest of this
|
|
135
|
+
// function is, at the conventional 96 dpi the schema names, and there is no
|
|
136
|
+
// header to read: the engine read it once, for every target, and a file too
|
|
137
|
+
// short to state a size never reached a target at all (`docs/adr/0067`).
|
|
138
|
+
let w = event.width * PER_PX;
|
|
139
|
+
let h = event.height * PER_PX;
|
|
117
140
|
let width = event.fit === "width" ? avail : Math.min(w, avail);
|
|
118
141
|
return {
|
|
119
142
|
bytes: event.bytes,
|
|
@@ -149,7 +172,7 @@ let blockOf = (canvas, event, avail, under = null) => {
|
|
|
149
172
|
};
|
|
150
173
|
}
|
|
151
174
|
let size = sizeOf(text, canvas.settings.base);
|
|
152
|
-
let lines =
|
|
175
|
+
let lines = wrap(atoms(canvas.settings, event.tokens, text), inner, size);
|
|
153
176
|
return {
|
|
154
177
|
lines,
|
|
155
178
|
bg,
|
|
@@ -174,23 +197,24 @@ let shrink = (block, room) => {
|
|
|
174
197
|
block.h = inner + block.inset.t + block.inset.b;
|
|
175
198
|
};
|
|
176
199
|
|
|
177
|
-
// Draw one block whole at the cursor.
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
200
|
+
// Draw one block whole at the cursor. The box is the width the block was
|
|
201
|
+
// given, exactly as `drawLines` paints one: an image's background and border
|
|
202
|
+
// span the content width, or the slot's share inside a split, and never the
|
|
203
|
+
// picture's own width. `fit` sizes the picture and `align` places it in the
|
|
204
|
+
// inner box; neither moves an edge. Text is placed line by line as each is
|
|
205
|
+
// drawn; a picture is one box, so where it sits is worked out once.
|
|
181
206
|
/** @type {(canvas: Canvas, block: Block, x: number, avail: number) => void} */
|
|
182
207
|
let drawPicture = (canvas, block, x, avail) => {
|
|
183
208
|
let inset = block.inset;
|
|
184
209
|
let { bytes, format, w, h } = /** @type {Picture} */ (block.picture);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
canvas.box(block.path, at, canvas.y, outer, block.h);
|
|
210
|
+
paintBox(canvas, x, canvas.y, avail, block.h, block.style, block.bg);
|
|
211
|
+
canvas.box(block.path, x, canvas.y, avail, block.h);
|
|
212
|
+
let inner = Math.max(avail - inset.l - inset.r, 1);
|
|
189
213
|
canvas.picture(
|
|
190
214
|
block.path,
|
|
191
215
|
bytes,
|
|
192
216
|
format,
|
|
193
|
-
|
|
217
|
+
x + inset.l + shift(block.align, inner - w),
|
|
194
218
|
canvas.y - inset.t - block.drop - h,
|
|
195
219
|
w,
|
|
196
220
|
h,
|
|
@@ -686,7 +710,7 @@ let stripWidth = (state, count) => (state.canvas.content - GUTTER * (count - 1))
|
|
|
686
710
|
//
|
|
687
711
|
// It opens buffering: what the strips do with the content depends on how much
|
|
688
712
|
// of it there is, and that is not known until the region ends or outgrows a
|
|
689
|
-
// page. `held` is that buffer; `null` means the region has
|
|
713
|
+
// page. `held` is that buffer; `null` means the region has decided.
|
|
690
714
|
/** @type {(state: Flow) => void} */
|
|
691
715
|
let openRegion = (state) => {
|
|
692
716
|
let owed = state.pending;
|
|
@@ -808,13 +832,15 @@ let stripHeight = (state, region, own) =>
|
|
|
808
832
|
? null
|
|
809
833
|
: balance(unitsOf(state, own), region.count, roomOf(state, region), rowHeight(state.canvas));
|
|
810
834
|
|
|
811
|
-
//
|
|
835
|
+
// Decide the region: settle the strip height, stop holding, and hand the held
|
|
812
836
|
// entries back in the order they arrived, for `flow`'s `replay` to walk.
|
|
837
|
+
// Not `commit`: the editor's session already spends that word on a gesture
|
|
838
|
+
// ending, and git spends it on a commit.
|
|
813
839
|
//
|
|
814
840
|
// Marking the spans is also what picks out the region's own children, and the
|
|
815
841
|
// height is decided from those — so it runs before the replay, not with it.
|
|
816
842
|
/** @type {(state: Flow) => any[]} */
|
|
817
|
-
let
|
|
843
|
+
let decide = (state) => {
|
|
818
844
|
let region = /** @type {Region} */ (state.region);
|
|
819
845
|
let held = /** @type {any[]} */ (region.held);
|
|
820
846
|
region.height = stripHeight(state, region, spans(held));
|
|
@@ -1031,7 +1057,7 @@ let anchor = (state) => {
|
|
|
1031
1057
|
};
|
|
1032
1058
|
|
|
1033
1059
|
/** @type {(state: Flow) => Group[]} */
|
|
1034
|
-
let pending = (state) => state.open.filter((group) => group.
|
|
1060
|
+
let pending = (state) => state.open.filter((group) => group.pending.length);
|
|
1035
1061
|
|
|
1036
1062
|
/** @type {(state: Flow, extra: number, runs: Block[][]) => number} */
|
|
1037
1063
|
let needed = (state, extra, runs) =>
|
|
@@ -1042,12 +1068,12 @@ let needed = (state, extra, runs) =>
|
|
|
1042
1068
|
// for good: every other path drew its headers inside the room a page still
|
|
1043
1069
|
// had, so a run always leaves space for the content it introduces.
|
|
1044
1070
|
/** @type {(state: Flow, holding: Group[]) => void} */
|
|
1045
|
-
let
|
|
1046
|
-
let
|
|
1047
|
-
for (let group of holding) group.
|
|
1071
|
+
let dropPending = (state, holding) => {
|
|
1072
|
+
let waiting = holding.flatMap((group) => group.pending);
|
|
1073
|
+
for (let group of holding) group.pending = [];
|
|
1048
1074
|
state.gap = 0;
|
|
1049
1075
|
anchor(state);
|
|
1050
|
-
for (let entry of
|
|
1076
|
+
for (let entry of waiting) item(state, entry.event, entry.block);
|
|
1051
1077
|
};
|
|
1052
1078
|
|
|
1053
1079
|
/** @type {(state: Flow, need: number) => void} */
|
|
@@ -1065,12 +1091,12 @@ let settle = (state, need) => {
|
|
|
1065
1091
|
// — the turn above already did — so an instance taking its blocks the moment
|
|
1066
1092
|
// they are drawn can never have them replayed on top of themselves.
|
|
1067
1093
|
/** @type {(state: Flow, holding: Group[], runs: Block[][], avail: number) => void} */
|
|
1068
|
-
let
|
|
1094
|
+
let drawPending = (state, holding, runs, avail) => {
|
|
1069
1095
|
let x = originOf(state);
|
|
1070
1096
|
for (let [i, group] of holding.entries()) {
|
|
1071
1097
|
for (let block of runs[i]) drawBlock(state.canvas, block, x, avail);
|
|
1072
1098
|
group.blocks.push(...runs[i]);
|
|
1073
|
-
group.
|
|
1099
|
+
group.pending = [];
|
|
1074
1100
|
}
|
|
1075
1101
|
};
|
|
1076
1102
|
|
|
@@ -1086,14 +1112,14 @@ let flush = (state, extra) => {
|
|
|
1086
1112
|
// at this same width, so a replayed run reuses those rather than wrapping
|
|
1087
1113
|
// the same text a second time (ADR 0027).
|
|
1088
1114
|
let runs = runsFor.map((group) =>
|
|
1089
|
-
group.
|
|
1115
|
+
group.pending.map((entry) => entry.block || blockOf(canvas, entry.event, avail)),
|
|
1090
1116
|
);
|
|
1091
1117
|
let need = needed(state, extra, runs);
|
|
1092
1118
|
// Against what a fresh column has left once the page has replayed what it
|
|
1093
1119
|
// carries. Inside a region that is a strip, and the replay sits above it.
|
|
1094
|
-
if (need > ceilOf(state) - carried(state) - floorOf(state)) return
|
|
1120
|
+
if (need > ceilOf(state) - carried(state) - floorOf(state)) return dropPending(state, runsFor);
|
|
1095
1121
|
settle(state, need);
|
|
1096
|
-
|
|
1122
|
+
drawPending(state, runsFor, runs, avail);
|
|
1097
1123
|
};
|
|
1098
1124
|
|
|
1099
1125
|
// Enough of an item that its group header is never left introducing nothing.
|
|
@@ -1126,7 +1152,7 @@ let keepBlock = (state, event, width) =>
|
|
|
1126
1152
|
// Is anything waiting on this item's arrival — a header run, or the gap an
|
|
1127
1153
|
// instance opens with?
|
|
1128
1154
|
/** @type {(state: Flow) => boolean} */
|
|
1129
|
-
let holding = (state) => state.open.some((group) => group.
|
|
1155
|
+
let holding = (state) => state.open.some((group) => group.pending.length) || state.gap > 0;
|
|
1130
1156
|
|
|
1131
1157
|
// A half-line gap before each instance (dropped at a page top by `flush`)
|
|
1132
1158
|
// keeps groups reading as blocks. Every instance becomes an outline entry.
|
|
@@ -1159,7 +1185,7 @@ let openGroup = (state, event) => {
|
|
|
1159
1185
|
let skip = state.skipGap;
|
|
1160
1186
|
state.gap = skip ? 0 : Math.max(state.gap, instanceGap(state.canvas));
|
|
1161
1187
|
state.skipGap = false;
|
|
1162
|
-
state.open.push({
|
|
1188
|
+
state.open.push({ pending: [], blocks: [], gap, skip, titled: false });
|
|
1163
1189
|
state.marks.push(markFor(event));
|
|
1164
1190
|
};
|
|
1165
1191
|
|
|
@@ -1195,7 +1221,6 @@ let markFor = (event) => ({
|
|
|
1195
1221
|
// display(), not String(): a Date group key must title its outline bookmark
|
|
1196
1222
|
// with the same ISO 8601 UTC text its cells render, on every machine.
|
|
1197
1223
|
title: event.name + ": " + display(event.key),
|
|
1198
|
-
titled: false,
|
|
1199
1224
|
depth: event.depth,
|
|
1200
1225
|
page: -1,
|
|
1201
1226
|
y: 0,
|
|
@@ -1205,24 +1230,24 @@ let markFor = (event) => ({
|
|
|
1205
1230
|
// page bottom. The instance's first header titles its outline entry.
|
|
1206
1231
|
/** @type {(state: Flow, event: any, title: string | null, block?: Block | null) => void} */
|
|
1207
1232
|
let holdHeader = (state, event, title, block = null) => {
|
|
1233
|
+
let group = state.open[state.open.length - 1];
|
|
1208
1234
|
let mark = state.marks[state.marks.length - 1];
|
|
1235
|
+
// Whether a header has claimed this entry is the open instance's own fact,
|
|
1236
|
+
// not the outline entry's: a Mark is what a consumer receives, and it
|
|
1237
|
+
// carries only what `index.d.ts` declares. The two records correspond here
|
|
1238
|
+
// because `openGroup` pushes them together and this only ever runs inside
|
|
1239
|
+
// that instance's own header band, before any nested group opens -- groups
|
|
1240
|
+
// pop from `open` while their marks stay, so nowhere else may assume it.
|
|
1241
|
+
//
|
|
1209
1242
|
// A picture has no words to title an outline entry with, so it holds its
|
|
1210
1243
|
// place in the run and leaves the title to the next header that has some.
|
|
1211
|
-
if (mark && !
|
|
1212
|
-
|
|
1244
|
+
if (mark && !group.titled && title != null) {
|
|
1245
|
+
group.titled = true;
|
|
1213
1246
|
mark.title = title;
|
|
1214
1247
|
}
|
|
1215
|
-
|
|
1248
|
+
group.pending.push({ event, block });
|
|
1216
1249
|
};
|
|
1217
1250
|
|
|
1218
|
-
// A table row's floor height and the gap a group instance opens with: named
|
|
1219
|
-
// because the region's estimator reads both, and a constant with two readers
|
|
1220
|
-
// and no name drifts between them.
|
|
1221
|
-
/** @type {(canvas: Canvas) => number} */
|
|
1222
|
-
let rowHeight = (canvas) => LEAD * canvas.settings.base;
|
|
1223
|
-
/** @type {(canvas: Canvas) => number} */
|
|
1224
|
-
let instanceGap = (canvas) => 0.5 * LEAD * canvas.settings.base;
|
|
1225
|
-
|
|
1226
1251
|
// What a fresh strip has to hold to take an instance: the span less the
|
|
1227
1252
|
// opening gap, which `settle` drops at a strip head. This is the one fact the
|
|
1228
1253
|
// balance model and the placement rule have to agree on, so both read it here
|
|
@@ -1232,60 +1257,6 @@ let headOf = (canvas, span) => span - instanceGap(canvas);
|
|
|
1232
1257
|
|
|
1233
1258
|
// --- tables -----------------------------------------------------------------
|
|
1234
1259
|
|
|
1235
|
-
// Pre-measure one cell: wrapped lines per column width come later; natural
|
|
1236
|
-
// width first (no wrapping except hard breaks). `mt`/`mb` go unread: SCHEMA.md
|
|
1237
|
-
// gives flow spacing no meaning inside a table row, in either target.
|
|
1238
|
-
/** @type {(canvas: Canvas, cell: any, rowStyle: any) => any} */
|
|
1239
|
-
let cellOf = (canvas, cell, rowStyle) => {
|
|
1240
|
-
// A row's block reaches its cells with no box in it: the engine resolved
|
|
1241
|
-
// that half onto the cells themselves before the event was emitted
|
|
1242
|
-
// (SCHEMA.md, "Style declarations"), which is why the inset and the ink
|
|
1243
|
-
// both read the cell and a row is never asked for a box it cannot have.
|
|
1244
|
-
let style = merge(rowStyle, unbox(cell.style));
|
|
1245
|
-
let inset = insetOf(cell.style, CELL_PAD);
|
|
1246
|
-
let list = atoms(canvas.settings, cell.tokens, style);
|
|
1247
|
-
let natural = wrap(list, Infinity, sizeOf(style, canvas.settings.base)).reduce(
|
|
1248
|
-
(widest, line) => Math.max(widest, line.w),
|
|
1249
|
-
0,
|
|
1250
|
-
);
|
|
1251
|
-
return {
|
|
1252
|
-
list,
|
|
1253
|
-
natural: natural + inset.l + inset.r,
|
|
1254
|
-
align: style.align,
|
|
1255
|
-
valign: style.valign,
|
|
1256
|
-
bg: col(style.background),
|
|
1257
|
-
underline: !!style.underline,
|
|
1258
|
-
strikethrough: !!style.strikethrough,
|
|
1259
|
-
inset,
|
|
1260
|
-
style: cell.style,
|
|
1261
|
-
path: cell.path,
|
|
1262
|
-
};
|
|
1263
|
-
};
|
|
1264
|
-
|
|
1265
|
-
/** @type {(canvas: Canvas, cells: any[], widths: number[]) => { cells: any[], h: number }} */
|
|
1266
|
-
let rowOf = (canvas, cells, widths) => {
|
|
1267
|
-
let h = rowHeight(canvas);
|
|
1268
|
-
/** @type {number[]} */
|
|
1269
|
-
let owns = [];
|
|
1270
|
-
let out = cells.map((cell, i) => {
|
|
1271
|
-
let inner = Math.max(widths[i] - cell.inset.l - cell.inset.r, 1);
|
|
1272
|
-
// A glyph or a hard break occupies; spaces alone are an empty cell.
|
|
1273
|
-
let lines = cell.list.some(
|
|
1274
|
-
(/** @type {{ hard: boolean, space: boolean }} */ atom) => atom.hard || !atom.space,
|
|
1275
|
-
)
|
|
1276
|
-
? dress(wrap(cell.list, inner, cell.list[0].size), cell)
|
|
1277
|
-
: [];
|
|
1278
|
-
let own = heightOf(lines) + cell.inset.t + cell.inset.b;
|
|
1279
|
-
if (own > h) h = own;
|
|
1280
|
-
owns.push(own);
|
|
1281
|
-
return { ...cell, lines };
|
|
1282
|
-
});
|
|
1283
|
-
// The row's height is known only now, so the slack each cell's `valign`
|
|
1284
|
-
// reads is measured here, as `splitBlock` measures a slot's.
|
|
1285
|
-
for (let [i, cell] of out.entries()) cell.drop = vshift(cell.valign, h - owns[i]);
|
|
1286
|
-
return { cells: out, h };
|
|
1287
|
-
};
|
|
1288
|
-
|
|
1289
1260
|
/** @type {(canvas: Canvas, cell: any, bg: any, x: number, y: number, w: number, h: number) => void} */
|
|
1290
1261
|
let fillCell = (canvas, cell, bg, x, y, w, h) => {
|
|
1291
1262
|
if (cell.bg && cell.bg !== bg) canvas.rect(cell.bg, x, y, w, h);
|
|
@@ -1345,107 +1316,7 @@ let drawRow = (canvas, row, xOffsets, widths, bg) => {
|
|
|
1345
1316
|
);
|
|
1346
1317
|
};
|
|
1347
1318
|
|
|
1348
|
-
// The column geometry one row sees. A cell covering several columns starts
|
|
1349
|
-
// where the first of them starts and is as wide as all of them together; a row
|
|
1350
|
-
// that spans nothing sees the columns themselves, and pays nothing for the
|
|
1351
|
-
// feature. `null` spans is that row -- every data row, and every total row
|
|
1352
|
-
// whose cells each cover one column.
|
|
1353
|
-
/** @type {(cells: any[]) => number[] | null} */
|
|
1354
|
-
let spansOf = (cells) =>
|
|
1355
|
-
cells.some((cell) => cell.span > 1) ? cells.map((cell) => cell.span || 1) : null;
|
|
1356
|
-
// One value per cell, walking the columns each of them covers. Reached only
|
|
1357
|
-
// for a row that spans, so the closure it takes costs nothing per data row.
|
|
1358
|
-
/** @type {(spans: number[], pick: (at: number, span: number) => number) => number[]} */
|
|
1359
|
-
let overSpans = (spans, pick) => {
|
|
1360
|
-
/** @type {number[]} */
|
|
1361
|
-
let out = [];
|
|
1362
|
-
let at = 0;
|
|
1363
|
-
for (let span of spans) {
|
|
1364
|
-
out.push(pick(at, span));
|
|
1365
|
-
at += span;
|
|
1366
|
-
}
|
|
1367
|
-
return out;
|
|
1368
|
-
};
|
|
1369
|
-
/** @type {(widths: number[], spans: number[] | null) => number[]} */
|
|
1370
|
-
let spanWidths = (widths, spans) =>
|
|
1371
|
-
spans ? overSpans(spans, (at, span) => sum(widths.slice(at, at + span))) : widths;
|
|
1372
1319
|
/** @type {(xOffsets: number[], spans: number[] | null) => number[]} */
|
|
1373
|
-
let spanOffsets = (xOffsets, spans) => (spans ? overSpans(spans, (at) => xOffsets[at]) : xOffsets);
|
|
1374
|
-
|
|
1375
|
-
// The widest of each column's header, rows and totals, padding included.
|
|
1376
|
-
//
|
|
1377
|
-
// A cell covering more than one column has no say in their widths (SCHEMA.md):
|
|
1378
|
-
// what a span states is which columns a cell reaches across, never how wide
|
|
1379
|
-
// they are. So a column can end up with no voter at all -- every cell above it
|
|
1380
|
-
// spans over it -- which only an empty table reaches, since a data row never
|
|
1381
|
-
// spans. It opens at the padding floor rather than at nothing, so an empty
|
|
1382
|
-
// table still shows the geometry it promises.
|
|
1383
|
-
/**
|
|
1384
|
-
* @typedef {{ cells: any[], spans: number[] | null }} Voting
|
|
1385
|
-
*/
|
|
1386
|
-
/** @type {(cols: any[], header: Voting, rows: Voting[], totals: Voting[]) => number[]} */
|
|
1387
|
-
let naturalWidths = (cols, header, rows, totals) => {
|
|
1388
|
-
/** @type {(number | null)[]} */
|
|
1389
|
-
let widest = cols.map(() => null);
|
|
1390
|
-
/** @type {(at: number, natural: number) => void} */
|
|
1391
|
-
let widen = (at, natural) => {
|
|
1392
|
-
let held = widest[at];
|
|
1393
|
-
if (held === null || natural > held) widest[at] = natural;
|
|
1394
|
-
};
|
|
1395
|
-
/** @type {(row: Voting) => void} */
|
|
1396
|
-
let vote = (row) => {
|
|
1397
|
-
let at = 0;
|
|
1398
|
-
for (let [i, cell] of row.cells.entries()) {
|
|
1399
|
-
let span = row.spans ? row.spans[i] : 1;
|
|
1400
|
-
if (span === 1) widen(at, cell.natural);
|
|
1401
|
-
at += span;
|
|
1402
|
-
}
|
|
1403
|
-
};
|
|
1404
|
-
vote(header);
|
|
1405
|
-
for (let row of rows) vote(row);
|
|
1406
|
-
for (let row of totals) vote(row);
|
|
1407
|
-
return widest.map((width) => (width === null ? CELL_PAD.l + CELL_PAD.r : width));
|
|
1408
|
-
};
|
|
1409
|
-
|
|
1410
|
-
/** @type {(values: number[]) => number} */
|
|
1411
|
-
let sum = (values) => values.reduce((total, value) => total + value, 0);
|
|
1412
|
-
|
|
1413
|
-
// An authored `width` percentage fixes its column; the rest share what is left
|
|
1414
|
-
// in proportion to their natural widths. Three cases, one return each — the
|
|
1415
|
-
// fall-through is all-authored within budget, honoured exactly.
|
|
1416
|
-
//
|
|
1417
|
-
// There is no over-commitment case: the engine rejects a document whose fixed
|
|
1418
|
-
// shares leave the width-less columns nothing, so `left` is positive whenever
|
|
1419
|
-
// an auto column exists and the fixed shares never exceed the content width.
|
|
1420
|
-
// That invariant is asserted rather than trusted — falling through with auto
|
|
1421
|
-
// columns unplaced would draw them at zero width, an invisible failure — the
|
|
1422
|
-
// same fail-loud posture as the measuring canvas's `newPage`.
|
|
1423
|
-
/** @type {(cols: any[], natural: number[], avail: number) => number[]} */
|
|
1424
|
-
let columnWidths = (cols, natural, avail) => {
|
|
1425
|
-
let fixed = cols.map((column) => Number.isFinite(column.width));
|
|
1426
|
-
if (!fixed.some(Boolean)) {
|
|
1427
|
-
// Natural widths always carry the cell padding, so the sum is never zero.
|
|
1428
|
-
let wanted = sum(natural);
|
|
1429
|
-
return natural.map((width) => (width * avail) / wanted);
|
|
1430
|
-
}
|
|
1431
|
-
let widths = cols.map((col, i) => (fixed[i] ? (col.width / 100) * avail : 0));
|
|
1432
|
-
let auto = natural.map((width, i) => (fixed[i] ? 0 : width));
|
|
1433
|
-
let left = avail - sum(widths);
|
|
1434
|
-
let wanted = sum(auto);
|
|
1435
|
-
// Room for the auto columns: they share what the fixed ones left.
|
|
1436
|
-
if (wanted > 0) {
|
|
1437
|
-
if (left <= 0) throw new Error("over-committed column widths reached the layout");
|
|
1438
|
-
return widths.map((width, i) => (fixed[i] ? width : (auto[i] * left) / wanted));
|
|
1439
|
-
}
|
|
1440
|
-
return widths;
|
|
1441
|
-
};
|
|
1442
|
-
|
|
1443
|
-
// One table being emitted: what `carryOver`, `sliced` and `put` all need.
|
|
1444
|
-
/**
|
|
1445
|
-
* @typedef {{ cells: any[], h: number, style?: any, spans?: number[] | null }} TableRow
|
|
1446
|
-
* @typedef {{ state: Flow, head: TableRow,
|
|
1447
|
-
* xOffsets: number[], widths: number[], x: number, avail: number }} Grid
|
|
1448
|
-
*/
|
|
1449
1320
|
|
|
1450
1321
|
// Continue in a fresh column: a table that spans columns or pages restates its
|
|
1451
1322
|
// headings, under whatever headers the open groups replay above them. Both
|
|
@@ -1458,18 +1329,6 @@ let columnWidths = (cols, natural, avail) => {
|
|
|
1458
1329
|
/** @type {(row: { style?: any }) => any} */
|
|
1459
1330
|
let rowFill = (row) => col((row.style || {}).background);
|
|
1460
1331
|
|
|
1461
|
-
// What this row draws against: the grid's own columns, or the merged geometry
|
|
1462
|
-
// a spanning row sees. Read at draw time rather than kept on the row, because
|
|
1463
|
-
// `rebase` moves the offsets under it every time the table crosses a strip.
|
|
1464
|
-
/** @type {(grid: Grid, row: TableRow) => { xOffsets: number[], widths: number[] }} */
|
|
1465
|
-
let gridOf = (grid, row) =>
|
|
1466
|
-
row.spans
|
|
1467
|
-
? {
|
|
1468
|
-
xOffsets: spanOffsets(grid.xOffsets, row.spans),
|
|
1469
|
-
widths: spanWidths(grid.widths, row.spans),
|
|
1470
|
-
}
|
|
1471
|
-
: grid;
|
|
1472
|
-
|
|
1473
1332
|
/** @type {(grid: Grid) => void} */
|
|
1474
1333
|
let carryOver = (grid) => {
|
|
1475
1334
|
advance(grid.state);
|
|
@@ -1611,70 +1470,6 @@ let emitRows = (grid, laid, totalRows) => {
|
|
|
1611
1470
|
for (let row of totalRows) put(grid, row, rowFill(row), 0);
|
|
1612
1471
|
};
|
|
1613
1472
|
|
|
1614
|
-
// A buffered table, in the one shape `table` lays out: its columns come from
|
|
1615
|
-
// the opening event, its rows are the row events themselves, and its total is
|
|
1616
|
-
// the total row's cells. Both readers build it here — the replay, collecting
|
|
1617
|
-
// events as they arrive, and the region buffer, reading them back off what it
|
|
1618
|
-
// held — so a new table event kind cannot reach one and miss the other.
|
|
1619
|
-
/** @type {(events: any[]) => any} */
|
|
1620
|
-
let tableOf = (events) => {
|
|
1621
|
-
let opening = events[0];
|
|
1622
|
-
return {
|
|
1623
|
-
columns: opening.columns.map((/** @type {any} */ col) => ({ ...col })),
|
|
1624
|
-
// The header row arrives whole (docs/adr/0053), so both readers -- the
|
|
1625
|
-
// replay and the region buffer -- take the same list rather than each
|
|
1626
|
-
// rebuilding it from the columns.
|
|
1627
|
-
header: opening.header.cells,
|
|
1628
|
-
headerStyle: opening.header.style,
|
|
1629
|
-
rows: events.filter((event) => event.type === "row"),
|
|
1630
|
-
// The events themselves, as `rows` are: a total row is a row, and the
|
|
1631
|
-
// buffer files each corrected height under the event it was measured from.
|
|
1632
|
-
totals: events.filter((event) => event.type === "total-row"),
|
|
1633
|
-
};
|
|
1634
|
-
};
|
|
1635
|
-
|
|
1636
|
-
/**
|
|
1637
|
-
* @typedef {{ widths: number[], head: any, laid: any[], totalRows: any[] }} Measured
|
|
1638
|
-
*/
|
|
1639
|
-
// Measure a buffered table at a given width: the column widths every cell has
|
|
1640
|
-
// a say in, then each row laid out against them. Everything here is
|
|
1641
|
-
// arithmetic over the cells, so the region's buffer can ask for it before
|
|
1642
|
-
// anything is drawn — which is the only way a row's real height is known
|
|
1643
|
-
// early enough to balance on (bead `quario-cgk`).
|
|
1644
|
-
/** @type {(canvas: Canvas, buffered: any, avail: number) => Measured} */
|
|
1645
|
-
let measureTable = (canvas, buffered, avail) => {
|
|
1646
|
-
let cols = /** @type {any[]} */ (buffered.columns);
|
|
1647
|
-
/** @type {(cells: any[], style: any, spans: number[] | null) => Voting & { style: any }} */
|
|
1648
|
-
let measured = (cells, style, spans) => ({
|
|
1649
|
-
cells: cells.map((/** @type {any} */ cell) => cellOf(canvas, cell, style)),
|
|
1650
|
-
spans,
|
|
1651
|
-
style,
|
|
1652
|
-
});
|
|
1653
|
-
let header = measured(buffered.header, buffered.headerStyle, spansOf(buffered.header));
|
|
1654
|
-
// A data row's cells are the columns' own, so nothing in one can span
|
|
1655
|
-
// (SCHEMA.md, "Span"). Asking each row anyway would scan every cell of the
|
|
1656
|
-
// table to rediscover what the schema already guarantees.
|
|
1657
|
-
let rows = buffered.rows.map((/** @type {any} */ row) => measured(row.cells, row.style, null));
|
|
1658
|
-
let totals = buffered.totals.map((/** @type {any} */ row) =>
|
|
1659
|
-
measured(row.cells, row.style, spansOf(row.cells)),
|
|
1660
|
-
);
|
|
1661
|
-
let widths = columnWidths(cols, naturalWidths(cols, header, rows, totals), avail);
|
|
1662
|
-
// Each row wraps against the geometry it sees, which is the columns' own
|
|
1663
|
-
// unless one of its cells spans.
|
|
1664
|
-
/** @type {(row: Voting & { style: any }) => any} */
|
|
1665
|
-
let laidOut = (row) => ({
|
|
1666
|
-
...rowOf(canvas, row.cells, spanWidths(widths, row.spans)),
|
|
1667
|
-
spans: row.spans,
|
|
1668
|
-
style: row.style,
|
|
1669
|
-
});
|
|
1670
|
-
return {
|
|
1671
|
-
widths,
|
|
1672
|
-
head: laidOut(header),
|
|
1673
|
-
laid: rows.map(laidOut),
|
|
1674
|
-
totalRows: totals.map(laidOut),
|
|
1675
|
-
};
|
|
1676
|
-
};
|
|
1677
|
-
|
|
1678
1473
|
// Lay out one table from the events it arrived as: measure every cell,
|
|
1679
1474
|
// allocate the columns, then emit the header, the rows and the total,
|
|
1680
1475
|
// breaking pages as needed.
|
|
@@ -1943,7 +1738,7 @@ let flow = (canvas) => {
|
|
|
1943
1738
|
// The nesting the router is at. While the buffer holds, routing runs ahead of
|
|
1944
1739
|
// placement: nothing has been placed, so `state.open` is frozen at the depth
|
|
1945
1740
|
// the region opened at and only the buffer knows where the walk really is.
|
|
1946
|
-
// Once `
|
|
1741
|
+
// Once `decide` nulls `held`, the replay *is* the placement timeline and the
|
|
1947
1742
|
// open stack is exact again.
|
|
1948
1743
|
let nesting = () =>
|
|
1949
1744
|
state.region && state.region.held ? state.region.nesting : state.open.length - 1;
|
|
@@ -1993,14 +1788,14 @@ let flow = (canvas) => {
|
|
|
1993
1788
|
if (buffer(state, event, payload, hollows.has(event))) replay();
|
|
1994
1789
|
};
|
|
1995
1790
|
|
|
1996
|
-
// Lay the buffer out: `
|
|
1791
|
+
// Lay the buffer out: `decide` settles the strip height and hands the held
|
|
1997
1792
|
// entries back in order, and this walks them through `route`. The walk lives
|
|
1998
1793
|
// here rather than under the region section marker for the reason everything
|
|
1999
1794
|
// there is module-level — that section is reachable without the router, and
|
|
2000
1795
|
// a walk through `route` is not.
|
|
2001
1796
|
let replay = () => {
|
|
2002
1797
|
let region = /** @type {Region} */ (state.region);
|
|
2003
|
-
for (let entry of
|
|
1798
|
+
for (let entry of decide(state)) {
|
|
2004
1799
|
if (breaksFor(state, region, entry.span)) advance(state);
|
|
2005
1800
|
route(entry.event, entry.payload);
|
|
2006
1801
|
}
|