@beltype/cicero 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/seam/client.d.ts +5 -1
- package/dist/seam/client.js +21 -3
- package/dist/seam/rim.d.ts +2 -1
- package/dist/seam/rim.js +3 -1
- package/dist/src/compose-dom.js +5 -2
- package/dist/src/data-beltype.d.ts +2 -0
- package/dist/src/data-beltype.js +4 -0
- package/dist/src/document-state.d.ts +13 -2
- package/dist/src/document-state.js +18 -5
- package/dist/src/driver.d.ts +34 -8
- package/dist/src/driver.js +143 -39
- package/dist/src/fragmentation.d.ts +2 -2
- package/dist/src/fragmentation.js +2 -2
- package/dist/src/fragments.js +7 -0
- package/dist/src/gauge.js +22 -0
- package/dist/src/measure.d.ts +3 -1
- package/dist/src/measure.js +29 -3
- package/dist/src/reads.d.ts +14 -8
- package/dist/src/reads.js +45 -32
- package/dist/src/scope.d.ts +23 -0
- package/dist/src/scope.js +60 -0
- package/dist/src/stonehand.d.ts +1 -1
- package/dist/src/stonehand.js +20 -9
- package/dist/src/word-space.d.ts +11 -0
- package/dist/src/word-space.js +40 -0
- package/package.json +1 -1
package/dist/src/driver.js
CHANGED
|
@@ -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,46 @@ 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
|
-
*
|
|
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
|
|
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 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`). What `counterStateAt` takes as its `depth`.
|
|
126
|
+
*
|
|
127
|
+
* @remarks
|
|
128
|
+
* 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. Exported for a host that reads the same log over its own addresses (*the engine knows no view*).
|
|
129
|
+
*
|
|
130
|
+
* @param ancestors - How many elements the log records above the top-level blocks (`documentOpsOf`'s `ancestors`), two for the driver: `html` and `body`.
|
|
131
|
+
*/
|
|
132
|
+
export const depthOfCut = (cut, ancestors) => ancestors + cut.path.length - 1 + (cut.word != null || cut.px != null ? 1 : 0);
|
|
133
|
+
/**
|
|
134
|
+
* 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.
|
|
135
|
+
*
|
|
136
|
+
* @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
137
|
*
|
|
101
138
|
* @param el - The box the page begins in.
|
|
102
139
|
* @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 +141,15 @@ export function gapAbove(box, report, win) {
|
|
|
104
141
|
*/
|
|
105
142
|
export function pageNameDownFrom(el, { styleOf, runBefore, blockChildren = blockChildrenOf }) {
|
|
106
143
|
for (;;) {
|
|
144
|
+
const st = styleOf(el);
|
|
145
|
+
if (isOutOfFlow(st) || !laysClassA(st.display ?? ''))
|
|
146
|
+
break; // a page a float begins is named by the float's place in the flow, never by its interior
|
|
107
147
|
const next = blockChildren(el).find((c) => !isOutOfFlow(styleOf(c)));
|
|
108
|
-
if (!next || runBefore(el, next))
|
|
148
|
+
if (!next || runBefore(el, next) || !pageApplies(styleOf(next), st))
|
|
109
149
|
break;
|
|
110
150
|
el = next;
|
|
111
151
|
}
|
|
112
|
-
return
|
|
152
|
+
return usedPageName(el, styleOf);
|
|
113
153
|
}
|
|
114
154
|
/**
|
|
115
155
|
* 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 +256,7 @@ export const combineParity = (earlier, later) => later ?? earlier;
|
|
|
216
256
|
*
|
|
217
257
|
* 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
258
|
*
|
|
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
|
|
259
|
+
* 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
260
|
*
|
|
221
261
|
* `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
262
|
*
|
|
@@ -240,41 +280,84 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
|
|
|
240
280
|
/**
|
|
241
281
|
* @param [rowish]
|
|
242
282
|
*/
|
|
243
|
-
|
|
283
|
+
/**
|
|
284
|
+
* 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.
|
|
285
|
+
*
|
|
286
|
+
* @param [rowish]
|
|
287
|
+
* @param parentStyle - the parent's style, null in the body where nothing reads it.
|
|
288
|
+
* @param parentName - the parent's used page name, which an anonymous block box in it takes.
|
|
289
|
+
*/
|
|
290
|
+
const visit = (el, b, sealedBy, rowish, parentStyle, parentName) => {
|
|
244
291
|
const st = styleOf(el);
|
|
245
292
|
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
|
|
293
|
+
return null; // §3.1's must not: it neither carries forcing nor receives it, so it is not an event at all
|
|
247
294
|
const parallel = isOutOfFlow(st);
|
|
248
|
-
const
|
|
295
|
+
const applies = pageApplies(st, parentStyle);
|
|
296
|
+
const anonymous = !applies && !parallel && isInlineLevel(st.display ?? '') && laysClassA(parentStyle?.display ?? 'block');
|
|
297
|
+
const used = applies ? pageNameOf(el) : parentName;
|
|
298
|
+
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
299
|
const k = events.length;
|
|
250
300
|
events.push(e);
|
|
251
301
|
const inner = sealedBy >= 0 ? sealedBy : neverCutInside(el, st) ? k : -1;
|
|
302
|
+
let first = null;
|
|
303
|
+
let last = null;
|
|
252
304
|
if (!parallel && !monolith(el, st)) {
|
|
253
305
|
const kids = blockChildren(el);
|
|
254
|
-
|
|
306
|
+
// 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.
|
|
307
|
+
const runs = !!extentOf && (holdsAnonymousBlocks(st.display ?? '') || /^(inline-)?(flex|grid)$/.test(st.display ?? ''));
|
|
255
308
|
const row = rowish || SIDE_BY_SIDE.has(st.display ?? '');
|
|
309
|
+
const flow = laysClassA(st.display ?? '');
|
|
310
|
+
const own = pageNameOf(el);
|
|
311
|
+
// 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.
|
|
312
|
+
const meet = (at, start, end) => {
|
|
313
|
+
if (!flow)
|
|
314
|
+
return;
|
|
315
|
+
if (last !== null && last !== start)
|
|
316
|
+
events[at].nameBreak = true;
|
|
317
|
+
first ??= start;
|
|
318
|
+
last = end;
|
|
319
|
+
};
|
|
320
|
+
if (runs && kids.length > 0 && extentOf(kids[0]).before > extentOf(el).before)
|
|
321
|
+
first = last = own;
|
|
256
322
|
for (let i = 0; i < kids.length; i++) {
|
|
257
|
-
|
|
323
|
+
const at = events.length;
|
|
324
|
+
const met = visit(kids[i], b, inner, row, st, own);
|
|
325
|
+
if (met)
|
|
326
|
+
meet(at, met.start, met.end);
|
|
258
327
|
if (!runs)
|
|
259
328
|
continue;
|
|
260
329
|
// 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
|
|
262
|
-
const
|
|
263
|
-
const done =
|
|
330
|
+
const extent = extentOf;
|
|
331
|
+
const ext = extent(el);
|
|
332
|
+
const done = extent(kids[i]);
|
|
264
333
|
const word = done.before + done.count;
|
|
265
334
|
const next = kids[i + 1];
|
|
266
|
-
if ((next ?
|
|
335
|
+
if ((next ? extent(next).before : ext.before + ext.count) <= word)
|
|
267
336
|
continue;
|
|
268
|
-
events.push({ b, el, cut: cutOf(el, b, word -
|
|
337
|
+
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] });
|
|
338
|
+
meet(events.length - 1, own, own);
|
|
269
339
|
}
|
|
270
340
|
}
|
|
271
|
-
e.after =
|
|
341
|
+
e.after = isForced(st.breakAfter); // a float's too (§3.1's should): pending into what follows its place in the flow
|
|
272
342
|
e.afterParity = parityOf(st.breakAfter);
|
|
273
343
|
e.end = events.length;
|
|
344
|
+
if (!applies && !anonymous)
|
|
345
|
+
return null;
|
|
346
|
+
return { start: applies ? first ?? used : used, end: applies ? last ?? used : used };
|
|
274
347
|
};
|
|
348
|
+
const root = blocks[0]?.parentElement ?? null;
|
|
349
|
+
const rootStyle = root ? styleOf(root) : null;
|
|
350
|
+
const rootName = root ? pageNameOf(root) : '';
|
|
351
|
+
let before = null;
|
|
275
352
|
for (let b = 0; b < blocks.length; b++) {
|
|
276
353
|
blockStart.push(events.length);
|
|
277
|
-
|
|
354
|
+
const at = events.length;
|
|
355
|
+
const met = visit(blocks[b], b, -1, false, rootStyle, rootName);
|
|
356
|
+
if (!met)
|
|
357
|
+
continue;
|
|
358
|
+
if (before !== null && before !== met.start)
|
|
359
|
+
events[at].nameBreak = true;
|
|
360
|
+
before = met.end;
|
|
278
361
|
}
|
|
279
362
|
const n = events.length;
|
|
280
363
|
// 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 +371,13 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
|
|
|
288
371
|
for (let k = n - 1; k >= 0; k--)
|
|
289
372
|
nextForced[k] = closing[k] || events[k].before ? k : nextForced[k + 1];
|
|
290
373
|
const nextName = new Array(n + 1).fill(n);
|
|
291
|
-
for (let k = n -
|
|
292
|
-
nextName[k] = events[k
|
|
293
|
-
const hit = (k, pending,
|
|
374
|
+
for (let k = n - 1; k >= 0; k--)
|
|
375
|
+
nextName[k] = events[k].nameBreak ? k : nextName[k + 1];
|
|
376
|
+
const hit = (k, pending, namesOnly) => {
|
|
294
377
|
const e = events[k];
|
|
295
378
|
const before = !namesOnly && e.before;
|
|
296
379
|
// §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.
|
|
380
|
+
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
381
|
};
|
|
299
382
|
/** 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
383
|
const firstAfter = (cursorCut) => {
|
|
@@ -314,20 +397,24 @@ export function forcingIndexOf(blocks, { styleOf, pageNameOf, cutOf, posOf, posO
|
|
|
314
397
|
};
|
|
315
398
|
return {
|
|
316
399
|
size: n,
|
|
317
|
-
/**
|
|
318
|
-
|
|
400
|
+
/**
|
|
401
|
+
* The first forcing event strictly after `cursorCut`, or null.
|
|
402
|
+
*
|
|
403
|
+
* @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.
|
|
404
|
+
*/
|
|
405
|
+
find(cursorCut, _name, { namesOnly = false } = {}) {
|
|
319
406
|
const s = firstAfter(cursorCut);
|
|
320
407
|
if (s >= n)
|
|
321
408
|
return null;
|
|
322
409
|
// 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
410
|
const pending = namesOnly ? null : closing[s];
|
|
324
|
-
if (pending || events[s].
|
|
325
|
-
return hit(s, pending,
|
|
411
|
+
if (pending || events[s].nameBreak || (!namesOnly && events[s].before))
|
|
412
|
+
return hit(s, pending, namesOnly);
|
|
326
413
|
const k = s + 1;
|
|
327
414
|
if (k >= n)
|
|
328
415
|
return null;
|
|
329
|
-
const f = Math.min(
|
|
330
|
-
return f < n ? hit(f, namesOnly ? null : closing[f],
|
|
416
|
+
const f = Math.min(nextName[k], namesOnly ? n : nextForced[k]);
|
|
417
|
+
return f < n ? hit(f, namesOnly ? null : closing[f], namesOnly) : null;
|
|
331
418
|
},
|
|
332
419
|
/**
|
|
333
420
|
* 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 +618,8 @@ export function paginate(doc, opts = {}) {
|
|
|
531
618
|
const boxes = opts.originals ?? topLevelBoxes(doc);
|
|
532
619
|
const hasMarks = answerHas(doc, boxes, keep);
|
|
533
620
|
performance.measure('cicero:has-words', { start: t0, end: performance.now(), detail: { boxes: boxes.length, marked: hasMarks.size, ...(memo ? { memo: memo.tally.has ?? null } : {}) } });
|
|
621
|
+
// 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.
|
|
622
|
+
registerGcpmProperties(doc.defaultView);
|
|
534
623
|
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
624
|
}
|
|
536
625
|
/**
|
|
@@ -577,6 +666,8 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
577
666
|
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
667
|
const win = doc.defaultView;
|
|
579
668
|
const blocks = intakeBlocks ?? topLevelBlocks(doc);
|
|
669
|
+
// 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`).
|
|
670
|
+
foldTallColumns(doc, readScope(win, blocks), table.pageFor({ first: true }));
|
|
580
671
|
// 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
672
|
const running = intakeRunning ?? runningBoxes(doc, memo?.keep);
|
|
582
673
|
/** 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 +734,14 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
643
734
|
*
|
|
644
735
|
* 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
736
|
*/
|
|
646
|
-
const
|
|
737
|
+
const ancestors = [doc.documentElement, doc.body].filter(Boolean);
|
|
738
|
+
const runningAt = running.map((r) => ({ name: r.name, pos: runningPosition(r, index) }));
|
|
739
|
+
// 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`).
|
|
740
|
+
const documentOps = laid.ops ? withRunning(laid.ops, runningAt) : documentOpsOf(blocks, index, {
|
|
647
741
|
styleOf: (el) => win.getComputedStyle(el),
|
|
648
742
|
pseudoStyleOf: sheetsCountInPseudos(doc) ? (el, pseudo) => win.getComputedStyle(el, pseudo) : null,
|
|
649
|
-
ancestors
|
|
650
|
-
running:
|
|
743
|
+
ancestors,
|
|
744
|
+
running: runningAt,
|
|
651
745
|
keep: memo?.keep
|
|
652
746
|
});
|
|
653
747
|
// 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 +766,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
672
766
|
: 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
767
|
performance.measure('cicero:forcing-index', { start: t, end: (t = performance.now()), detail: { events: forcing.size, ...(memo ? { memoHits: memo.hits, memoMisses: memo.misses } : {}) } });
|
|
674
768
|
/**
|
|
675
|
-
* The
|
|
769
|
+
* 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
770
|
*
|
|
677
771
|
* 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
772
|
*/
|
|
@@ -681,9 +775,10 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
681
775
|
if (!el)
|
|
682
776
|
return '';
|
|
683
777
|
// A line before a child is the page's first content, and it is the container's (*the anonymous box is in the procession*).
|
|
778
|
+
const styleOf = (c) => win.getComputedStyle(c);
|
|
684
779
|
if (cut.word == null && cut.px == null)
|
|
685
|
-
return pageNameDownFrom(el, { styleOf
|
|
686
|
-
return
|
|
780
|
+
return pageNameDownFrom(el, { styleOf, runBefore: (parent, child) => index.wordsBefore(child) !== index.wordsBefore(parent) });
|
|
781
|
+
return usedPageName(el, styleOf);
|
|
687
782
|
};
|
|
688
783
|
/**
|
|
689
784
|
* The document's own first page — css-page-3 §3.3, and it is a parity offset rather than a blank entry.
|
|
@@ -796,7 +891,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
796
891
|
// 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
892
|
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
893
|
// 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));
|
|
894
|
+
const counters = withoutPageCounters(counterStateAt(documentOps, pos, away, depthOfCut(cut, ancestors.length)));
|
|
800
895
|
return Object.freeze({ ...geo, name, pageNumber, parity, blank, counters, dpr: win.devicePixelRatio });
|
|
801
896
|
};
|
|
802
897
|
/**
|
|
@@ -1139,6 +1234,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
1139
1234
|
const conditions = conditionsFor(account.length);
|
|
1140
1235
|
const recreated = ctx === null || conditions.contentW !== ctxWidth;
|
|
1141
1236
|
if (recreated) {
|
|
1237
|
+
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
1238
|
ctx = createMeasureContext(doc, conditions.contentW);
|
|
1143
1239
|
ctxWidth = conditions.contentW;
|
|
1144
1240
|
}
|
|
@@ -1218,10 +1314,15 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
1218
1314
|
let container;
|
|
1219
1315
|
let pageStart;
|
|
1220
1316
|
let spentLead;
|
|
1317
|
+
/** The place the last pour stood at when the check failed: a doubled bound that names it again is the same feed. */
|
|
1318
|
+
let short = null;
|
|
1221
1319
|
for (;;) {
|
|
1222
1320
|
pours++;
|
|
1223
1321
|
// 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
|
-
|
|
1322
|
+
let place = castOff.bound(cursor, words);
|
|
1323
|
+
// 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).
|
|
1324
|
+
while (place && place.el === short)
|
|
1325
|
+
place = castOff.bound(cursor, (words = words * 2));
|
|
1225
1326
|
const forcingCuts = !!forcedEvent && !rowForced && (!place || !cutAfter(forcedEvent.cut, place.cut, posOfCut));
|
|
1226
1327
|
bounded = !forcingCuts && !!place;
|
|
1227
1328
|
({ items, cloneOfSource } = buildFeed(forcingCuts ? forcedEvent : bounded ? place : null, forcingCuts ? 'the grab' : 'the Cast-off'));
|
|
@@ -1240,7 +1341,9 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
1240
1341
|
pageStart = (truncateLead ? report.contentTop : 0) + spentLead;
|
|
1241
1342
|
if (rowForced) {
|
|
1242
1343
|
/** 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
|
-
|
|
1344
|
+
// 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.
|
|
1345
|
+
const ev = forcedEvent;
|
|
1346
|
+
const clone = cloneOfSource.get(ev.runAfter ?? ev.el);
|
|
1244
1347
|
const at = clone ? report.boxOf(clone) : null;
|
|
1245
1348
|
rowEventIn = !!clone;
|
|
1246
1349
|
if (clone) {
|
|
@@ -1254,7 +1357,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
1254
1357
|
item = a;
|
|
1255
1358
|
}
|
|
1256
1359
|
const sequence = at ? report.processionOf(at) : [];
|
|
1257
|
-
if (!at || !item || nested || sequence.indexOf(at) < 1 || at.rect.top <= item.rect.top + 0.25) {
|
|
1360
|
+
if (!at || !item || nested || (!ev.runAfter && (sequence.indexOf(at) < 1 || at.rect.top <= item.rect.top + 0.25))) {
|
|
1258
1361
|
rowForced = false; // not a row's: the grab's, as before
|
|
1259
1362
|
continue;
|
|
1260
1363
|
}
|
|
@@ -1262,6 +1365,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
1262
1365
|
}
|
|
1263
1366
|
if (!bounded || reachesBelowTheEdge({ report, sourceOf, pageStart, contentHeight: conditions.contentH }))
|
|
1264
1367
|
break;
|
|
1368
|
+
short = place.el;
|
|
1265
1369
|
// @ts-expect-error `words` is a number wherever the feed is bounded, and a feed that is not bounded broke out above
|
|
1266
1370
|
words *= 2;
|
|
1267
1371
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
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/page`.
|
|
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; `usedPageName` and `depthOfCut`, by which the run names a page it resumes inside a box and closes its counters, 2026-11-13); 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
|
*/
|
|
@@ -10,7 +10,7 @@ export { effectivePageName, readGeometryTable, registerGcpmProperties, sheetReac
|
|
|
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';
|
|
13
|
-
export { forcingIndexOf, gapAbove, pageNameDownFrom, runningBoxes, runningPosition, topLevelBlocks } from './driver.ts';
|
|
13
|
+
export { depthOfCut, forcingIndexOf, gapAbove, pageNameDownFrom, runningBoxes, runningPosition, topLevelBlocks, usedPageName } from './driver.ts';
|
|
14
14
|
export { ForcingMemo } from './forcing-memo.ts';
|
|
15
15
|
export type { Keep } from './forcing-memo.ts';
|
|
16
16
|
export { joinedWords, mutateToContinuation, mutateToFirstFragment, pinTrack, truncateLeadingDecoration } from './fragments.ts';
|
|
@@ -2,7 +2,7 @@
|
|
|
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/page`.
|
|
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; `usedPageName` and `depthOfCut`, by which the run names a page it resumes inside a box and closes its counters, 2026-11-13); 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
|
*/
|
|
@@ -10,7 +10,7 @@ export { effectivePageName, readGeometryTable, registerGcpmProperties, sheetReac
|
|
|
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';
|
|
13
|
-
export { forcingIndexOf, gapAbove, pageNameDownFrom, runningBoxes, runningPosition, topLevelBlocks } from './driver.js';
|
|
13
|
+
export { depthOfCut, forcingIndexOf, gapAbove, pageNameDownFrom, runningBoxes, runningPosition, topLevelBlocks, usedPageName } from './driver.js';
|
|
14
14
|
export { ForcingMemo } from './forcing-memo.js';
|
|
15
15
|
export { joinedWords, mutateToContinuation, mutateToFirstFragment, pinTrack, truncateLeadingDecoration } from './fragments.js';
|
|
16
16
|
export { takeReport } from './gauge.js';
|
package/dist/src/fragments.js
CHANGED
|
@@ -762,6 +762,10 @@ export function mutateToContinuation(clone, { cut = null, remainderPx = null, so
|
|
|
762
762
|
const mine = itemsOf(flows ?? flowsOf(items), path, source, cloneOf);
|
|
763
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.
|
|
764
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);
|
|
765
769
|
if (divides(cut)) {
|
|
766
770
|
const target = targetOf(cut, source, cloneOf);
|
|
767
771
|
if (!target)
|
|
@@ -914,6 +918,9 @@ function shellAt(clone, target, px, keep = new Set(), hollow = false) {
|
|
|
914
918
|
el.style.boxSizing = 'border-box';
|
|
915
919
|
el.style.height = `${Math.max(0, px.remaining)}px`;
|
|
916
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');
|
|
917
924
|
return clone;
|
|
918
925
|
}
|
|
919
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.
|
package/dist/src/gauge.js
CHANGED
|
@@ -193,6 +193,28 @@ export function takeReport(container, { contentHeight }) {
|
|
|
193
193
|
for (const item of row)
|
|
194
194
|
rowOfBox.set(item, row);
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* A cell spanning rows (`rowspan`) stands beside the cells of every row it spans, so the cells of those rows are one row of parallel flows, in document order: a cut through the second spanned row divides the spanning cell and that row's cells, each at its own verdict, and leaves the rows above it spent and the rows below it whole, as Chromium prints it (CSS 2.1 §17.5.3; css-break-3 §2.1; the essay's case 92, *the cell that spans the cut*). A span stays inside its row group, as §17.2 says.
|
|
198
|
+
*/
|
|
199
|
+
for (const box of boxes) {
|
|
200
|
+
const trs = box.blockChildren.filter((c) => !c.anonymous && win.getComputedStyle(c.el).display === 'table-row');
|
|
201
|
+
for (let i = 0; i < trs.length; i++) {
|
|
202
|
+
let end = i;
|
|
203
|
+
for (let j = i; j <= end; j++) {
|
|
204
|
+
for (const cell of trs[j].blockChildren) {
|
|
205
|
+
const span = cell.el.rowSpan ?? 1;
|
|
206
|
+
end = Math.min(trs.length - 1, Math.max(end, span === 0 ? trs.length - 1 : j + span - 1));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (end === i)
|
|
210
|
+
continue;
|
|
211
|
+
const row = trs.slice(i, end + 1).flatMap((tr) => tr.blockChildren.filter((c) => !['absolute', 'fixed'].includes(win.getComputedStyle(c.el).position)));
|
|
212
|
+
if (row.length > 1)
|
|
213
|
+
for (const item of row)
|
|
214
|
+
rowOfBox.set(item, row);
|
|
215
|
+
i = end;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
196
218
|
const topLevel = boxes.filter((b) => !b.parent);
|
|
197
219
|
const contentTop = topLevel.length ? topLevel[0].rect.top : 0;
|
|
198
220
|
let flowBottom = contentTop;
|
package/dist/src/measure.d.ts
CHANGED
|
@@ -14,7 +14,9 @@ export type MeasureContext = ReturnType<typeof createMeasureContext>;
|
|
|
14
14
|
*
|
|
15
15
|
* A vertical or sideways root is refused by name too (Codeberg issue 12): every read in this engine is horizontal, the lines by vertical overlap, the cut by a y, the page area by its height, so under `writing-mode: sideways-lr` on the root the text of a positioned paragraph fell below the clip and was never resumed, and the printer's tiling invariant refused what the engine should have. Vertical writing is outside the beta, and a refusal that says so at intake is cheaper than one three tools later.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* A right-to-left root or body (`direction: rtl`) is refused by name as well, as out of scope (SPEC II.1, item 6); a vertical box below the root is refused by `readScope` (`src/scope.ts`).
|
|
18
|
+
*
|
|
19
|
+
* @throws Error — bare and deliberate (`src/` may not import BelType's error types): a device pixel ratio other than 1 without `relaxDpr`, a document root whose line-height is not explicit (*render-identical conditions*, I5), a document root whose writing mode is not `horizontal-tb`, or a root or body that computes `direction: rtl`.
|
|
18
20
|
*/
|
|
19
21
|
export declare function assertRenderConditions(doc: Document, { relaxDpr }?: {
|
|
20
22
|
relaxDpr?: boolean;
|
package/dist/src/measure.js
CHANGED
|
@@ -17,7 +17,9 @@ const NEUTRALIZE_ID = '__beltype-neutralize';
|
|
|
17
17
|
*
|
|
18
18
|
* A vertical or sideways root is refused by name too (Codeberg issue 12): every read in this engine is horizontal, the lines by vertical overlap, the cut by a y, the page area by its height, so under `writing-mode: sideways-lr` on the root the text of a positioned paragraph fell below the clip and was never resumed, and the printer's tiling invariant refused what the engine should have. Vertical writing is outside the beta, and a refusal that says so at intake is cheaper than one three tools later.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
20
|
+
* A right-to-left root or body (`direction: rtl`) is refused by name as well, as out of scope (SPEC II.1, item 6); a vertical box below the root is refused by `readScope` (`src/scope.ts`).
|
|
21
|
+
*
|
|
22
|
+
* @throws Error — bare and deliberate (`src/` may not import BelType's error types): a device pixel ratio other than 1 without `relaxDpr`, a document root whose line-height is not explicit (*render-identical conditions*, I5), a document root whose writing mode is not `horizontal-tb`, or a root or body that computes `direction: rtl`.
|
|
21
23
|
*/
|
|
22
24
|
export function assertRenderConditions(doc, { relaxDpr = false } = {}) {
|
|
23
25
|
const win = doc.defaultView;
|
|
@@ -29,6 +31,10 @@ export function assertRenderConditions(doc, { relaxDpr = false } = {}) {
|
|
|
29
31
|
if (!/^-?[\d.]+px$/.test(lh)) {
|
|
30
32
|
throw new Error(`I5 (DPR ruling): explicit line-height required, document root computes '${lh}'`);
|
|
31
33
|
}
|
|
34
|
+
const rtl = [root, doc.body ? win.getComputedStyle(doc.body) : null].find((st) => st?.direction === 'rtl');
|
|
35
|
+
if (rtl) {
|
|
36
|
+
throw new Error(`right-to-left text is outside this engine's scope: the document computes direction 'rtl' on its ${rtl === root ? 'root' : 'body'}, and every read here is left to right (the line's start edge, the hanging of a margin, the page's left and right). Write the document ltr, or print it with another engine.`);
|
|
37
|
+
}
|
|
32
38
|
if (root.writingMode && root.writingMode !== 'horizontal-tb') {
|
|
33
39
|
throw new Error(`a vertical or sideways root is outside this engine's beta: the document root computes writing-mode '${root.writingMode}', and every read here is horizontal (the line by vertical overlap, the cut by a y). Write the document horizontal-tb, or print it with another engine.`);
|
|
34
40
|
}
|
|
@@ -114,12 +120,32 @@ export async function awaitCloneImages(container) {
|
|
|
114
120
|
unsettled = true;
|
|
115
121
|
break;
|
|
116
122
|
}
|
|
117
|
-
|
|
123
|
+
const objects = objectsFetching(container);
|
|
124
|
+
if (!unsettled && !objects.length)
|
|
118
125
|
return false;
|
|
119
|
-
await awaitImages(container);
|
|
126
|
+
await Promise.all([unsettled ? awaitImages(container) : null, ...objects]);
|
|
120
127
|
void container.offsetHeight;
|
|
121
128
|
return true;
|
|
122
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* The clones' `<object>` elements that fetch their `data`, each as a promise of its `load` or `error`. A copy of an `<object>` fetches its data again and is 0 by 0 until the fetch answers, and only then does it show its resource or its fallback content (measured 2026-11-09: a copy of an object whose data fails measured 0 px in the turn it was mounted, 150 px at its `error` and 700 px, its fallback's height, one task later). Measured early, an object whose fallback is taller than the page was cut as nothing and presented clipped on one page (`cicero-object-fallback-throws`, assessment 2026-11-07). An object has no `complete`, so every fetching copy is awaited, and one that never answers is given up after two seconds rather than holding the run.
|
|
132
|
+
*/
|
|
133
|
+
function objectsFetching(container) {
|
|
134
|
+
const win = container.ownerDocument.defaultView;
|
|
135
|
+
const out = [];
|
|
136
|
+
for (const o of container.querySelectorAll('object[data]')) {
|
|
137
|
+
if (o.getAttribute('is') || win.getComputedStyle(o).display === 'none')
|
|
138
|
+
continue; // a presenter's element and a hidden one are measured by nobody's fetch
|
|
139
|
+
out.push(new Promise((resolve) => {
|
|
140
|
+
// The fallback is laid out a task after `error` fires (measured: 150 px, the default object size, at the event; 700 px a `setTimeout(0)` later), so the promise settles one task after the event.
|
|
141
|
+
const later = () => setTimeout(resolve, 0);
|
|
142
|
+
o.addEventListener('load', later, { once: true });
|
|
143
|
+
o.addEventListener('error', later, { once: true });
|
|
144
|
+
setTimeout(resolve, 2000); // ponytail: a fixed ceiling for a fetch that never answers; the rim hears nothing of an object, so a later answer is not re-cut
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
123
149
|
/** The declarations {@link publishOn} last wrote on a container, so the next call takes them off. */
|
|
124
150
|
const PUBLISHED = new WeakMap();
|
|
125
151
|
/** Write a page's published values on the measure container, and take the previous page's off. */
|