@beltype/cicero 0.1.7 → 0.1.9
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/src/capture.d.ts +5 -1
- package/dist/src/capture.js +37 -2
- package/dist/src/chase.js +3 -1
- package/dist/src/data-beltype.d.ts +2 -2
- package/dist/src/data-beltype.js +4 -4
- package/dist/src/document-state.d.ts +25 -5
- package/dist/src/document-state.js +63 -24
- package/dist/src/dressing.d.ts +5 -1
- package/dist/src/dressing.js +23 -16
- package/dist/src/driver.js +23 -4
- package/dist/src/fragmentation.d.ts +4 -4
- package/dist/src/fragmentation.js +4 -4
- package/dist/src/fragments.d.ts +6 -0
- package/dist/src/fragments.js +66 -0
- package/dist/src/reads.d.ts +3 -2
- package/dist/src/reads.js +26 -4
- package/dist/src/stonehand.js +28 -6
- package/package.json +1 -1
package/dist/src/capture.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @packageDocumentation
|
|
11
11
|
*/
|
|
12
12
|
import type { Decl } from './margin-boxes.ts';
|
|
13
|
-
import type
|
|
13
|
+
import { type counterStateAt, type PageCounterOps } from './document-state.ts';
|
|
14
14
|
import type { Px } from './gauge.ts';
|
|
15
15
|
export type Side = 'top' | 'right' | 'bottom' | 'left';
|
|
16
16
|
/** One value per side of a box. */
|
|
@@ -40,6 +40,8 @@ export type PageGeometry = {
|
|
|
40
40
|
/** Raw `content` values per margin box, transliterated per page by {@link transliterateContent}. */
|
|
41
41
|
marginBoxes: Record<string, string>;
|
|
42
42
|
pageStyle: PageStyle;
|
|
43
|
+
/** What the page context's rules do to the page counter, which `document-state.ts`'s `pageNumberAt` applies and the host never wears. */
|
|
44
|
+
pageCounter: PageCounterOps;
|
|
43
45
|
};
|
|
44
46
|
/** What a page is asked for by: the four conditions css-page-3 §4 selects `@page` rules on. */
|
|
45
47
|
export type PageCondition = {
|
|
@@ -102,6 +104,8 @@ export declare function styleSheetsOf(doc: Document): CSSStyleSheet[];
|
|
|
102
104
|
* A box outside a fragment's interval is kept hollow, an element with no children, and every structural selector matches it as it matched the deep clone it replaced — except a `:has()` whose argument reaches inside it. Since 2026-10-10 the host rewrites every `:has()` it can (`seam/rewrite.js`) into a union whose second arm reads a word the engine writes on the copy (`data-beltype.js`, `has-words.js`), so a hollow copy matches as its original does and the answer here is NO for a document whose `:has()` arguments are all rewritten. What is left is the `:has()` whose argument reads state a word computed once cannot follow — `:hover`, `:checked`, `:focus-within`, `:target` and their kin — and a sheet no host rewrote at all: for those the fragments are built deep, as before. Grouping rules, nested rules and imported sheets are walked, since a selector inside `@media` or `@layer` still matches; a sheet the engine cannot read answers yes, because it might. The reading is textual and counts: a rule whose selector holds more `:has(` than unions has one that nothing answers for.
|
|
103
105
|
*/
|
|
104
106
|
export declare function sheetsUseHas(doc: Document): boolean;
|
|
107
|
+
/** Whether a rule sets a counter on a `::before` or an `::after`: only then does the counter walk ask every element's pseudo-elements for their counters (`document-state.ts`'s `documentOpsOf`), which costs two style reads an element. A sheet the engine cannot read answers yes. */
|
|
108
|
+
export declare function sheetsCountInPseudos(doc: Document): boolean;
|
|
105
109
|
/** The arguments the host rewrote, read back from the registry rules (`:not(*) { --beltype-has-<h>: "<A>" }`, one per rewritten sheet): hash → the argument's canonical text. The capture reads the CSSOM and nothing else, so the arguments come from the rule the rewriter appended and no selector is ever parsed back. */
|
|
106
110
|
export declare function hasArgumentsOf(doc: Document): Map<string, string>;
|
|
107
111
|
/**
|
package/dist/src/capture.js
CHANGED
|
@@ -13,6 +13,7 @@ import { MARGIN_BOXES } from './margin-boxes.js';
|
|
|
13
13
|
import { expandBox } from './box-shorthand.js';
|
|
14
14
|
import { CUSTOM_PROPS, decodeContentFn, decodePageName, encodeStringProp } from './gcpm-encoding.js';
|
|
15
15
|
import { HAS_PROPERTY_PREFIX, bareHasCount, decodeHasArgument, isHasRegistryRule } from './data-beltype.js';
|
|
16
|
+
import { pairsOf } from './document-state.js';
|
|
16
17
|
const PX_PER = { px: 1, pt: 96 / 72, in: 96, cm: 96 / 2.54, mm: 96 / 25.4, q: 96 / 101.6 };
|
|
17
18
|
/**
|
|
18
19
|
* Every sheet the document wears — `document.styleSheets` first, then `document.adoptedStyleSheets`.
|
|
@@ -32,6 +33,10 @@ export function styleSheetsOf(doc) {
|
|
|
32
33
|
export function sheetsUseHas(doc) {
|
|
33
34
|
return someRule(doc, (rule) => !!rule.selectorText && bareHasCount(rule.selectorText) > 0);
|
|
34
35
|
}
|
|
36
|
+
/** Whether a rule sets a counter on a `::before` or an `::after`: only then does the counter walk ask every element's pseudo-elements for their counters (`document-state.ts`'s `documentOpsOf`), which costs two style reads an element. A sheet the engine cannot read answers yes. */
|
|
37
|
+
export function sheetsCountInPseudos(doc) {
|
|
38
|
+
return someRule(doc, (rule) => !!rule.selectorText && /::?(?:before|after)\b/i.test(rule.selectorText) && /\bcounter-(?:reset|increment|set)\s*:/.test(rule.style?.cssText ?? ''));
|
|
39
|
+
}
|
|
35
40
|
/** The arguments the host rewrote, read back from the registry rules (`:not(*) { --beltype-has-<h>: "<A>" }`, one per rewritten sheet): hash → the argument's canonical text. The capture reads the CSSOM and nothing else, so the arguments come from the rule the rewriter appended and no selector is ever parsed back. */
|
|
36
41
|
export function hasArgumentsOf(doc) {
|
|
37
42
|
const found = new Map();
|
|
@@ -659,6 +664,36 @@ function declarationsOf(style, owned = ownedByCicero) {
|
|
|
659
664
|
}
|
|
660
665
|
return out;
|
|
661
666
|
}
|
|
667
|
+
/** The three counter properties, each with the value an unnumbered name takes. */
|
|
668
|
+
const PAGE_COUNTER_PROPS = { 'counter-reset': 0, 'counter-increment': 1, 'counter-set': 0 };
|
|
669
|
+
/**
|
|
670
|
+
* A page rule's declarations with the page counter taken out of its `counter-reset`, `counter-increment` and `counter-set`: the page number is `pageNumberAt`'s, written on the host as `page N`, and an increment the host wore beside it was added to N (`@page { counter-increment: page 2 }` printed 3, 4, 5 where Chromium prints 2, 4, 6) while the host's inline reset hid every `@page` reset. Another counter's pair stays where it was.
|
|
671
|
+
*/
|
|
672
|
+
function withoutPageCounter(decls) {
|
|
673
|
+
return decls.flatMap((d) => {
|
|
674
|
+
const fallback = PAGE_COUNTER_PROPS[d[0]];
|
|
675
|
+
if (fallback === undefined)
|
|
676
|
+
return [d];
|
|
677
|
+
const pairs = pairsOf(d[1], fallback);
|
|
678
|
+
if (!pairs.some(([name]) => name === 'page'))
|
|
679
|
+
return [d];
|
|
680
|
+
const rest = pairs.filter(([name]) => name !== 'page');
|
|
681
|
+
return rest.length ? [[d[0], rest.map(([name, value]) => `${name} ${value}`).join(' '), d[2]]] : [];
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/** The page counter's value in each of a condition's three winning counter declarations, `null` where the winner names no `page`: the cascade over the applicable rules, in ascending specificity, `!important` beating normal. */
|
|
685
|
+
function pageCounterOf(rules) {
|
|
686
|
+
const winners = {};
|
|
687
|
+
for (const r of rules)
|
|
688
|
+
for (const d of r.style)
|
|
689
|
+
if (d[0] in PAGE_COUNTER_PROPS && (d[2] === 'important' || winners[d[0]]?.[2] !== 'important'))
|
|
690
|
+
winners[d[0]] = d;
|
|
691
|
+
const pageOf = (prop) => {
|
|
692
|
+
const pair = pairsOf(winners[prop]?.[1], PAGE_COUNTER_PROPS[prop]).findLast(([name]) => name === 'page');
|
|
693
|
+
return pair ? pair[1] : null;
|
|
694
|
+
};
|
|
695
|
+
return { reset: pageOf('counter-reset'), increment: pageOf('counter-increment'), set: pageOf('counter-set') };
|
|
696
|
+
}
|
|
662
697
|
/**
|
|
663
698
|
* The faces page furniture asks for, as `FontFaceSet.load()` arguments (the platform's font preloading) — read from the `@page` rules' own declarations, where the families are already in hand.
|
|
664
699
|
*
|
|
@@ -1051,7 +1086,7 @@ export function readGeometryTable(doc) {
|
|
|
1051
1086
|
try {
|
|
1052
1087
|
for (const r of rules) {
|
|
1053
1088
|
r.split = { host: [], cell: [] };
|
|
1054
|
-
for (const decl of r.style)
|
|
1089
|
+
for (const decl of withoutPageCounter(r.style))
|
|
1055
1090
|
(inheritance.reaches(decl) ? r.split.cell : r.split.host).push(decl);
|
|
1056
1091
|
}
|
|
1057
1092
|
}
|
|
@@ -1114,7 +1149,7 @@ export function readGeometryTable(doc) {
|
|
|
1114
1149
|
const b = Object.fromEntries(SIDES.map((side) => [side, borderIsDrawn(borderStyles[side]) ? resolve(borderWidths[side] ?? 'medium', basePx(side), `${label} border-${side}-width`, 'borderTopWidth') : 0]));
|
|
1115
1150
|
const area = pageArea({ pageW, pageH, margins: m, borders: b, paddings: p });
|
|
1116
1151
|
assertPageArea(area, label);
|
|
1117
|
-
return { pageW, pageH, margins: m, borders: b, paddings: p, ...area, marginBoxes, pageStyle };
|
|
1152
|
+
return { pageW, pageH, margins: m, borders: b, paddings: p, ...area, marginBoxes, pageStyle, pageCounter: pageCounterOf(applicable) };
|
|
1118
1153
|
}
|
|
1119
1154
|
/**
|
|
1120
1155
|
* The intake sweep (*page lengths are measured*, decision 3). Every reachable condition is resolved once, here — where the document is live and the fonts are awaited — so an unresolvable length refuses at intake rather than throwing from inside the per-page loop, pages deep, after the measure context has been built. The set is finite and small: each declared page name plus the unnamed page, × `:first`, × parity. Afterwards the memo answers every per-page call, so `pageFor` stays synchronous and keeps its signature.
|
package/dist/src/chase.js
CHANGED
|
@@ -312,9 +312,11 @@ export class BeltypePage extends HTMLElement {
|
|
|
312
312
|
const keys = names.map((n, i) => `${this.#dressText} ${n} ${generated[i]} ${(this.#reads[n] ?? []).map((r) => `${r}=${this.#published.get(r) ?? ''}`).join(',')} ${cellKey(cells[i])}`);
|
|
313
313
|
let pairs = keys.map((k) => INTRINSICS.get(k));
|
|
314
314
|
if (pairs.some((p) => !p)) {
|
|
315
|
+
// A rect is drawn under every transform above the page, and a host that zooms with `transform: scale(s)` would hand §5.3.2 widths in its pixels, not the page's, and the cache would keep them for every scale after (assessment 2026-11-07). The band spans exactly `#bandWidth` of the page's own pixels, so its rect says the scale.
|
|
316
|
+
const scale = el.getBoundingClientRect().width / this.#bandWidth || 1;
|
|
315
317
|
const read = (track) => {
|
|
316
318
|
el.style.gridTemplateColumns = `${track} ${track} ${track}`;
|
|
317
|
-
return cells.map((c) => c.getBoundingClientRect().width);
|
|
319
|
+
return cells.map((c) => c.getBoundingClientRect().width / scale);
|
|
318
320
|
};
|
|
319
321
|
const max = read('max-content');
|
|
320
322
|
const min = read('min-content');
|
|
@@ -153,9 +153,9 @@ export declare const CUT_OFF: string;
|
|
|
153
153
|
* The rules, as text. Each is a sentence about a word.
|
|
154
154
|
*
|
|
155
155
|
* - `away`: a box put away on this page is not displayed, and it stays an element so every structural selector reads as it does in the source (*every branch gets a place*, 2026-09-21).
|
|
156
|
-
* - 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
|
|
156
|
+
* - 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).
|
|
157
157
|
* - 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.
|
|
158
|
-
* - A cut-off piece gives up the margin before the break, and unless `wrapped` its border and
|
|
158
|
+
* - A cut-off piece gives up the margin before the break, and unless `wrapped` its border, padding and rounding there.
|
|
159
159
|
* - `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.
|
|
160
160
|
*/
|
|
161
161
|
export declare const WORDS_RULES: string;
|
package/dist/src/data-beltype.js
CHANGED
|
@@ -292,18 +292,18 @@ const RESUMED_BOX = `${RESUMED}:not(${w(SHELL)})`;
|
|
|
292
292
|
* The rules, as text. Each is a sentence about a word.
|
|
293
293
|
*
|
|
294
294
|
* - `away`: a box put away on this page is not displayed, and it stays an element so every structural selector reads as it does in the source (*every branch gets a place*, 2026-09-21).
|
|
295
|
-
* - 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
|
|
295
|
+
* - 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).
|
|
296
296
|
* - 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.
|
|
297
|
-
* - A cut-off piece gives up the margin before the break, and unless `wrapped` its border and
|
|
297
|
+
* - A cut-off piece gives up the margin before the break, and unless `wrapped` its border, padding and rounding there.
|
|
298
298
|
* - `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.
|
|
299
299
|
*/
|
|
300
300
|
export const WORDS_RULES = `
|
|
301
301
|
@layer ${LAYER} {
|
|
302
302
|
${w(AWAY)} { display: none !important; }
|
|
303
303
|
${RESUMED_BOX} { margin-top: 0 !important; text-indent: 0 !important; counter-reset: none !important; counter-increment: none !important; counter-set: none !important; }
|
|
304
|
-
${RESUMED_BOX}:not(${w(WRAPPED)}) { border-top-width: 0 !important; padding-top: 0 !important; }
|
|
304
|
+
${RESUMED_BOX}:not(${w(WRAPPED)}) { border-top-width: 0 !important; padding-top: 0 !important; border-top-left-radius: 0 !important; border-top-right-radius: 0 !important; }
|
|
305
305
|
${CUT_OFF} { margin-bottom: 0 !important; }
|
|
306
|
-
${CUT_OFF}:not(${w(WRAPPED)}) { border-bottom-width: 0 !important; padding-bottom: 0 !important; }
|
|
306
|
+
${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; }
|
|
307
307
|
${w(ADJOIN)} { margin-top: 0 !important; }
|
|
308
308
|
}
|
|
309
309
|
`;
|
|
@@ -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.** A
|
|
16
|
+
* **What is not read, each a real gap and each showing as a counter reading low.** A list item's implicit increment, so a `counter(list-item)` an author's own content reads, a margin box's among them, reads low on a page a list continues onto; the list's markers are right since 2026-11-07, because each divided `<ol>`'s copy carries a `start` of HTML's own numbering (`fragments.js`'s `numberDividedLists`); css-lists-3's `reversed()` counters; 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 `::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.
|
|
17
17
|
*
|
|
18
18
|
* @packageDocumentation
|
|
19
19
|
*/
|
|
@@ -62,6 +62,8 @@ export type Op = {
|
|
|
62
62
|
at: number;
|
|
63
63
|
};
|
|
64
64
|
};
|
|
65
|
+
/** `counter-reset: a 2 b` → `[['a', 2], ['b', 0]]`. The computed value normalizes name/value pairs, but an absent number still defaults per property, so the default is a parameter. `none` and the empty string yield nothing. */
|
|
66
|
+
export declare function pairsOf(value: unknown, fallback: number): [string, number][];
|
|
65
67
|
/**
|
|
66
68
|
* A `<content-list>` into terms — GCPM §1.1.1's own grammar and no more of CSS than it needs.
|
|
67
69
|
*
|
|
@@ -78,11 +80,13 @@ export declare const collapse: (text: unknown) => string;
|
|
|
78
80
|
/** One top-level block's part of the log: its operations in document order, each with its position and depth relative to the block — the words of the block before the element, and its depth below the block (*a resumed run keeps its reads*, 2026-10-22). {@link documentOpsOf} lays the parts end to end, a part's positions offset by its block's base and its depths by the ancestors above the blocks. */
|
|
79
81
|
export declare function blockOpsOf(block: any, index: {
|
|
80
82
|
wordsBefore: (el: any) => number;
|
|
83
|
+
wordCountIn?: (el: any) => number;
|
|
81
84
|
}, reads: {
|
|
82
85
|
styleOf: (el: any) => any;
|
|
83
86
|
textOf: (el: any) => string;
|
|
84
87
|
attrOf: (el: any, name: string) => string | null;
|
|
85
88
|
marks: Set<object> | null;
|
|
89
|
+
pseudoStyleOf?: ((el: any, pseudo: '::before' | '::after') => any) | null;
|
|
86
90
|
}): Op[];
|
|
87
91
|
/**
|
|
88
92
|
* The counter operations of a document, in document order, each with the flat word position it sits at and its depth in the tree.
|
|
@@ -90,6 +94,7 @@ export declare function blockOpsOf(block: any, index: {
|
|
|
90
94
|
* @param blocks - the top-level flow blocks, in order — `driver.js`'s `topLevelBlocks`.
|
|
91
95
|
* @param index - the position index (`positions.js`), which holds every element inside every block.
|
|
92
96
|
* @param opts.styleOf - the computed style of an element — injected, so a plain object tree exercises this without a browser.
|
|
97
|
+
* @param opts.pseudoStyleOf - the computed style of an element's `::before` or `::after`, whose counter operations are logged as the element's first and last child's; absent, a pseudo-element's counters are unread. The `::after` needs the index's `wordCountIn`.
|
|
93
98
|
* @param opts.textOf - an element's text; absent, its `textContent`.
|
|
94
99
|
* @param opts.attrOf - an element's attribute; absent, its `getAttribute`.
|
|
95
100
|
* @param opts.ancestors - the elements above the blocks, outermost first (`html`, `body`). They open before the document's first word and are recorded at position −1.
|
|
@@ -100,11 +105,13 @@ export declare function blockOpsOf(block: any, index: {
|
|
|
100
105
|
export declare function documentOpsOf(blocks: ArrayLike<Element>, index: {
|
|
101
106
|
bases: number[];
|
|
102
107
|
wordsBefore: (el: any) => number;
|
|
108
|
+
wordCountIn?: (el: any) => number;
|
|
103
109
|
entryFor?: (el: any) => {
|
|
104
110
|
block: number;
|
|
105
111
|
} | null;
|
|
106
|
-
}, { styleOf, textOf, attrOf, ancestors, marks, running, keep }: {
|
|
112
|
+
}, { styleOf, pseudoStyleOf, textOf, attrOf, ancestors, marks, running, keep }: {
|
|
107
113
|
styleOf: (el: any) => object;
|
|
114
|
+
pseudoStyleOf?: ((el: any, pseudo: '::before' | '::after') => object) | null;
|
|
108
115
|
textOf?: (el: any) => string;
|
|
109
116
|
attrOf?: (el: any, name: string) => string | null;
|
|
110
117
|
ancestors?: Element[];
|
|
@@ -128,8 +135,16 @@ export declare function counterStateAt(ops: ReturnType<typeof documentOpsOf>, po
|
|
|
128
135
|
from: number;
|
|
129
136
|
to: number;
|
|
130
137
|
}>): Record<string, number[]>;
|
|
138
|
+
/** 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`). */
|
|
139
|
+
export type PageCounterOps = {
|
|
140
|
+
reset: number | null;
|
|
141
|
+
increment: number | null;
|
|
142
|
+
set: number | null;
|
|
143
|
+
};
|
|
144
|
+
/** No page-context rule touches the page counter. */
|
|
145
|
+
export declare const NO_PAGE_COUNTER_OPS: PageCounterOps;
|
|
131
146
|
/**
|
|
132
|
-
* The page number of page `pageIndex`, which begins at `pos`: the run's own count,
|
|
147
|
+
* The page number of page `pageIndex`, which begins at `pos`: the run's own count, with an element's reset of the page counter counted on from the page it begins on, and the page context's own `counter-reset`, `counter-increment` and `counter-set` applied as Chromium's print applies them.
|
|
133
148
|
*
|
|
134
149
|
* @remarks
|
|
135
150
|
* css-page-3 names a reset of the page counter only in the page context, but an author writes `counter-reset: page 1` on a chapter or on each letter of a print run to number its pages from one, and once the engine applies it the pages after must count on from there rather than stay at the value (Codeberg issue 1). The page counter is the page's own, reset on the host as `page N` (`chase.js`'s `setCounters`), so the reset is folded into N here and never delivered as an author counter: delivered, it reset the flow and the margin cells to the same value on every later page in the reset's scope. `target-counter()` to a page reads the same N (`reads.js`'s `pageValues`), so a reference answers the value the counter has at its target (Codeberg issue 2).
|
|
@@ -138,16 +153,21 @@ export declare function counterStateAt(ops: ReturnType<typeof documentOpsOf>, po
|
|
|
138
153
|
*
|
|
139
154
|
* **A blank page the engine makes to bring the element to its side begins where the element does and stands before it** (Codeberg issue 13): the reset is not in force on it, so it continues the count it stands in, and it is not counted toward the pages after it, so the element's page bears the value. A blank page inside the scope, after the element began, counts as every page does.
|
|
140
155
|
*
|
|
141
|
-
*
|
|
156
|
+
* **The page context's rules are the page box's, and each page box is its own scope.** Measured 2026-11-07 on Chromium's print (`test/browser/page-context-counters.browser.ts`): every page adds its `counter-increment` of `page`, or 1 where none is declared, to the count the pages carry; a `counter-set` sets that count, and the pages after count on from it (`@page :first { counter-set: page 7 }` prints 7, 8, 9); a `counter-reset` opens a counter of the page box's own, so the page prints the reset value plus that page's own declared increment and the pages after continue the count as though it had not been there (`@page :first { counter-reset: page 10 }` prints 10, 1, 2, and with `@page { counter-increment: page 2 }` beside it 12, 2, 4). An element's reset is applied to the count before the page's own rules, and the page it begins on takes no implicit increment. Until 2026-11-07 these declarations reached the page host as author style, where `counter-increment: page 2` added to the host's `page N` and the host's inline reset hid every `@page` reset: 3, 4, 5 for Chromium's 7, 2, 4.
|
|
157
|
+
*
|
|
158
|
+
* @param pages - each page before `pageIndex`, by index: its first word position, whether it is blank, and its page context's rules.
|
|
142
159
|
* @param blank - whether page `pageIndex` is one the engine made blank.
|
|
160
|
+
* @param counter - page `pageIndex`'s page context's rules.
|
|
143
161
|
* @param away - the remainders the page's opening cut carries in, as for {@link counterStateAt}.
|
|
144
162
|
*/
|
|
145
|
-
export declare function pageNumberAt(ops: ReadonlyArray<Op>, pageIndex: number, pos: number, { pages, blank, away }: {
|
|
163
|
+
export declare function pageNumberAt(ops: ReadonlyArray<Op>, pageIndex: number, pos: number, { pages, blank, counter, away }: {
|
|
146
164
|
pages: (i: number) => {
|
|
147
165
|
start: number;
|
|
148
166
|
blank: boolean;
|
|
167
|
+
counter?: PageCounterOps;
|
|
149
168
|
};
|
|
150
169
|
blank?: boolean;
|
|
170
|
+
counter?: PageCounterOps;
|
|
151
171
|
away?: ReadonlyArray<{
|
|
152
172
|
from: number;
|
|
153
173
|
to: number;
|
|
@@ -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.** A
|
|
16
|
+
* **What is not read, each a real gap and each showing as a counter reading low.** A list item's implicit increment, so a `counter(list-item)` an author's own content reads, a margin box's among them, reads low on a page a list continues onto; the list's markers are right since 2026-11-07, because each divided `<ol>`'s copy carries a `start` of HTML's own numbering (`fragments.js`'s `numberDividedLists`); css-lists-3's `reversed()` counters; 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 `::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.
|
|
17
17
|
*
|
|
18
18
|
* @packageDocumentation
|
|
19
19
|
*/
|
|
@@ -21,7 +21,7 @@ import { STRING_SET_PROP, RUNNING_PROP } from './gcpm-encoding.js';
|
|
|
21
21
|
import { format as formatCounter } from './counter-styles.js';
|
|
22
22
|
import { inRemainders } from './positions.js';
|
|
23
23
|
/** `counter-reset: a 2 b` → `[['a', 2], ['b', 0]]`. The computed value normalizes name/value pairs, but an absent number still defaults per property, so the default is a parameter. `none` and the empty string yield nothing. */
|
|
24
|
-
function pairsOf(value, fallback) {
|
|
24
|
+
export function pairsOf(value, fallback) {
|
|
25
25
|
const out = [];
|
|
26
26
|
const tokens = String(value ?? '').trim().split(/\s+/).filter(Boolean);
|
|
27
27
|
if (!tokens.length || tokens[0] === 'none')
|
|
@@ -197,6 +197,11 @@ function opAt(el, pos, depth, { styleOf, textOf, attrOf, marks }) {
|
|
|
197
197
|
const mark = marks?.has(el) ? el : null;
|
|
198
198
|
return op || strings || mark ? { reset: [], increment: [], set: [], ...op, strings, pos, depth, ...(mark ? { mark } : {}) } : null;
|
|
199
199
|
}
|
|
200
|
+
/** A pseudo-element's counter operations, or `null` where it has none or is not generated: a `::before` or an `::after` whose `content` is `none` or `normal` is no box, and its computed `counter-increment` counts nothing (measured 2026-11-07: Chromium answers `fig 1` for it and the counter stays at 0). */
|
|
201
|
+
function pseudoOpOf(el, pseudo, styleOf) {
|
|
202
|
+
const style = styleOf?.(el, pseudo);
|
|
203
|
+
return style && !/^(?:none|normal)?$/.test(String(style.content ?? '').trim()) ? opOf(style) : null;
|
|
204
|
+
}
|
|
200
205
|
/** One top-level block's part of the log: its operations in document order, each with its position and depth relative to the block — the words of the block before the element, and its depth below the block (*a resumed run keeps its reads*, 2026-10-22). {@link documentOpsOf} lays the parts end to end, a part's positions offset by its block's base and its depths by the ancestors above the blocks. */
|
|
201
206
|
export function blockOpsOf(block, index, reads) {
|
|
202
207
|
const ops = [];
|
|
@@ -207,11 +212,22 @@ export function blockOpsOf(block, index, reads) {
|
|
|
207
212
|
// A `position: fixed` box has zero footprint at any depth (*a fixed box at any depth*, 2026-09-15), so it has no position in the word space this log is ordered by — `indexBlocks` skips it and so does this. A counter operation or a `string-set` inside such a box is therefore unread, which is exactly what was already true of a top-level fixed box and is recorded on the checklist beside it.
|
|
208
213
|
if (style.position === 'fixed' || String(customOf(style, RUNNING_PROP)).trim())
|
|
209
214
|
return;
|
|
210
|
-
const
|
|
211
|
-
|
|
215
|
+
const pos = index.wordsBefore(el);
|
|
216
|
+
const op = opAt(el, pos, depth, reads);
|
|
217
|
+
// A `::before` is the element's first child and an `::after` its last (CSS 2.1 §12.1), so their counter operations are logged one level down, the one at the element's first word and the other at its end. The element's named strings and its mark are evaluated after its `::before`: the number a heading or a figure prints there is the one its `string-set` and a `target-counter()` to it read.
|
|
218
|
+
const before = pseudoOpOf(el, '::before', reads.pseudoStyleOf);
|
|
219
|
+
if (before) {
|
|
220
|
+
if (op && (op.reset.length || op.increment.length || op.set.length))
|
|
221
|
+
ops.push({ ...op, strings: null, mark: undefined });
|
|
222
|
+
ops.push({ ...before, strings: op?.strings ?? null, pos, depth: depth + 1, ...(op?.mark ? { mark: op.mark } : {}) });
|
|
223
|
+
}
|
|
224
|
+
else if (op)
|
|
212
225
|
ops.push(op);
|
|
213
226
|
for (const child of el.children ?? [])
|
|
214
227
|
walk(child, depth + 1);
|
|
228
|
+
const after = index.wordCountIn ? pseudoOpOf(el, '::after', reads.pseudoStyleOf) : null;
|
|
229
|
+
if (after)
|
|
230
|
+
ops.push({ ...after, strings: null, pos: pos + index.wordCountIn(el), depth: depth + 1 });
|
|
215
231
|
};
|
|
216
232
|
walk(block, 0);
|
|
217
233
|
return ops;
|
|
@@ -222,6 +238,7 @@ export function blockOpsOf(block, index, reads) {
|
|
|
222
238
|
* @param blocks - the top-level flow blocks, in order — `driver.js`'s `topLevelBlocks`.
|
|
223
239
|
* @param index - the position index (`positions.js`), which holds every element inside every block.
|
|
224
240
|
* @param opts.styleOf - the computed style of an element — injected, so a plain object tree exercises this without a browser.
|
|
241
|
+
* @param opts.pseudoStyleOf - the computed style of an element's `::before` or `::after`, whose counter operations are logged as the element's first and last child's; absent, a pseudo-element's counters are unread. The `::after` needs the index's `wordCountIn`.
|
|
225
242
|
* @param opts.textOf - an element's text; absent, its `textContent`.
|
|
226
243
|
* @param opts.attrOf - an element's attribute; absent, its `getAttribute`.
|
|
227
244
|
* @param opts.ancestors - the elements above the blocks, outermost first (`html`, `body`). They open before the document's first word and are recorded at position −1.
|
|
@@ -229,8 +246,8 @@ export function blockOpsOf(block, index, reads) {
|
|
|
229
246
|
* @param opts.running - the document's running elements in document order (`driver.js`' `runningBoxes`), each with the flat position the flow stood at where it was written (`driver.js`' `runningPosition`): each is logged as an assignment of its name there, carrying its place in this list as `running.at` (*a running element is a named node*, 2026-10-21). A running element is out of the flow at any depth, so the walk does not enter one, and its own counters and strings are unread, as a fixed box's are.
|
|
230
247
|
* @param opts.keep - the memo's door (`forcing-memo.js`): each block's {@link blockOpsOf} part is asked of it, and a part kept from an earlier run is laid in place of a walk (*a resumed run keeps its reads*, 2026-10-22). A part read under marks is kept apart from one read without, and answers only while the marks that fall in its block are the ones it was read under (the position index says which block a mark is in). Absent, every block is walked.
|
|
231
248
|
*/
|
|
232
|
-
export function documentOpsOf(blocks, index, { styleOf, textOf = (el) => el.textContent, attrOf = (el, name) => el.getAttribute(name), ancestors = [], marks = null, running = [], keep = null }) {
|
|
233
|
-
const reads = { styleOf, textOf, attrOf, marks };
|
|
249
|
+
export function documentOpsOf(blocks, index, { styleOf, pseudoStyleOf = null, textOf = (el) => el.textContent, attrOf = (el, name) => el.getAttribute(name), ancestors = [], marks = null, running = [], keep = null }) {
|
|
250
|
+
const reads = { styleOf, textOf, attrOf, marks, pseudoStyleOf };
|
|
234
251
|
const ops = [];
|
|
235
252
|
ancestors.forEach((el, i) => {
|
|
236
253
|
const op = opAt(el, -1, i, reads);
|
|
@@ -256,7 +273,7 @@ export function documentOpsOf(blocks, index, { styleOf, textOf = (el) => el.text
|
|
|
256
273
|
const mine = marksIn?.get(b) ?? new Set();
|
|
257
274
|
const read = () => ({ ops: blockOpsOf(blocks[b], index, reads), marks: mine });
|
|
258
275
|
const part = keep
|
|
259
|
-
? keep(marks ? 'ops:marked' : 'ops'
|
|
276
|
+
? keep(`${marks ? 'ops:marked' : 'ops'}${pseudoStyleOf ? ':pseudo' : ''}`, blocks[b], read, marks ? (p) => p.marks.size === mine.size && [...mine].every((m) => p.marks.has(m)) : undefined)
|
|
260
277
|
: read();
|
|
261
278
|
for (const op of part.ops)
|
|
262
279
|
ops.push({ ...op, pos: start + op.pos, depth: base + op.depth });
|
|
@@ -289,8 +306,10 @@ const runningOp = (r, at) => ({ reset: [], increment: [], set: [], strings: null
|
|
|
289
306
|
export function counterStateAt(ops, pos, away = []) {
|
|
290
307
|
return replay(ops, { until: pos, away });
|
|
291
308
|
}
|
|
309
|
+
/** No page-context rule touches the page counter. */
|
|
310
|
+
export const NO_PAGE_COUNTER_OPS = Object.freeze({ reset: null, increment: null, set: null });
|
|
292
311
|
/**
|
|
293
|
-
* The page number of page `pageIndex`, which begins at `pos`: the run's own count,
|
|
312
|
+
* The page number of page `pageIndex`, which begins at `pos`: the run's own count, with an element's reset of the page counter counted on from the page it begins on, and the page context's own `counter-reset`, `counter-increment` and `counter-set` applied as Chromium's print applies them.
|
|
294
313
|
*
|
|
295
314
|
* @remarks
|
|
296
315
|
* css-page-3 names a reset of the page counter only in the page context, but an author writes `counter-reset: page 1` on a chapter or on each letter of a print run to number its pages from one, and once the engine applies it the pages after must count on from there rather than stay at the value (Codeberg issue 1). The page counter is the page's own, reset on the host as `page N` (`chase.js`'s `setCounters`), so the reset is folded into N here and never delivered as an author counter: delivered, it reset the flow and the margin cells to the same value on every later page in the reset's scope. `target-counter()` to a page reads the same N (`reads.js`'s `pageValues`), so a reference answers the value the counter has at its target (Codeberg issue 2).
|
|
@@ -299,33 +318,53 @@ export function counterStateAt(ops, pos, away = []) {
|
|
|
299
318
|
*
|
|
300
319
|
* **A blank page the engine makes to bring the element to its side begins where the element does and stands before it** (Codeberg issue 13): the reset is not in force on it, so it continues the count it stands in, and it is not counted toward the pages after it, so the element's page bears the value. A blank page inside the scope, after the element began, counts as every page does.
|
|
301
320
|
*
|
|
302
|
-
*
|
|
321
|
+
* **The page context's rules are the page box's, and each page box is its own scope.** Measured 2026-11-07 on Chromium's print (`test/browser/page-context-counters.browser.ts`): every page adds its `counter-increment` of `page`, or 1 where none is declared, to the count the pages carry; a `counter-set` sets that count, and the pages after count on from it (`@page :first { counter-set: page 7 }` prints 7, 8, 9); a `counter-reset` opens a counter of the page box's own, so the page prints the reset value plus that page's own declared increment and the pages after continue the count as though it had not been there (`@page :first { counter-reset: page 10 }` prints 10, 1, 2, and with `@page { counter-increment: page 2 }` beside it 12, 2, 4). An element's reset is applied to the count before the page's own rules, and the page it begins on takes no implicit increment. Until 2026-11-07 these declarations reached the page host as author style, where `counter-increment: page 2` added to the host's `page N` and the host's inline reset hid every `@page` reset: 3, 4, 5 for Chromium's 7, 2, 4.
|
|
322
|
+
*
|
|
323
|
+
* @param pages - each page before `pageIndex`, by index: its first word position, whether it is blank, and its page context's rules.
|
|
303
324
|
* @param blank - whether page `pageIndex` is one the engine made blank.
|
|
325
|
+
* @param counter - page `pageIndex`'s page context's rules.
|
|
304
326
|
* @param away - the remainders the page's opening cut carries in, as for {@link counterStateAt}.
|
|
305
327
|
*/
|
|
306
|
-
export function pageNumberAt(ops, pageIndex, pos, { pages, blank = false, away = [] }) {
|
|
328
|
+
export function pageNumberAt(ops, pageIndex, pos, { pages, blank = false, counter = NO_PAGE_COUNTER_OPS, away = [] }) {
|
|
307
329
|
const inForce = (at, start, isBlank) => at < start || (at === start && !isBlank);
|
|
308
|
-
|
|
330
|
+
const resets = [];
|
|
309
331
|
for (const op of ops) {
|
|
310
332
|
if (op.pos > pos)
|
|
311
333
|
break;
|
|
312
|
-
if (!inForce(op.pos, pos, blank))
|
|
313
|
-
continue;
|
|
314
|
-
if (away.length && inRemainders(away, op.pos))
|
|
315
|
-
continue;
|
|
316
334
|
for (const [name, value] of [...op.reset, ...op.set])
|
|
317
335
|
if (name === 'page')
|
|
318
|
-
|
|
336
|
+
resets.push({ pos: op.pos, value });
|
|
319
337
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
338
|
+
/** The last element reset in force on a page; the current page alone knows the remainders it carries in. */
|
|
339
|
+
const resetOn = (start, isBlank, current) => {
|
|
340
|
+
let found = null;
|
|
341
|
+
for (const r of resets)
|
|
342
|
+
if (inForce(r.pos, start, isBlank) && !(current && away.length && inRemainders(away, r.pos)))
|
|
343
|
+
found = r;
|
|
344
|
+
return found;
|
|
345
|
+
};
|
|
346
|
+
let count = 0;
|
|
347
|
+
let shown = 0;
|
|
348
|
+
let before = null;
|
|
349
|
+
for (let i = 0; i <= pageIndex; i++) {
|
|
350
|
+
const page = i === pageIndex ? { start: pos, blank, counter } : pages(i);
|
|
351
|
+
const own = page.counter ?? NO_PAGE_COUNTER_OPS;
|
|
352
|
+
const reset = resets.length ? resetOn(page.start, page.blank, i === pageIndex) : null;
|
|
353
|
+
const elementReset = reset !== null && reset !== before;
|
|
354
|
+
before = reset;
|
|
355
|
+
if (elementReset)
|
|
356
|
+
count = reset.value;
|
|
357
|
+
if (own.reset !== null) {
|
|
358
|
+
// The page box's own counter: it shows here and the count goes on beneath it.
|
|
359
|
+
shown = own.set ?? own.reset + (own.increment ?? 0);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
count += own.increment ?? (elementReset ? 0 : 1);
|
|
363
|
+
if (own.set !== null)
|
|
364
|
+
count = own.set;
|
|
365
|
+
shown = count;
|
|
327
366
|
}
|
|
328
|
-
return
|
|
367
|
+
return shown;
|
|
329
368
|
}
|
|
330
369
|
/** The author's counters alone: the page's own two, `page` and `pages`, are the host's (`pageNumberAt`), and delivered as an author's they would reset the flow and the cells under the host's. */
|
|
331
370
|
export function withoutPageCounters(state) {
|
package/dist/src/dressing.d.ts
CHANGED
|
@@ -25,7 +25,11 @@ export type Dressing = {
|
|
|
25
25
|
/** whether the value words' properties are registered, which is done once a stylesheet the document wears names one. */
|
|
26
26
|
registered: boolean;
|
|
27
27
|
};
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Make sure the document has the words sheet's rules, and answer its record. A document the provision reached already has them and nothing is seated; any other gets the layer named first in the head and the rules adopted after whatever it wears.
|
|
30
|
+
*
|
|
31
|
+
* **The document is asked, not remembered** ([*the engine asks the document for its sheets*](../../../.claude/decisions/cicero/the-engine-asks-the-document-for-its-sheets.md)), as `ensureContinuationDressing` does: a host that re-seats a kept `Document` empties its adopted sheets, so a later call adopts the words sheet again, and names the layer again, where the document no longer carries them.
|
|
32
|
+
*/
|
|
29
33
|
export declare function ensureDressing(doc: Document): Dressing;
|
|
30
34
|
/**
|
|
31
35
|
* Make sure the document wears the continuation dressing, and answer its sheet: a piece that resumes a box shows none of the box's beginnings, `::before`, `::marker`, `::first-letter` and `::first-line`, and a piece the cut passes through shows no `::after`. The sheet is made once per document and adopted after whatever the document wears; a later call adopts it again only where the document no longer wears it.
|
package/dist/src/dressing.js
CHANGED
|
@@ -36,25 +36,31 @@ function providesRules(doc) {
|
|
|
36
36
|
const provision = doc.head.querySelector('style[data-beltype~="cicero-provision"]');
|
|
37
37
|
return !!provision && (provision.textContent ?? '').includes(WORDS_RULES);
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Make sure the document has the words sheet's rules, and answer its record. A document the provision reached already has them and nothing is seated; any other gets the layer named first in the head and the rules adopted after whatever it wears.
|
|
41
|
+
*
|
|
42
|
+
* **The document is asked, not remembered** ([*the engine asks the document for its sheets*](../../../.claude/decisions/cicero/the-engine-asks-the-document-for-its-sheets.md)), as `ensureContinuationDressing` does: a host that re-seats a kept `Document` empties its adopted sheets, so a later call adopts the words sheet again, and names the layer again, where the document no longer carries them.
|
|
43
|
+
*/
|
|
40
44
|
export function ensureDressing(doc) {
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
let dressing = DRESSINGS.get(doc);
|
|
46
|
+
if (!dressing) {
|
|
47
|
+
dressing = { words: null, values: null, delivered: new Set(), registered: false };
|
|
48
|
+
DRESSINGS.set(doc, dressing);
|
|
49
|
+
if (providesRules(doc))
|
|
50
|
+
return dressing;
|
|
51
|
+
dressing.words = new (doc.defaultView.CSSStyleSheet)();
|
|
52
|
+
dressing.words.replaceSync(WORDS_RULES);
|
|
53
|
+
}
|
|
54
|
+
if (!dressing.words)
|
|
47
55
|
return dressing;
|
|
48
|
-
const Sheet = doc.defaultView.CSSStyleSheet;
|
|
49
56
|
if (!doc.getElementById(LAYERS_ID)) {
|
|
50
57
|
const layers = doc.createElement('style');
|
|
51
58
|
layers.id = LAYERS_ID;
|
|
52
59
|
layers.textContent = `@layer ${LAYER};`;
|
|
53
60
|
doc.head.prepend(layers);
|
|
54
61
|
}
|
|
55
|
-
dressing.words
|
|
56
|
-
|
|
57
|
-
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, dressing.words];
|
|
62
|
+
if (!doc.adoptedStyleSheets.includes(dressing.words))
|
|
63
|
+
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, dressing.words];
|
|
58
64
|
return dressing;
|
|
59
65
|
}
|
|
60
66
|
/** The continuation dressing each document wears. */
|
|
@@ -134,13 +140,14 @@ export function deliverValues(doc, words) {
|
|
|
134
140
|
dressing.delivered.add(word);
|
|
135
141
|
grew = true;
|
|
136
142
|
}
|
|
137
|
-
if (!grew)
|
|
143
|
+
if (!grew && !dressing.values)
|
|
138
144
|
return;
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
// The document is asked, not remembered: a re-seated document wears no values sheet even when no word is new.
|
|
146
|
+
dressing.values ??= new (doc.defaultView.CSSStyleSheet)();
|
|
147
|
+
if (!doc.adoptedStyleSheets.includes(dressing.values))
|
|
141
148
|
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, dressing.values];
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
if (grew)
|
|
150
|
+
dressing.values.replaceSync([...dressing.delivered].map((word) => `${selectorOf(word)} { ${PROPERTY_OF.get(word.slice(0, word.indexOf(':')))}: ${word.slice(word.indexOf(':') + 1)}; }`).join('\n'));
|
|
144
151
|
}
|
|
145
152
|
/** Is this adopted sheet one of the engine's three sheets keyed on words — the words sheet a host without the provision gets, the values sheet, and the continuation dressing? A host reading the sheets the document wears (`seam/client.js`' conditions key) leaves them out, as it leaves out the root's own sheets and the reads' delivery sheet. */
|
|
146
153
|
export function isWordsSheet(doc, sheet) {
|
package/dist/src/driver.js
CHANGED
|
@@ -19,7 +19,7 @@ import { holdsAnonymousBlocks, takeReport } from './gauge.js';
|
|
|
19
19
|
import { adjudicate, adjudicateFlow, firstBelowTheEdge, isAbsolutelyPositioned, isOutOfFlow, isMonolith, neverCutInside, reachesBelowTheEdge, processionBottoms, fitsPage } from './stonehand.js';
|
|
20
20
|
import { composeDocument, applyRecordsUnder } from './compose-dom.js';
|
|
21
21
|
import { CompositionCache } from './composition-key.js';
|
|
22
|
-
import { readGeometryTable, effectivePageName, pageRulesOf, styleSheetsOf, registerGcpmProperties, sheetsUseHas, sheetReach } from './capture.js';
|
|
22
|
+
import { readGeometryTable, effectivePageName, pageRulesOf, styleSheetsOf, registerGcpmProperties, sheetsUseHas, sheetReach, sheetsCountInPseudos } from './capture.js';
|
|
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';
|
|
@@ -648,6 +648,7 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
648
648
|
*/
|
|
649
649
|
const documentOps = documentOpsOf(blocks, index, {
|
|
650
650
|
styleOf: (el) => win.getComputedStyle(el),
|
|
651
|
+
pseudoStyleOf: sheetsCountInPseudos(doc) ? (el, pseudo) => win.getComputedStyle(el, pseudo) : null,
|
|
651
652
|
ancestors: [doc.documentElement, doc.body].filter(Boolean),
|
|
652
653
|
running: running.map((r) => ({ name: r.name, pos: runningPosition(r, index) })),
|
|
653
654
|
keep: memo?.keep
|
|
@@ -780,14 +781,23 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
780
781
|
let compositionKey = null;
|
|
781
782
|
// Entry N determines page N+1's conditions (*geometry changes only at page boundaries*, C8): name from the cursor's own address, parity from the page number, :first for page one. A width change re-creates the measure context and recomposes through the cache's width key — *cut-invariance* (C7) suspended locally, exactly as designed; the v3 constraint (widths change only at named-switch boundaries) keeps that suspension off mid-paragraph continuations.
|
|
782
783
|
const conditionsFor = (pageIndex, { blank = false } = {}) => conditionsAt(pageIndex, cursor.cut, cursor.pos, { blank, carried: cursor.carried });
|
|
784
|
+
/** A kept page's `@page` counter rules, by its conditions: one geometry read per kind of page, not one per page per page. */
|
|
785
|
+
const pageCounters = new Map();
|
|
786
|
+
const pageCounterOf = (i, c) => {
|
|
787
|
+
const key = JSON.stringify([c.name, i === 0, c.parity, c.blank]);
|
|
788
|
+
let ops = pageCounters.get(key);
|
|
789
|
+
if (!ops)
|
|
790
|
+
pageCounters.set(key, ops = table.pageFor({ name: c.name, first: i === 0, parity: c.parity, blank: c.blank }).pageCounter);
|
|
791
|
+
return ops;
|
|
792
|
+
};
|
|
783
793
|
/** The conditions of page `pageIndex`, which begins at `cut` (projected to `pos`) — {@link conditionsFor} with the cursor made a parameter, so a resumed run can write the conditions of a page it keeps from the address that page began at (*the run resumes from an entry*, build two, 2026-10-08). */
|
|
784
794
|
const conditionsAt = (pageIndex, cut, pos, { blank = false, carried = null } = {}) => {
|
|
785
795
|
const name = nameAt(cut);
|
|
786
796
|
const away = remaindersOf(carried, index.endOf);
|
|
787
|
-
// The run's count, or an author's reset of the page counter counted on from the page it begins on (`document-state.js`'s `pageNumberAt`). Entry `i` closes page `i`, so page `i` begins where entry `i - 1` ended.
|
|
788
|
-
const pageNumber = pageNumberAt(documentOps, pageIndex, pos, { pages: (i) => ({ start: i === 0 ? 0 : account[i - 1].pos ?? 0, blank: account[i].blank === true }), blank, away });
|
|
789
797
|
const parity = parityAt(pageIndex);
|
|
790
798
|
const geo = table.pageFor({ name, first: pageIndex === 0, parity, blank });
|
|
799
|
+
// 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.
|
|
800
|
+
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 });
|
|
791
801
|
// 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.
|
|
792
802
|
const counters = withoutPageCounters(counterStateAt(documentOps, pos, away));
|
|
793
803
|
return Object.freeze({ ...geo, name, pageNumber, parity, blank, counters, dpr: win.devicePixelRatio });
|
|
@@ -1376,7 +1386,8 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
1376
1386
|
const cutY = v.y;
|
|
1377
1387
|
const places = v.items.map(({ item, state, verdict }) => ({
|
|
1378
1388
|
item,
|
|
1379
|
-
|
|
1389
|
+
// An item that is `done` has shown all its content and continues as a shell of what its box has left; the Stonehand said so knowing where a divided cell's content stands (`cellLift`), which the geometry of the feed does not.
|
|
1390
|
+
at: state === 'above' ? { box: item, word: null, px: { remaining: 0, paint: null }, flows: [] } : state === 'done' ? { box: item, word: null, px: { remaining: item.rect.bottom - cutY, paint: null }, flows: [] } : state === 'below' || state === 'held' ? { box: item, word: null, px: null, flows: [] } : verdict ? verdictPlace(verdict) : placesOf(item, cutY)
|
|
1380
1391
|
}));
|
|
1381
1392
|
const last = places.pop();
|
|
1382
1393
|
return { ...last.at, itemBox: last.item, flows: [...places.map(({ item, at }) => ({ ...at, itemBox: item, remainingPx: item.rect.top < cutY && item.rect.bottom > cutY ? item.rect.bottom - cutY : null })), ...last.at.flows] };
|
|
@@ -1607,6 +1618,14 @@ async function paginateOnce(doc, { composed = false, compositionCache = new Comp
|
|
|
1607
1618
|
visit(f);
|
|
1608
1619
|
})(rowPlace);
|
|
1609
1620
|
}
|
|
1621
|
+
else if (v.kind !== 'E') {
|
|
1622
|
+
// A table cell the main path's cut divides gets the same extension: Chromium's print aligns a divided cell's content within its first fragment, which reaches the foot, and the Stonehand judged the cut so (`cellLift`).
|
|
1623
|
+
const foot = pageStart + usableH;
|
|
1624
|
+
for (let p = v.kind === 'A' ? v.box.parent : v.box; p; p = p.parent) {
|
|
1625
|
+
if (win.getComputedStyle(p.el).display === 'table-cell' && p.rect.top < v.y - 0.25 && p.rect.bottom > v.y + 0.25)
|
|
1626
|
+
extents.push({ path: pathOf(sourceOf(p.el), blocks[cutB], cutB), px: foot - p.rect.top });
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1610
1629
|
const tf = performance.now();
|
|
1611
1630
|
const flat = flattenResume({ pos, box: sourceOf(v.box.el), cut, flows });
|
|
1612
1631
|
addr.flatten += performance.now() - tf;
|
|
@@ -2,18 +2,18 @@
|
|
|
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
|
|
5
|
+
* Exactly the names the first view adapter imported by deep path on 2026-10-29 (68 of them, from 16 modules), re-exported unchanged, and the names a view has asked for since (the leading-decoration door, `WRAPPED`, `clonesDecoration` and `truncateLeadingDecoration`, 2026-11-07); nothing is un-exported. Which of them stay public, merge or leave is the shape round's question. The face loads in node: `chase.ts`, which defines the `<beltype-page>` element and so needs a DOM at load, is not here; a view imports it for that effect as `@beltype/cicero/internal/chase`.
|
|
6
6
|
*
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/
|
|
9
9
|
export { effectivePageName, readGeometryTable, registerGcpmProperties, topLevelReach, transliterateContent } from './capture.ts';
|
|
10
|
-
export { ADJOIN, ARTIFACT, AWAY, END, FRAGMENT, SHELL, SPLIT, START, addWord, hasWord, removeWord } from './data-beltype.ts';
|
|
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
13
|
export { forcingIndexOf, gapAbove, pageNameDownFrom, runningBoxes, runningPosition, topLevelBlocks } from './driver.ts';
|
|
14
14
|
export { ForcingMemo } from './forcing-memo.ts';
|
|
15
15
|
export type { Keep } from './forcing-memo.ts';
|
|
16
|
-
export { joinedWords, mutateToContinuation, mutateToFirstFragment, pinTrack } from './fragments.ts';
|
|
16
|
+
export { joinedWords, mutateToContinuation, mutateToFirstFragment, pinTrack, truncateLeadingDecoration } from './fragments.ts';
|
|
17
17
|
export { takeReport } from './gauge.ts';
|
|
18
18
|
export { STRING_KEYWORDS, encodeStringProp } from './gcpm-encoding.ts';
|
|
19
19
|
export { assertRenderConditions, awaitCloneImages, awaitFonts, createMeasureContext } from './measure.ts';
|
|
@@ -21,4 +21,4 @@ export { divides, elAt, extensionPx, indexBlocks, normalizePath, pageContaining,
|
|
|
21
21
|
export { present, presentation } from './presenters.ts';
|
|
22
22
|
export { cssString, findPageSites, settleReads } from './reads.ts';
|
|
23
23
|
export { adjudicate, firstBelowTheEdge, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.ts';
|
|
24
|
-
export { BREAKING_SPACE, copyOf, isFixedBox, isInlineBox, isLineBreak, noteSource } from './word-space.ts';
|
|
24
|
+
export { BREAKING_SPACE, clonesDecoration, copyOf, isFixedBox, isInlineBox, isLineBreak, noteSource } from './word-space.ts';
|
|
@@ -2,17 +2,17 @@
|
|
|
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
|
|
5
|
+
* Exactly the names the first view adapter imported by deep path on 2026-10-29 (68 of them, from 16 modules), re-exported unchanged, and the names a view has asked for since (the leading-decoration door, `WRAPPED`, `clonesDecoration` and `truncateLeadingDecoration`, 2026-11-07); nothing is un-exported. Which of them stay public, merge or leave is the shape round's question. The face loads in node: `chase.ts`, which defines the `<beltype-page>` element and so needs a DOM at load, is not here; a view imports it for that effect as `@beltype/cicero/internal/chase`.
|
|
6
6
|
*
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/
|
|
9
9
|
export { effectivePageName, readGeometryTable, registerGcpmProperties, topLevelReach, transliterateContent } from './capture.js';
|
|
10
|
-
export { ADJOIN, ARTIFACT, AWAY, END, FRAGMENT, SHELL, SPLIT, START, addWord, hasWord, removeWord } from './data-beltype.js';
|
|
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
13
|
export { forcingIndexOf, gapAbove, pageNameDownFrom, runningBoxes, runningPosition, topLevelBlocks } from './driver.js';
|
|
14
14
|
export { ForcingMemo } from './forcing-memo.js';
|
|
15
|
-
export { joinedWords, mutateToContinuation, mutateToFirstFragment, pinTrack } from './fragments.js';
|
|
15
|
+
export { joinedWords, mutateToContinuation, mutateToFirstFragment, pinTrack, truncateLeadingDecoration } from './fragments.js';
|
|
16
16
|
export { takeReport } from './gauge.js';
|
|
17
17
|
export { STRING_KEYWORDS, encodeStringProp } from './gcpm-encoding.js';
|
|
18
18
|
export { assertRenderConditions, awaitCloneImages, awaitFonts, createMeasureContext } from './measure.js';
|
|
@@ -20,4 +20,4 @@ export { divides, elAt, extensionPx, indexBlocks, normalizePath, pageContaining,
|
|
|
20
20
|
export { present, presentation } from './presenters.js';
|
|
21
21
|
export { cssString, findPageSites, settleReads } from './reads.js';
|
|
22
22
|
export { adjudicate, firstBelowTheEdge, fitsPage, isOutOfFlow, neverCutInside, processionBottoms, reachesBelowTheEdge } from './stonehand.js';
|
|
23
|
-
export { BREAKING_SPACE, copyOf, isFixedBox, isInlineBox, isLineBreak, noteSource } from './word-space.js';
|
|
23
|
+
export { BREAKING_SPACE, clonesDecoration, copyOf, isFixedBox, isInlineBox, isLineBreak, noteSource } from './word-space.js';
|
package/dist/src/fragments.d.ts
CHANGED
|
@@ -141,6 +141,12 @@ export declare function mutateToContinuation(clone: Element, { cut, remainderPx,
|
|
|
141
141
|
hollow?: boolean;
|
|
142
142
|
forced?: boolean;
|
|
143
143
|
}): Element;
|
|
144
|
+
/**
|
|
145
|
+
* The ordinal value of each item an `<ol>` owns, as HTML numbers them (§4.4.8) and css-lists-3 states it as the `list-item` counter: the list resets the counter one step before its `start`, which under `reversed` defaults to the number of items; each item adds its step, 1 or −1 under `reversed` unless a `counter-increment` names `list-item`, and a `counter-set` of `list-item`, or else a `value` attribute, sets it.
|
|
146
|
+
*
|
|
147
|
+
* @remarks A `counter-reset` of `list-item` on an item, which opens a list of its own inside the list, is not modelled.
|
|
148
|
+
*/
|
|
149
|
+
export declare function listOrdinals(ol: Element): Map<Element, number>;
|
|
144
150
|
/**
|
|
145
151
|
* A clone of a top-level block for one interval, hollow outside it (*the clone is hollow outside the interval*, 2026-09-27) — the one construction behind the measure feed and every presented fragment, and the one place the clone-to-source map is filled.
|
|
146
152
|
*
|
package/dist/src/fragments.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { applyRecordsUnder, hyphenCharOf } from './compose-dom.js';
|
|
21
21
|
import { addWord, ADJOIN, ARTIFACT, AWAY, END, ENGINE_MARKS, hasAnyWord, hasWord, removeWord, SHELL, SPLIT, START, WRAPPED } from './data-beltype.js';
|
|
22
|
+
import { pairsOf } from './document-state.js';
|
|
22
23
|
import { divides, elAtIn, flowsOf } from './positions.js';
|
|
23
24
|
import { BREAKING_SPACE, clonesDecoration, cutRanges, emptyText, leaveOut, replaceChars, isEngineText, isFixedBox, isInlineBox, isLineBreak, copyOf, shellOf, noteText, READS_ITS_CHILDREN, sourceOf, sourceStyle } from './word-space.js';
|
|
24
25
|
/** The word space, clone side: words are maximal non-whitespace runs of the subtree's joined text — a run crossing a text-node or inline-element seam is one word, exactly as the Gauge reads, and exactly as the position index counts over sources (src/positions.js — the two tokenizations must never drift; counting per node instead shifts every offset by the seam-spanning words before it, which the test-document's inline markup caught). **A non-inline boundary ends the run** (word-space.js), which is where this stopped agreeing with the page for two months. This walk survives on the clone side only: consumption must find the resume word's actual text nodes in the mutated copy, which no source-side index can answer. */
|
|
@@ -790,8 +791,72 @@ export function mutateToContinuation(clone, { cut = null, remainderPx = null, so
|
|
|
790
791
|
applyCarriedRemainder(clone, source, remainderPx);
|
|
791
792
|
for (const m of mine)
|
|
792
793
|
mutateToContinuation(m.itemClone, { cut: m.cut, remainderPx: m.e.remainingPx ?? null, source: m.itemSource, cloneOf, flows: m.nested, rootPath: m.e.item, hollow });
|
|
794
|
+
numberDividedLists(root, cut, source, cloneOf);
|
|
793
795
|
return root;
|
|
794
796
|
}
|
|
797
|
+
/** The list items an `<ol>` owns, in tree order: HTML's list owner is the nearest `ol`, `ul` or `menu` ancestor. An item that is not displayed as a list item has no ordinal, where a style can be read. */
|
|
798
|
+
function ownedItems(ol) {
|
|
799
|
+
const out = [];
|
|
800
|
+
(function walk(el) {
|
|
801
|
+
for (const c of el.children) {
|
|
802
|
+
if (/^(?:OL|UL|MENU)$/i.test(c.tagName))
|
|
803
|
+
continue;
|
|
804
|
+
if (/^LI$/i.test(c.tagName)) {
|
|
805
|
+
const display = sourceStyle(c)?.display;
|
|
806
|
+
if (display == null || display.includes('list-item'))
|
|
807
|
+
out.push(c);
|
|
808
|
+
}
|
|
809
|
+
walk(c);
|
|
810
|
+
}
|
|
811
|
+
})(ol);
|
|
812
|
+
return out;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* The ordinal value of each item an `<ol>` owns, as HTML numbers them (§4.4.8) and css-lists-3 states it as the `list-item` counter: the list resets the counter one step before its `start`, which under `reversed` defaults to the number of items; each item adds its step, 1 or −1 under `reversed` unless a `counter-increment` names `list-item`, and a `counter-set` of `list-item`, or else a `value` attribute, sets it.
|
|
816
|
+
*
|
|
817
|
+
* @remarks A `counter-reset` of `list-item` on an item, which opens a list of its own inside the list, is not modelled.
|
|
818
|
+
*/
|
|
819
|
+
export function listOrdinals(ol) {
|
|
820
|
+
const items = ownedItems(ol);
|
|
821
|
+
const reversed = ol.hasAttribute('reversed');
|
|
822
|
+
const dflt = reversed ? -1 : 1;
|
|
823
|
+
const start = parseInt(ol.getAttribute('start') ?? '', 10);
|
|
824
|
+
let n = (Number.isFinite(start) ? start : reversed ? items.length : 1) - dflt;
|
|
825
|
+
const out = new Map();
|
|
826
|
+
for (const li of items) {
|
|
827
|
+
const style = sourceStyle(li);
|
|
828
|
+
const named = (value, fallback) => pairsOf(value, fallback).find(([name]) => name === 'list-item')?.[1];
|
|
829
|
+
n += named(style?.counterIncrement, 1) ?? dflt;
|
|
830
|
+
const value = parseInt(li.getAttribute('value') ?? '', 10);
|
|
831
|
+
const set = named(style?.counterSet, 0) ?? (Number.isFinite(value) ? value : null);
|
|
832
|
+
if (set != null)
|
|
833
|
+
n = set;
|
|
834
|
+
out.set(li, n);
|
|
835
|
+
}
|
|
836
|
+
return out;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Every `<ol>` a cut divides is numbered from the first item its piece keeps: the piece's copy carries a `start` attribute of that item's ordinal, so the browser's own marker counts on from the source's numbering. Without it a continued list numbered from 1 again on every page, and the first piece of a `reversed` list counted down from the items it kept (measured 2026-11-07 against Chromium's print, `test/browser/list-continuation.browser.ts`). Read from the source, so it holds whichever cut ran first.
|
|
840
|
+
*/
|
|
841
|
+
function numberDividedLists(root, cut, source, cloneOf) {
|
|
842
|
+
if (!divides(cut) || !source || !cloneOf)
|
|
843
|
+
return;
|
|
844
|
+
const target = elAtIn(source, cut.path);
|
|
845
|
+
for (let a = target?.parentElement ?? null; a; a = a === source ? null : a.parentElement) {
|
|
846
|
+
if (!/^OL$/i.test(a.tagName))
|
|
847
|
+
continue;
|
|
848
|
+
const copy = cloneOf.get(a);
|
|
849
|
+
if (!copy)
|
|
850
|
+
continue;
|
|
851
|
+
for (const [li, n] of listOrdinals(a)) {
|
|
852
|
+
const c = cloneOf.get(li);
|
|
853
|
+
if (!c || !root.contains(c) || hasWord(c, AWAY))
|
|
854
|
+
continue;
|
|
855
|
+
copy.setAttribute('start', String(n));
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
795
860
|
/** The shell branch of the start cut: the box the path names continues at exactly the extent it has left, blank or painted. */
|
|
796
861
|
function shellAt(clone, target, px, keep = new Set(), hollow = false) {
|
|
797
862
|
const painted = px.paint != null;
|
|
@@ -1004,6 +1069,7 @@ export function mutateToFirstFragment(clone, { cut = null, extPx = 0, source = n
|
|
|
1004
1069
|
/** And the cut line stays justified. css-text-3 §7.2 says a line before a forced or fragmentation break is not the last line of its block, so `text-align: justify` binds it — but in a clone cut AT the break it is the last line and the browser would set it ragged. The alignment is restated rather than inherited, and it is read from the source through the word space's map, because a detached clone has no computed style to ask. The Compositor makes the same move for the same reason (`compose-dom.js`), which is what makes this the engine's one answer written twice for two different line providers rather than two answers. */
|
|
1005
1070
|
if (sourceStyle(inner)?.textAlign === 'justify')
|
|
1006
1071
|
inner.style.textAlignLast = 'justify';
|
|
1072
|
+
numberDividedLists(clone, cut, source, cloneOf);
|
|
1007
1073
|
return clone;
|
|
1008
1074
|
}
|
|
1009
1075
|
/**
|
package/dist/src/reads.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
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
|
*/
|
|
@@ -224,7 +224,7 @@ export declare function linesOfSites<S extends {
|
|
|
224
224
|
*
|
|
225
225
|
* **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.
|
|
226
226
|
*
|
|
227
|
-
* @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).
|
|
227
|
+
* @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.
|
|
228
228
|
*/
|
|
229
229
|
export declare function settleReads<R extends {
|
|
230
230
|
account: any[];
|
|
@@ -232,6 +232,7 @@ export declare function settleReads<R extends {
|
|
|
232
232
|
resumed?: Resumed | null;
|
|
233
233
|
why?: string | null;
|
|
234
234
|
laidFacts?: PagePublish[] | null;
|
|
235
|
+
complete?: boolean;
|
|
235
236
|
}>(doc: Document, { run, blocksOf, originals, warm, warmFacts, hide, keep }: {
|
|
236
237
|
run: (laid: Laid) => Promise<R>;
|
|
237
238
|
blocksOf: () => Element[];
|
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';
|
|
@@ -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
|
@@ -283,6 +283,13 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
|
283
283
|
}
|
|
284
284
|
return px;
|
|
285
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
|
+
};
|
|
286
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. */
|
|
287
294
|
const bottomOf = (box) => {
|
|
288
295
|
const row = rowOf(box);
|
|
@@ -313,17 +320,19 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
|
313
320
|
else if (item.rect.top >= h - eps)
|
|
314
321
|
plan.push({ item, state: 'below', verdict: null });
|
|
315
322
|
else {
|
|
316
|
-
|
|
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);
|
|
317
326
|
if (begins === null)
|
|
318
327
|
plan.push({ item, state: 'divided', verdict: null }); // holds nothing to show: a stretched box, divided by the geometry alone
|
|
319
|
-
else if (begins >= h - eps)
|
|
328
|
+
else if (begins >= h + lift - eps)
|
|
320
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
|
|
321
330
|
else if (!reachesBelow)
|
|
322
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
|
|
323
332
|
else if (monolithic(item))
|
|
324
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)
|
|
325
334
|
else {
|
|
326
|
-
const v = flowVerdict(w, item, h, eps);
|
|
335
|
+
const v = flowVerdict(w, item, h + lift, eps);
|
|
327
336
|
if (!v)
|
|
328
337
|
return null; // content crosses the height and the item offers no place above it: it shows nothing here
|
|
329
338
|
plan.push({ item, state: 'divided', verdict: v });
|
|
@@ -357,7 +366,7 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
|
357
366
|
candidates.push({
|
|
358
367
|
kind: 'A',
|
|
359
368
|
cls: 'A',
|
|
360
|
-
y: bottomOf(anchor),
|
|
369
|
+
y: bottomOf(anchor) - liftIn(box.parent),
|
|
361
370
|
docIndex,
|
|
362
371
|
box,
|
|
363
372
|
cost: clonedBlockEnd(box.parent), // the box itself goes whole to the next page; what this boundary breaks is its container and up
|
|
@@ -430,7 +439,7 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
|
430
439
|
stats.gapMs += performance.now() - tg;
|
|
431
440
|
if (gap) {
|
|
432
441
|
const y = Math.min(gap.bottom, limit);
|
|
433
|
-
if (y - gap.base > eps) {
|
|
442
|
+
if (y - (gap.base - liftIn(box)) > eps) {
|
|
434
443
|
candidates.push({
|
|
435
444
|
kind: 'C',
|
|
436
445
|
cls: 'C',
|
|
@@ -485,11 +494,12 @@ function collect(w, { boxes, limit, pageStart, eps, stats }) {
|
|
|
485
494
|
const orphans = parseInt(st.orphans, 10) || 1;
|
|
486
495
|
const widows = parseInt(st.widows, 10) || 1;
|
|
487
496
|
const window_ = classBWindow(T, orphans, widows);
|
|
497
|
+
const lift = liftIn(box);
|
|
488
498
|
for (let k = 0; k < T - 1; k++) {
|
|
489
499
|
out.push({
|
|
490
500
|
kind: 'B',
|
|
491
501
|
cls: 'B',
|
|
492
|
-
y: lines[k].bottom,
|
|
502
|
+
y: lines[k].bottom - lift,
|
|
493
503
|
docIndex,
|
|
494
504
|
box,
|
|
495
505
|
cost: clonedBlockEnd(box),
|
|
@@ -550,6 +560,18 @@ function subtreeOf(boxes, root) {
|
|
|
550
560
|
}
|
|
551
561
|
return out;
|
|
552
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
|
+
}
|
|
553
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). */
|
|
554
576
|
function contentSpanOf(w, root, h, eps) {
|
|
555
577
|
let begins = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beltype/cicero",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|