@beltype/cicero 0.1.11 → 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.
@@ -35,7 +35,11 @@ declare function patchBox(index: number, path: number[], el: Element): Address |
35
35
  export type ChromePatch = {
36
36
  chrome: true;
37
37
  };
38
- /** Resolves when the rim has nothing pending and no run is under way (a run stopped at a page is at rest). The door a venue test and a host that wants to know it is looking at the settled document call. */
38
+ /**
39
+ * Resolves when the document will not be cut again unless something is done to it: no call of `run()` is unanswered, no run is under way (a run stopped at a page is at rest), the rim has nothing it is answering or judging, `document.fonts` has nothing loading and every picture of the document's boxes has arrived (CHANGELOG, Unreleased). The door a venue test, a host that wants to know it is looking at the settled document and a printer that reads the presented pages call.
40
+ *
41
+ * @remarks `run()` resolving is not this. A face a page's margin boxes ask for is requested when the presentation is laid out, which is after the last run began, so it arrives after the run's answer, is heard as a late fact (`loadingdone`) and cuts the document again under whoever read the pages in between: measured 2026-11-08 on the essay's `margin-boxes/content-001`, a data-URL face first requested by the presented margin boxes, heard 33 ms into the first run and cutting again 3 ms later, and Caster refusing 'the pages were replaced while they were being printed' one print in three. So the wait is a loop: lay out, so every face the pages ask for is asked for; await the faces and the pictures; await a frame, so what they caused is heard; and look again, leaving only when a whole pass found nothing under way. A released client is settled.
42
+ */
39
43
  declare function settled(): Promise<void>;
40
44
  /** One run, from the call that started it to the moment it is complete or let go. */
41
45
  export type Session = {
@@ -273,10 +273,28 @@ async function quiet() {
273
273
  while (working || calls.size || (live && !live.gate))
274
274
  await Promise.race([working, ...calls, live?.finished].filter(Boolean)).catch(() => { });
275
275
  }
276
- /** Resolves when the rim has nothing pending and no run is under way (a run stopped at a page is at rest). The door a venue test and a host that wants to know it is looking at the settled document call. */
276
+ /**
277
+ * Resolves when the document will not be cut again unless something is done to it: no call of `run()` is unanswered, no run is under way (a run stopped at a page is at rest), the rim has nothing it is answering or judging, `document.fonts` has nothing loading and every picture of the document's boxes has arrived (CHANGELOG, Unreleased). The door a venue test, a host that wants to know it is looking at the settled document and a printer that reads the presented pages call.
278
+ *
279
+ * @remarks `run()` resolving is not this. A face a page's margin boxes ask for is requested when the presentation is laid out, which is after the last run began, so it arrives after the run's answer, is heard as a late fact (`loadingdone`) and cuts the document again under whoever read the pages in between: measured 2026-11-08 on the essay's `margin-boxes/content-001`, a data-URL face first requested by the presented margin boxes, heard 33 ms into the first run and cutting again 3 ms later, and Caster refusing 'the pages were replaced while they were being printed' one print in three. So the wait is a loop: lay out, so every face the pages ask for is asked for; await the faces and the pictures; await a frame, so what they caused is heard; and look again, leaving only when a whole pass found nothing under way. A released client is settled.
280
+ */
277
281
  async function settled() {
278
- while (working || (live && !live.gate))
279
- await (working ?? live.finished);
282
+ for (;;) {
283
+ await quiet();
284
+ if (released)
285
+ return;
286
+ void document.documentElement.offsetHeight; // the layout that requests the faces the presented pages ask for
287
+ await document.fonts.ready;
288
+ await picturesArrived();
289
+ await frame();
290
+ if (released || !(working || calls.size || (live && !live.gate) || rim?.judging() || document.fonts.status === 'loading'))
291
+ return;
292
+ }
293
+ }
294
+ /** Resolves when every `<img>` of the document's top-level boxes that was still loading has loaded or failed. */
295
+ async function picturesArrived() {
296
+ const pending = (presented?.boxes ?? []).flatMap((box) => [...box.querySelectorAll('img')]).filter((img) => !img.complete);
297
+ await Promise.all(pending.map((img) => new Promise((resolve) => { img.addEventListener('load', () => resolve(), { once: true }); img.addEventListener('error', () => resolve(), { once: true }); })));
280
298
  }
281
299
  /** The run that stopped at a page, if there is one (*the pages are cut as far as they are asked*, 2026-10-12): the driver is waiting inside its page loop with its place, its measure context and its Ledger kept, and the pages it has cut are on screen. `run()` with no `resumeAt` on the same mode goes on from here; anything else lets it go. */
282
300
  let live = null;
@@ -41,7 +41,7 @@ export declare function bringsNewFace(event: Event): boolean;
41
41
  * @param p.idle - resolves when no run is under way and nothing the rim told is still being answered: a size is judged then, against the table the run leaves.
42
42
  * @param p.unbuilt - whether some page of the presentation is not built yet: a picture's box that is not drawn as itself may stand on it, and is judged again when the observer next reports. Absent, every page is built.
43
43
  * @param p.busy - whether a run is under way or a signal is being answered now: asked again just before a size is judged, since a named signal may have come while the ear waited for the frame.
44
- * @returns `add` subscribes to a box, releasing what the box it replaced (a patched edit) and the box itself already held, so one load is one signal; `pieces` observes the pieces a page was just built with, each beside its source box, and lets go of the ones a presentation took out; `stop` releases everything.
44
+ * @returns `add` subscribes to a box, releasing what the box it replaced (a patched edit) and the box itself already held, so one load is one signal; `pieces` observes the pieces a page was just built with, each beside its source box, and lets go of the ones a presentation took out; `judging` says whether a size or a picture was heard and not yet judged; `stop` releases everything.
45
45
  */
46
46
  export declare function watchLateTruth({ document: doc, fonts, originals, onSignal, presented, pageHolding, measured, idle, busy, unbuilt }: {
47
47
  document: Document;
@@ -60,6 +60,7 @@ export declare function watchLateTruth({ document: doc, fonts, originals, onSign
60
60
  unbuilt?: () => boolean;
61
61
  }): {
62
62
  stop: () => void;
63
+ judging: () => boolean;
63
64
  add: (box: Element, replaced?: Element) => void;
64
65
  pieces: (built: Array<[Element, Element]>, removed: Iterable<Element>) => void;
65
66
  };
package/dist/seam/rim.js CHANGED
@@ -75,7 +75,7 @@ export function bringsNewFace(event) {
75
75
  * @param p.idle - resolves when no run is under way and nothing the rim told is still being answered: a size is judged then, against the table the run leaves.
76
76
  * @param p.unbuilt - whether some page of the presentation is not built yet: a picture's box that is not drawn as itself may stand on it, and is judged again when the observer next reports. Absent, every page is built.
77
77
  * @param p.busy - whether a run is under way or a signal is being answered now: asked again just before a size is judged, since a named signal may have come while the ear waited for the frame.
78
- * @returns `add` subscribes to a box, releasing what the box it replaced (a patched edit) and the box itself already held, so one load is one signal; `pieces` observes the pieces a page was just built with, each beside its source box, and lets go of the ones a presentation took out; `stop` releases everything.
78
+ * @returns `add` subscribes to a box, releasing what the box it replaced (a patched edit) and the box itself already held, so one load is one signal; `pieces` observes the pieces a page was just built with, each beside its source box, and lets go of the ones a presentation took out; `judging` says whether a size or a picture was heard and not yet judged; `stop` releases everything.
79
79
  */
80
80
  export function watchLateTruth({ document: doc, fonts = doc.fonts, originals, onSignal, presented = () => false, pageHolding = () => null, measured = () => undefined, idle = async () => { }, busy = () => false, unbuilt = () => false }) {
81
81
  const win = doc.defaultView;
@@ -269,6 +269,8 @@ export function watchLateTruth({ document: doc, fonts = doc.fonts, originals, on
269
269
  releases.push(() => doc.removeEventListener('beltype:change', foreign));
270
270
  return {
271
271
  add,
272
+ /** Is a size or a picture heard and not yet judged? The client's `settled` waits for it. */
273
+ judging: () => judging,
272
274
  pieces(built, removed) {
273
275
  for (const el of removed) {
274
276
  if (!pieces.delete(el))
@@ -15,7 +15,7 @@ import { addWord, AWAY, BREAK, FILL, hasWord } from './data-beltype.js';
15
15
  import { createMeasureContext } from './measure.js';
16
16
  import { indexBlocks, pageIntervals, presentationWidths } from './positions.js';
17
17
  import { hyphenationOffsets, languageFor, resolveLimits } from './hyphenate.js';
18
- import { BREAKING_SPACE, measuringCopyOf, HTML_NS, isAtom, isFixedBox, isInlineBox, isInlineLevel, noteEngineText, rangeOf, sourceOf, splitTextAt, UNSPACED_SCRIPT } from './word-space.js';
18
+ import { BREAKING_SPACE, measuringCopyOf, HTML_NS, isAtom, isFixedBox, isInlineBox, isInlineLevel, isOutsideMarker, noteEngineText, rangeOf, sourceOf, splitTextAt, UNSPACED_SCRIPT } from './word-space.js';
19
19
  import { attributesOf, blockKey, CompositionCache, lookup, paragraphKey, remember, shapeOf } from './composition-key.js';
20
20
  /** The computed displays of a block container, one of which a paragraph's own box must have (*the paragraph is what the browser breaks*, clause one). */
21
21
  const PARAGRAPH_DISPLAY = new Set(['block', 'flow-root', 'list-item', 'table-cell', 'table-caption']);
@@ -42,7 +42,7 @@ export function isComposable(el) {
42
42
  for (const child of el.children) {
43
43
  if (child.tagName === 'BR')
44
44
  continue;
45
- if (!isInlineBox(child))
45
+ if (!isInlineBox(child) && !isOutsideMarker(child))
46
46
  return false;
47
47
  }
48
48
  for (let a = el.parentElement; a; a = a.parentElement)
@@ -119,6 +119,9 @@ export function collectWords(el) {
119
119
  i = j;
120
120
  }
121
121
  }
122
+ else if (child.nodeType === 1 && isOutsideMarker(child)) {
123
+ // a hanging number: out of the text being set
124
+ }
122
125
  else if (child.nodeType === 1 && child.tagName === 'BR') {
123
126
  current = null;
124
127
  if (words.length && runStarts[runStarts.length - 1] !== words.length)
@@ -60,6 +60,7 @@ export declare const FILL = "fill";
60
60
  export declare const WRAPPED = "wrapped";
61
61
  export declare const ADJOIN = "adjoin";
62
62
  export declare const AWAY = "away";
63
+ export declare const ONE_COLUMN = "one-column";
63
64
  /** PDF's own name for pagination furniture: a copy the engine repeats that a reader does not read (the registry entry says which). */
64
65
  export declare const ARTIFACT = "artifact";
65
66
  /** The words that mark an element as the engine's own — what the "class starts with `__beltype-`" test used to say, as a named set: a divided box (it carried `__beltype-cont` or `-cut`, and a first fragment `-first`), a fixed box's copy, and the three elements the engine creates. `fragment` is not here: a composed block's clone carried no class and was never an engine mark. */
@@ -160,6 +161,7 @@ export declare const CUT_OFF: string;
160
161
  * - A resumed piece gives up the margin that adjoins the break, and the indent (the box is not beginning); and, unless the box wraps each fragment whole (`wrapped`, css-break-3 §5.4's `clone`), its border, its padding and its corners' rounding at that edge: a slice is the box cut through, so the cut edge is square (Chromium's print, measured 2026-11-07).
161
162
  * - A resumed piece's counter operations already happened where the box began (`counter-reset`, `counter-increment` and `counter-set` are `none` on it): the page's start delivers the counters as they stood (`chase.js`'s `setCounters`), and a clone that fired them again would count the box twice.
162
163
  * - A cut-off piece gives up the margin before the break, and unless `wrapped` its border, padding and rounding there.
164
+ * - `one-column`: a multi-column box too tall for any page is laid out as one column, since the engine does not divide columns yet (`src/scope.ts`).
163
165
  * - `adjoin`: the box a bare-path cut resumes at, and the first-child chain through which its margin collapses, give up the margin that adjoins the break (css-break-3 §5.2) and nothing else — they are not divided.
164
166
  */
165
167
  export declare const WORDS_RULES: string;
@@ -39,6 +39,7 @@ export const REGISTRY = Object.freeze([
39
39
  { name: 'adjoin', kind: 'flag', status: 'in use', writer: 'engine', from: 'cut', facing: 'engine', meaning: 'the first-child chain after a cut BETWEEN boxes: its block-start margin adjoins the cut and is truncated, and the box is not divided' },
40
40
  { name: 'away', kind: 'flag', status: 'in use', writer: 'engine', from: 'cut', facing: 'engine', meaning: 'a box put away on this page (`display: none`): kept as an element so structural selectors read as in the source' },
41
41
  { name: 'host', kind: 'flag', status: 'in use', writer: 'host', from: 'host', facing: 'engine', meaning: 'the host\'s word on an element of its own machinery that stands among the document\'s boxes (a hidden `<iframe>`, a probe); the intake passes over it, so it gets no address and moves none (ruled 2026-11-08)' },
42
+ { name: 'one-column', kind: 'flag', status: 'in use', writer: 'engine', from: 'original', facing: 'engine', meaning: 'a multi-column box too tall for any page, laid out as one column on its copies because the engine does not yet divide columns (the console names the element)' },
42
43
  { name: 'artifact', kind: 'flag', status: 'in use', writer: 'engine', from: 'cut', facing: 'host', meaning: 'a copy the engine repeats that is not the one a reader reads: a running element\'s copy in a margin box, a fixed box\'s copy on every page after the first, and a table\'s repeated header or footer group; PDF\'s own word for pagination furniture, and nothing else. Every artifact is also aria-hidden' }
43
44
  ].map((e) => Object.freeze(e)));
44
45
  /** The flags in use. */
@@ -63,6 +64,7 @@ export const FILL = 'fill';
63
64
  export const WRAPPED = 'wrapped';
64
65
  export const ADJOIN = 'adjoin';
65
66
  export const AWAY = 'away';
67
+ export const ONE_COLUMN = 'one-column';
66
68
  /** PDF's own name for pagination furniture: a copy the engine repeats that a reader does not read (the registry entry says which). */
67
69
  export const ARTIFACT = 'artifact';
68
70
  /** The words that mark an element as the engine's own — what the "class starts with `__beltype-`" test used to say, as a named set: a divided box (it carried `__beltype-cont` or `-cut`, and a first fragment `-first`), a fixed box's copy, and the three elements the engine creates. `fragment` is not here: a composed block's clone carried no class and was never an engine mark. */
@@ -300,6 +302,7 @@ const RESUMED_BOX = `${RESUMED}:not(${w(SHELL)})`;
300
302
  * - A resumed piece gives up the margin that adjoins the break, and the indent (the box is not beginning); and, unless the box wraps each fragment whole (`wrapped`, css-break-3 §5.4's `clone`), its border, its padding and its corners' rounding at that edge: a slice is the box cut through, so the cut edge is square (Chromium's print, measured 2026-11-07).
301
303
  * - A resumed piece's counter operations already happened where the box began (`counter-reset`, `counter-increment` and `counter-set` are `none` on it): the page's start delivers the counters as they stood (`chase.js`'s `setCounters`), and a clone that fired them again would count the box twice.
302
304
  * - A cut-off piece gives up the margin before the break, and unless `wrapped` its border, padding and rounding there.
305
+ * - `one-column`: a multi-column box too tall for any page is laid out as one column, since the engine does not divide columns yet (`src/scope.ts`).
303
306
  * - `adjoin`: the box a bare-path cut resumes at, and the first-child chain through which its margin collapses, give up the margin that adjoins the break (css-break-3 §5.2) and nothing else — they are not divided.
304
307
  */
305
308
  export const WORDS_RULES = `
@@ -310,6 +313,7 @@ export const WORDS_RULES = `
310
313
  ${CUT_OFF} { margin-bottom: 0 !important; }
311
314
  ${CUT_OFF}:not(${w(WRAPPED)}) { border-bottom-width: 0 !important; padding-bottom: 0 !important; border-bottom-left-radius: 0 !important; border-bottom-right-radius: 0 !important; }
312
315
  ${w(ADJOIN)} { margin-top: 0 !important; }
316
+ ${w(ONE_COLUMN)} { column-count: auto !important; column-width: auto !important; }
313
317
  }
314
318
  `;
315
319
  /**
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * **A position is the flat word index** (`positions.js`), and an operation applies when its position is strictly before the one asked about — and not inside a remainder a carried flow has still to show, which stands before the page's first word in document order and is on that page or after it (beltype#43; `positions.js`' `remaindersOf`). That is §6.1's *"the deepest element in the normal flow that spans the page break"* said in this engine's own space: an element whose first word is the page's first word begins on the new page and does not span the break, so its own operation is not yet in force, while an element that spans the break started before that word and is included. The ancestors above the top-level blocks sit at position **−1** — they open before the document's first word and span every break there is — which is what makes the first page's answer fall out of the same comparison rather than needing a case of its own.
15
15
  *
16
- * **What is not read, each a real gap and each showing as a counter reading low.** css-lists-3's `reversed()` counters (an author's `counter-reset: reversed(name)`), a top-level `position: fixed` box, which is not among the flow blocks the position index is built over; and a scope that ended before the position asked about with no operation logged between its end and the position, since the replay closes a scope only when an operation at or above its depth arrives: a `counter(list-item)` read on a page past a list whose last item holds a list reads the inner list's count (3 where Chromium prints 2, measured 2026-11-07), and an author's nested reset is read the same way. `display: none` elements are skipped, which is CSS 2.1's own rule rather than an omission. A list's and a list item's implicit `list-item` operations are read since 2026-11-07 (`implicitListOps`), so a `counter(list-item)` read on a page a list continued onto or past it counts the items before it. A `::before`'s and an `::after`'s counters are read since 2026-11-07, where a sheet sets one (`pseudoStyleOf`). **And `content(before)`, `content(after)` and `content(first-letter)` are read as `content()`** — a stated limit rather than a discovered one: a pseudo-element's own string is generated content this walk does not see, and answering the element's text is the nearest honest answer there is.
16
+ * **What is not read, each a real gap and each showing as a counter reading low.** css-lists-3's `reversed()` counters (an author's `counter-reset: reversed(name)`), and a top-level `position: fixed` box, which is not among the flow blocks the position index is built over. `display: none` elements are skipped, which is CSS 2.1's own rule rather than an omission. A list's and a list item's implicit `list-item` operations are read since 2026-11-07 (`implicitListOps`), so a `counter(list-item)` read on a page a list continued onto or past it counts the items before it. A `::before`'s and an `::after`'s counters are read since 2026-11-07, where a sheet sets one (`pseudoStyleOf`). A scope that ended before the page with no operation logged between its end and the page's first word is closed since 2026-11-09, by the depth of the page's opening place (`counterStateAt`'s `depth`): until then a `counter(list-item)` read on a page past a list whose last item holds a list read the inner list's count, 3 where Chromium prints 2, and an author's nested reset the same. **And `content(before)`, `content(after)` and `content(first-letter)` are read as `content()`** — a stated limit rather than a discovered one: a pseudo-element's own string is generated content this walk does not see, and answering the element's text is the nearest honest answer there is.
17
17
  *
18
18
  * @packageDocumentation
19
19
  */
@@ -126,6 +126,16 @@ export declare function documentOpsOf(blocks: ArrayLike<Element>, index: {
126
126
  }>;
127
127
  keep?: Keep | null;
128
128
  }): Op[];
129
+ /**
130
+ * A log with the document's running elements laid in, each at the position the flow stood at where it was written: {@link documentOpsOf}'s `running`, for a log walked without them.
131
+ *
132
+ * @remarks
133
+ * A running element has no word of its own, so it joins the log before every operation past its position: the log stays in position order, which the replay's early stop needs. Its depth is infinite, so it ends no counter scope.
134
+ */
135
+ export declare function withRunning(ops: Op[], running: ReadonlyArray<{
136
+ name: string;
137
+ pos: number;
138
+ }>): Op[];
129
139
  /**
130
140
  * The document's counters at a position — every operation strictly before it, replayed under CSS 2.1's *Nested counters and scope*.
131
141
  *
@@ -133,12 +143,13 @@ export declare function documentOpsOf(blocks: ArrayLike<Element>, index: {
133
143
  *
134
144
  * @param pos - the flat word position asked about — for a page, its first word.
135
145
  * @param away - the remainders the page's opening cut carries in (`positions.js`' `remaindersOf`): an operation before `pos` inside one of them is on this page or a later one, not on a page before it, and is not yet in force (beltype#43). The replay still runs in document order.
146
+ * @param depth - the depth in the log's terms of the place `pos` names (`driver.ts`' `depthOfCut`): every instance created deeper is ended there, as an operation at that depth would end it, so a scope that ended before the page with nothing logged between is closed (cicero-counter-scope-stays-open-at-page, 2026-11-09). Absent, the replay keeps what the last operation before `pos` left open.
136
147
  * @returns per counter name, the values of every instance in scope, outermost first. Plain data, so it crosses a `page.evaluate` boundary in the account's `closes` like every other condition.
137
148
  */
138
149
  export declare function counterStateAt(ops: ReturnType<typeof documentOpsOf>, pos: number, away?: ReadonlyArray<{
139
150
  from: number;
140
151
  to: number;
141
- }>): Record<string, number[]>;
152
+ }>, depth?: number): Record<string, number[]>;
142
153
  /** What the page context's own rules do to the page counter on one page: the `page` value of the winning `counter-reset`, `counter-increment` and `counter-set` of the `@page` rules that apply to it, `null` where the winning declaration names no `page` (`capture.ts`'s `pageFor`). */
143
154
  export type PageCounterOps = {
144
155
  reset: number | null;
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * **A position is the flat word index** (`positions.js`), and an operation applies when its position is strictly before the one asked about — and not inside a remainder a carried flow has still to show, which stands before the page's first word in document order and is on that page or after it (beltype#43; `positions.js`' `remaindersOf`). That is §6.1's *"the deepest element in the normal flow that spans the page break"* said in this engine's own space: an element whose first word is the page's first word begins on the new page and does not span the break, so its own operation is not yet in force, while an element that spans the break started before that word and is included. The ancestors above the top-level blocks sit at position **−1** — they open before the document's first word and span every break there is — which is what makes the first page's answer fall out of the same comparison rather than needing a case of its own.
15
15
  *
16
- * **What is not read, each a real gap and each showing as a counter reading low.** css-lists-3's `reversed()` counters (an author's `counter-reset: reversed(name)`), a top-level `position: fixed` box, which is not among the flow blocks the position index is built over; and a scope that ended before the position asked about with no operation logged between its end and the position, since the replay closes a scope only when an operation at or above its depth arrives: a `counter(list-item)` read on a page past a list whose last item holds a list reads the inner list's count (3 where Chromium prints 2, measured 2026-11-07), and an author's nested reset is read the same way. `display: none` elements are skipped, which is CSS 2.1's own rule rather than an omission. A list's and a list item's implicit `list-item` operations are read since 2026-11-07 (`implicitListOps`), so a `counter(list-item)` read on a page a list continued onto or past it counts the items before it. A `::before`'s and an `::after`'s counters are read since 2026-11-07, where a sheet sets one (`pseudoStyleOf`). **And `content(before)`, `content(after)` and `content(first-letter)` are read as `content()`** — a stated limit rather than a discovered one: a pseudo-element's own string is generated content this walk does not see, and answering the element's text is the nearest honest answer there is.
16
+ * **What is not read, each a real gap and each showing as a counter reading low.** css-lists-3's `reversed()` counters (an author's `counter-reset: reversed(name)`), and a top-level `position: fixed` box, which is not among the flow blocks the position index is built over. `display: none` elements are skipped, which is CSS 2.1's own rule rather than an omission. A list's and a list item's implicit `list-item` operations are read since 2026-11-07 (`implicitListOps`), so a `counter(list-item)` read on a page a list continued onto or past it counts the items before it. A `::before`'s and an `::after`'s counters are read since 2026-11-07, where a sheet sets one (`pseudoStyleOf`). A scope that ended before the page with no operation logged between its end and the page's first word is closed since 2026-11-09, by the depth of the page's opening place (`counterStateAt`'s `depth`): until then a `counter(list-item)` read on a page past a list whose last item holds a list read the inner list's count, 3 where Chromium prints 2, and an author's nested reset the same. **And `content(before)`, `content(after)` and `content(first-letter)` are read as `content()`** — a stated limit rather than a discovered one: a pseudo-element's own string is generated content this walk does not see, and answering the element's text is the nearest honest answer there is.
17
17
  *
18
18
  * @packageDocumentation
19
19
  */
@@ -354,9 +354,17 @@ export function documentOpsOf(blocks, index, { styleOf, pseudoStyleOf = null, te
354
354
  for (const op of part.ops)
355
355
  ops.push({ ...op, pos: start + op.pos, depth: base + op.depth });
356
356
  }
357
+ return withRunning(ops, running);
358
+ }
359
+ /**
360
+ * A log with the document's running elements laid in, each at the position the flow stood at where it was written: {@link documentOpsOf}'s `running`, for a log walked without them.
361
+ *
362
+ * @remarks
363
+ * A running element has no word of its own, so it joins the log before every operation past its position: the log stays in position order, which the replay's early stop needs. Its depth is infinite, so it ends no counter scope.
364
+ */
365
+ export function withRunning(ops, running) {
357
366
  if (!running.length)
358
367
  return ops;
359
- // A running element has no word of its own, so it joins the log at the position the flow stood at, before every operation past it: the log stays in position order, which the replay's early stop needs. Its depth is infinite, so it ends no counter scope.
360
368
  const merged = [];
361
369
  let r = 0;
362
370
  for (const op of ops) {
@@ -377,10 +385,11 @@ const runningOp = (r, at) => ({ reset: [], increment: [], set: [], strings: null
377
385
  *
378
386
  * @param pos - the flat word position asked about — for a page, its first word.
379
387
  * @param away - the remainders the page's opening cut carries in (`positions.js`' `remaindersOf`): an operation before `pos` inside one of them is on this page or a later one, not on a page before it, and is not yet in force (beltype#43). The replay still runs in document order.
388
+ * @param depth - the depth in the log's terms of the place `pos` names (`driver.ts`' `depthOfCut`): every instance created deeper is ended there, as an operation at that depth would end it, so a scope that ended before the page with nothing logged between is closed (cicero-counter-scope-stays-open-at-page, 2026-11-09). Absent, the replay keeps what the last operation before `pos` left open.
380
389
  * @returns per counter name, the values of every instance in scope, outermost first. Plain data, so it crosses a `page.evaluate` boundary in the account's `closes` like every other condition.
381
390
  */
382
- export function counterStateAt(ops, pos, away = []) {
383
- return replay(ops, { until: pos, away });
391
+ export function counterStateAt(ops, pos, away = [], depth = Infinity) {
392
+ return replay(ops, { until: pos, away, depth });
384
393
  }
385
394
  /** No page-context rule touches the page counter. */
386
395
  export const NO_PAGE_COUNTER_OPS = Object.freeze({ reset: null, increment: null, set: null });
@@ -452,7 +461,7 @@ export function withoutPageCounters(state) {
452
461
  return out;
453
462
  }
454
463
  /** One replay, two readers. `counterStateAt` stops at a position and returns the state there; `stringAssignmentsOf` runs the log out and is handed the state at each element that assigns a named string. Written once because the scope arithmetic is the delicate part and two copies of it would agree only until one of them was fixed. */
455
- function replay(ops, { until = Infinity, onOp = null, away = [] } = {}) {
464
+ function replay(ops, { until = Infinity, onOp = null, away = [], depth = Infinity } = {}) {
456
465
  /**
457
466
  * @type name → [{ depth, value }], innermost last.
458
467
  */
@@ -496,6 +505,10 @@ function replay(ops, { until = Infinity, onOp = null, away = [] } = {}) {
496
505
  if (onOp && (op.strings || op.mark))
497
506
  onOp(op, snapshot());
498
507
  }
508
+ // The place asked about is entered as an element at `depth` would be: what was created deeper has had its parent left behind.
509
+ for (const stack of scopes.values())
510
+ while (stack.length && stack[stack.length - 1].depth > depth)
511
+ stack.pop();
499
512
  return snapshot();
500
513
  }
501
514
  /** One `<content-list>` term against the element's own facts and the counter state in force at it. */
@@ -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;