@beltype/cicero 0.1.10 → 0.1.12

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.
@@ -30,12 +30,12 @@ export type ForcingEvent = {
30
30
  b: number;
31
31
  el: Element;
32
32
  cut: Address;
33
- /** Its effective page name. */
34
- name: string;
33
+ /** A class A break point stands in front of it where the end page value before and its start page value differ (css-page-3 §8.1), so a page break is forced there and the page after it takes its start page value. */
34
+ nameBreak: boolean;
35
35
  /** Its `break-before` is forced. */
36
36
  before: boolean;
37
37
  beforeParity: 'left' | 'right' | null;
38
- /** Its `break-after` is forced, read only of an in-flow box. */
38
+ /** Its `break-after` is forced, read of an in-flow box and of a float (css-break-3 §3.1). */
39
39
  after: boolean;
40
40
  afterParity: 'left' | 'right' | null;
41
41
  /** The index just past its subtree. */
@@ -99,9 +99,22 @@ export declare const standsAfterArtifact: (el: Element) => boolean;
99
99
  */
100
100
  export declare function gapAbove(box: Box, report: ReturnType<typeof takeReport>, win: Window): number;
101
101
  /**
102
- * The effective page name of the deepest in-flow block that begins where a box does (css-page-3 §8.1): down the box's first in-flow block child while no run of words stands before that child, then `effectivePageName` from the box found. A first in-flow block child begins exactly where its parent does (§3.1.1's own coincidence), so the deepest box that starts the page is the end of the first-child chain.
102
+ * Does the `page` property apply to a box (css-page-3 §8.1, *"boxes that create class A break points"*)? An in-flow block-level box whose parent lays its children at class A break points (css-break-3 §4.1), and a table's row group or row.
103
103
  *
104
- * @remarks The driver's `nameAt` asks this of a cut that resumes at a box whole, with the position index's word counts for `runBefore`; a host with no index passes its own reading of the same question (*the engine knows no view*). Two reads are the host's: how a box's style is asked, and whether a run stands before a child.
104
+ * @remarks
105
+ * css-break-3 §4.1 counts *"a float and an immediately-adjacent in-flow or floated box"* among class A points too; the WPT tests `css-page/page-name-float-001` and `-002` have a name on a float force nothing, as Chromium prints them, and the engine follows the tests. A flex item, a grid item, a table cell and an inline-level box are not boxes it applies to (`page-name-flex-*`, `page-name-inline-block-003`).
106
+ *
107
+ * @param parent - the parent box's style, or null where the box stands in the body and nothing reads it.
108
+ */
109
+ export declare function pageApplies(st: CSSStyleDeclaration, parent: CSSStyleDeclaration | null): boolean;
110
+ /**
111
+ * The used page name of the box at `el` (css-page-3 §8.1): `effectivePageName` where the property applies to it, else its parent's. A box it does not apply to takes no name of its own; an inline box standing among blocks is in an anonymous block box, which the property applies to, named as its parent is.
112
+ */
113
+ export declare function usedPageName(el: Element, styleOf: (el: Element) => CSSStyleDeclaration): string;
114
+ /**
115
+ * The start page value of a box (css-page-3 §8.1): *"the value (if any) propagated from its first … child box, else the used value on the box itself"*, where *"a child propagates its own start … page value if and only if the page property applies to it"*. So down the first in-flow child while it is a box the property applies to ({@link pageApplies}) and no run of words stands before it, then {@link usedPageName} from the box found. The driver's `nameAt` names a page by it, and the forcing index compares it against the end page value before it.
116
+ *
117
+ * @remarks The driver's `nameAt` asks this of a cut that resumes at a box whole, with the position index's word counts for `runBefore`; a host with no index passes its own reading of the same question (*the engine knows no view*). Two reads are the host's: how a box's style is asked, and whether a run stands before a child. A run before the child is the box's first child box, an anonymous block box, which carries the box's own used value.
105
118
  *
106
119
  * @param el - The box the page begins in.
107
120
  * @param reads.runBefore - Whether a line of words stands before `child` in `parent`, which is the page's first content and the container's.
@@ -182,7 +195,7 @@ export declare const combineParity: (earlier: 'left' | 'right' | null, later: 'l
182
195
  *
183
196
  * This used to be a loop over consecutive feed items, and a feed item is a top-level block — so a command declared on a descendant was invisible, which is every heading in every BelType document since the belt began wrapping them in sections. The law (*no forced break lies inside an interval*, C1) always required this reading; the mechanism was narrower than the law. Descending closes it at word granularity, and §3.1.1's child→parent propagation falls out as a corollary rather than as walks: a `break-before` on a first in-flow child sits at the same word position as its container's start edge, and a `break-after` on a last in-flow child at the same position as its container's end, so the descent finds both where a promotion would have put them. `forcedBreakAfter` — the third of the rule's four walks, and the only forced one ever built — is deleted for exactly that reason.
184
197
  *
185
- * What the walk refuses to enter, each for its own citation: an absolutely positioned box (§3.1's must not — it neither carries forcing nor receives it, and §4.3's name switch is forcing under another name), a float's interior (§2.1 — a forced break inside a parallel flow does not affect the content outside it), and a monolith's interior (*the line box is the atom*, I3, and §4.1 in as many words: a forced break inside monolithic content must be ignored). A float's and a monolith's own commands are still read, which keeps case 14's declared §3.1 divergence exactly where the sixth round left it — except `break-after` on an out-of-flow box, which `forcedBreakAfter` never read either and which this walk goes on not reading rather than changing two things at once.
198
+ * What the walk refuses to enter, each for its own citation: an absolutely positioned box (§3.1's must not — it neither carries forcing nor receives it, and §4.3's name switch is forcing under another name), a float's interior (§2.1 — a forced break inside a parallel flow does not affect the content outside it), and a monolith's interior (*the line box is the atom*, I3, and §4.1 in as many words: a forced break inside monolithic content must be ignored). A float's and a monolith's own commands are still read: §3.1 says *"User agents should also apply these properties to floated boxes whose containing block is in the normal flow of the root fragmented element"*, so a float's forced `break-before` falls before its place in the flow and its forced `break-after` is pending into the box or run after it, never inside the float. A float the walk reaches has a containing block in that flow, since it never enters a float's or an absolutely positioned box's interior.
186
199
  *
187
200
  * `namesOnly` answers the composition region's version of the question. A name switch is the only forcing that changes the measure, so it is the only forcing a region must stop at: lines past a forced break are composed at the right width and merely unused.
188
201
  *
@@ -214,8 +227,12 @@ export declare function forcingIndexOf(blocks: Element[], { styleOf, pageNameOf,
214
227
  }) | null;
215
228
  }): {
216
229
  size: number;
217
- /** The first forcing event strictly after `cursorCut` for a page named `name`, or null. */
218
- find(cursorCut: Address, name: string, { namesOnly }?: {
230
+ /**
231
+ * The first forcing event strictly after `cursorCut`, or null.
232
+ *
233
+ * @param _name - the page's name, which a host still passes; a name break is the index's own, the end page value before a class A break point against the start page value after it (css-page-3 §8.1), and no longer a box's name against the page's.
234
+ */
235
+ find(cursorCut: Address, _name: string, { namesOnly }?: {
219
236
  namesOnly?: boolean;
220
237
  }): {
221
238
  b: number;
@@ -23,12 +23,13 @@ import { readGeometryTable, effectivePageName, pageRulesOf, styleSheetsOf, regis
23
23
  import { snapshotOfStyle } from './forcing-memo.js';
24
24
  import { cloneFlowPiece, declineRepeats, mayRepeat, mutateToContinuation, truncateFrom } from './fragments.js';
25
25
  import { cutAfter, divides, elAtIn, flattenResume, indexBlocks, inFlowIndex, inRemainders, normalizePath, pathOf, placeAt, remaindersOf } from './positions.js';
26
- import { documentOpsOf, counterStateAt, pageNumberAt, withoutPageCounters, stringAssignmentsOf, runningAssignmentsOf, namedStringsForPage } from './document-state.js';
26
+ import { documentOpsOf, withRunning, counterStateAt, pageNumberAt, withoutPageCounters, stringAssignmentsOf, runningAssignmentsOf, namedStringsForPage } from './document-state.js';
27
27
  import { RUNNING_PROP } from './gcpm-encoding.js';
28
28
  import { ARTIFACT, HOST, hasWord } from './data-beltype.js';
29
- import { blockChildrenOf, footerBeforeRows, inDisplayOrder, isFixedBox, SIDE_BY_SIDE, sourceOf } from './word-space.js';
29
+ import { blockChildrenOf, footerBeforeRows, inDisplayOrder, isFixedBox, isInlineLevel, SIDE_BY_SIDE, sourceOf } from './word-space.js';
30
30
  import { changedPairs, settleReads } from './reads.js';
31
31
  import { converge, holderPage, namesNoPlace, priorKeys, restartPage } from './resume.js';
32
+ import { readScope, foldTallColumns } from './scope.js';
32
33
  import { answerHas, boxesWhoseWordsMoved } from './has-words.js';
33
34
  const PX_PER = { px: 1, pt: 96 / 72, in: 96, cm: 96 / 2.54, mm: 96 / 25.4, q: 96 / 101.6 };
34
35
  function toPx(len) {
@@ -93,10 +94,37 @@ export function gapAbove(box, report, win) {
93
94
  }
94
95
  return gap;
95
96
  }
97
+ /** The displays besides a block container's whose children stand at class A break points: a table's row groups and a row group's rows (css-break-3 §4.1). */
98
+ const ROWS_HOLDER = /^(inline-)?table$|^table-(row-group|header-group|footer-group)$/;
99
+ /** Does a box of this display lay its children out at class A break points, a block container's in-flow block-level children or a table's rows (css-break-3 §4.1)? A flex or grid container does not: css-page-3 §8.1 gives `page` to no flex or grid item. */
100
+ const laysClassA = (display) => holdsAnonymousBlocks(display) || ROWS_HOLDER.test(display);
96
101
  /**
97
- * The effective page name of the deepest in-flow block that begins where a box does (css-page-3 §8.1): down the box's first in-flow block child while no run of words stands before that child, then `effectivePageName` from the box found. A first in-flow block child begins exactly where its parent does (§3.1.1's own coincidence), so the deepest box that starts the page is the end of the first-child chain.
102
+ * Does the `page` property apply to a box (css-page-3 §8.1, *"boxes that create class A break points"*)? An in-flow block-level box whose parent lays its children at class A break points (css-break-3 §4.1), and a table's row group or row.
98
103
  *
99
- * @remarks The driver's `nameAt` asks this of a cut that resumes at a box whole, with the position index's word counts for `runBefore`; a host with no index passes its own reading of the same question (*the engine knows no view*). Two reads are the host's: how a box's style is asked, and whether a run stands before a child.
104
+ * @remarks
105
+ * css-break-3 §4.1 counts *"a float and an immediately-adjacent in-flow or floated box"* among class A points too; the WPT tests `css-page/page-name-float-001` and `-002` have a name on a float force nothing, as Chromium prints them, and the engine follows the tests. A flex item, a grid item, a table cell and an inline-level box are not boxes it applies to (`page-name-flex-*`, `page-name-inline-block-003`).
106
+ *
107
+ * @param parent - the parent box's style, or null where the box stands in the body and nothing reads it.
108
+ */
109
+ export function pageApplies(st, parent) {
110
+ if (isOutOfFlow(st))
111
+ return false;
112
+ const display = st.display ?? '';
113
+ if (isInlineLevel(display) || /^table-(cell|column|column-group|caption)$/.test(display))
114
+ return false;
115
+ return laysClassA(parent?.display ?? 'block');
116
+ }
117
+ /**
118
+ * The used page name of the box at `el` (css-page-3 §8.1): `effectivePageName` where the property applies to it, else its parent's. A box it does not apply to takes no name of its own; an inline box standing among blocks is in an anonymous block box, which the property applies to, named as its parent is.
119
+ */
120
+ export function usedPageName(el, styleOf) {
121
+ const parent = el.parentElement;
122
+ return !parent || pageApplies(styleOf(el), styleOf(parent)) ? effectivePageName(el) : effectivePageName(parent);
123
+ }
124
+ /**
125
+ * The start page value of a box (css-page-3 §8.1): *"the value (if any) propagated from its first … child box, else the used value on the box itself"*, where *"a child propagates its own start … page value if and only if the page property applies to it"*. So down the first in-flow child while it is a box the property applies to ({@link pageApplies}) and no run of words stands before it, then {@link usedPageName} from the box found. The driver's `nameAt` names a page by it, and the forcing index compares it against the end page value before it.
126
+ *
127
+ * @remarks The driver's `nameAt` asks this of a cut that resumes at a box whole, with the position index's word counts for `runBefore`; a host with no index passes its own reading of the same question (*the engine knows no view*). Two reads are the host's: how a box's style is asked, and whether a run stands before a child. A run before the child is the box's first child box, an anonymous block box, which carries the box's own used value.
100
128
  *
101
129
  * @param el - The box the page begins in.
102
130
  * @param reads.runBefore - Whether a line of words stands before `child` in `parent`, which is the page's first content and the container's.
@@ -104,12 +132,15 @@ export function gapAbove(box, report, win) {
104
132
  */
105
133
  export function pageNameDownFrom(el, { styleOf, runBefore, blockChildren = blockChildrenOf }) {
106
134
  for (;;) {
135
+ const st = styleOf(el);
136
+ if (isOutOfFlow(st) || !laysClassA(st.display ?? ''))
137
+ break; // a page a float begins is named by the float's place in the flow, never by its interior
107
138
  const next = blockChildren(el).find((c) => !isOutOfFlow(styleOf(c)));
108
- if (!next || runBefore(el, next))
139
+ if (!next || runBefore(el, next) || !pageApplies(styleOf(next), st))
109
140
  break;
110
141
  el = next;
111
142
  }
112
- return effectivePageName(el);
143
+ return usedPageName(el, styleOf);
113
144
  }
114
145
  /**
115
146
  * Read the document's @page geometry from the CSSOM — every sheet the document wears, adopted ones included, and the rules inside its layers and conditions (`capture.ts`'s `pageRulesOf`). Returns page and content box sizes in px.
@@ -216,7 +247,7 @@ export const combineParity = (earlier, later) => later ?? earlier;
216
247
  *
217
248
  * This used to be a loop over consecutive feed items, and a feed item is a top-level block — so a command declared on a descendant was invisible, which is every heading in every BelType document since the belt began wrapping them in sections. The law (*no forced break lies inside an interval*, C1) always required this reading; the mechanism was narrower than the law. Descending closes it at word granularity, and §3.1.1's child→parent propagation falls out as a corollary rather than as walks: a `break-before` on a first in-flow child sits at the same word position as its container's start edge, and a `break-after` on a last in-flow child at the same position as its container's end, so the descent finds both where a promotion would have put them. `forcedBreakAfter` — the third of the rule's four walks, and the only forced one ever built — is deleted for exactly that reason.
218
249
  *
219
- * What the walk refuses to enter, each for its own citation: an absolutely positioned box (§3.1's must not — it neither carries forcing nor receives it, and §4.3's name switch is forcing under another name), a float's interior (§2.1 — a forced break inside a parallel flow does not affect the content outside it), and a monolith's interior (*the line box is the atom*, I3, and §4.1 in as many words: a forced break inside monolithic content must be ignored). A float's and a monolith's own commands are still read, which keeps case 14's declared §3.1 divergence exactly where the sixth round left it — except `break-after` on an out-of-flow box, which `forcedBreakAfter` never read either and which this walk goes on not reading rather than changing two things at once.
250
+ * What the walk refuses to enter, each for its own citation: an absolutely positioned box (§3.1's must not — it neither carries forcing nor receives it, and §4.3's name switch is forcing under another name), a float's interior (§2.1 — a forced break inside a parallel flow does not affect the content outside it), and a monolith's interior (*the line box is the atom*, I3, and §4.1 in as many words: a forced break inside monolithic content must be ignored). A float's and a monolith's own commands are still read: §3.1 says *"User agents should also apply these properties to floated boxes whose containing block is in the normal flow of the root fragmented element"*, so a float's forced `break-before` falls before its place in the flow and its forced `break-after` is pending into the box or run after it, never inside the float. A float the walk reaches has a containing block in that flow, since it never enters a float's or an absolutely positioned box's interior.
220
251
  *
221
252
  * `namesOnly` answers the composition region's version of the question. A name switch is the only forcing that changes the measure, so it is the only forcing a region must stop at: lines past a forced break are composed at the right width and merely unused.
222
253
  *
@@ -240,41 +271,84 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
240
271
  /**
241
272
  * @param [rowish]
242
273
  */
243
- const visit = (el, b, sealedBy, rowish = false) => {
274
+ /**
275
+ * css-page-3 §8.1's two values ride the walk: `visit` answers a box's start and end page values where it stands among its siblings at class A break points — a box the property applies to, or an inline box standing among blocks, which is an anonymous block box named as its parent is — and null for any other. Each sibling whose start value differs from the end value before it is a name break.
276
+ *
277
+ * @param [rowish]
278
+ * @param parentStyle - the parent's style, null in the body where nothing reads it.
279
+ * @param parentName - the parent's used page name, which an anonymous block box in it takes.
280
+ */
281
+ const visit = (el, b, sealedBy, rowish, parentStyle, parentName) => {
244
282
  const st = styleOf(el);
245
283
  if (isAbsolutelyPositioned(st))
246
- return; // §3.1's must not: it neither carries forcing nor receives it, so it is not an event at all
284
+ return null; // §3.1's must not: it neither carries forcing nor receives it, so it is not an event at all
247
285
  const parallel = isOutOfFlow(st);
248
- const e = { b, el, cut: cutOf(el, b), name: pageNameOf(el), before: isForced(st.breakBefore), beforeParity: parityOf(st.breakBefore), after: false, afterParity: null, end: 0, pos: posOf(el, b), inFlow: !parallel, sealedBy, rowish };
286
+ const applies = pageApplies(st, parentStyle);
287
+ const anonymous = !applies && !parallel && isInlineLevel(st.display ?? '') && laysClassA(parentStyle?.display ?? 'block');
288
+ const used = applies ? pageNameOf(el) : parentName;
289
+ const e = { b, el, cut: cutOf(el, b), nameBreak: false, before: isForced(st.breakBefore), beforeParity: parityOf(st.breakBefore), after: false, afterParity: null, end: 0, pos: posOf(el, b), inFlow: !parallel, sealedBy, rowish };
249
290
  const k = events.length;
250
291
  events.push(e);
251
292
  const inner = sealedBy >= 0 ? sealedBy : neverCutInside(el, st) ? k : -1;
293
+ let first = null;
294
+ let last = null;
252
295
  if (!parallel && !monolith(el, st)) {
253
296
  const kids = blockChildren(el);
254
- const runs = !!extentOf && holdsAnonymousBlocks(st.display ?? '');
297
+ // A run of lines among block children is an anonymous block box, or in a flex or grid container an anonymous item (CSS Flexbox §4, CSS Grid §6), which a forced break before it reaches the same way.
298
+ const runs = !!extentOf && (holdsAnonymousBlocks(st.display ?? '') || /^(inline-)?(flex|grid)$/.test(st.display ?? ''));
255
299
  const row = rowish || SIDE_BY_SIDE.has(st.display ?? '');
300
+ const flow = laysClassA(st.display ?? '');
301
+ const own = pageNameOf(el);
302
+ // The siblings' sequence: each box's start against the end before it, a run of lines being an anonymous block box named as `el` is.
303
+ const meet = (at, start, end) => {
304
+ if (!flow)
305
+ return;
306
+ if (last !== null && last !== start)
307
+ events[at].nameBreak = true;
308
+ first ??= start;
309
+ last = end;
310
+ };
311
+ if (runs && kids.length > 0 && extentOf(kids[0]).before > extentOf(el).before)
312
+ first = last = own;
256
313
  for (let i = 0; i < kids.length; i++) {
257
- visit(kids[i], b, inner, row);
314
+ const at = events.length;
315
+ const met = visit(kids[i], b, inner, row, st, own);
316
+ if (met)
317
+ meet(at, met.start, met.end);
258
318
  if (!runs)
259
319
  continue;
260
320
  // A run of lines after this child is an anonymous block box, and the box a forced `break-after` closing here is pending into: an event at its first word, in the word space of the box that holds it (*the anonymous box is in the procession*). Words standing between the child's end and the next child's start, or this box's end, are the run; none is no box.
261
- const ext = extentOf;
262
- const own = ext(el);
263
- const done = ext(kids[i]);
321
+ const extent = extentOf;
322
+ const ext = extent(el);
323
+ const done = extent(kids[i]);
264
324
  const word = done.before + done.count;
265
325
  const next = kids[i + 1];
266
- if ((next ? ext(next).before : own.before + own.count) <= word)
326
+ if ((next ? extent(next).before : ext.before + ext.count) <= word)
267
327
  continue;
268
- events.push({ b, el, cut: cutOf(el, b, word - own.before), name: e.name, before: false, beforeParity: null, after: false, afterParity: null, end: events.length + 1, pos: e.pos + word - own.before, inFlow: true, sealedBy: inner, rowish: row, runAfter: kids[i] });
328
+ events.push({ b, el, cut: cutOf(el, b, word - ext.before), nameBreak: false, before: false, beforeParity: null, after: false, afterParity: null, end: events.length + 1, pos: e.pos + word - ext.before, inFlow: true, sealedBy: inner, rowish: row, runAfter: kids[i] });
329
+ meet(events.length - 1, own, own);
269
330
  }
270
331
  }
271
- e.after = !parallel && isForced(st.breakAfter); // a float's own break-after was never read, and still is not
332
+ e.after = isForced(st.breakAfter); // a float's too (§3.1's should): pending into what follows its place in the flow
272
333
  e.afterParity = parityOf(st.breakAfter);
273
334
  e.end = events.length;
335
+ if (!applies && !anonymous)
336
+ return null;
337
+ return { start: applies ? first ?? used : used, end: applies ? last ?? used : used };
274
338
  };
339
+ const root = blocks[0]?.parentElement ?? null;
340
+ const rootStyle = root ? styleOf(root) : null;
341
+ const rootName = root ? pageNameOf(root) : '';
342
+ let before = null;
275
343
  for (let b = 0; b < blocks.length; b++) {
276
344
  blockStart.push(events.length);
277
- visit(blocks[b], b, -1);
345
+ const at = events.length;
346
+ const met = visit(blocks[b], b, -1, false, rootStyle, rootName);
347
+ if (!met)
348
+ continue;
349
+ if (before !== null && before !== met.start)
350
+ events[at].nameBreak = true;
351
+ before = met.end;
278
352
  }
279
353
  const n = events.length;
280
354
  // What the walk carries as `pending` into event k from the boxes that close just in front of it: those whose subtree ends at k, deepest first — a larger index is a deeper box — so the outermost's parity is the one that stands.
@@ -288,13 +362,13 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
288
362
  for (let k = n - 1; k >= 0; k--)
289
363
  nextForced[k] = closing[k] || events[k].before ? k : nextForced[k + 1];
290
364
  const nextName = new Array(n + 1).fill(n);
291
- for (let k = n - 2; k >= 0; k--)
292
- nextName[k] = events[k + 1].name !== events[k].name ? k + 1 : nextName[k + 1];
293
- const hit = (k, pending, name, namesOnly) => {
365
+ for (let k = n - 1; k >= 0; k--)
366
+ nextName[k] = events[k].nameBreak ? k : nextName[k + 1];
367
+ const hit = (k, pending, namesOnly) => {
294
368
  const e = events[k];
295
369
  const before = !namesOnly && e.before;
296
370
  // §4.3's combination at the one break point where two forced values meet: the `break-after` pending into the box, and the box's own `break-before`, the later element in the flow, whose parity wins where it has one. The address is a copy, so no page can reach into the index through it.
297
- return { b: e.b, namedSwitch: e.name !== name, el: e.el, cut: { ...e.cut, path: [...e.cut.path] }, parity: combineParity(pending?.parity ?? null, before ? e.beforeParity : null), rowish: e.rowish, runAfter: e.runAfter };
371
+ return { b: e.b, namedSwitch: e.nameBreak, el: e.el, cut: { ...e.cut, path: [...e.cut.path] }, parity: combineParity(pending?.parity ?? null, before ? e.beforeParity : null), rowish: e.rowish, runAfter: e.runAfter };
298
372
  };
299
373
  /** The first event strictly after the cursor — a binary search, because a bare address never precedes the one before it in walk order — or `n` where there is none. */
300
374
  const firstAfter = (cursorCut) => {
@@ -314,20 +388,24 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
314
388
  };
315
389
  return {
316
390
  size: n,
317
- /** The first forcing event strictly after `cursorCut` for a page named `name`, or null. */
318
- find(cursorCut, name, { namesOnly = false } = {}) {
391
+ /**
392
+ * The first forcing event strictly after `cursorCut`, or null.
393
+ *
394
+ * @param _name - the page's name, which a host still passes; a name break is the index's own, the end page value before a class A break point against the start page value after it (css-page-3 §8.1), and no longer a box's name against the page's.
395
+ */
396
+ find(cursorCut, _name, { namesOnly = false } = {}) {
319
397
  const s = firstAfter(cursorCut);
320
398
  if (s >= n)
321
399
  return null;
322
400
  // Pending into s: the forced break-afters whose subtree closes exactly in front of it — never one that closed earlier, which was spent where it fell (*a box is not its first child*).
323
401
  const pending = namesOnly ? null : closing[s];
324
- if (pending || events[s].name !== name || (!namesOnly && events[s].before))
325
- return hit(s, pending, name, namesOnly);
402
+ if (pending || events[s].nameBreak || (!namesOnly && events[s].before))
403
+ return hit(s, pending, namesOnly);
326
404
  const k = s + 1;
327
405
  if (k >= n)
328
406
  return null;
329
- const f = Math.min(events[k].name !== name ? k : nextName[k], namesOnly ? n : nextForced[k]);
330
- return f < n ? hit(f, namesOnly ? null : closing[f], name, namesOnly) : null;
407
+ const f = Math.min(nextName[k], namesOnly ? n : nextForced[k]);
408
+ return f < n ? hit(f, namesOnly ? null : closing[f], namesOnly) : null;
331
409
  },
332
410
  /**
333
411
  * The Cast-off's place (*the Cast-off bounds the feed*, 2026-09-26): the first block boundary strictly after `cursorCut` whose word position is at least `pos`, where a feed may be cut without moving any box before it — or null, where no such place is left and the feed is the whole remainder.
@@ -531,6 +609,8 @@ export function paginate(doc, opts = {}) {
531
609
  const boxes = opts.originals ?? topLevelBoxes(doc);
532
610
  const hasMarks = answerHas(doc, boxes, keep);
533
611
  performance.measure('cicero:has-words', { start: t0, end: performance.now(), detail: { boxes: boxes.length, marked: hasMarks.size, ...(memo ? { memo: memo.tally.has ?? null } : {}) } });
612
+ // Before the reads' counter walk, whose log the run takes: the reason is paginateOnce's, where the call stays for a caller that runs once.
613
+ registerGcpmProperties(doc.defaultView);
534
614
  return settleReads(doc, { run: (laid) => paginateOnce(doc, opts, laid, { boxes, hasMarks, memo }), blocksOf: () => opts.blocks ?? topLevelBlocks(doc), originals: opts.originals ?? opts.blocks ?? null, warm: opts.prior && opts.dirtyAt ? opts.prior.delivered : null, warmFacts: opts.prior && opts.dirtyAt ? opts.prior.facts ?? null : null, hide: !!opts.pace, keep });
535
615
  }
536
616
  /**
@@ -577,6 +657,8 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
577
657
  await awaitFonts(doc, table.furnitureFaces); // the faces page furniture names, requested now and not when the first margin box is presented (the table is read after the first wait, so its lengths are measured as they always were)
578
658
  const win = doc.defaultView;
579
659
  const blocks = intakeBlocks ?? topLevelBlocks(doc);
660
+ // What is out of scope is refused here, and a multi-column box too tall for a page becomes one column on its copies (`src/scope.ts`).
661
+ foldTallColumns(doc, readScope(win, blocks), table.pageFor({ first: true }));
580
662
  // The running elements and where each was written, handed back with the blocks by a host that took them at intake, for the same reason: by now the body holds the root and nothing else.
581
663
  const running = intakeRunning ?? runningBoxes(doc, memo?.keep);
582
664
  /** Whether a box outside an interval may be hollow (*the clone is hollow outside the interval*, 2026-09-27; *one attribute has words*, 2026-10-10). It may, unless a stylesheet the document wears has a `:has(` that nothing answers for: the host rewrites every `:has()` it can into a union that a `has:` word on the copy satisfies (`has-words.js`), and only an argument reading user state (`:hover`, `:checked`, `:focus-within`, `:target`, …) is left alone, with a console line from the rewrite naming the rule. Then every feed is built deep, as before, and the console says so once per run. `root.js` asks the same question of the same sheets when it presents. */
@@ -643,11 +725,14 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
643
725
  *
644
726
  * The ancestors are `html` and `body` — every top-level block is a body child — and they are recorded before the document's first word, which is what puts a `body { counter-reset }` in force on page one.
645
727
  */
646
- const documentOps = documentOpsOf(blocks, index, {
728
+ const ancestors = [doc.documentElement, doc.body].filter(Boolean);
729
+ const runningAt = running.map((r) => ({ name: r.name, pos: runningPosition(r, index) }));
730
+ // The reads' walk, where they took one, is this walk under their marks: a mark ends what an operation at its depth would end, and adds nothing else (`reads.ts`' `settleReads`).
731
+ const documentOps = laid.ops ? withRunning(laid.ops, runningAt) : documentOpsOf(blocks, index, {
647
732
  styleOf: (el) => win.getComputedStyle(el),
648
733
  pseudoStyleOf: sheetsCountInPseudos(doc) ? (el, pseudo) => win.getComputedStyle(el, pseudo) : null,
649
- ancestors: [doc.documentElement, doc.body].filter(Boolean),
650
- running: running.map((r) => ({ name: r.name, pos: runningPosition(r, index) })),
734
+ ancestors,
735
+ running: runningAt,
651
736
  keep: memo?.keep
652
737
  });
653
738
  // The same walk's second reading (*the host rewrites what the parser drops*, 2026-09-14): every named-string assignment the document makes, with the position it sits at. Evaluated here, where the source is still in its real ancestor chain — `content()` is the element's own text and would come back right from an unassigned slottable, but `counter()` inside a content list would not, and neither would anything inherited.
@@ -672,7 +757,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
672
757
  : forcingIndexOf(blocks, { styleOf: (el) => win.getComputedStyle(el), pageNameOf: effectivePageName, cutOf: (el, b, word = null) => cutTo(el, b, { word }), posOf: (el, b) => index.posOf(b, index.wordsBefore(el)), posOfCut, extentOf });
673
758
  performance.measure('cicero:forcing-index', { start: t, end: (t = performance.now()), detail: { events: forcing.size, ...(memo ? { memoHits: memo.hits, memoMisses: memo.misses } : {}) } });
674
759
  /**
675
- * The effective page name at an address: the deepest in-flow block that begins there, then css-page-3 §8.1's ancestor walk from it. A page may begin inside a block whose interior named a different page, so the cursor's block is not the thing to ask.
760
+ * The page name at an address: the start page value of the box that begins there (css-page-3 §8.1, `pageNameDownFrom`), or, where the page resumes inside a box, that box's used page name. A page may begin inside a block whose interior named a different page, so the cursor's block is not the thing to ask.
676
761
  *
677
762
  * A bare path resumes at a box whole, and a first in-flow block child begins exactly where its parent does (§3.1.1's own coincidence), so the deepest box that starts the page is the end of the first-child chain — which is where the old search for *the box containing word w* arrived, without the ambiguity a wordless first child gave it. A word or a pixel extent names a box the page resumes inside, and that box is itself the deepest; where the old search read the box after an end gap, it was naming the wrong side of a cut that resumes in the gap.
678
763
  */
@@ -681,9 +766,10 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
681
766
  if (!el)
682
767
  return '';
683
768
  // A line before a child is the page's first content, and it is the container's (*the anonymous box is in the procession*).
769
+ const styleOf = (c) => win.getComputedStyle(c);
684
770
  if (cut.word == null && cut.px == null)
685
- return pageNameDownFrom(el, { styleOf: (c) => win.getComputedStyle(c), runBefore: (parent, child) => index.wordsBefore(child) !== index.wordsBefore(parent) });
686
- return effectivePageName(el);
771
+ return pageNameDownFrom(el, { styleOf, runBefore: (parent, child) => index.wordsBefore(child) !== index.wordsBefore(parent) });
772
+ return usedPageName(el, styleOf);
687
773
  };
688
774
  /**
689
775
  * The document's own first page — css-page-3 §3.3, and it is a parity offset rather than a blank entry.
@@ -787,6 +873,13 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
787
873
  pageCounters.set(key, ops = table.pageFor({ name: c.name, first: i === 0, parity: c.parity, blank: c.blank }).pageCounter);
788
874
  return ops;
789
875
  };
876
+ /**
877
+ * The depth in the counter log's terms of the place a cut names: the element its path reaches, below the ancestors, or one level further in where the cut falls inside it, among its children (a `word` or a `px`).
878
+ *
879
+ * @remarks
880
+ * A place inside an element is taken at its children's depth, so an instance a grandchild opened before the cut is ended there even where the cut falls inside the child that holds it: a counter reset on an inline element nested in another inside a paragraph a page divides. No case claims it.
881
+ */
882
+ const depthOfCut = (cut) => ancestors.length + cut.path.length - 1 + (cut.word != null || cut.px != null ? 1 : 0);
790
883
  /** The conditions of page `pageIndex`, which begins at `cut` (projected to `pos`) — {@link conditionsFor} with the cursor made a parameter, so a resumed run can write the conditions of a page it keeps from the address that page began at (*the run resumes from an entry*, build two, 2026-10-08). */
791
884
  const conditionsAt = (pageIndex, cut, pos, { blank = false, carried = null } = {}) => {
792
885
  const name = nameAt(cut);
@@ -796,7 +889,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
796
889
  // The run's count, an author's reset of the page counter counted on from the page it begins on, and each page's own `@page` counter rules (`document-state.js`'s `pageNumberAt`). Entry `i` closes page `i`, so page `i` begins where entry `i - 1` ended, under the conditions it closes.
797
890
  const pageNumber = pageNumberAt(documentOps, pageIndex, pos, { pages: (i) => ({ start: i === 0 ? 0 : account[i - 1].pos ?? 0, blank: account[i].blank === true, counter: pageCounterOf(i, account[i].closes) }), blank, counter: geo.pageCounter, away });
798
891
  // The document's counters as they stand at this page's first word — strictly before it, which is §6.1's *spans the page break* in the flat position space (`document-state.js`) — less what the flows carried onto this page have still to show, which stands before that word and is on this page or after it (beltype#43). The page's own two are not among them: they are the host's, and `pageNumber` carries the reset.
799
- const counters = withoutPageCounters(counterStateAt(documentOps, pos, away));
892
+ const counters = withoutPageCounters(counterStateAt(documentOps, pos, away, depthOfCut(cut)));
800
893
  return Object.freeze({ ...geo, name, pageNumber, parity, blank, counters, dpr: win.devicePixelRatio });
801
894
  };
802
895
  /**
@@ -1139,6 +1232,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
1139
1232
  const conditions = conditionsFor(account.length);
1140
1233
  const recreated = ctx === null || conditions.contentW !== ctxWidth;
1141
1234
  if (recreated) {
1235
+ ctx?.dispose(); // each context seats a container of its own (since 2026-11-06), and one left standing is laid out in the document and printed
1142
1236
  ctx = createMeasureContext(doc, conditions.contentW);
1143
1237
  ctxWidth = conditions.contentW;
1144
1238
  }
@@ -1218,10 +1312,15 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
1218
1312
  let container;
1219
1313
  let pageStart;
1220
1314
  let spentLead;
1315
+ /** The place the last pour stood at when the check failed: a doubled bound that names it again is the same feed. */
1316
+ let short = null;
1221
1317
  for (;;) {
1222
1318
  pours++;
1223
1319
  // The place first, then the feed cloned to it (*the clone is hollow outside the interval*, 2026-09-27): neither the Cast-off's place nor the forced event needs anything but the cursor and the index.
1224
- const place = castOff.bound(cursor, words);
1320
+ let place = castOff.bound(cursor, words);
1321
+ // A doubled bound whose place is the one the last pour stopped above the edge at, its words landing inside the same long paragraph or the same box the bound never cuts inside, would pour the same feed again: it is doubled on without pouring (critic2-long-paragraph-quadratic, 2026-11-09).
1322
+ while (place && place.el === short)
1323
+ place = castOff.bound(cursor, (words = words * 2));
1225
1324
  const forcingCuts = !!forcedEvent && !rowForced && (!place || !cutAfter(forcedEvent.cut, place.cut, posOfCut));
1226
1325
  bounded = !forcingCuts && !!place;
1227
1326
  ({ items, cloneOfSource } = buildFeed(forcingCuts ? forcedEvent : bounded ? place : null, forcingCuts ? 'the grab' : 'the Cast-off'));
@@ -1240,7 +1339,9 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
1240
1339
  pageStart = (truncateLead ? report.contentTop : 0) + spentLead;
1241
1340
  if (rowForced) {
1242
1341
  /** A forced break inside a box that lays its children side by side is not the grab's to cut at (*a parallel flow is a fragmentainer*, 2026-10-08; *a forced break inside a cell*, P37c): the cut falls in the item's flow and only the row's height crosses the wall, so the feed is not cut at the event — a word position cannot say a cell's place, and cutting there put away the neighbour cell with everything after the paragraph in document order. Whether the event is inside a row item is a fact of the layout, asked here of the report: the box stands inside an item of a row that is not itself inside another item's, and after a sibling in the item's procession (a break before an item's first block is the item's own start). Where it is not, the grab cuts as it always did. An event past the feed the Cast-off bounded is not this page's, and a longer pour may reach it. */
1243
- const clone = cloneOfSource.get(forcedEvent.el);
1342
+ // A run has no box: its event is asked through the block child it follows, after which it stands, so it is never its item's start.
1343
+ const ev = forcedEvent;
1344
+ const clone = cloneOfSource.get(ev.runAfter ?? ev.el);
1244
1345
  const at = clone ? report.boxOf(clone) : null;
1245
1346
  rowEventIn = !!clone;
1246
1347
  if (clone) {
@@ -1254,7 +1355,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
1254
1355
  item = a;
1255
1356
  }
1256
1357
  const sequence = at ? report.processionOf(at) : [];
1257
- if (!at || !item || nested || sequence.indexOf(at) < 1 || at.rect.top <= item.rect.top + 0.25) {
1358
+ if (!at || !item || nested || (!ev.runAfter && (sequence.indexOf(at) < 1 || at.rect.top <= item.rect.top + 0.25))) {
1258
1359
  rowForced = false; // not a row's: the grab's, as before
1259
1360
  continue;
1260
1361
  }
@@ -1262,6 +1363,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
1262
1363
  }
1263
1364
  if (!bounded || reachesBelowTheEdge({ report, sourceOf, pageStart, contentHeight: conditions.contentH }))
1264
1365
  break;
1366
+ short = place.el;
1265
1367
  // @ts-expect-error `words` is a number wherever the feed is bounded, and a feed that is not bounded broke out above
1266
1368
  words *= 2;
1267
1369
  }
@@ -2,11 +2,11 @@
2
2
  * The fragmentation face: the engine's members a view adapter builds on, `@beltype/cicero/fragmentation`.
3
3
  *
4
4
  * @remarks
5
- * Exactly the names the first view adapter imported by deep path on 2026-10-29 (68 of them, from 16 modules), re-exported unchanged, and the names a view has asked for since (the leading-decoration door, `WRAPPED`, `clonesDecoration` and `truncateLeadingDecoration`, 2026-11-07); nothing is un-exported. Which of them stay public, merge or leave is the shape round's question. The face loads in node: `chase.ts`, which defines the `<beltype-page>` element and so needs a DOM at load, is not here; a view imports it for that effect as `@beltype/cicero/internal/chase`.
5
+ * Exactly the names the first view adapter imported by deep path on 2026-10-29 (68 of them, from 16 modules), re-exported unchanged, and the names a view has asked for since (the leading-decoration door, `WRAPPED`, `clonesDecoration` and `truncateLeadingDecoration`, 2026-11-07); nothing is un-exported. Which of them stay public, merge or leave is the shape round's question. The face loads in node: `chase.ts`, which defines the `<beltype-page>` element and so needs a DOM at load, is not here; a view imports it for that effect as `@beltype/cicero/page`.
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
- export { effectivePageName, readGeometryTable, registerGcpmProperties, topLevelReach, transliterateContent } from './capture.ts';
9
+ export { effectivePageName, readGeometryTable, registerGcpmProperties, sheetReach, topLevelReach, transliterateContent } from './capture.ts';
10
10
  export { ADJOIN, ARTIFACT, AWAY, END, FRAGMENT, SHELL, SPLIT, START, WRAPPED, addWord, hasWord, removeWord } from './data-beltype.ts';
11
11
  export { counterStateAt, documentOpsOf, namedStringsForPage, runningAssignmentsOf, stringAssignmentsOf } from './document-state.ts';
12
12
  export { ensureContinuationDressing, ensureDressing } from './dressing.ts';
@@ -19,6 +19,6 @@ export { STRING_KEYWORDS, encodeStringProp } from './gcpm-encoding.ts';
19
19
  export { assertRenderConditions, awaitCloneImages, awaitFonts, createMeasureContext } from './measure.ts';
20
20
  export { divides, elAt, extensionPx, indexBlocks, normalizePath, pageContaining, pageIntervals, pathOf, placeAt } from './positions.ts';
21
21
  export { present, presentation } from './presenters.ts';
22
- export { cssString, findPageSites, settleReads } from './reads.ts';
22
+ export { cssString, findPageSites, findSites, settleReads } from './reads.ts';
23
23
  export { adjudicate, firstBelowTheEdge, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.ts';
24
24
  export { BREAKING_SPACE, clonesDecoration, copyOf, isFixedBox, isInlineBox, isLineBreak, noteSource } from './word-space.ts';
@@ -2,11 +2,11 @@
2
2
  * The fragmentation face: the engine's members a view adapter builds on, `@beltype/cicero/fragmentation`.
3
3
  *
4
4
  * @remarks
5
- * Exactly the names the first view adapter imported by deep path on 2026-10-29 (68 of them, from 16 modules), re-exported unchanged, and the names a view has asked for since (the leading-decoration door, `WRAPPED`, `clonesDecoration` and `truncateLeadingDecoration`, 2026-11-07); nothing is un-exported. Which of them stay public, merge or leave is the shape round's question. The face loads in node: `chase.ts`, which defines the `<beltype-page>` element and so needs a DOM at load, is not here; a view imports it for that effect as `@beltype/cicero/internal/chase`.
5
+ * Exactly the names the first view adapter imported by deep path on 2026-10-29 (68 of them, from 16 modules), re-exported unchanged, and the names a view has asked for since (the leading-decoration door, `WRAPPED`, `clonesDecoration` and `truncateLeadingDecoration`, 2026-11-07); nothing is un-exported. Which of them stay public, merge or leave is the shape round's question. The face loads in node: `chase.ts`, which defines the `<beltype-page>` element and so needs a DOM at load, is not here; a view imports it for that effect as `@beltype/cicero/page`.
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
- export { effectivePageName, readGeometryTable, registerGcpmProperties, topLevelReach, transliterateContent } from './capture.js';
9
+ export { effectivePageName, readGeometryTable, registerGcpmProperties, sheetReach, topLevelReach, transliterateContent } from './capture.js';
10
10
  export { ADJOIN, ARTIFACT, AWAY, END, FRAGMENT, SHELL, SPLIT, START, WRAPPED, addWord, hasWord, removeWord } from './data-beltype.js';
11
11
  export { counterStateAt, documentOpsOf, namedStringsForPage, runningAssignmentsOf, stringAssignmentsOf } from './document-state.js';
12
12
  export { ensureContinuationDressing, ensureDressing } from './dressing.js';
@@ -18,6 +18,6 @@ export { STRING_KEYWORDS, encodeStringProp } from './gcpm-encoding.js';
18
18
  export { assertRenderConditions, awaitCloneImages, awaitFonts, createMeasureContext } from './measure.js';
19
19
  export { divides, elAt, extensionPx, indexBlocks, normalizePath, pageContaining, pageIntervals, pathOf, placeAt } from './positions.js';
20
20
  export { present, presentation } from './presenters.js';
21
- export { cssString, findPageSites, settleReads } from './reads.js';
21
+ export { cssString, findPageSites, findSites, settleReads } from './reads.js';
22
22
  export { adjudicate, firstBelowTheEdge, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.js';
23
23
  export { BREAKING_SPACE, clonesDecoration, copyOf, isFixedBox, isInlineBox, isLineBreak, noteSource } from './word-space.js';
@@ -18,10 +18,10 @@
18
18
  * @packageDocumentation
19
19
  */
20
20
  import { applyRecordsUnder, hyphenCharOf } from './compose-dom.js';
21
- import { addWord, ADJOIN, ARTIFACT, AWAY, END, ENGINE_MARKS, hasAnyWord, hasWord, removeWord, SHELL, SPLIT, START, WRAPPED } from './data-beltype.js';
21
+ import { addWord, ADJOIN, ARTIFACT, AWAY, BREAK, END, ENGINE_MARKS, hasAnyWord, hasWord, removeWord, SHELL, SPLIT, START, WRAPPED } from './data-beltype.js';
22
22
  import { pairsOf } from './document-state.js';
23
23
  import { divides, elAtIn, flowsOf } from './positions.js';
24
- import { BREAKING_SPACE, clonesDecoration, cutRanges, emptyText, inDisplayOrder, leaveOut, replaceChars, isEngineText, mayBeAutoHyphenated, isFixedBox, isInlineBox, isLineBreak, copyOf, shellOf, noteText, READS_ITS_CHILDREN, sourceOf, sourceStyle } from './word-space.js';
24
+ import { BREAKING_SPACE, clonesDecoration, cutRanges, emptyText, inDisplayOrder, leaveOut, replaceChars, isEngineText, mayBeAutoHyphenated, noteEngineText, splitTextAt, isFixedBox, isInlineBox, isLineBreak, copyOf, shellOf, noteText, READS_ITS_CHILDREN, sourceOf, sourceStyle } from './word-space.js';
25
25
  /** The word space, clone side: words are maximal non-whitespace runs of the subtree's joined text — a run crossing a text-node or inline-element seam is one word, exactly as the Gauge reads, and exactly as the position index counts over sources (src/positions.js — the two tokenizations must never drift; counting per node instead shifts every offset by the seam-spanning words before it, which the test-document's inline markup caught). **A non-inline boundary ends the run** (word-space.js), which is where this stopped agreeing with the page for two months. This walk survives on the clone side only: consumption must find the resume word's actual text nodes in the mutated copy, which no source-side index can answer. */
26
26
  export function joinedWords(root, isInline = isInlineBox) {
27
27
  const words = [];
@@ -545,6 +545,7 @@ function consumeTrailingWords(clone, n, char = 0, hollow = false) {
545
545
  for (const [node, ranges] of perNode)
546
546
  cutRanges(node, ranges);
547
547
  // The hyphen is written at the cut (*the hyphen is written at the cut*, 2026-10-06). A soft hyphen the browser broke the line at is drawn there; one at the end of a fragment's last line is not, because nothing breaks after it. So the last kept character, where it is a soft hyphen, becomes the hyphenation character the browser drew — the box's own `hyphenate-character`, else U+2010 (css-text-4 §6.3.2) — and the empty string, which the property allows, removes it. A replacement: the word keeps its length, and the Compositor's own break, whose hyphen is already written and whose soft hyphen stands after it, cuts before the soft hyphen and never meets this.
548
+ let autoHyphen = null;
548
549
  if (char > 0 && words[n].text[char - 1] === '\u00AD') {
549
550
  const soft = charRanges(words[n], char - 1, char).find((r) => !r.engine);
550
551
  if (soft) {
@@ -558,7 +559,7 @@ function consumeTrailingWords(clone, n, char = 0, hollow = false) {
558
559
  const kept = last.find((r) => !r.engine);
559
560
  const style = kept && sourceStyle(kept.node.parentElement);
560
561
  if (kept && style && !last.some((r) => r.engine) && mayBeAutoHyphenated(words[n].text[char - 1], words[n].text[char], style.hyphens))
561
- replaceChars(kept.node, kept.end, kept.end, hyphenCharOf(style));
562
+ autoHyphen = { node: kept.node, end: kept.end, char: hyphenCharOf(style) };
562
563
  }
563
564
  leaveOutAfter(clone, keepNode);
564
565
  // Structural pass: an element with no remaining words, standing entirely after the boundary, is spent — its edges adjoin the cut (§5.2) and it leaves the fragment.
@@ -582,6 +583,19 @@ function consumeTrailingWords(clone, n, char = 0, hollow = false) {
582
583
  })(clone);
583
584
  for (const el of spent)
584
585
  putAway(el, hollow);
586
+ // Written last, after the leave-out and the structural pass that would take an element and a text with no source for spent: the hyphen the browser drew stands where the Compositor's stands, a text node of its own, the engine's, before a break mark (*the hyphen is written at the cut*; the printer finds the hyphen by that shape and the letters before it in the source's own run).
587
+ if (autoHyphen && autoHyphen.char) {
588
+ const { node, end } = autoHyphen;
589
+ const parent = node.parentNode;
590
+ const doc = node.parentElement.ownerDocument;
591
+ const before = end < node.data.length ? splitTextAt(node, end) : node.nextSibling;
592
+ const written = doc.createTextNode(autoHyphen.char);
593
+ noteEngineText(written);
594
+ const br = doc.createElement('br');
595
+ addWord(br, BREAK);
596
+ parent.insertBefore(written, before);
597
+ parent.insertBefore(br, before);
598
+ }
585
599
  for (let el = inner; el && el !== clone; el = el.parentElement)
586
600
  truncateTrailingDecoration(el);
587
601
  return inner;
@@ -748,6 +762,10 @@ export function mutateToContinuation(clone, { cut = null, remainderPx = null, so
748
762
  const mine = itemsOf(flows ?? flowsOf(items), path, source, cloneOf);
749
763
  // The items of a divided row stand before the place this cut resumes at, and are not spent: each has a place of its own, applied below.
750
764
  const keep = new Set(mine.map((m) => m.itemClone));
765
+ // A row a spanning cell's group reaches stands before the place too (the Gauge's rows: a `rowspan` makes the cells of every row it spans one row), and every cell in it is an item with a place of its own, so the row stays and its cells are cut below.
766
+ for (const m of mine)
767
+ if (m.itemClone.parentElement && m.itemClone.parentElement !== clone && /^TR$/i.test(m.itemClone.parentElement.tagName))
768
+ keep.add(m.itemClone.parentElement);
751
769
  if (divides(cut)) {
752
770
  const target = targetOf(cut, source, cloneOf);
753
771
  if (!target)
@@ -843,14 +861,14 @@ export function listOrdinals(ol) {
843
861
  return out;
844
862
  }
845
863
  /**
846
- * Every `<ol>` a cut divides is numbered from the first item its piece keeps: the piece's copy carries a `start` attribute of that item's ordinal, so the browser's own marker counts on from the source's numbering. Without it a continued list numbered from 1 again on every page, and the first piece of a `reversed` list counted down from the items it kept (measured 2026-11-07 against Chromium's print, `test/browser/list-continuation.browser.ts`). Read from the source, so it holds whichever cut ran first.
864
+ * Every `<ol>` (or numbered `<ul>` or `<menu>`) a cut divides is numbered from the first item its piece keeps: the piece's copy carries a `start` attribute of that item's ordinal, so the browser's own marker counts on from the source's numbering. Without it a continued list numbered from 1 again on every page, and the first piece of a `reversed` list counted down from the items it kept (measured 2026-11-07 against Chromium's print, `test/browser/list-continuation.browser.ts`). Read from the source, so it holds whichever cut ran first.
847
865
  */
848
866
  function numberDividedLists(root, cut, source, cloneOf) {
849
867
  if (!divides(cut) || !source || !cloneOf)
850
868
  return;
851
869
  const target = elAtIn(source, cut.path);
852
870
  for (let a = target?.parentElement ?? null; a; a = a === source ? null : a.parentElement) {
853
- if (!/^OL$/i.test(a.tagName))
871
+ if (!/^(?:OL|UL|MENU)$/i.test(a.tagName))
854
872
  continue;
855
873
  const copy = cloneOf.get(a);
856
874
  if (!copy)
@@ -859,7 +877,11 @@ function numberDividedLists(root, cut, source, cloneOf) {
859
877
  const c = cloneOf.get(li);
860
878
  if (!c || !root.contains(c) || hasWord(c, AWAY))
861
879
  continue;
862
- copy.setAttribute('start', String(n));
880
+ // HTML's `start` belongs to `<ol>` alone, and Chromium's marker ignores a `counter-reset` of `list-item` (both measured 2026-11-08), so a `ul` or `menu` continues by the first kept item's own `value`.
881
+ if (/^OL$/i.test(a.tagName))
882
+ copy.setAttribute('start', String(n));
883
+ else
884
+ c.setAttribute('value', String(n));
863
885
  break;
864
886
  }
865
887
  }
@@ -896,6 +918,9 @@ function shellAt(clone, target, px, keep = new Set(), hollow = false) {
896
918
  el.style.boxSizing = 'border-box';
897
919
  el.style.height = `${Math.max(0, px.remaining)}px`;
898
920
  el.style.minHeight = '0';
921
+ // Its block-end padding is spent with the rest (`block-end-padding-taller-than-page`, assessment 2026-11-07): the shell has no content, so its padding paints exactly as the content box it gives way to, and the border box is the remainder alone. A padding taller than the page area otherwise held every shell at its own height, the remainder never shrank, and the walk refused. A box that clones its decoration wears its padding on every fragment, so it keeps it.
922
+ if (!clonesDecoration(el))
923
+ el.style.setProperty('padding-bottom', '0', 'important');
899
924
  return clone;
900
925
  }
901
926
  // The painted shell: a slice wrapper takes the box's place in flow at exactly the leftover height and clips there; the box keeps its natural size — an image must never be resized into the remainder — and a negative top margin pulls the already-shown part above the clip.