@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/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, imageError, isReportBand, text } from "quario";
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 { BAND, col, GUTTER, LEAD, merge, roled, shift, sizeOf, vshift } from "./style.js";
31
- import { atoms, dress, heightOf, wrap } from "./text.js";
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 {{ held: any[], blocks: Block[], gap: number, skip: boolean }} Group
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
- // Located here rather than in `image.js`, which is handed bytes and a
107
- // format and has never heard of a schema: this is the innermost place that
108
- // knows both what went wrong and which item asked for it. SCHEMA.md calls
109
- // this failure a render error, and a render error names its node.
110
- let sized;
111
- try {
112
- sized = intrinsic(event.bytes, event.format);
113
- } catch (cause) {
114
- throw imageError(event.path, /** @type {Error} */ (cause).message, cause);
115
- }
116
- let { w, h } = sized;
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 = dress(wrap(atoms(canvas.settings, event.tokens, text), inner, size), text);
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. A picture's background fills the
178
- // image's own box rather than the width of the flow -- there is no line for
179
- // it to sit behind. Text is placed line by line as each is drawn; a picture
180
- // is one box, so where it sits in the width it was given is worked out once.
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
- let outer = w + inset.l + inset.r;
186
- let at = x + shift(block.align, avail - outer);
187
- paintBox(canvas, at, canvas.y, outer, block.h, block.style, block.bg);
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
- at + inset.l,
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 committed.
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
- // Commit the region: decide the strip height, stop holding, and hand the held
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 commit = (state) => {
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.held.length);
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 dropHeld = (state, holding) => {
1046
- let held = holding.flatMap((group) => group.held);
1047
- for (let group of holding) group.held = [];
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 held) item(state, entry.event, entry.block);
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 drawHeld = (state, holding, runs, avail) => {
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.held = [];
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.held.map((entry) => entry.block || blockOf(canvas, entry.event, avail)),
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 dropHeld(state, runsFor);
1120
+ if (need > ceilOf(state) - carried(state) - floorOf(state)) return dropPending(state, runsFor);
1095
1121
  settle(state, need);
1096
- drawHeld(state, runsFor, runs, avail);
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.held.length) || state.gap > 0;
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({ held: [], blocks: [], gap, skip });
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 && !mark.titled && title != null) {
1212
- mark.titled = true;
1244
+ if (mark && !group.titled && title != null) {
1245
+ group.titled = true;
1213
1246
  mark.title = title;
1214
1247
  }
1215
- state.open[state.open.length - 1].held.push({ event, block });
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 `commit` nulls `held`, the replay *is* the placement timeline and the
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: `commit` decides the strip height and hands the held
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 commit(state)) {
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
  }