@beltype/cicero 0.1.6 → 0.1.8
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/index.js +3 -1
- package/dist/src/capture.d.ts +22 -2
- package/dist/src/capture.js +164 -70
- package/dist/src/chase.js +2 -0
- package/dist/src/data-beltype.d.ts +2 -2
- package/dist/src/data-beltype.js +4 -4
- package/dist/src/document-state.d.ts +49 -2
- package/dist/src/document-state.js +93 -7
- package/dist/src/dressing.d.ts +5 -1
- package/dist/src/dressing.js +23 -16
- package/dist/src/driver.d.ts +10 -3
- package/dist/src/driver.js +118 -40
- package/dist/src/fragmentation.d.ts +1 -1
- package/dist/src/fragmentation.js +1 -1
- package/dist/src/fragments.d.ts +2 -2
- package/dist/src/fragments.js +49 -10
- package/dist/src/gauge.d.ts +4 -0
- package/dist/src/gauge.js +14 -2
- package/dist/src/hyphen-patterns/en-us.d.ts +1 -0
- package/dist/src/hyphen-patterns/en-us.js +2 -0
- package/dist/src/hyphen-patterns/it.d.ts +21 -0
- package/dist/src/hyphen-patterns/it.js +52 -0
- package/dist/src/hyphenate.d.ts +5 -2
- package/dist/src/hyphenate.js +14 -6
- package/dist/src/measure.d.ts +5 -3
- package/dist/src/measure.js +15 -10
- package/dist/src/reads.d.ts +3 -2
- package/dist/src/reads.js +27 -5
- package/dist/src/stonehand.js +41 -14
- package/dist/src/word-space.d.ts +8 -0
- package/dist/src/word-space.js +15 -0
- package/package.json +1 -1
package/dist/src/reads.js
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
*
|
|
8
8
|
* **A missing target shows nothing**: no declaration is delivered for it, the fallback `""` stands, the pseudo-element still has its box (the leader the author drew still draws), and the console names the link once per paginate.
|
|
9
9
|
*
|
|
10
|
-
* **The loop is a consumer's too, over a chain of its own** (*the engine knows no view*, `.claude/decisions/project/the-engine-knows-no-view.md`, ruling 3). `settleReads`'s `run` and `blocksOf` are the door: a view that holds its own pages settles through this loop, with a `run` that refills the dirty entries of its chain, and never through a copy of it. What the loop assumes of its host, read in its body and its helpers: the sites are found in `doc`'s sheets and matched among `originals` (the whole document where none are given), and a site or a target counts only where it stands inside `blocksOf()`'s blocks, since the position index places it; so `originals` are the boxes that hold the blocks, never a presented copy of them. A target is `doc.getElementById`'s answer
|
|
10
|
+
* **The loop is a consumer's too, over a chain of its own** (*the engine knows no view*, `.claude/decisions/project/the-engine-knows-no-view.md`, ruling 3). `settleReads`'s `run` and `blocksOf` are the door: a view that holds its own pages settles through this loop, with a `run` that refills the dirty entries of its chain, and never through a copy of it. What the loop assumes of its host, read in its body and its helpers: the sites are found in `doc`'s sheets and matched among `originals` (the whole document where none are given), and a site or a target counts only where it stands inside `blocksOf()`'s blocks, since the position index places it; so `originals` are the boxes that hold the blocks, never a presented copy of them. A target is the first element in tree order carrying the id among `originals` (`doc.getElementById`'s answer where none are given), which holds where the source boxes precede every copy that keeps the id, as `<beltype-root>` keeps its originals before its copies, or where the copies carry no id; a host's own element that carries an author's id is outside the originals and is never a target, and a `target-text()` target outside `blocksOf()`'s blocks shows nothing, as a counter or page target there does. The blocks are any elements in document order: nothing asks that they share a parent or be `body`'s children, since the position index, the counter walk and `pathOf` each walk one block on its own. The counter walk's ancestors are `html` and `body`, the two the driver's own walk reads, so another ancestor of the blocks must carry no counter operation. A changed site is measured on a clone of its block in the measure container, at its page's content width, the ruler the run measures with. The run's result carries an `account` whose entries have the driver's shape (`pos`, `cut`, `carried`, `blank`, `trace.forced`, and `closes` with `pageNumber`, `contentW`, `contentH` and `strings`), every address in `blocksOf()`'s numbering, and the `index` it was handed; and, where its pages publish values of their own, `laidFacts`, the page values each page was laid out under (a flow site reads them), which the loop compares the truth with in place of the table it handed the run (a run that returns none was laid out under the table it was handed, as the driver's is; see CHANGELOG 2026-10-28); a layout sent round again is handed `resume.at`, the addresses of the moved boxes, which such a `run` maps to the entries they dirty. The values reach every element in `doc`'s own tree through one adopted sheet, and the page facts are the page hosts' (`chase.js`). One assumption was the root's alone and is a parameter: the sheet that hides a reservation scopes its rules to `beltype-root`, and `hide` names another scope.
|
|
11
11
|
*
|
|
12
12
|
* @packageDocumentation
|
|
13
13
|
*/
|
|
14
14
|
import { READ_PREFIX, STRING_KEYWORDS, STRING_PROP_PREFIX, decodeRead, encodeStringProp, splitTopLevel } from './gcpm-encoding.js';
|
|
15
|
-
import { styleSheetsOf } from './capture.js';
|
|
15
|
+
import { sheetsCountInPseudos, styleSheetsOf } from './capture.js';
|
|
16
16
|
import { format as formatCounter } from './counter-styles.js';
|
|
17
17
|
import { collapse, counterStatesAfter, counterValue, counterValues, documentOpsOf } from './document-state.js';
|
|
18
18
|
import { indexBlocks, pageContaining, pageIntervals, pathOf } from './positions.js';
|
|
@@ -26,7 +26,7 @@ export const MAX_RUNS = 3;
|
|
|
26
26
|
/** The reservation a forward layout read stands at during the first run, Egildo's ruling of 2026-10-03: wrong for nearly every reference, so nothing rests on it, and never printed, because the loop keeps the last true values. */
|
|
27
27
|
export const RESERVATION = '0';
|
|
28
28
|
/** A CSS string literal for any text: a quote, a backslash and a line break escaped, nothing else. */
|
|
29
|
-
export const cssString = (text) => `"${String(text).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(
|
|
29
|
+
export const cssString = (text) => `"${String(text).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/[\u0000-\u001f\u007f]/g, (ch) => `\\${ch.charCodeAt(0).toString(16)} `)}"`;
|
|
30
30
|
/** The selector a delivery is keyed by: the attribute the function reads, at the value this element carries. */
|
|
31
31
|
export const attrSelector = (attr, value) => `[${attr}=${cssString(value)}]`;
|
|
32
32
|
/**
|
|
@@ -285,7 +285,7 @@ export function findSites(doc, originals = null, keep = null) {
|
|
|
285
285
|
read,
|
|
286
286
|
link,
|
|
287
287
|
key: read.attr != null ? (link == null ? null : attrSelector(read.attr, link)) : '',
|
|
288
|
-
target: fragment == null ? null : doc
|
|
288
|
+
target: fragment == null ? null : targetOf(doc, fragment, originals),
|
|
289
289
|
pseudos
|
|
290
290
|
});
|
|
291
291
|
}
|
|
@@ -293,6 +293,19 @@ export function findSites(doc, originals = null, keep = null) {
|
|
|
293
293
|
}
|
|
294
294
|
return sites.length ? sites : null;
|
|
295
295
|
}
|
|
296
|
+
/** The element a fragment names: `doc.getElementById`'s answer, or, where the host hands its originals, the first element carrying the id among them, so a host's own element that shares an author's id is never a target. */
|
|
297
|
+
function targetOf(doc, fragment, originals) {
|
|
298
|
+
const first = doc.getElementById(fragment);
|
|
299
|
+
if (!originals || !first || originals.some((box) => box.contains(first)))
|
|
300
|
+
return first;
|
|
301
|
+
const selector = `[id="${fragment.replace(/["\\]/g, '\\$&').replace(/\n/g, '\\a ')}"]`;
|
|
302
|
+
for (const box of originals) {
|
|
303
|
+
const el = box.id === fragment ? box : box.querySelector(selector);
|
|
304
|
+
if (el)
|
|
305
|
+
return el;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
296
309
|
/** The engine's one delivery sheet per document, replaced at every delivery and withdrawn when a paginate finds nothing to deliver. */
|
|
297
310
|
const SHEETS = new WeakMap();
|
|
298
311
|
/** Deliver a table of values: one constructed sheet, adopted once, its text replaced. */
|
|
@@ -377,6 +390,7 @@ export function staticValues(sites, { doc, blocks, index = null, keep = null })
|
|
|
377
390
|
const win = doc.defaultView;
|
|
378
391
|
const ops = documentOpsOf(blocks, index ?? indexBlocks(blocks), {
|
|
379
392
|
styleOf: (el) => win.getComputedStyle(el),
|
|
393
|
+
pseudoStyleOf: sheetsCountInPseudos(doc) ? (el, pseudo) => win.getComputedStyle(el, pseudo) : null,
|
|
380
394
|
ancestors: [doc.documentElement, doc.body].filter(Boolean),
|
|
381
395
|
marks: new Set(counted.map((s) => s.target)),
|
|
382
396
|
keep
|
|
@@ -514,7 +528,7 @@ function boxAddress(site, index, blocks) {
|
|
|
514
528
|
*
|
|
515
529
|
* **A layout sent round again resumes from the one before it** (*delivery resumes from the moved page*, 2026-10-21). A delivered value that moves a line is an edit at its site's page, so `run` is handed `resume`, the layout just checked and the addresses of the boxes whose lines moved, and the driver restarts one page before the first of them, re-cuts, and stops where it converges past the last, exactly as it resumes from a host's edit. The result's `resumed` and `why` stay the call's: two resumes compose into one from the prior the host handed (`resume.js`' `composeResumed`), a whole layout anywhere in the loop makes `resumed` null, and each resumed layout leaves a `cicero:reads-run` measure naming the pages it cut again.
|
|
516
530
|
*
|
|
517
|
-
* @param how - `run` lays the document out once, whole or resumed (`because`: the references whose page number moved a line of their own in the run before, when this is not the first; `facts`: the page values a flow site reads, per page, as the run before decided them, or null for the reservation; it returns `laidFacts` beside `account` when the page values it was laid out under are not the ones it was handed); `blocksOf` names the top-level blocks; `originals` the intake's top-level boxes, where the document has been presented (see {@link findSites}); `warm` and `warmFacts` the tables the previous run's layout was cut under, for a run that resumes; `hide` that pages are shown before the run is complete (*the pages are cut as far as they are asked*), so the reservations still standing are painted invisible until the true numbers are delivered, under `<beltype-root>` for `true` and under the selector a string names for a host that is not the root; `keep` the memo's door, through which the site search, the position index and the counter walk take the parts of the boxes no edit replaced (*a resumed run keeps its reads*, 2026-10-22).
|
|
531
|
+
* @param how - `run` lays the document out once, whole or resumed (`because`: the references whose page number moved a line of their own in the run before, when this is not the first; `facts`: the page values a flow site reads, per page, as the run before decided them, or null for the reservation; it returns `laidFacts` beside `account` when the page values it was laid out under are not the ones it was handed); `blocksOf` names the top-level blocks; `originals` the intake's top-level boxes, where the document has been presented (see {@link findSites}); `warm` and `warmFacts` the tables the previous run's layout was cut under, for a run that resumes; `hide` that pages are shown before the run is complete (*the pages are cut as far as they are asked*), so the reservations still standing are painted invisible until the true numbers are delivered, under `<beltype-root>` for `true` and under the selector a string names for a host that is not the root; `keep` the memo's door, through which the site search, the position index and the counter walk take the parts of the boxes no edit replaced (*a resumed run keeps its reads*, 2026-10-22). A `run` that answers `complete: false` laid out only part of the document (a view's chain that stopped short): the loop ends there, without checking or delivering page values, so the reservations stay hidden and the values it returns are the ones that layout was cut under; the driver's own run is always complete.
|
|
518
532
|
*/
|
|
519
533
|
export async function settleReads(doc, { run, blocksOf, originals = null, warm = null, warmFacts = null, hide = false, keep = null }) {
|
|
520
534
|
let t = performance.now(); // the site search is the reads' own cost and is inside their measure (*a resumed run keeps its reads*, 2026-10-22)
|
|
@@ -529,6 +543,10 @@ export async function settleReads(doc, { run, blocksOf, originals = null, warm =
|
|
|
529
543
|
const name = (s) => ('read' in s ? `<${s.el.tagName.toLowerCase()}${s.link == null ? '' : ` ${s.read.attr ?? 'url'}="${s.link}"`}>` : `<${s.el.tagName.toLowerCase()}${s.el.id ? ` id="${s.el.id}"` : ''}>`);
|
|
530
544
|
const named = (list) => [...new Set(list.map(name))].join(', ');
|
|
531
545
|
const blocks = blocksOf();
|
|
546
|
+
// A text target counts only inside the blocks, as every other target does through the position index and the counter walk: an element outside them (a host's own) is no target, and the reference shows nothing.
|
|
547
|
+
for (const s of sites)
|
|
548
|
+
if (s.target && s.read.fn === 'text' && !blocks.some((b) => b.contains(s.target)))
|
|
549
|
+
s.target = null;
|
|
532
550
|
// One walk of the words, shared with the run (*the run resumes from an entry*, build two: 60 ms of every manual run was this walk, done twice). The counter log is not shared, and the reason is worth a line: it is read here before the run registers the GCPM properties, and an unregistered `string-set` inherits, so a log taken now would assign every heading's string to its descendants too.
|
|
533
551
|
const index = indexBlocks(blocks, undefined, undefined, keep);
|
|
534
552
|
const fixed = staticValues(sites, { doc, blocks, index, keep });
|
|
@@ -573,6 +591,10 @@ export async function settleReads(doc, { run, blocksOf, originals = null, warm =
|
|
|
573
591
|
}
|
|
574
592
|
runs++;
|
|
575
593
|
t = performance.now();
|
|
594
|
+
if (result.complete === false) { // a partial chain is not the document: its pages place only the targets cut so far, so the reservations stay hidden and the table it was laid out under stays the warm one
|
|
595
|
+
facts = laidFacts;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
576
598
|
showReservations(doc); // the run is complete: what was reserved is delivered true on the next lines, or was true already
|
|
577
599
|
const truth = pageValues(paged, result);
|
|
578
600
|
unplaced = truth.unplaced;
|
package/dist/src/stonehand.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @packageDocumentation
|
|
13
13
|
*/
|
|
14
14
|
import { isAtom, READS_ITS_CHILDREN } from './word-space.js';
|
|
15
|
+
import { ARTIFACT, hasWord } from './data-beltype.js';
|
|
15
16
|
/**
|
|
16
17
|
* THE MAKE-UP ORDER (*breaks are classified*, I3, amended in the fifth round) — which constraint yields first, and the standing fact that a constraint which has yielded still pulls.
|
|
17
18
|
*
|
|
@@ -158,7 +159,7 @@ function walkOf(report, sourceOf, root = null, styleCache = new Map(), forcing =
|
|
|
158
159
|
const monolithic = (box) => {
|
|
159
160
|
if (box.anonymous)
|
|
160
161
|
return false; // a run of lines, never one unit
|
|
161
|
-
if (box.laidOutLast)
|
|
162
|
+
if (box.laidOutLast || box.laidOutFirst)
|
|
162
163
|
return true; // a footer written before its rows: its words stand before the rows in the word space, so no place inside it could be addressed (*the footer before the rows*)
|
|
163
164
|
const live = sourceOf(box.el) ?? box.el;
|
|
164
165
|
const held = isMonolith(box.el, styleOfBox(box), live);
|
|
@@ -197,9 +198,11 @@ function walkOf(report, sourceOf, root = null, styleCache = new Map(), forcing =
|
|
|
197
198
|
/** A row's items are parallel flows too (*every branch gets a place*, 2026-09-21; §2.1's own list — the cells of a row, grid items, flex items). `report.rowOf` is the Gauge's answer: the side-by-side siblings a box stands among, or null. An item's interior offers nothing, exactly as a float's does, and the boundary between two items of one row is not a place in any procession — they stand beside each other, so nothing lies between them on the block axis. A row therefore offers two readings and no more: the boundary before it, on its first item, and the page edge through it. */
|
|
198
199
|
const rowOf = (box) => (box === root ? null : report.rowOf?.(box) ?? null);
|
|
199
200
|
const insideRowItem = (box) => climb(box, (p) => !!rowOf(p), false);
|
|
200
|
-
/**
|
|
201
|
+
/** A copy the engine repeats (`artifact`), the table's header on a continuation: furniture with a footprint, no content of the flow, so no break point stands in it (*an artifact is never read*). */
|
|
202
|
+
const copy = (box) => !box.anonymous && hasWord(box.el, ARTIFACT);
|
|
203
|
+
/** Does the walk read this box at all? Not an inline box, nothing inside a monolith, a parallel flow, a row item or a repeated copy — and a row answers once, at its first item (see `rowOf`). */
|
|
201
204
|
const walks = (box) => {
|
|
202
|
-
if (box.inline || insideMonolith(box) || insideParallel(box) || insideRowItem(box))
|
|
205
|
+
if (box.inline || copy(box) || insideMonolith(box) || insideParallel(box) || insideRowItem(box) || climb(box, copy, false))
|
|
203
206
|
return false;
|
|
204
207
|
const row = rowOf(box);
|
|
205
208
|
return !row || row[0] === box;
|
|
@@ -210,7 +213,7 @@ function walkOf(report, sourceOf, root = null, styleCache = new Map(), forcing =
|
|
|
210
213
|
* A float, an absolutely positioned box and a row are not where it stops: a float's top is where the float was pushed, not the procession's, and a row's first item is not the row's top. The premise is stated rather than proved — that in-flow tops do not go backward in document order — and a negative margin taller than the box above it, or a relative offset, would break it. A table's footer written before its rows broke it too, and is walked where it is laid out instead (`gauge.js`'s `laidOutLast`, case 110).
|
|
211
214
|
*/
|
|
212
215
|
const standsBelow = (box, limit, eps) => !rowOf(box) && !parallelBox(box) && box.rect.top > limit + eps;
|
|
213
|
-
return { report, sourceOf, boxes, win, root, styleCache, forcing, styleOfBox, monolithic, insideMonolith, parallelBox, insideParallel, rowOf, insideRowItem, walks, standsBelow };
|
|
216
|
+
return { report, sourceOf, boxes, win, root, styleCache, forcing, styleOfBox, monolithic, insideMonolith, parallelBox, insideParallel, rowOf, insideRowItem, copy, walks, standsBelow };
|
|
214
217
|
}
|
|
215
218
|
/** Does this feed hold the box the retreat walk stops at — the first in-flow box whose top lies below the page's limit? The check a bounded feed must pass before anything is decided on it (*the Cast-off bounds the feed*, 2026-09-26): a feed that holds that box gives the walk everything it reads, and one that does not stopped above the edge and is poured again. */
|
|
216
219
|
export function reachesBelowTheEdge({ report, sourceOf, pageStart, contentHeight, eps = 0.25 }) {
|
|
@@ -261,7 +264,7 @@ export function firstBelowTheEdge({ report, sourceOf, limit, eps = 0.25, sourced
|
|
|
261
264
|
* @param a.stats - when given, counts what the collection costs: the boxes it visited, the line stacks it measured and the ones it skipped, and the milliseconds the lines and the end gaps took (`adjudicate` publishes it as the measure's detail).
|
|
262
265
|
*/
|
|
263
266
|
function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
264
|
-
const { report, win, root, styleOfBox, monolithic, parallelBox, rowOf, walks, standsBelow } = w;
|
|
267
|
+
const { report, win, root, styleOfBox, monolithic, parallelBox, rowOf, copy, walks, standsBelow } = w;
|
|
265
268
|
/**
|
|
266
269
|
* §5.3's *leaving room*, priced (*the fragment that wraps itself*, 2026-09-15). *"When a box breaks, its content box extends to fill any remaining fragmentainer extent (leaving room for any margins/borders/padding applied by `box-decoration-break: clone`)."* So a cut inside a box that clones its decoration costs the page that box's block-end border and padding, on top of the content above it — and every ancestor the same cut breaks costs its own.
|
|
267
270
|
*
|
|
@@ -280,6 +283,13 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
|
280
283
|
}
|
|
281
284
|
return px;
|
|
282
285
|
};
|
|
286
|
+
/** The rise a cut grants inside a table cell it divides: the cell's own nearest, the box included and the root not (a row item's rise is its row's, `rowPlan`). */
|
|
287
|
+
const liftIn = (box) => {
|
|
288
|
+
for (let p = box; p && p !== root; p = p.parent)
|
|
289
|
+
if (styleOfBox(p).display === 'table-cell')
|
|
290
|
+
return cellLift(w, p);
|
|
291
|
+
return 0;
|
|
292
|
+
};
|
|
283
293
|
/** A box's bottom as a place in the procession: a row ends where its tallest item does, not where the item that happens to stand last in the source does. */
|
|
284
294
|
const bottomOf = (box) => {
|
|
285
295
|
const row = rowOf(box);
|
|
@@ -310,17 +320,19 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
|
310
320
|
else if (item.rect.top >= h - eps)
|
|
311
321
|
plan.push({ item, state: 'below', verdict: null });
|
|
312
322
|
else {
|
|
313
|
-
|
|
323
|
+
// A cell the height divides sets its content from its own top on this page (`cellLift`), so its content is judged that much lower.
|
|
324
|
+
const lift = cellLift(w, item);
|
|
325
|
+
const [begins, reachesBelow] = contentSpanOf(w, item, h + lift, eps);
|
|
314
326
|
if (begins === null)
|
|
315
327
|
plan.push({ item, state: 'divided', verdict: null }); // holds nothing to show: a stretched box, divided by the geometry alone
|
|
316
|
-
else if (begins >= h - eps)
|
|
328
|
+
else if (begins >= h + lift - eps)
|
|
317
329
|
plan.push({ item, state: 'below', verdict: null }); // its content begins at or below the height: the break falls in the gap above it (§4.1's Class C, whatever made the gap — padding, an alignment), and the item resumes whole
|
|
318
330
|
else if (!reachesBelow)
|
|
319
331
|
plan.push({ item, state: 'done', verdict: null }); // all it has to show stands above the height; only its box reaches below, stretched by the row
|
|
320
332
|
else if (monolithic(item))
|
|
321
333
|
plan.push({ item, state: 'held', verdict: null }); // a monolith the height crosses resumes whole: the stated limit, as a float's (the item is the graphic, and nothing inside it is a place)
|
|
322
334
|
else {
|
|
323
|
-
const v = flowVerdict(w, item, h, eps);
|
|
335
|
+
const v = flowVerdict(w, item, h + lift, eps);
|
|
324
336
|
if (!v)
|
|
325
337
|
return null; // content crosses the height and the item offers no place above it: it shows nothing here
|
|
326
338
|
plan.push({ item, state: 'divided', verdict: v });
|
|
@@ -344,15 +356,17 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
|
344
356
|
const siblings = box === root ? [] : report.processionOf(box).filter((b) => !isAbsolutelyPositioned(styleOfBox(b)));
|
|
345
357
|
const si = siblings.indexOf(box);
|
|
346
358
|
// A footer written before its rows offers no boundary before it (*the footer before the rows*, 2026-10-15): the place is after the last row in the layout and before the first in the document, and an address can only say the second.
|
|
347
|
-
|
|
359
|
+
// Nor does the box after a header written after its rows (the mirror): the place is after the header in the layout and before every row in the document, so the page would hold the header's copy alone.
|
|
360
|
+
if (si > 0 && !box.laidOutLast && !siblings[si - 1].laidOutFirst) {
|
|
348
361
|
// Two siblings answer here, and they are not always the same one. **The keeps** are the immediately-adjacent box's, because §4.1 names *"a float and an immediately-adjacent in-flow or floated box"* as a Class A pair in as many words — a float's own `break-after` binds the boundary after it, which is §3.1's *should* on floats that this engine honours and Chromium declines. **The position** is the previous in-flow sibling's bottom border edge, because a parallel flow's extent is not a place in the procession the cuts divide (§2.1): the float is laid out beside the content, so the page closes where the previous in-flow box ended and the float's own bottom says nothing about it. Pricing this at the float's bottom is how a 96mm rail put every slab standing beside it off the page — measured, `cases/21-parallel-flows`, where the boundary before the first slab was priced at y 400 in a 264px page area.
|
|
349
362
|
const pair = siblings[si - 1];
|
|
350
363
|
const anchor = last(siblings.slice(0, si).filter((b) => !parallelBox(b))) ?? null;
|
|
351
|
-
|
|
364
|
+
// Before the rows of a continuation only the repeated header stands, and a copy is furniture (*an artifact is never read*): a boundary there takes nothing of the box, and would send the page back to where the box began.
|
|
365
|
+
if (anchor && !siblings.slice(0, si).every((b) => parallelBox(b) || copy(b))) {
|
|
352
366
|
candidates.push({
|
|
353
367
|
kind: 'A',
|
|
354
368
|
cls: 'A',
|
|
355
|
-
y: bottomOf(anchor),
|
|
369
|
+
y: bottomOf(anchor) - liftIn(box.parent),
|
|
356
370
|
docIndex,
|
|
357
371
|
box,
|
|
358
372
|
cost: clonedBlockEnd(box.parent), // the box itself goes whole to the next page; what this boundary breaks is its container and up
|
|
@@ -425,7 +439,7 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
|
425
439
|
stats.gapMs += performance.now() - tg;
|
|
426
440
|
if (gap) {
|
|
427
441
|
const y = Math.min(gap.bottom, limit);
|
|
428
|
-
if (y - gap.base > eps) {
|
|
442
|
+
if (y - (gap.base - liftIn(box)) > eps) {
|
|
429
443
|
candidates.push({
|
|
430
444
|
kind: 'C',
|
|
431
445
|
cls: 'C',
|
|
@@ -444,7 +458,7 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
|
444
458
|
// The place it names is before the box's first in-flow child, past the gap (*the cut divides its ancestors*, 2026-09-29): the page before carries the box's border, its padding and whatever stands in the gap — an empty float, the clearance under it — and the page after resumes the box divided, its first child whole. So the position is the child's outer top, which is the content edge plus the gap; it used to be the box's top plus its border, which put the padding on the next page and offered a break point §4.1 does not provide (`packages/essay/cases/71-the-box-that-begins-a-page`).
|
|
445
459
|
const startGap = box.blockChildren.length ? report.gapAt({ box, edge: 'start' }) : 0;
|
|
446
460
|
const firstInFlow = startGap > eps ? box.blockChildren.find((c) => !parallelBox(c)) : undefined;
|
|
447
|
-
if (firstInFlow) {
|
|
461
|
+
if (firstInFlow && !copy(firstInFlow)) {
|
|
448
462
|
const prevSibling = si > 0 ? siblings[si - 1] : null;
|
|
449
463
|
const bs = win.getComputedStyle(box.el);
|
|
450
464
|
candidates.push({
|
|
@@ -480,11 +494,12 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
|
480
494
|
const orphans = parseInt(st.orphans, 10) || 1;
|
|
481
495
|
const widows = parseInt(st.widows, 10) || 1;
|
|
482
496
|
const window_ = classBWindow(T, orphans, widows);
|
|
497
|
+
const lift = liftIn(box);
|
|
483
498
|
for (let k = 0; k < T - 1; k++) {
|
|
484
499
|
out.push({
|
|
485
500
|
kind: 'B',
|
|
486
501
|
cls: 'B',
|
|
487
|
-
y: lines[k].bottom,
|
|
502
|
+
y: lines[k].bottom - lift,
|
|
488
503
|
docIndex,
|
|
489
504
|
box,
|
|
490
505
|
cost: clonedBlockEnd(box),
|
|
@@ -545,6 +560,18 @@ function subtreeOf(boxes, root) {
|
|
|
545
560
|
}
|
|
546
561
|
return out;
|
|
547
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* How far a table cell's content rises when a cut divides the cell: the distance from its content box's top to its first line or monolith. A cell's `vertical-align` places its content in the cell's whole height; Chromium's print aligns a divided cell's first fragment within that fragment instead (`FinalizeTableCellLayout`: *the fragment will start at the block-start edge* on a resumption, and the free space is the fragment's own), so content the alignment set below the cut stands above it once the cell is divided. The presentation matches it by pinning the divided cell's height to the page's foot. Zero for anything else, and for a cell aligned at its top.
|
|
565
|
+
*
|
|
566
|
+
* @remarks Under collapsed borders the content box is read with the cell's whole border width where half of it is used, so the rise is under by half a border, never over.
|
|
567
|
+
*/
|
|
568
|
+
function cellLift(w, cell) {
|
|
569
|
+
const st = w.styleOfBox(cell);
|
|
570
|
+
if (st.display !== 'table-cell')
|
|
571
|
+
return 0;
|
|
572
|
+
const [begins] = contentSpanOf(w, cell, -Infinity, 0);
|
|
573
|
+
return begins === null ? 0 : Math.max(0, begins - (cell.rect.top + (parseFloat(st.borderTopWidth) || 0) + (parseFloat(st.paddingTop) || 0)));
|
|
574
|
+
}
|
|
548
575
|
/** Where a flow's content stands against a height: where it begins (the top of its first line or monolith, null where it holds none) and whether any of it stands below the height — asked of the leaves that reach below it alone, so a flow of a thousand paragraphs above the foot costs the leaves it needs and not its line stacks (a leaf whose box ends above the height has every line above it too). */
|
|
549
576
|
function contentSpanOf(w, root, h, eps) {
|
|
550
577
|
let begins = null;
|
package/dist/src/word-space.d.ts
CHANGED
|
@@ -216,6 +216,14 @@ export declare function blockChildrenOf(el: Element, isInline?: (el: Element) =>
|
|
|
216
216
|
* @param el - a clone or a source element.
|
|
217
217
|
*/
|
|
218
218
|
export declare function footerBeforeRows(el: Element): boolean;
|
|
219
|
+
/**
|
|
220
|
+
* Is this a table's header group written after a row or row group of its table, which CSS 2.1 §17.2 lays out before them all the same (*"always displayed before all other rows and row groups"*)?
|
|
221
|
+
*
|
|
222
|
+
* @remarks The mirror of {@link footerBeforeRows} (*thead-after-rows-and-tall-footer-lose-rows*, 2026-11-07): the Gauge walks such a header first, the Stonehand offers no place inside it or right after it, and its words stand after the rows in the word space, so a page before its words shows it by its repeated copy alone. One that does not repeat, in a divided table, is refused by the driver. Read by computed display, so a clone answers for its source.
|
|
223
|
+
*
|
|
224
|
+
* @param el - a clone or a source element.
|
|
225
|
+
*/
|
|
226
|
+
export declare function headerAfterRows(el: Element): boolean;
|
|
219
227
|
/**
|
|
220
228
|
* The computed style of the box an author declared — the clone's registered source, or the element itself where it is live and connected. `null` for everything else, which is the honest answer rather than a default: a detached clone with no registration is a `<br>` or a filler span the Compositor inserted, and an element nobody declared has no declaration to read.
|
|
221
229
|
*
|
package/dist/src/word-space.js
CHANGED
|
@@ -495,6 +495,21 @@ export function footerBeforeRows(el) {
|
|
|
495
495
|
return true;
|
|
496
496
|
return false;
|
|
497
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* Is this a table's header group written after a row or row group of its table, which CSS 2.1 §17.2 lays out before them all the same (*"always displayed before all other rows and row groups"*)?
|
|
500
|
+
*
|
|
501
|
+
* @remarks The mirror of {@link footerBeforeRows} (*thead-after-rows-and-tall-footer-lose-rows*, 2026-11-07): the Gauge walks such a header first, the Stonehand offers no place inside it or right after it, and its words stand after the rows in the word space, so a page before its words shows it by its repeated copy alone. One that does not repeat, in a divided table, is refused by the driver. Read by computed display, so a clone answers for its source.
|
|
502
|
+
*
|
|
503
|
+
* @param el - a clone or a source element.
|
|
504
|
+
*/
|
|
505
|
+
export function headerAfterRows(el) {
|
|
506
|
+
if (sourceStyle(el)?.display !== 'table-header-group')
|
|
507
|
+
return false;
|
|
508
|
+
for (let s = el.previousElementSibling; s; s = s.previousElementSibling)
|
|
509
|
+
if (/^table-row(-group)?$/.test(sourceStyle(s)?.display ?? ''))
|
|
510
|
+
return true;
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
498
513
|
/**
|
|
499
514
|
* The computed style of the box an author declared — the clone's registered source, or the element itself where it is live and connected. `null` for everything else, which is the honest answer rather than a default: a detached clone with no registration is a `<br>` or a filler span the Compositor inserted, and an element nobody declared has no declaration to read.
|
|
500
515
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beltype/cicero",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Cicero, a paged-media engine for the live browser: it cuts a document into pages under CSS Paged Media and presents them as real frames. Beta.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|