@beltype/cicero 0.1.9 → 0.1.11
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/README.md +7 -6
- package/dist/seam/client.d.ts +12 -4
- package/dist/seam/client.js +57 -21
- package/dist/seam/facsimile.d.ts +13 -5
- package/dist/seam/facsimile.js +28 -12
- package/dist/seam/index.js +1 -1
- package/dist/seam/rewrite.js +7 -1
- package/dist/seam/rim.d.ts +6 -2
- package/dist/seam/rim.js +54 -11
- package/dist/src/capture.d.ts +28 -0
- package/dist/src/capture.js +121 -45
- package/dist/src/compose-dom.d.ts +8 -0
- package/dist/src/compose-dom.js +62 -8
- package/dist/src/compositor.d.ts +11 -2
- package/dist/src/compositor.js +47 -15
- package/dist/src/data-beltype.d.ts +11 -1
- package/dist/src/data-beltype.js +20 -1
- package/dist/src/document-state.d.ts +6 -2
- package/dist/src/document-state.js +87 -11
- package/dist/src/driver.d.ts +6 -0
- package/dist/src/driver.js +36 -46
- package/dist/src/forcing-memo.d.ts +10 -0
- package/dist/src/forcing-memo.js +22 -0
- package/dist/src/fragmentation.d.ts +3 -3
- package/dist/src/fragmentation.js +3 -3
- package/dist/src/fragments.d.ts +1 -1
- package/dist/src/fragments.js +47 -16
- package/dist/src/gauge.d.ts +0 -4
- package/dist/src/gauge.js +16 -30
- package/dist/src/gcpm-encoding.d.ts +2 -0
- package/dist/src/gcpm-encoding.js +2 -0
- package/dist/src/measure.d.ts +17 -12
- package/dist/src/measure.js +30 -16
- package/dist/src/positions.js +21 -9
- package/dist/src/reads.d.ts +4 -2
- package/dist/src/reads.js +10 -4
- package/dist/src/root.d.ts +18 -1
- package/dist/src/root.js +29 -3
- package/dist/src/stonehand.js +2 -6
- package/dist/src/word-space.d.ts +10 -8
- package/dist/src/word-space.js +44 -16
- package/package.json +5 -1
package/dist/src/compositor.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Compositor — whole-paragraph line breaking (tablet, The Line; architecture, The Compositor; second-coding-round.md item 1). One of the system's two deciders; this module is the decision alone: DOM-free, widths through an injected provider (the whole-line Range policy lives in the provider, built browser-side by compose-dom.js), output is a Hints value the feed writer materializes.
|
|
3
3
|
*
|
|
4
|
-
* Knuth–Plass-shaped total-fit dynamic programming over break candidates: word boundaries plus hyphenation offsets (Liang's, or an author's soft hyphens — the caller supplies them; this module does not know where they came from). Badness is slack against the measure under the alignment's stretch model, and there are two of those (see `STRETCH` below) — aesthetic error only, never geometric: a chosen line fits because its measured width is the oracle's own. Demerits add hyphen, consecutive-hyphen, and last-line-orphan penalties. Ties break toward earlier breaks (candidates priced ascending, replaced only on strictly lower total) — a total order, no randomness, so the determinism that *cut-invariance* (C7) relies on holds by construction.
|
|
4
|
+
* Knuth–Plass-shaped total-fit dynamic programming over break candidates: word boundaries plus hyphenation offsets (Liang's, or an author's soft hyphens — the caller supplies them; this module does not know where they came from). Badness is slack against the measure under the alignment's stretch model, and there are two of those (see `STRETCH` below) — aesthetic error only, never geometric: a chosen line fits because its measured width is the oracle's own. Demerits add hyphen, consecutive-hyphen, and last-line-orphan penalties, and a run of hyphenated line ends longer than `limitLines` is no candidate at all. Ties break toward earlier breaks (candidates priced ascending, replaced only on strictly lower total) — a total order, no randomness, so the determinism that *cut-invariance* (C7) relies on holds by construction.
|
|
5
5
|
*
|
|
6
6
|
* Each node reads only its feasible window (*the composition lays out once*, 2026-09-27): its predecessors are scanned downward and the scan stops at the first overfull span, on the premise that for a fixed end a span starting earlier is never narrower. What was read is then priced in ascending order, so the choice and its tie-breaking are the ones pricing every predecessor would make. On BelType's own manual it took the composition's Range reads from 6.2 to 4.8 million.
|
|
7
7
|
*
|
|
@@ -46,6 +46,13 @@ export type Hints = {
|
|
|
46
46
|
/** The justify filler, serialized as a literal: the last line's slack. */
|
|
47
47
|
fillerPx: number;
|
|
48
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* The most lines in a row the composed mode ends on a hyphen (Codeberg issue 5) — Bringhurst's rule, *avoid more than three consecutive hyphenated lines* (The Elements of Typographic Style, section 2.4), and the value css-text-4 §6.3.5's `hyphenate-limit-lines` would carry, which Chromium does not support and so never reaches computed style.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* A judgement with a lineage, as `RAG_TOLERANCE_EM` is. The consecutive penalty alone did not hold it: measured 2026-11-07 on the delibera specimen, its first paragraph ended seven lines in a row on a hyphen at 3000, six at 10000 (TeX's `\doublehyphendemerits`), three at 30000. Under the limit its longest run is three and its hyphens 10 for 13, the interword space averaging 1.356 of the natural space for 1.333, its spread 0.247 for 0.250, its widest unchanged.
|
|
54
|
+
*/
|
|
55
|
+
export declare const HYPHENATE_LIMIT_LINES = 3;
|
|
49
56
|
/**
|
|
50
57
|
* Compose one run: choose its line breaks by total fit.
|
|
51
58
|
*
|
|
@@ -54,10 +61,11 @@ export type Hints = {
|
|
|
54
61
|
* @param p.measure - the width provider.
|
|
55
62
|
* @param p.available - the line measure in px.
|
|
56
63
|
* @param p.indent - first-line text-indent in px — debits line 1's measure.
|
|
64
|
+
* @param p.limitLines - css-text-4 §6.3.5's `hyphenate-limit-lines`: the most consecutive lines that may end on a hyphen, `null` for `no-limit`. A limit extends the DP's state to (node, the run of hyphenated line ends behind it), so the limit is a constraint and not a price; with none the state is the node alone and the composition is the one it always was. A paragraph the limit leaves no way through but an overfull line is composed without it.
|
|
57
65
|
* @param p.breakAnywhere - css-text-4 §6.4's `overflow-wrap: break-word | anywhere`, which lets *"an otherwise unbreakable sequence of characters be broken at an arbitrary point if there are no otherwise-acceptable break points in the line"*, and says outright that **no hyphenation character is inserted**. Such a break is a node like any other, marked `glyph: false`, so it costs no hyphen advance and carries no hyphen demerit.
|
|
58
66
|
* @throws Error — bare and deliberate (`src/` may not import BelType's error types): ragged alignment with no usable `measure.emWidth`, a refusal rather than a NaN.
|
|
59
67
|
*/
|
|
60
|
-
export declare function compose({ words, hyphenOffsets, measure, available, indent, align, breakAnywhere }: {
|
|
68
|
+
export declare function compose({ words, hyphenOffsets, measure, available, indent, align, breakAnywhere, limitLines }: {
|
|
61
69
|
words: string[];
|
|
62
70
|
hyphenOffsets?: (wordIndex: number) => number[];
|
|
63
71
|
measure: Measure;
|
|
@@ -65,4 +73,5 @@ export declare function compose({ words, hyphenOffsets, measure, available, inde
|
|
|
65
73
|
indent?: number;
|
|
66
74
|
align?: 'left' | 'right' | 'center' | 'justify';
|
|
67
75
|
breakAnywhere?: boolean;
|
|
76
|
+
limitLines?: number | null;
|
|
68
77
|
}): Hints;
|
package/dist/src/compositor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Compositor — whole-paragraph line breaking (tablet, The Line; architecture, The Compositor; second-coding-round.md item 1). One of the system's two deciders; this module is the decision alone: DOM-free, widths through an injected provider (the whole-line Range policy lives in the provider, built browser-side by compose-dom.js), output is a Hints value the feed writer materializes.
|
|
3
3
|
*
|
|
4
|
-
* Knuth–Plass-shaped total-fit dynamic programming over break candidates: word boundaries plus hyphenation offsets (Liang's, or an author's soft hyphens — the caller supplies them; this module does not know where they came from). Badness is slack against the measure under the alignment's stretch model, and there are two of those (see `STRETCH` below) — aesthetic error only, never geometric: a chosen line fits because its measured width is the oracle's own. Demerits add hyphen, consecutive-hyphen, and last-line-orphan penalties. Ties break toward earlier breaks (candidates priced ascending, replaced only on strictly lower total) — a total order, no randomness, so the determinism that *cut-invariance* (C7) relies on holds by construction.
|
|
4
|
+
* Knuth–Plass-shaped total-fit dynamic programming over break candidates: word boundaries plus hyphenation offsets (Liang's, or an author's soft hyphens — the caller supplies them; this module does not know where they came from). Badness is slack against the measure under the alignment's stretch model, and there are two of those (see `STRETCH` below) — aesthetic error only, never geometric: a chosen line fits because its measured width is the oracle's own. Demerits add hyphen, consecutive-hyphen, and last-line-orphan penalties, and a run of hyphenated line ends longer than `limitLines` is no candidate at all. Ties break toward earlier breaks (candidates priced ascending, replaced only on strictly lower total) — a total order, no randomness, so the determinism that *cut-invariance* (C7) relies on holds by construction.
|
|
5
5
|
*
|
|
6
6
|
* Each node reads only its feasible window (*the composition lays out once*, 2026-09-27): its predecessors are scanned downward and the scan stops at the first overfull span, on the premise that for a fixed end a span starting earlier is never narrower. What was read is then priced in ascending order, so the choice and its tie-breaking are the ones pricing every predecessor would make. On BelType's own manual it took the composition's Range reads from 6.2 to 4.8 million.
|
|
7
7
|
*
|
|
@@ -14,6 +14,13 @@ const LINE_PENALTY = 10;
|
|
|
14
14
|
const HYPHEN_DEMERITS = 2500;
|
|
15
15
|
const CONSECUTIVE_HYPHEN_DEMERITS = 3000;
|
|
16
16
|
const LAST_LINE_ORPHAN_DEMERITS = 5000;
|
|
17
|
+
/**
|
|
18
|
+
* The most lines in a row the composed mode ends on a hyphen (Codeberg issue 5) — Bringhurst's rule, *avoid more than three consecutive hyphenated lines* (The Elements of Typographic Style, section 2.4), and the value css-text-4 §6.3.5's `hyphenate-limit-lines` would carry, which Chromium does not support and so never reaches computed style.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* A judgement with a lineage, as `RAG_TOLERANCE_EM` is. The consecutive penalty alone did not hold it: measured 2026-11-07 on the delibera specimen, its first paragraph ended seven lines in a row on a hyphen at 3000, six at 10000 (TeX's `\doublehyphendemerits`), three at 30000. Under the limit its longest run is three and its hyphens 10 for 13, the interword space averaging 1.356 of the natural space for 1.333, its spread 0.247 for 0.250, its widest unchanged.
|
|
22
|
+
*/
|
|
23
|
+
export const HYPHENATE_LIMIT_LINES = 3;
|
|
17
24
|
const OVERFULL_DEMERITS = 1e7;
|
|
18
25
|
/** css-text-4 §6.4's emergency break: dearer than any hyphen and cheaper than an overflow, which is the whole ordering the section asks for once the candidates only exist where they are needed. */
|
|
19
26
|
const EMERGENCY_DEMERITS = 1e5;
|
|
@@ -40,10 +47,11 @@ const STRETCH = { justify: 0.5 };
|
|
|
40
47
|
* @param p.measure - the width provider.
|
|
41
48
|
* @param p.available - the line measure in px.
|
|
42
49
|
* @param p.indent - first-line text-indent in px — debits line 1's measure.
|
|
50
|
+
* @param p.limitLines - css-text-4 §6.3.5's `hyphenate-limit-lines`: the most consecutive lines that may end on a hyphen, `null` for `no-limit`. A limit extends the DP's state to (node, the run of hyphenated line ends behind it), so the limit is a constraint and not a price; with none the state is the node alone and the composition is the one it always was. A paragraph the limit leaves no way through but an overfull line is composed without it.
|
|
43
51
|
* @param p.breakAnywhere - css-text-4 §6.4's `overflow-wrap: break-word | anywhere`, which lets *"an otherwise unbreakable sequence of characters be broken at an arbitrary point if there are no otherwise-acceptable break points in the line"*, and says outright that **no hyphenation character is inserted**. Such a break is a node like any other, marked `glyph: false`, so it costs no hyphen advance and carries no hyphen demerit.
|
|
44
52
|
* @throws Error — bare and deliberate (`src/` may not import BelType's error types): ragged alignment with no usable `measure.emWidth`, a refusal rather than a NaN.
|
|
45
53
|
*/
|
|
46
|
-
export function compose({ words, hyphenOffsets = () => [], measure, available, indent = 0, align = 'left', breakAnywhere = false }) {
|
|
54
|
+
export function compose({ words, hyphenOffsets = () => [], measure, available, indent = 0, align = 'left', breakAnywhere = false, limitLines = null }) {
|
|
47
55
|
const N = words.length;
|
|
48
56
|
if (N === 0)
|
|
49
57
|
return { breaks: [], lines: [], lineCount: 0, fillerPx: 0 };
|
|
@@ -92,14 +100,18 @@ export function compose({ words, hyphenOffsets = () => [], measure, available, i
|
|
|
92
100
|
return Math.max(0, lastWord - firstWhole);
|
|
93
101
|
};
|
|
94
102
|
const lineWidth = (a, b) => measure.width(a, b) + (hyphenated(b) ? measure.hyphenWidth : 0);
|
|
103
|
+
/** The longest run of hyphenated line ends a state may carry: the limit, or 1 with none, where only whether the line before ended on a hyphen is ever read and the state is the node alone. */
|
|
104
|
+
const cap = limitLines != null && limitLines >= 1 ? Math.floor(limitLines) : 1;
|
|
105
|
+
const limited = limitLines != null && limitLines >= 1;
|
|
106
|
+
/** By node, by the run of hyphenated line ends that reaching it closes with (0 for a node that is no hyphen). */
|
|
95
107
|
const best = new Array(nodes.length).fill(null);
|
|
96
|
-
best[0] = { total: 0, prev: -1, width: 0, avail: 0, overfull: false };
|
|
108
|
+
best[0] = [{ total: 0, prev: -1, prevRun: 0, width: 0, avail: 0, overfull: false }];
|
|
97
109
|
/** The widths read for the current node, by predecessor: the feasible window, and `j = 0` where a negative indent makes it the one exception. */
|
|
98
110
|
const feasibleW = new Array(nodes.length);
|
|
99
111
|
const feasibleJ = [];
|
|
100
112
|
for (let k = 1; k < nodes.length; k++) {
|
|
101
113
|
const isLast = k === last;
|
|
102
|
-
|
|
114
|
+
const states = [];
|
|
103
115
|
/** The feasible window. The premise, stated so it can be broken: for a fixed end, a span that starts earlier is never narrower — a Range's rect over one nowrap line is the union of what it holds, and an earlier start holds everything a later one does. So once the span from `j` is overfull, every `j` below it is too against the same measure, and the scan stops there. Every `j` above 0 has the same measure; `j = 0` alone is debited the indent, which a positive indent makes narrower (overfull wherever the `j` above it is, so the stop stands) and a negative one wider — so under a negative indent a scan that stopped above it reads it on its own. */
|
|
104
116
|
feasibleJ.length = 0;
|
|
105
117
|
let stoppedAt = -1;
|
|
@@ -143,35 +155,55 @@ export function compose({ words, hyphenOffsets = () => [], measure, available, i
|
|
|
143
155
|
d += EMERGENCY_DEMERITS;
|
|
144
156
|
if (isLast && k - 1 >= 0 && j !== 0 && gapsIn(nodes[j], nodes[k]) === 0)
|
|
145
157
|
d += LAST_LINE_ORPHAN_DEMERITS;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
best[j].forEach((from, run) => {
|
|
159
|
+
if (!from)
|
|
160
|
+
return;
|
|
161
|
+
const next = hyphenated(nodes[k]) ? Math.min(run + 1, cap) : 0;
|
|
162
|
+
if (limited && hyphenated(nodes[k]) && run + 1 > cap)
|
|
163
|
+
return;
|
|
164
|
+
const total = from.total + d;
|
|
165
|
+
const held = states[next];
|
|
166
|
+
if (!held || total < held.total - 1e-9)
|
|
167
|
+
states[next] = { total, prev: j, prevRun: run, width: W, avail, overfull: false };
|
|
168
|
+
});
|
|
149
169
|
}
|
|
150
|
-
if (!
|
|
170
|
+
if (!states.length && !sawFeasible) {
|
|
151
171
|
// Nothing fits: take the shortest line from the nearest reachable node — overfull, crushing demerits, but the paragraph composes (the spirit of *the build cannot fail*, C2).
|
|
152
172
|
for (let j = k - 1; j >= 0; j--) {
|
|
153
173
|
if (!best[j])
|
|
154
174
|
continue;
|
|
175
|
+
const run = best[j].findIndex(Boolean);
|
|
176
|
+
const from = best[j][run];
|
|
155
177
|
const avail = available - (j === 0 ? indent : 0);
|
|
156
178
|
const W = lineWidth(nodes[j], nodes[k]);
|
|
157
|
-
|
|
179
|
+
states[hyphenated(nodes[k]) ? Math.min(run + 1, cap) : 0] = { total: from.total + OVERFULL_DEMERITS + (W - avail) ** 2, prev: j, prevRun: run, width: W, avail, overfull: true };
|
|
158
180
|
break;
|
|
159
181
|
}
|
|
160
182
|
}
|
|
161
|
-
best[k] =
|
|
183
|
+
best[k] = states.length ? states : null;
|
|
162
184
|
}
|
|
163
185
|
// Read back the chosen chain.
|
|
164
186
|
const chain = [];
|
|
165
|
-
for (let k = last
|
|
166
|
-
|
|
187
|
+
for (let k = last, run = 0; k > 0;) {
|
|
188
|
+
const state = best[k][run];
|
|
189
|
+
chain.push({ k, state });
|
|
190
|
+
k = state.prev;
|
|
191
|
+
run = state.prevRun;
|
|
192
|
+
}
|
|
167
193
|
chain.reverse();
|
|
168
194
|
const lines = [];
|
|
169
195
|
let from = 0;
|
|
170
|
-
for (const k of chain) {
|
|
171
|
-
lines.push({ from: nodes[from], to: nodes[k], width:
|
|
196
|
+
for (const { k, state } of chain) {
|
|
197
|
+
lines.push({ from: nodes[from], to: nodes[k], width: state.width, avail: state.avail, overfull: state.overfull });
|
|
172
198
|
from = k;
|
|
173
199
|
}
|
|
174
|
-
|
|
200
|
+
// A limit that leaves the paragraph no way through but an overfull line is not a reason to set one: the paragraph is set without the limit, where that fits.
|
|
201
|
+
if (limited && lines.some((l) => l.overfull)) {
|
|
202
|
+
const free = compose({ words, hyphenOffsets, measure, available, indent, align, breakAnywhere, limitLines: null });
|
|
203
|
+
if (!free.lines.some((l) => l.overfull))
|
|
204
|
+
return free;
|
|
205
|
+
}
|
|
206
|
+
const breaks = chain.slice(0, -1).map(({ k }) => nodes[k]);
|
|
175
207
|
const lastLine = lines[lines.length - 1];
|
|
176
208
|
const fillerPx = Math.max(0, lastLine.avail - lastLine.width);
|
|
177
209
|
return { breaks, lines, lineCount: lines.length, fillerPx };
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
*
|
|
12
12
|
* **No key may end with another key** (`pos:` and `xpos:` cannot both exist), because "every element carrying a key" is selected by substring and a substring cannot anchor to the start of a word. {@link assertNoKeySuffix} checks it once, at load.
|
|
13
13
|
*
|
|
14
|
-
* **One rule, one registry.** The registry is a table of facts, each declared once — its name, its kind, what it is computed from, who writes it and, for a value, the custom property a generated sheet delivers it into — so that a later step adds entries and not a second mechanism. It holds the `has:` family, the line stamp `pos:`, `fragment`, the piece words `split`/`start`/`end`/`slice:N`, `fixed`, the engine-created elements' `shell`/`break`/`fill`, the three words the zeroing needs (`wrapped`, `adjoin`, `away`)
|
|
14
|
+
* **One rule, one registry.** The registry is a table of facts, each declared once — its name, its kind, what it is computed from, who writes it and, for a value, the custom property a generated sheet delivers it into — so that a later step adds entries and not a second mechanism. It holds the `has:` family, the line stamp `pos:`, `fragment`, the piece words `split`/`start`/`end`/`slice:N`, `fixed`, the engine-created elements' `shell`/`break`/`fill`, the three words the zeroing needs (`wrapped`, `adjoin`, `away`), `artifact` (*the piece words*, 2026-10-11; see CHANGELOG 2026-10-11) and the one word a host writes on an element of its own, `host` (ruled 2026-11-08); `hollow` is a later step and is not written here.
|
|
15
15
|
*
|
|
16
16
|
* @packageDocumentation
|
|
17
17
|
*/
|
|
18
18
|
/** The attribute's name. A single place for the literal; the grammar below is the only code that spells the value. */
|
|
19
19
|
export declare const ATTRIBUTE = "data-beltype";
|
|
20
|
+
/** The attribute a printer's preparation sets on every element it makes from generated content, valued by the pseudo it was made from (`before`, `after`, `marker`): its own attribute and not a word, because it is set by the host on the source, before the engine sees it. `content()` and `string-set` leave out what carries it, as css-gcpm-3's `content()` is the element's text without its pseudo-elements, and `content(before)` and `content(after)` read exactly that (ruled 2026-11-07). The name and its values are `GENERATED_ATTRIBUTE` and `GeneratedPseudo` in `@beltype/common/data-beltype`; `src/` imports nothing, so the literal is spelled here once, and is replaced by the import if the engine's layering ever admits common. */
|
|
21
|
+
export declare const GENERATED_ATTRIBUTE = "data-beltype-generated";
|
|
20
22
|
/** One fact the registry declares. */
|
|
21
23
|
export type Entry = {
|
|
22
24
|
/** The flag, or the key of a value word (`has` for `has:<hash>`). */
|
|
@@ -44,6 +46,8 @@ export declare const KEYS: ReadonlySet<string>;
|
|
|
44
46
|
export declare const TRANSFORM = "transform";
|
|
45
47
|
export declare const CICERO_PROVISION = "cicero-provision";
|
|
46
48
|
export declare const CICERO_INTERFACE = "cicero-interface";
|
|
49
|
+
/** The word a host puts on an element of its own machinery that stands among the document's boxes, so the intake passes over it (`topLevelBoxes`, `driver.js`). A host that holds the document in an element of its own names that element with the `root` option first. */
|
|
50
|
+
export declare const HOST = "host";
|
|
47
51
|
/** The words step 2 writes, named once: the engine, the editing tools, Caster and the essay spell them through these and never as a literal of their own. */
|
|
48
52
|
export declare const FRAGMENT = "fragment";
|
|
49
53
|
export declare const SPLIT = "split";
|
|
@@ -159,3 +163,9 @@ export declare const CUT_OFF: string;
|
|
|
159
163
|
* - `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
164
|
*/
|
|
161
165
|
export declare const WORDS_RULES: string;
|
|
166
|
+
/**
|
|
167
|
+
* The id a URL fragment names, percent-decoded; the raw text when it is not valid percent-encoding (`#%E0%A4%A`), where `decodeURIComponent` throws.
|
|
168
|
+
*
|
|
169
|
+
* @param hash - A `location.hash`, with or without its `#`.
|
|
170
|
+
*/
|
|
171
|
+
export declare function decodeFragment(hash: string): string;
|
package/dist/src/data-beltype.js
CHANGED
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
*
|
|
12
12
|
* **No key may end with another key** (`pos:` and `xpos:` cannot both exist), because "every element carrying a key" is selected by substring and a substring cannot anchor to the start of a word. {@link assertNoKeySuffix} checks it once, at load.
|
|
13
13
|
*
|
|
14
|
-
* **One rule, one registry.** The registry is a table of facts, each declared once — its name, its kind, what it is computed from, who writes it and, for a value, the custom property a generated sheet delivers it into — so that a later step adds entries and not a second mechanism. It holds the `has:` family, the line stamp `pos:`, `fragment`, the piece words `split`/`start`/`end`/`slice:N`, `fixed`, the engine-created elements' `shell`/`break`/`fill`, the three words the zeroing needs (`wrapped`, `adjoin`, `away`)
|
|
14
|
+
* **One rule, one registry.** The registry is a table of facts, each declared once — its name, its kind, what it is computed from, who writes it and, for a value, the custom property a generated sheet delivers it into — so that a later step adds entries and not a second mechanism. It holds the `has:` family, the line stamp `pos:`, `fragment`, the piece words `split`/`start`/`end`/`slice:N`, `fixed`, the engine-created elements' `shell`/`break`/`fill`, the three words the zeroing needs (`wrapped`, `adjoin`, `away`), `artifact` (*the piece words*, 2026-10-11; see CHANGELOG 2026-10-11) and the one word a host writes on an element of its own, `host` (ruled 2026-11-08); `hollow` is a later step and is not written here.
|
|
15
15
|
*
|
|
16
16
|
* @packageDocumentation
|
|
17
17
|
*/
|
|
18
18
|
/** The attribute's name. A single place for the literal; the grammar below is the only code that spells the value. */
|
|
19
19
|
export const ATTRIBUTE = 'data-beltype';
|
|
20
|
+
/** The attribute a printer's preparation sets on every element it makes from generated content, valued by the pseudo it was made from (`before`, `after`, `marker`): its own attribute and not a word, because it is set by the host on the source, before the engine sees it. `content()` and `string-set` leave out what carries it, as css-gcpm-3's `content()` is the element's text without its pseudo-elements, and `content(before)` and `content(after)` read exactly that (ruled 2026-11-07). The name and its values are `GENERATED_ATTRIBUTE` and `GeneratedPseudo` in `@beltype/common/data-beltype`; `src/` imports nothing, so the literal is spelled here once, and is replaced by the import if the engine's layering ever admits common. */
|
|
21
|
+
export const GENERATED_ATTRIBUTE = 'data-beltype-generated';
|
|
20
22
|
/** Every word and key, once. The table `docs/reference.md` documents, in the same order. */
|
|
21
23
|
export const REGISTRY = Object.freeze([
|
|
22
24
|
{ name: 'transform', kind: 'flag', status: 'in use', writer: 'belt', from: 'source', facing: 'host', meaning: 'the site engine\'s JSON binding block (`<script type="application/json">`)' },
|
|
@@ -36,6 +38,7 @@ export const REGISTRY = Object.freeze([
|
|
|
36
38
|
{ name: 'wrapped', kind: 'flag', status: 'in use', writer: 'engine', from: 'original', facing: 'engine', meaning: 'the box wraps each fragment whole (`box-decoration-break: clone`), so border and padding stay at a cut; decided by the engine because a selector cannot test the property' },
|
|
37
39
|
{ name: 'adjoin', kind: 'flag', status: 'in use', writer: 'engine', from: 'cut', facing: 'engine', meaning: 'the first-child chain after a cut BETWEEN boxes: its block-start margin adjoins the cut and is truncated, and the box is not divided' },
|
|
38
40
|
{ name: 'away', kind: 'flag', status: 'in use', writer: 'engine', from: 'cut', facing: 'engine', meaning: 'a box put away on this page (`display: none`): kept as an element so structural selectors read as in the source' },
|
|
41
|
+
{ name: 'host', kind: 'flag', status: 'in use', writer: 'host', from: 'host', facing: 'engine', meaning: 'the host\'s word on an element of its own machinery that stands among the document\'s boxes (a hidden `<iframe>`, a probe); the intake passes over it, so it gets no address and moves none (ruled 2026-11-08)' },
|
|
39
42
|
{ name: 'artifact', kind: 'flag', status: 'in use', writer: 'engine', from: 'cut', facing: 'host', meaning: 'a copy the engine repeats that is not the one a reader reads: a running element\'s copy in a margin box, a fixed box\'s copy on every page after the first, and a table\'s repeated header or footer group; PDF\'s own word for pagination furniture, and nothing else. Every artifact is also aria-hidden' }
|
|
40
43
|
].map((e) => Object.freeze(e)));
|
|
41
44
|
/** The flags in use. */
|
|
@@ -46,6 +49,8 @@ export const KEYS = new Set(REGISTRY.filter((e) => e.kind === 'value').map((e) =
|
|
|
46
49
|
export const TRANSFORM = 'transform';
|
|
47
50
|
export const CICERO_PROVISION = 'cicero-provision';
|
|
48
51
|
export const CICERO_INTERFACE = 'cicero-interface';
|
|
52
|
+
/** The word a host puts on an element of its own machinery that stands among the document's boxes, so the intake passes over it (`topLevelBoxes`, `driver.js`). A host that holds the document in an element of its own names that element with the `root` option first. */
|
|
53
|
+
export const HOST = 'host';
|
|
49
54
|
/** The words step 2 writes, named once: the engine, the editing tools, Caster and the essay spell them through these and never as a literal of their own. */
|
|
50
55
|
export const FRAGMENT = 'fragment';
|
|
51
56
|
export const SPLIT = 'split';
|
|
@@ -307,3 +312,17 @@ export const WORDS_RULES = `
|
|
|
307
312
|
${w(ADJOIN)} { margin-top: 0 !important; }
|
|
308
313
|
}
|
|
309
314
|
`;
|
|
315
|
+
/**
|
|
316
|
+
* The id a URL fragment names, percent-decoded; the raw text when it is not valid percent-encoding (`#%E0%A4%A`), where `decodeURIComponent` throws.
|
|
317
|
+
*
|
|
318
|
+
* @param hash - A `location.hash`, with or without its `#`.
|
|
319
|
+
*/
|
|
320
|
+
export function decodeFragment(hash) {
|
|
321
|
+
const raw = hash.startsWith('#') ? hash.slice(1) : hash;
|
|
322
|
+
try {
|
|
323
|
+
return decodeURIComponent(raw);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return raw;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -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.**
|
|
16
|
+
* **What is not read, each a real gap and each showing as a counter reading low.** css-lists-3's `reversed()` counters (an author's `counter-reset: reversed(name)`), a top-level `position: fixed` box, which is not among the flow blocks the position index is built over; and a scope that ended before the position asked about with no operation logged between its end and the position, since the replay closes a scope only when an operation at or above its depth arrives: a `counter(list-item)` read on a page past a list whose last item holds a list reads the inner list's count (3 where Chromium prints 2, measured 2026-11-07), and an author's nested reset is read the same way. `display: none` elements are skipped, which is CSS 2.1's own rule rather than an omission. A list's and a list item's implicit `list-item` operations are read since 2026-11-07 (`implicitListOps`), so a `counter(list-item)` read on a page a list continued onto or past it counts the items before it. A `::before`'s and an `::after`'s counters are read since 2026-11-07, where a sheet sets one (`pseudoStyleOf`). **And `content(before)`, `content(after)` and `content(first-letter)` are read as `content()`** — a stated limit rather than a discovered one: a pseudo-element's own string is generated content this walk does not see, and answering the element's text is the nearest honest answer there is.
|
|
17
17
|
*
|
|
18
18
|
* @packageDocumentation
|
|
19
19
|
*/
|
|
@@ -48,6 +48,8 @@ export type Op = {
|
|
|
48
48
|
name: string;
|
|
49
49
|
terms: Term[];
|
|
50
50
|
text: string;
|
|
51
|
+
before?: string;
|
|
52
|
+
after?: string;
|
|
51
53
|
attrs: Record<string, string>;
|
|
52
54
|
}[] | null;
|
|
53
55
|
/** The flat word position the operation sits at. */
|
|
@@ -77,6 +79,8 @@ export declare function parseStringSet(value: unknown): {
|
|
|
77
79
|
}[];
|
|
78
80
|
/** The whitespace collapsing GCPM asks for: `content()` is *"the string value of the element, determined as if `white-space: normal` had been set"*. */
|
|
79
81
|
export declare const collapse: (text: unknown) => string;
|
|
82
|
+
/** An element's text without the elements a printer made from its generated content ({@link GENERATED_ATTRIBUTE}): css-gcpm-3's `content()`, the element's text and not its pseudo-elements'. An element holding none answers its `textContent`, as before. */
|
|
83
|
+
export declare function contentTextOf(el: any): string;
|
|
80
84
|
/** 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. */
|
|
81
85
|
export declare function blockOpsOf(block: any, index: {
|
|
82
86
|
wordsBefore: (el: any) => number;
|
|
@@ -95,7 +99,7 @@ export declare function blockOpsOf(block: any, index: {
|
|
|
95
99
|
* @param index - the position index (`positions.js`), which holds every element inside every block.
|
|
96
100
|
* @param opts.styleOf - the computed style of an element — injected, so a plain object tree exercises this without a browser.
|
|
97
101
|
* @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`.
|
|
98
|
-
* @param opts.textOf - an element's text; absent, its `textContent
|
|
102
|
+
* @param opts.textOf - an element's text; absent, {@link contentTextOf}, its `textContent` without what a printer made from its generated content.
|
|
99
103
|
* @param opts.attrOf - an element's attribute; absent, its `getAttribute`.
|
|
100
104
|
* @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.
|
|
101
105
|
* @param opts.marks - elements whose counters are asked about after their own operations (`counterStatesAfter`): each is logged even where it does nothing, carrying itself as `mark`, so the replay reaches it in document order (*a pointer is a read*, 2026-10-03).
|
|
@@ -13,13 +13,14 @@
|
|
|
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.**
|
|
16
|
+
* **What is not read, each a real gap and each showing as a counter reading low.** css-lists-3's `reversed()` counters (an author's `counter-reset: reversed(name)`), a top-level `position: fixed` box, which is not among the flow blocks the position index is built over; and a scope that ended before the position asked about with no operation logged between its end and the position, since the replay closes a scope only when an operation at or above its depth arrives: a `counter(list-item)` read on a page past a list whose last item holds a list reads the inner list's count (3 where Chromium prints 2, measured 2026-11-07), and an author's nested reset is read the same way. `display: none` elements are skipped, which is CSS 2.1's own rule rather than an omission. A list's and a list item's implicit `list-item` operations are read since 2026-11-07 (`implicitListOps`), so a `counter(list-item)` read on a page a list continued onto or past it counts the items before it. A `::before`'s and an `::after`'s counters are read since 2026-11-07, where a sheet sets one (`pseudoStyleOf`). **And `content(before)`, `content(after)` and `content(first-letter)` are read as `content()`** — a stated limit rather than a discovered one: a pseudo-element's own string is generated content this walk does not see, and answering the element's text is the nearest honest answer there is.
|
|
17
17
|
*
|
|
18
18
|
* @packageDocumentation
|
|
19
19
|
*/
|
|
20
20
|
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
|
+
import { GENERATED_ATTRIBUTE } from './data-beltype.js';
|
|
23
24
|
/** `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
25
|
export function pairsOf(value, fallback) {
|
|
25
26
|
const out = [];
|
|
@@ -180,6 +181,24 @@ function splitArgs(raw) {
|
|
|
180
181
|
const unquote = (s) => (/^(["']).*\1$/.test(s) ? s.slice(1, -1) : s);
|
|
181
182
|
/** The whitespace collapsing GCPM asks for: `content()` is *"the string value of the element, determined as if `white-space: normal` had been set"*. */
|
|
182
183
|
export const collapse = (text) => String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
184
|
+
/** An element's text without the elements a printer made from its generated content ({@link GENERATED_ATTRIBUTE}): css-gcpm-3's `content()`, the element's text and not its pseudo-elements'. An element holding none answers its `textContent`, as before. */
|
|
185
|
+
export function contentTextOf(el) {
|
|
186
|
+
if (!el?.querySelector?.(`[${GENERATED_ATTRIBUTE}]`))
|
|
187
|
+
return el?.textContent ?? '';
|
|
188
|
+
let out = '';
|
|
189
|
+
for (const node of el.childNodes) {
|
|
190
|
+
if (node.nodeType === 3 || node.nodeType === 4)
|
|
191
|
+
out += node.data;
|
|
192
|
+
else if (node.nodeType === 1 && !node.hasAttribute(GENERATED_ATTRIBUTE))
|
|
193
|
+
out += contentTextOf(node);
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
/** The text of an element's own `::before` or `::after` where a printer made it real: its children carrying {@link GENERATED_ATTRIBUTE} with that value, or `null` where it has none. */
|
|
198
|
+
function generatedTextOf(el, which) {
|
|
199
|
+
const made = [...(el?.children ?? [])].filter((child) => child.getAttribute?.(GENERATED_ATTRIBUTE) === which);
|
|
200
|
+
return made.length ? made.map((child) => child.textContent).join('') : null;
|
|
201
|
+
}
|
|
183
202
|
/** One element's entry in the log, or `null` where it does nothing and is not marked: its three counter declarations, its rewritten `string-set` with the text and attributes its terms name, and the mark, read off the one `styleOf` answer. */
|
|
184
203
|
function opAt(el, pos, depth, { styleOf, textOf, attrOf, marks }) {
|
|
185
204
|
const style = styleOf(el) ?? {};
|
|
@@ -191,6 +210,10 @@ function opAt(el, pos, depth, { styleOf, textOf, attrOf, marks }) {
|
|
|
191
210
|
name,
|
|
192
211
|
terms,
|
|
193
212
|
text: collapse(textOf(el)),
|
|
213
|
+
...Object.fromEntries(['before', 'after'].flatMap((which) => {
|
|
214
|
+
const made = terms.some((t) => t.kind === 'content' && t.which === which) ? generatedTextOf(el, which) : null;
|
|
215
|
+
return made === null ? [] : [[which, collapse(made)]];
|
|
216
|
+
})),
|
|
194
217
|
attrs: Object.fromEntries(terms.filter((t) => t.kind === 'attr').map((t) => [t.name, attrOf(el, t.name) ?? '']))
|
|
195
218
|
}))
|
|
196
219
|
: null;
|
|
@@ -205,7 +228,7 @@ function pseudoOpOf(el, pseudo, styleOf) {
|
|
|
205
228
|
/** 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. */
|
|
206
229
|
export function blockOpsOf(block, index, reads) {
|
|
207
230
|
const ops = [];
|
|
208
|
-
const walk = (el, depth) => {
|
|
231
|
+
const walk = (el, depth, list) => {
|
|
209
232
|
const style = reads.styleOf(el) ?? {};
|
|
210
233
|
if (style.display === 'none')
|
|
211
234
|
return; // CSS 2.1 §12.4.3: an element with `display: none` does not increment or reset counters
|
|
@@ -213,7 +236,18 @@ export function blockOpsOf(block, index, reads) {
|
|
|
213
236
|
if (style.position === 'fixed' || String(customOf(style, RUNNING_PROP)).trim())
|
|
214
237
|
return;
|
|
215
238
|
const pos = index.wordsBefore(el);
|
|
216
|
-
const
|
|
239
|
+
const implicit = implicitListOps(el, style, list, reads);
|
|
240
|
+
let op = opAt(el, pos, depth, reads);
|
|
241
|
+
if (implicit.reset || implicit.increment || implicit.set) {
|
|
242
|
+
op ??= { reset: [], increment: [], set: [], strings: null, pos, depth };
|
|
243
|
+
const names = (pairs) => pairs.some(([name]) => name === 'list-item');
|
|
244
|
+
if (implicit.reset && !names(op.reset))
|
|
245
|
+
op.reset = [...op.reset, implicit.reset];
|
|
246
|
+
if (implicit.increment && !names(op.increment))
|
|
247
|
+
op.increment = [...op.increment, implicit.increment];
|
|
248
|
+
if (implicit.set && !names(op.set))
|
|
249
|
+
op.set = [...op.set, implicit.set];
|
|
250
|
+
}
|
|
217
251
|
// 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
252
|
const before = pseudoOpOf(el, '::before', reads.pseudoStyleOf);
|
|
219
253
|
if (before) {
|
|
@@ -224,14 +258,56 @@ export function blockOpsOf(block, index, reads) {
|
|
|
224
258
|
else if (op)
|
|
225
259
|
ops.push(op);
|
|
226
260
|
for (const child of el.children ?? [])
|
|
227
|
-
walk(child, depth + 1);
|
|
261
|
+
walk(child, depth + 1, implicit.inner);
|
|
228
262
|
const after = index.wordCountIn ? pseudoOpOf(el, '::after', reads.pseudoStyleOf) : null;
|
|
229
263
|
if (after)
|
|
230
264
|
ops.push({ ...after, strings: null, pos: pos + index.wordCountIn(el), depth: depth + 1 });
|
|
231
265
|
};
|
|
232
|
-
walk(block, 0);
|
|
266
|
+
walk(block, 0, OUTSIDE_LISTS);
|
|
233
267
|
return ops;
|
|
234
268
|
}
|
|
269
|
+
const OUTSIDE_LISTS = { step: 1, ol: false };
|
|
270
|
+
/** The list items a list owns, as HTML's list owner has them: the elements displayed as `list-item` below it, nested lists' own left out. */
|
|
271
|
+
function ownedItemCount(list, styleOf) {
|
|
272
|
+
let n = 0;
|
|
273
|
+
for (const c of list.children ?? []) {
|
|
274
|
+
if (/^(?:OL|UL|MENU)$/i.test(c.tagName ?? ''))
|
|
275
|
+
continue;
|
|
276
|
+
const display = String(styleOf(c)?.display ?? '');
|
|
277
|
+
if (display === 'none')
|
|
278
|
+
continue;
|
|
279
|
+
if (display.includes('list-item'))
|
|
280
|
+
n++;
|
|
281
|
+
n += ownedItemCount(c, styleOf);
|
|
282
|
+
}
|
|
283
|
+
return n;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* The `list-item` operations an element has with no declaration of them: css-lists-3 §4.3 and HTML's list rendering.
|
|
287
|
+
*
|
|
288
|
+
* @remarks
|
|
289
|
+
* A list (`ol`, `ul`, `menu`) resets `list-item`: an `<ol>` to one step before its `start`, which under `reversed` defaults to the number of items it owns, and a list of another kind to 0. Each element displayed as `list-item` adds its list's step, 1 or −1 under `reversed`, and an `<li>` an `<ol>` owns is set to its `value`. Each is the element's only where its own `counter-reset`, `counter-increment` or `counter-set` does not name `list-item`, which the caller tests. The walk read none of it before 2026-11-07, so a `counter(list-item)` read on a page past a list, in a margin box or in the list's scope after it, read 0.
|
|
290
|
+
*
|
|
291
|
+
* Measured 2026-11-07, Chromium's own `counter(list-item)` differs from this in two places, both left as the spec has them because the marker Cicero numbers by (`fragments.js`' `listOrdinals`) is HTML's and agrees with it: under `reversed` without a `start` Chromium counts down from 0 (0, −1, −2 for HTML's 3, 2, 1), and it ignores `value` (an item after `value="20"` reads 3 where its marker prints 21).
|
|
292
|
+
*/
|
|
293
|
+
function implicitListOps(el, style, list, reads) {
|
|
294
|
+
const tag = String(el.tagName ?? '').toUpperCase();
|
|
295
|
+
const item = String(style.display ?? '').includes('list-item');
|
|
296
|
+
const value = item && list.ol && tag === 'LI' ? parseInt(reads.attrOf(el, 'value') ?? '', 10) : NaN;
|
|
297
|
+
const out = { reset: null, increment: item ? ['list-item', list.step] : null, set: Number.isFinite(value) ? ['list-item', value] : null, inner: list };
|
|
298
|
+
if (tag === 'OL') {
|
|
299
|
+
const reversed = reads.attrOf(el, 'reversed') != null;
|
|
300
|
+
const start = parseInt(reads.attrOf(el, 'start') ?? '', 10);
|
|
301
|
+
const step = reversed ? -1 : 1;
|
|
302
|
+
out.reset = ['list-item', (Number.isFinite(start) ? start : reversed ? ownedItemCount(el, reads.styleOf) : 1) - step];
|
|
303
|
+
out.inner = { step, ol: true };
|
|
304
|
+
}
|
|
305
|
+
else if (tag === 'UL' || tag === 'MENU') {
|
|
306
|
+
out.reset = ['list-item', 0];
|
|
307
|
+
out.inner = OUTSIDE_LISTS;
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
235
311
|
/**
|
|
236
312
|
* The counter operations of a document, in document order, each with the flat word position it sits at and its depth in the tree.
|
|
237
313
|
*
|
|
@@ -239,14 +315,14 @@ export function blockOpsOf(block, index, reads) {
|
|
|
239
315
|
* @param index - the position index (`positions.js`), which holds every element inside every block.
|
|
240
316
|
* @param opts.styleOf - the computed style of an element — injected, so a plain object tree exercises this without a browser.
|
|
241
317
|
* @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`.
|
|
242
|
-
* @param opts.textOf - an element's text; absent, its `textContent
|
|
318
|
+
* @param opts.textOf - an element's text; absent, {@link contentTextOf}, its `textContent` without what a printer made from its generated content.
|
|
243
319
|
* @param opts.attrOf - an element's attribute; absent, its `getAttribute`.
|
|
244
320
|
* @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.
|
|
245
321
|
* @param opts.marks - elements whose counters are asked about after their own operations (`counterStatesAfter`): each is logged even where it does nothing, carrying itself as `mark`, so the replay reaches it in document order (*a pointer is a read*, 2026-10-03).
|
|
246
322
|
* @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.
|
|
247
323
|
* @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.
|
|
248
324
|
*/
|
|
249
|
-
export function documentOpsOf(blocks, index, { styleOf, pseudoStyleOf = null, textOf =
|
|
325
|
+
export function documentOpsOf(blocks, index, { styleOf, pseudoStyleOf = null, textOf = contentTextOf, attrOf = (el, name) => el.getAttribute(name), ancestors = [], marks = null, running = [], keep = null }) {
|
|
250
326
|
const reads = { styleOf, textOf, attrOf, marks, pseudoStyleOf };
|
|
251
327
|
const ops = [];
|
|
252
328
|
ancestors.forEach((el, i) => {
|
|
@@ -423,11 +499,11 @@ function replay(ops, { until = Infinity, onOp = null, away = [] } = {}) {
|
|
|
423
499
|
return snapshot();
|
|
424
500
|
}
|
|
425
501
|
/** One `<content-list>` term against the element's own facts and the counter state in force at it. */
|
|
426
|
-
function evaluateTerm(term, { text, attrs, counters }) {
|
|
502
|
+
function evaluateTerm(term, { text, before, after, attrs, counters }) {
|
|
427
503
|
switch (term.kind) {
|
|
428
504
|
case 'string': return term.value;
|
|
429
|
-
// `before
|
|
430
|
-
case 'content': return text;
|
|
505
|
+
// `before` and `after` read the pseudo a printer made real, where it did; otherwise they and `first-letter` are read as `text`: a native pseudo-element's own string is generated content this walk does not see, and the module header states the limit rather than pretending to four readings.
|
|
506
|
+
case 'content': return (term.which === 'before' ? before : term.which === 'after' ? after : undefined) ?? text;
|
|
431
507
|
case 'attr': return attrs?.[term.name] ?? '';
|
|
432
508
|
case 'counter': return formatCounter(counterValue(counters, term.name), term.style);
|
|
433
509
|
case 'counters': return counterValues(counters, term.name).map((v) => formatCounter(v, term.style)).join(term.separator);
|
|
@@ -444,7 +520,7 @@ export function stringAssignmentsOf(ops) {
|
|
|
444
520
|
replay(ops, {
|
|
445
521
|
onOp: (op, counters) => {
|
|
446
522
|
for (const s of op.strings ?? [])
|
|
447
|
-
out.push({ name: s.name, pos: op.pos, value: s.terms.map((t) => evaluateTerm(t, { text: s.text, attrs: s.attrs, counters })).join('') });
|
|
523
|
+
out.push({ name: s.name, pos: op.pos, value: s.terms.map((t) => evaluateTerm(t, { text: s.text, before: s.before, after: s.after, attrs: s.attrs, counters })).join('') });
|
|
448
524
|
}
|
|
449
525
|
});
|
|
450
526
|
return out;
|
package/dist/src/driver.d.ts
CHANGED
|
@@ -253,6 +253,12 @@ export declare function forcingIndexOf(blocks: Element[], { styleOf, pageNameOf,
|
|
|
253
253
|
};
|
|
254
254
|
} | null;
|
|
255
255
|
};
|
|
256
|
+
/**
|
|
257
|
+
* The element children of `root` (the body, unless a host names another) minus what is not the document's: engine scaffolding (an `id` starting `__beltype`), the tags that never render ({@link NEVER_RENDERED}), and any element carrying the host word ({@link HOST}), a host's own machinery standing among the boxes (ruled 2026-11-08). Every top-level box the document declares, in or out of the flow. The two functions below divide it. **This is the one filter**: the client's intake (`seam/client.js`) calls it too, so a box this passes over has no address anywhere. A host that holds the document in an element of its own still names it with `root` first.
|
|
258
|
+
*
|
|
259
|
+
* @param root - the element the boxes stand in.
|
|
260
|
+
*/
|
|
261
|
+
export declare function topLevelBoxes(doc: Document, root?: Element): Element[];
|
|
256
262
|
/**
|
|
257
263
|
* The document's top-level blocks — the flow, which is what the cuts divide. One definition, shared by the build loop and the differential's judge measure.
|
|
258
264
|
*
|