@beltype/cicero 0.1.8 → 0.1.10
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 +55 -19
- package/dist/seam/facsimile.d.ts +13 -5
- package/dist/seam/facsimile.js +28 -12
- 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 +114 -25
- package/dist/src/chase.js +3 -1
- package/dist/src/compose-dom.js +3 -2
- package/dist/src/compositor.d.ts +11 -2
- package/dist/src/compositor.js +47 -15
- package/dist/src/data-beltype.d.ts +5 -1
- package/dist/src/data-beltype.js +6 -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 +4 -4
- package/dist/src/fragmentation.js +4 -4
- package/dist/src/fragments.d.ts +6 -0
- package/dist/src/fragments.js +90 -11
- package/dist/src/gauge.d.ts +0 -4
- package/dist/src/gauge.js +16 -30
- 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.js +3 -3
- package/dist/src/root.d.ts +18 -1
- package/dist/src/root.js +27 -1
- 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/README.md
CHANGED
|
@@ -14,17 +14,18 @@ Cicero is a paged-media engine for the live browser. It cuts a document into pag
|
|
|
14
14
|
| `@beltype/cicero/rewrite` | `rewriteDocumentSheets` alone, for a page: the host face also carries `ciceroEngine`, which imports node's `url` and so cannot load in a browser. |
|
|
15
15
|
| `@beltype/cicero/client` | The page side: loading it sets `window.__beltypeCicero`, typed in `seam/handle.d.ts`. |
|
|
16
16
|
| `@beltype/cicero/facsimile` | The embedded PDF page (`<object type="application/pdf">`). |
|
|
17
|
-
| `@beltype/cicero/fragmentation` | The members a view adapter builds on, re-exported unchanged. |
|
|
17
|
+
| `@beltype/cicero/fragmentation` | The members a view adapter builds on, re-exported unchanged, listed in `SPEC.md` XIII.3: among them the leading-decoration door (`WRAPPED`, `clonesDecoration`, `truncateLeadingDecoration`) and `firstBelowTheEdge`. |
|
|
18
|
+
| `@beltype/cicero/rim` | The rim, `seam/rim.js` itself (`SPEC.md` XIII.4): `watchLateTruth`, `bringsNewFace`, `knowLoadedFaces`, for a view that hears late truth on pages it holds, sharing the client's record of the faces already announced. |
|
|
18
19
|
| `@beltype/cicero/page` | The `<beltype-page>` element: loading it defines the element once. Needs a DOM, so it is not in `./fragmentation`, which loads in node. |
|
|
19
|
-
| `@beltype/cicero/internal/*` | Every module of `src
|
|
20
|
+
| `@beltype/cicero/internal/*` | Every module of `src/` (not `seam/`), for BelType's essay venue. **Unstable**: no promise across versions. |
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
Published to npm, with provenance:
|
|
22
23
|
|
|
23
24
|
```json
|
|
24
|
-
"@beltype/cicero": "
|
|
25
|
+
"@beltype/cicero": "0.1.9"
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
A git dependency on a tag still works, since `prepare` builds `dist/`.
|
|
28
29
|
|
|
29
30
|
## Working on it
|
|
30
31
|
|
|
@@ -35,6 +36,6 @@ pnpm test # node:test, the .ts tests run directly
|
|
|
35
36
|
pnpm run build # dist/
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
The battery
|
|
39
|
+
The battery is the essay, its own repository, `codeberg.org/BelType/essay`: `pnpm run prove` and `node src/cli.ts reftest` there, with this checkout linked in. The suites run against `dist/`, so `pnpm run build` first.
|
|
39
40
|
|
|
40
41
|
MIT.
|
package/dist/seam/client.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* **`run()` may be called again on the frame it presented** (*the run resumes from an entry*, build one, 2026-10-08). The first call takes the intake — the blocks, fixed boxes and running elements, and the geometry table — and keeps it in this module; a later call hands the run the lists it took instead of re-reading `document.body.children`, which by then holds the root and nothing else, and measures the sources where they stand, parked or slotted, with the dressing sheet out of the cascade. The Ledger it returns is byte-equal to the first call's in both modes within one realm and one viewport. Without `resumeAt` the call is a whole run, measured and presented from page one (build two adds the other: see `run`); the editing tools patch their frame and call it with `resumeAt` since build four (`patch`, `box`, `addressOf` below).
|
|
9
9
|
*
|
|
10
|
-
* **The rim is wired here** (`rim.js`, *late truth is an edit*, 2026-10-13; *a box announces its own size*, 2026-10-21): it is handed the top-level boxes, the root's `isPresented`, the table of heights the last run measured (`previous`, beside the warm start's values) and a way to know the runs have ended, and its signals come back to {@link answerLateTruth}. A box's size is answered as an element is, one resume at the box, or, where it was heard through a piece a page draws it with, one resume at the address the earliest such page begins at (the root's `startOf`, *a fragment announces its own size*, 2026-10-22), three per box until the next whole run whichever of its pieces spoke; a piece's expected height is its first reading, so the root's `onPieces` hands the rim each piece from the frame it is built in; a call of `run()` counts as under way from the moment it is made, so a host that patches a box and runs in the same task is judged after its run and never answered twice. A size a stylesheet caused is a changed condition, and its resume is refused and runs whole, as every resume under moved conditions does; a size a diagram's own `<style>` caused is not, since a sheet inside an `<svg>` is the diagram's and not the document's (*the conditions are the sheets of the document*, 2026-10-22).
|
|
10
|
+
* **The rim is wired here** (`rim.js`, *late truth is an edit*, 2026-10-13; *a box announces its own size*, 2026-10-21): it is handed the top-level boxes, the root's `isPresented`, the table of heights the last run measured (`previous`, beside the warm start's values), whether some page is not built yet, and a way to know the runs have ended, and its signals come back to {@link answerLateTruth}. A box's size is answered as an element is, one resume at the box, or, where it was heard through a piece a page draws it with, one resume at the address the earliest such page begins at (the root's `startOf`, *a fragment announces its own size*, 2026-10-22), three per box until the next whole run whichever of its pieces spoke; a piece's expected height is its first reading, so the root's `onPieces` hands the rim each piece from the frame it is built in; a call of `run()` counts as under way from the moment it is made, so a host that patches a box and runs in the same task is judged after its run and never answered twice. A size a stylesheet caused is a changed condition, and its resume is refused and runs whole, as every resume under moved conditions does; a size a diagram's own `<style>` caused is not, since a sheet inside an `<svg>` is the diagram's and not the document's (*the conditions are the sheets of the document*, 2026-10-22).
|
|
11
11
|
*
|
|
12
12
|
* The first call is made by the shell's bootstrap. `run()` is called by the shell's bootstrap after the compiler's own client stages (mermaid pre-render), because a diagram that has not drawn yet has the wrong height and every cut downstream of it would be adjudicated against a lie.
|
|
13
13
|
*
|
|
@@ -24,11 +24,17 @@ import type { BeltypeRoot } from '../src/root.ts';
|
|
|
24
24
|
/**
|
|
25
25
|
* Replace the box an edit fell in, in the document this module presented, with the one the host compiled (*the run resumes from an entry*, build four, 2026-10-08).
|
|
26
26
|
*
|
|
27
|
-
* `index` is the top-level box's place in the document's own order (`presented.boxes`) and `path` the element-child indices from it to the box that changed — empty where the top-level box itself did. The answer is the new box's address, the one `run({ resumeAt })` takes, or `null` when the seam will not promise the replacement is sound and the host reloads instead. It refuses when: nothing was presented; the index is not a flow block (a fixed box or a running element is not edited this way); the path does not lead to an element of the same name; a top-level box's own attributes differ (a class that makes a block a running element is the one change the host cannot see); the
|
|
27
|
+
* `index` is the top-level box's place in the document's own order (`presented.boxes`) and `path` the element-child indices from it to the box that changed — empty where the top-level box itself did. The answer is the new box's address, the one `run({ resumeAt })` takes, or `null` when the seam will not promise the replacement is sound and the host reloads instead. It refuses when: nothing was presented; the index is not a flow block (a fixed box or a running element is not edited this way); the path does not lead to an element of the same name; a top-level box's own attributes differ (a class that makes a block a running element is the one change the host cannot see); or the replacement is, or holds, a fixed box or a running element (a change to the intake's lists, which is a run's work, so the old box is put back and the host reloads). A block that holds a fixed box or a running element is patched like any other: the intake leaves them where the author wrote them, so the block the host compiled again and the presented block are the same tree, and the path walks both alike (see CHANGELOG, Unreleased). The second test is a computed style, so it is asked once the replacement stands in the tree, parked where it is a new top-level block; a sheet's text is never the reason (*a fixed box refuses only its own block*, 2026-10-22).
|
|
28
|
+
*
|
|
29
|
+
* **A running element's edit is a chrome event** (*the run resumes from an entry*, amended 2026-11-08, Egildo's ruling of that day). A top-level running element is out of the flow and drawn in the margin boxes of the pages it runs on, which have fixed sizes, so a word changed there moves no cut, and which element a page shows is a fact about positions that did not move. The replacement takes the old element's place in the run's list, and the chrome of every standing page that shows it is drawn again (`root.js`' `redrawRunning`); nothing is cut, and the answer is `{ chrome: true }`, which `run({ resumeAt })` answers at once. A host tells it from a flow patch by its `chrome` member: an address has none. The same rules hold as for a block, plus two: a replacement whose top-level box no longer names the same running element under the sheets is put back and refused, and so is any running edit while a sheet holds a `:has()` or a container query (`sheetReach` 2), by which the element's content could restyle the flow. A running element's own counters and strings are unread (`document-state.js`), so no value the reads deliver is moved by it. A running element nested in a block is not among `presented.boxes`: an edit at it arrives as a flow patch of the block, with a path, and is refused because the replacement is a running element.
|
|
28
30
|
*
|
|
29
31
|
* A new top-level block is parked before anything reads it (`root.js`: *an unplaced original is parked, not unassigned*), because `getComputedStyle` answers nothing for a slottable no slot takes and every reading the composition and the measure take from the source would go blank. A box inside a block is already in the flat tree and needs nothing. The old box is detached; the pages that show it are pages the run is about to cut again, and the ones it does not are not showing it (a page before the restart ends before the box begins, and the clause that an equal address is believed only past the edited box keeps the pages after).
|
|
30
32
|
*/
|
|
31
|
-
declare function patchBox(index: number, path: number[], el: Element): Address | null;
|
|
33
|
+
declare function patchBox(index: number, path: number[], el: Element): Address | ChromePatch | null;
|
|
34
|
+
/** What `patch` answers for an edit of a running element, and what `run({ resumeAt })` takes back to answer at once: nothing in the flow moved, so there is no address to resume at (*the run resumes from an entry*, amended 2026-11-08). */
|
|
35
|
+
export type ChromePatch = {
|
|
36
|
+
chrome: true;
|
|
37
|
+
};
|
|
32
38
|
/** Resolves when the rim has nothing pending and no run is under way (a run stopped at a page is at rest). The door a venue test and a host that wants to know it is looking at the settled document call. */
|
|
33
39
|
declare function settled(): Promise<void>;
|
|
34
40
|
/** One run, from the call that started it to the moment it is complete or let go. */
|
|
@@ -81,13 +87,15 @@ export type Session = {
|
|
|
81
87
|
*
|
|
82
88
|
* **`resumeAt` makes the call a resumed run** (*the run resumes from an entry*, build two, 2026-10-08): the address of the box edited since the last call, as `addressOf` spells it. The call then reuses the Ledger the last one kept, restarts one page before the page holding that address, stops at the first page that ends where the prior run's page ended, keeps the prior pages after it and refills only the pages it cut again. It falls back to a whole run, silently and correctly, when the last call was in the other mode or under other conditions (`conditionsKey`), or when the document's top-level blocks are not as many as the prior Ledger was cut over — so the result's `resumed` is `null` exactly when nothing was reused. In the composed mode the composition re-composes only the paragraphs the edit touched (build three): the cache is kept across calls and keyed by the paragraph, not the element object. **An edit while the document is not complete resumes within the pages cut and, unless `until` says otherwise, stops at the horizon it had** (an edit beyond it recuts the one page before it and nothing else, since the pages after it do not exist).
|
|
83
89
|
*
|
|
90
|
+
* **`resumeAt: { chrome: true }`, what `patch` answers for a running element's edit, is answered at once** (*the run resumes from an entry*, amended 2026-11-08): the patch drew the chrome itself, so the call cuts nothing, lets no stopped run go, and answers the last call's result with `resumed` `{ from: 1, to: 0, shift: 0, converged: true, kept: pages }`, no page cut again and every page kept. In the other mode than the last call's, or before any call has answered, it is the call it would be with no `resumeAt`.
|
|
91
|
+
*
|
|
84
92
|
* **`layer` names the cascade layer the host wants the document's own sheets in** (*the author's sheets in a layer the host names*, 2026-10-17): every same-origin sheet the document carries is wrapped in it by the rewrite, so a host's own sheets, layered after it, outrank the author's whatever their specificity. Absent, the frame keeps the layer it was last run with, and a frame no call ever named one for is rewritten exactly as before. A change of name clears the rewrite's verdicts; a sheet already wrapped in the old name keeps it.
|
|
85
93
|
*
|
|
86
94
|
* @returns what the console line reports; the presented pages stay live in the DOM. `pages` is how many are cut; `complete` whether that is the document's end. `resumed` names the pages cut again, from 1: `from` through `to`, with `shift` the pages the run added (or, negative, removed) and `kept` the prior pages after `to` left standing. `why` is a plain sentence, set exactly when `resumed` is `null`: the first run on a frame, no `resumeAt`, another mode, conditions that moved (and which part), a block count that moved, or a reference whose page number sent the run round again. `composition` is the composed mode's count (`null` in the default): `misses` the paragraphs composed, `hits` the ones found already composed, `blocks` their sum, and `laid` the top-level blocks cloned and measured to read their widths (the rest were answered by the cache whole).
|
|
87
95
|
*/
|
|
88
96
|
export declare function run(opts?: {
|
|
89
97
|
composed?: boolean;
|
|
90
|
-
resumeAt?: Address | null;
|
|
98
|
+
resumeAt?: Address | ChromePatch | null;
|
|
91
99
|
until?: number | Element | null;
|
|
92
100
|
layer?: string | null;
|
|
93
101
|
}): Promise<{
|
package/dist/seam/client.js
CHANGED
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
*
|
|
9
9
|
* **`run()` may be called again on the frame it presented** (*the run resumes from an entry*, build one, 2026-10-08). The first call takes the intake — the blocks, fixed boxes and running elements, and the geometry table — and keeps it in this module; a later call hands the run the lists it took instead of re-reading `document.body.children`, which by then holds the root and nothing else, and measures the sources where they stand, parked or slotted, with the dressing sheet out of the cascade. The Ledger it returns is byte-equal to the first call's in both modes within one realm and one viewport. Without `resumeAt` the call is a whole run, measured and presented from page one (build two adds the other: see `run`); the editing tools patch their frame and call it with `resumeAt` since build four (`patch`, `box`, `addressOf` below).
|
|
10
10
|
*
|
|
11
|
-
* **The rim is wired here** (`rim.js`, *late truth is an edit*, 2026-10-13; *a box announces its own size*, 2026-10-21): it is handed the top-level boxes, the root's `isPresented`, the table of heights the last run measured (`previous`, beside the warm start's values) and a way to know the runs have ended, and its signals come back to {@link answerLateTruth}. A box's size is answered as an element is, one resume at the box, or, where it was heard through a piece a page draws it with, one resume at the address the earliest such page begins at (the root's `startOf`, *a fragment announces its own size*, 2026-10-22), three per box until the next whole run whichever of its pieces spoke; a piece's expected height is its first reading, so the root's `onPieces` hands the rim each piece from the frame it is built in; a call of `run()` counts as under way from the moment it is made, so a host that patches a box and runs in the same task is judged after its run and never answered twice. A size a stylesheet caused is a changed condition, and its resume is refused and runs whole, as every resume under moved conditions does; a size a diagram's own `<style>` caused is not, since a sheet inside an `<svg>` is the diagram's and not the document's (*the conditions are the sheets of the document*, 2026-10-22).
|
|
11
|
+
* **The rim is wired here** (`rim.js`, *late truth is an edit*, 2026-10-13; *a box announces its own size*, 2026-10-21): it is handed the top-level boxes, the root's `isPresented`, the table of heights the last run measured (`previous`, beside the warm start's values), whether some page is not built yet, and a way to know the runs have ended, and its signals come back to {@link answerLateTruth}. A box's size is answered as an element is, one resume at the box, or, where it was heard through a piece a page draws it with, one resume at the address the earliest such page begins at (the root's `startOf`, *a fragment announces its own size*, 2026-10-22), three per box until the next whole run whichever of its pieces spoke; a piece's expected height is its first reading, so the root's `onPieces` hands the rim each piece from the frame it is built in; a call of `run()` counts as under way from the moment it is made, so a host that patches a box and runs in the same task is judged after its run and never answered twice. A size a stylesheet caused is a changed condition, and its resume is refused and runs whole, as every resume under moved conditions does; a size a diagram's own `<style>` caused is not, since a sheet inside an `<svg>` is the diagram's and not the document's (*the conditions are the sheets of the document*, 2026-10-22).
|
|
12
12
|
*
|
|
13
13
|
* The first call is made by the shell's bootstrap. `run()` is called by the shell's bootstrap after the compiler's own client stages (mermaid pre-render), because a diagram that has not drawn yet has the wrong height and every cut downstream of it would be adjudicated against a lie.
|
|
14
14
|
*
|
|
15
15
|
* @packageDocumentation
|
|
16
16
|
*/
|
|
17
|
-
import { paginate, topLevelBlocks, fixedBoxes, runningBoxes, describeConditions } from '../src/driver.js';
|
|
18
|
-
import { assertRenderConditions, awaitFonts
|
|
17
|
+
import { paginate, topLevelBlocks, topLevelBoxes, fixedBoxes, runningBoxes, runningIdentOf, describeConditions } from '../src/driver.js';
|
|
18
|
+
import { assertRenderConditions, awaitFonts } from '../src/measure.js';
|
|
19
19
|
import { pathOf } from '../src/positions.js';
|
|
20
|
-
import { readGeometryTable } from '../src/capture.js';
|
|
20
|
+
import { readGeometryTable, sheetReach } from '../src/capture.js';
|
|
21
21
|
import { composeForPresentation } from '../src/compose-dom.js';
|
|
22
22
|
import { attributesOf, CompositionCache } from '../src/composition-key.js';
|
|
23
23
|
import { dress, isDressingSheet } from '../src/root.js';
|
|
@@ -30,9 +30,8 @@ import { rewriteDocumentSheets } from './rewrite-document.js';
|
|
|
30
30
|
import { isLayerName } from './rewrite.js';
|
|
31
31
|
import { watchLateTruth, frame, bringsNewFace, knowLoadedFaces, isKnownFace } from './rim.js';
|
|
32
32
|
/**
|
|
33
|
-
* What the first call took at intake, kept for a later call on the same frame — `null` before any presentation. The root is the one `<beltype-root>` this module made; the blocks, fixed boxes and running elements are its light children; the table is the geometry the author's `@page` rules gave, captured before `present()` cleared their margin boxes' `content`.
|
|
33
|
+
* What the first call took at intake, kept for a later call on the same frame — `null` before any presentation. The root is the one `<beltype-root>` this module made; the blocks, and the fixed boxes and running elements the document declared at its top level, are its light children, and a fixed box or running element written inside a block stays inside it (*a fixed box at any depth*, 2026-09-15); the table is the geometry the author's `@page` rules gave, captured before `present()` cleared their margin boxes' `content`.
|
|
34
34
|
* `boxes` is every top-level box the document declared, in the document's own order and whatever kind it became (a block, a fixed box, a running element) — the order intake destroys by appending blocks, then fixed boxes, then running elements, and the one a host that compiled the document again addresses a box by (`patch`).
|
|
35
|
-
* `lifted` is the top-level boxes a fixed box or a running element was lifted out of at intake: each holds one box fewer than the block a host compiles again, so `patch` refuses an edit inside one (*a fixed box refuses only its own block*, 2026-10-22).
|
|
36
35
|
*/
|
|
37
36
|
let presented = null;
|
|
38
37
|
/** What the last call left, for a call that resumes: its paginate result, the mode it ran in and the conditions it ran under (`conditionsKey`). `null` before any. */
|
|
@@ -119,7 +118,9 @@ const sheetLabels = (doc) => wornSheets(doc).map((sheet) => {
|
|
|
119
118
|
/**
|
|
120
119
|
* Replace the box an edit fell in, in the document this module presented, with the one the host compiled (*the run resumes from an entry*, build four, 2026-10-08).
|
|
121
120
|
*
|
|
122
|
-
* `index` is the top-level box's place in the document's own order (`presented.boxes`) and `path` the element-child indices from it to the box that changed — empty where the top-level box itself did. The answer is the new box's address, the one `run({ resumeAt })` takes, or `null` when the seam will not promise the replacement is sound and the host reloads instead. It refuses when: nothing was presented; the index is not a flow block (a fixed box or a running element is not edited this way); the path does not lead to an element of the same name; a top-level box's own attributes differ (a class that makes a block a running element is the one change the host cannot see); the
|
|
121
|
+
* `index` is the top-level box's place in the document's own order (`presented.boxes`) and `path` the element-child indices from it to the box that changed — empty where the top-level box itself did. The answer is the new box's address, the one `run({ resumeAt })` takes, or `null` when the seam will not promise the replacement is sound and the host reloads instead. It refuses when: nothing was presented; the index is not a flow block (a fixed box or a running element is not edited this way); the path does not lead to an element of the same name; a top-level box's own attributes differ (a class that makes a block a running element is the one change the host cannot see); or the replacement is, or holds, a fixed box or a running element (a change to the intake's lists, which is a run's work, so the old box is put back and the host reloads). A block that holds a fixed box or a running element is patched like any other: the intake leaves them where the author wrote them, so the block the host compiled again and the presented block are the same tree, and the path walks both alike (see CHANGELOG, Unreleased). The second test is a computed style, so it is asked once the replacement stands in the tree, parked where it is a new top-level block; a sheet's text is never the reason (*a fixed box refuses only its own block*, 2026-10-22).
|
|
122
|
+
*
|
|
123
|
+
* **A running element's edit is a chrome event** (*the run resumes from an entry*, amended 2026-11-08, Egildo's ruling of that day). A top-level running element is out of the flow and drawn in the margin boxes of the pages it runs on, which have fixed sizes, so a word changed there moves no cut, and which element a page shows is a fact about positions that did not move. The replacement takes the old element's place in the run's list, and the chrome of every standing page that shows it is drawn again (`root.js`' `redrawRunning`); nothing is cut, and the answer is `{ chrome: true }`, which `run({ resumeAt })` answers at once. A host tells it from a flow patch by its `chrome` member: an address has none. The same rules hold as for a block, plus two: a replacement whose top-level box no longer names the same running element under the sheets is put back and refused, and so is any running edit while a sheet holds a `:has()` or a container query (`sheetReach` 2), by which the element's content could restyle the flow. A running element's own counters and strings are unread (`document-state.js`), so no value the reads deliver is moved by it. A running element nested in a block is not among `presented.boxes`: an edit at it arrives as a flow patch of the block, with a path, and is refused because the replacement is a running element.
|
|
123
124
|
*
|
|
124
125
|
* A new top-level block is parked before anything reads it (`root.js`: *an unplaced original is parked, not unassigned*), because `getComputedStyle` answers nothing for a slottable no slot takes and every reading the composition and the measure take from the source would go blank. A box inside a block is already in the flat tree and needs nothing. The old box is detached; the pages that show it are pages the run is about to cut again, and the ones it does not are not showing it (a page before the restart ends before the box begins, and the clause that an equal address is believed only past the edited box keeps the pages after).
|
|
125
126
|
*/
|
|
@@ -129,7 +130,8 @@ function patchBox(index, path, el) {
|
|
|
129
130
|
const { blocks, boxes, root } = presented;
|
|
130
131
|
const top = boxes[index];
|
|
131
132
|
const b = top ? blocks.indexOf(top) : -1;
|
|
132
|
-
|
|
133
|
+
const r = top ? presented.running.findIndex((x) => x.el === top) : -1;
|
|
134
|
+
if (b < 0 && r < 0)
|
|
133
135
|
return null;
|
|
134
136
|
let old = top;
|
|
135
137
|
for (const i of path)
|
|
@@ -139,6 +141,8 @@ function patchBox(index, path, el) {
|
|
|
139
141
|
if (!old || old.localName !== el.localName || (path.length === 0 && attributesOf(old) !== attributesOf(el)))
|
|
140
142
|
return null;
|
|
141
143
|
const park = root.shadowRoot?.querySelector('slot[data-beltype-park]');
|
|
144
|
+
if (r >= 0)
|
|
145
|
+
return patchRunning(r, index, path, old, el, park);
|
|
142
146
|
const slot = path.length === 0 ? old.assignedSlot : null;
|
|
143
147
|
const held = slot?.assignedNodes() ?? [];
|
|
144
148
|
old.replaceWith(el);
|
|
@@ -160,6 +164,31 @@ function patchBox(index, path, el) {
|
|
|
160
164
|
edited.push(address);
|
|
161
165
|
return address;
|
|
162
166
|
}
|
|
167
|
+
/** {@link patchBox} for a top-level running element, at `r` in the run's list: the replacement stands in its place and every page that shows it has its chrome drawn again, nothing cut. */
|
|
168
|
+
function patchRunning(r, index, path, old, el, park) {
|
|
169
|
+
const { boxes, root, running, table } = presented;
|
|
170
|
+
if (sheetReach(document) === 2)
|
|
171
|
+
return null; // a `:has()` or a container query can let the element's content restyle the flow, which is a run's to find
|
|
172
|
+
const top = boxes[index];
|
|
173
|
+
old.replaceWith(el);
|
|
174
|
+
const parkIt = (node) => park?.assign(...park.assignedNodes(), node); // a running element is parked (`root.js`), and a node that leaves its host loses its manual assignment
|
|
175
|
+
if (path.length === 0)
|
|
176
|
+
parkIt(el);
|
|
177
|
+
if (runningIdentOf(window, path.length === 0 ? el : top) !== running[r].name) {
|
|
178
|
+
el.replaceWith(old);
|
|
179
|
+
if (path.length === 0)
|
|
180
|
+
parkIt(old);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
forcingMemo.replaced(el);
|
|
184
|
+
if (path.length === 0) {
|
|
185
|
+
boxes[index] = el;
|
|
186
|
+
running[r] = { ...running[r], el }; // in place: a run under way and the next presentation read this array
|
|
187
|
+
}
|
|
188
|
+
rim?.add(boxes[index], top);
|
|
189
|
+
root.redrawRunning(r, running, (cond) => table.pageFor(cond).marginBoxes);
|
|
190
|
+
return { chrome: true };
|
|
191
|
+
}
|
|
163
192
|
/** THE RIM, from the intake until the frame lets the run go (`rim.js`, *late truth is an edit*): the subscription to the facts that arrive after a measure. `null` before the intake. */
|
|
164
193
|
let rim = null;
|
|
165
194
|
function stopRim() { rim?.stop(); rim = null; }
|
|
@@ -293,7 +322,7 @@ async function letGo() {
|
|
|
293
322
|
await (stopped ? settled : session.finished); // a run that is not stopped is on its way to the end, and the host that started it shows it
|
|
294
323
|
}
|
|
295
324
|
/**
|
|
296
|
-
* THE INTAKE (*the pages are cut as far as they are asked*, 2026-10-12): the document's boxes become the root's light DOM, parked, before the first cut. A run that can stop at a page measures the sources while pages stand in the document, so it measures them where a resumed run always has. Nothing else in the first call changes: the lists and the geometry table are read first, with the document where it was, exactly as the run read them before.
|
|
325
|
+
* THE INTAKE (*the pages are cut as far as they are asked*, 2026-10-12): the document's boxes become the root's light DOM, parked, before the first cut. Only its top-level boxes are adopted: a fixed box or a running element written inside a block stays where the author put it, since every mechanism that takes it out of the flow (`isFixedBox`) works at any depth and every page's copy is made from the original wherever it stands, so the tree a host compiles and the tree the engine holds are one (see CHANGELOG, Unreleased). A run that can stop at a page measures the sources while pages stand in the document, so it measures them where a resumed run always has. Nothing else in the first call changes: the lists and the geometry table are read first, with the document where it was, exactly as the run read them before.
|
|
297
326
|
*
|
|
298
327
|
* Fonts are awaited here as well as in the driver, because the driver's wait asks the browser which fonts are loading, and a document whose boxes are all parked is laying nothing out: a face the text would have asked for is never asked for. With the document still laid out, a forced layout asks for them, and the wait is real.
|
|
299
328
|
*/
|
|
@@ -302,19 +331,19 @@ async function takeIntake(relaxDpr) {
|
|
|
302
331
|
void document.body.offsetHeight;
|
|
303
332
|
await document.fonts.ready;
|
|
304
333
|
await takeFacsimile(); // a presenter is a condition of every copy: registered before the first one, with every size known
|
|
305
|
-
|
|
334
|
+
// No image is awaited (ruled 2026-11-08): the driver makes the lazy ones eager, and the rim started below hears every picture's first load as well as a later one, since it listens in the capture phase on each top-level box (`rim.js`), and resumes only where the arrival changed the box.
|
|
306
335
|
const table = readGeometryTable(document);
|
|
307
336
|
await awaitFonts(document, table.furnitureFaces); // before the rim exists: a face only the margin boxes use is otherwise requested after the cuts, and its `loadingdone` would be heard as a late condition
|
|
308
337
|
const blocks = topLevelBlocks(document);
|
|
309
338
|
const fixed = fixedBoxes(document);
|
|
310
339
|
const running = runningBoxes(document);
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
const
|
|
340
|
+
const boxes = topLevelBoxes(document); // the document's top-level boxes in its own order, taken before the root exists and before the appending below destroys the order, through the driver's one filter
|
|
341
|
+
// Only the top-level boxes are adopted: a fixed box or a running element inside a block stays in it, out of the word space, the feed and the presentation by `isFixedBox` at any depth, and copied per page from where it stands (*a fixed box at any depth*), so the host's tree and the engine's are one.
|
|
342
|
+
const adopted = [...blocks, ...fixed, ...running.map((r) => r.el)].filter((el) => el.parentElement === document.body);
|
|
343
|
+
const moved = adopted.map((el) => [el, el.parentNode, el.nextSibling]);
|
|
315
344
|
const root = document.createElement('beltype-root');
|
|
316
345
|
document.body.appendChild(root);
|
|
317
|
-
root.intake(...
|
|
346
|
+
root.intake(...adopted); // *Content is one document* (I6): the document adopted as the root's light DOM. A running element is adopted and never assigned, which is §1.2.1's *does not display there* with nothing arranged for it.
|
|
318
347
|
root.cutTo = (el) => cutTo(el);
|
|
319
348
|
root.onPieces = (built, removed) => rim?.pieces(built, removed); // the pieces a built page draws its blocks through, which the rim observes from the frame they are built in (*a fragment announces its own size*)
|
|
320
349
|
presented = {
|
|
@@ -324,7 +353,6 @@ async function takeIntake(relaxDpr) {
|
|
|
324
353
|
running,
|
|
325
354
|
table,
|
|
326
355
|
boxes,
|
|
327
|
-
lifted,
|
|
328
356
|
// A run that fails before it has shown a page puts the document back where it found it, so the host's message — *showing it unpaginated instead* — is true.
|
|
329
357
|
undo() {
|
|
330
358
|
for (const [el, parent, next] of moved.toReversed())
|
|
@@ -334,7 +362,7 @@ async function takeIntake(relaxDpr) {
|
|
|
334
362
|
presented = null;
|
|
335
363
|
}
|
|
336
364
|
};
|
|
337
|
-
rim = watchLateTruth({ document, fonts, originals: boxes, onSignal: answerLateTruth, presented: (box) => root.isPresented(box), pageHolding: (piece) => root.pageHolding(piece), measured: (box) => previous?.result.heights?.get(box), idle: quiet, busy: () => !!(working || calls.size || (live && !live.gate)) });
|
|
365
|
+
rim = watchLateTruth({ document, fonts, originals: boxes, onSignal: answerLateTruth, presented: (box) => root.isPresented(box), pageHolding: (piece) => root.pageHolding(piece), measured: (box) => previous?.result.heights?.get(box), idle: quiet, busy: () => !!(working || calls.size || (live && !live.gate)), unbuilt: () => { const b = root.built(); return b.built < b.pages; } });
|
|
338
366
|
}
|
|
339
367
|
/** The selector of the elements the facsimile presents, and the module itself once the document held one. Loaded here and never earlier: a document without a PDF pays nothing, and pdf.js is not fetched. */
|
|
340
368
|
const PDF_OBJECT = 'object[type="application/pdf"]';
|
|
@@ -437,7 +465,7 @@ function seatDoors(blocks) {
|
|
|
437
465
|
// The host's two doors onto the document it compiled again (*the run resumes from an entry*, build four): which source stands at a top-level box of the new document, and the replacement of an edited box with the new document's. Verbs, because the lists they work on are the module's own.
|
|
438
466
|
h.box = (i) => {
|
|
439
467
|
const box = presented?.boxes[i];
|
|
440
|
-
return box && presented?.blocks.includes(box) ? box : null;
|
|
468
|
+
return box && (presented?.blocks.includes(box) || presented?.running.some((r) => r.el === box)) ? box : null; // a running element is a box a host may patch, as a chrome event
|
|
441
469
|
};
|
|
442
470
|
h.patch = (i, path, el) => patchBox(i, path, el);
|
|
443
471
|
}
|
|
@@ -452,6 +480,8 @@ function seatDoors(blocks) {
|
|
|
452
480
|
*
|
|
453
481
|
* **`resumeAt` makes the call a resumed run** (*the run resumes from an entry*, build two, 2026-10-08): the address of the box edited since the last call, as `addressOf` spells it. The call then reuses the Ledger the last one kept, restarts one page before the page holding that address, stops at the first page that ends where the prior run's page ended, keeps the prior pages after it and refills only the pages it cut again. It falls back to a whole run, silently and correctly, when the last call was in the other mode or under other conditions (`conditionsKey`), or when the document's top-level blocks are not as many as the prior Ledger was cut over — so the result's `resumed` is `null` exactly when nothing was reused. In the composed mode the composition re-composes only the paragraphs the edit touched (build three): the cache is kept across calls and keyed by the paragraph, not the element object. **An edit while the document is not complete resumes within the pages cut and, unless `until` says otherwise, stops at the horizon it had** (an edit beyond it recuts the one page before it and nothing else, since the pages after it do not exist).
|
|
454
482
|
*
|
|
483
|
+
* **`resumeAt: { chrome: true }`, what `patch` answers for a running element's edit, is answered at once** (*the run resumes from an entry*, amended 2026-11-08): the patch drew the chrome itself, so the call cuts nothing, lets no stopped run go, and answers the last call's result with `resumed` `{ from: 1, to: 0, shift: 0, converged: true, kept: pages }`, no page cut again and every page kept. In the other mode than the last call's, or before any call has answered, it is the call it would be with no `resumeAt`.
|
|
484
|
+
*
|
|
455
485
|
* **`layer` names the cascade layer the host wants the document's own sheets in** (*the author's sheets in a layer the host names*, 2026-10-17): every same-origin sheet the document carries is wrapped in it by the rewrite, so a host's own sheets, layered after it, outrank the author's whatever their specificity. Absent, the frame keeps the layer it was last run with, and a frame no call ever named one for is rewritten exactly as before. A change of name clears the rewrite's verdicts; a sheet already wrapped in the old name keeps it.
|
|
456
486
|
*
|
|
457
487
|
* @returns what the console line reports; the presented pages stay live in the DOM. `pages` is how many are cut; `complete` whether that is the document's end. `resumed` names the pages cut again, from 1: `from` through `to`, with `shift` the pages the run added (or, negative, removed) and `kept` the prior pages after `to` left standing. `why` is a plain sentence, set exactly when `resumed` is `null`: the first run on a frame, no `resumeAt`, another mode, conditions that moved (and which part), a block count that moved, or a reference whose page number sent the run round again. `composition` is the composed mode's count (`null` in the default): `misses` the paragraphs composed, `hits` the ones found already composed, `blocks` their sum, and `laid` the top-level blocks cloned and measured to read their widths (the rest were answered by the cache whole).
|
|
@@ -466,7 +496,7 @@ export function run(opts) {
|
|
|
466
496
|
return call;
|
|
467
497
|
}
|
|
468
498
|
/** {@link run}, once its call is counted. @param [opts] */
|
|
469
|
-
async function runOnce({ composed = false, resumeAt = null, until = null, layer = frameLayer } = {}) {
|
|
499
|
+
async function runOnce({ composed = false, resumeAt: asked = null, until = null, layer = frameLayer } = {}) {
|
|
470
500
|
const t0 = performance.now();
|
|
471
501
|
if (layer != null && !isLayerName(layer))
|
|
472
502
|
throw new TypeError(`run: "${layer}" is not a cascade layer name`); // before anything is undressed or held
|
|
@@ -474,6 +504,12 @@ async function runOnce({ composed = false, resumeAt = null, until = null, layer
|
|
|
474
504
|
judgedSheets = new WeakMap(); // every sheet is judged again, under the new name
|
|
475
505
|
frameLayer = layer;
|
|
476
506
|
}
|
|
507
|
+
// A running element's patch was drawn when it was made (*the run resumes from an entry*, amended 2026-11-08): nothing is cut, a run stopped at a page keeps its place, and the answer is the last one's with every page kept. A call in the other mode, or before any answer, is the run it would have been without the address.
|
|
508
|
+
if (asked && 'chrome' in asked && handle.last && handle.last.composed === composed) {
|
|
509
|
+
handle.last = { ...handle.last, ms: Math.round(performance.now() - t0), resumed: { from: 1, to: 0, shift: 0, converged: true, kept: handle.last.pages }, why: null };
|
|
510
|
+
return handle.last;
|
|
511
|
+
}
|
|
512
|
+
const resumeAt = asked && 'chrome' in asked ? null : asked;
|
|
477
513
|
// A run stopped at a page goes on, unless the host asked for something that is not a continuation.
|
|
478
514
|
if (live && !resumeAt && live.composed === composed)
|
|
479
515
|
return carryOn(live, until, t0);
|
package/dist/seam/facsimile.d.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* **`make` runs often** — for every copy, each page that shows one, each hollow shell, each measuring copy — and so it is cheap and synchronous and starts no work: the drawing is deferred to when the element is connected AND seen (an `IntersectionObserver`), and what it draws is cached per (url, page, width, device pixel ratio). An element in the measure container does not draw at all.
|
|
13
13
|
*
|
|
14
|
+
* **A source with no alt is refused at intake by default**; `install`'s `missingAlt: 'warn'`, for an editor that must keep drawing the document, tells it once instead and copies it with no `role` and no `aria-label`, which Caster refuses at print by name (Egildo's ruling of 2026-11-08).
|
|
15
|
+
*
|
|
14
16
|
* Loaded only when the document holds a matching element (`seam/client.js`, at intake): a document without a PDF pays nothing, and pdf.js is not fetched. pdf.js is vendored — `pdfjs-dist`, pinned exactly, its module and worker published by `vendorAssets()` at `cicero/seam/pdfjs.js` and `cicero/seam/pdfjs-worker.js` (the one entries of that table that are not the file of their own name).
|
|
15
17
|
*
|
|
16
18
|
* @packageDocumentation
|
|
@@ -30,22 +32,28 @@ export declare function locate(source: Element): {
|
|
|
30
32
|
page: number;
|
|
31
33
|
pageGiven: boolean;
|
|
32
34
|
};
|
|
35
|
+
/** What a source with no fallback alt meets: refused at intake, or told and copied with no alternative text, for the print to refuse. */
|
|
36
|
+
export type MissingAlt = 'refuse' | 'warn';
|
|
33
37
|
/**
|
|
34
38
|
* The element a copy of `source` is: an `<object is="beltype-facsimile">`, in the contract the header states, with no child yet. Cheap and synchronous, and it starts nothing.
|
|
35
39
|
*
|
|
36
40
|
* @param source - a `<object type="application/pdf">`, parked or placed; its specified lengths are readable, its used width and height are not (measured: a parked source answers `50%` for a percentage width).
|
|
37
|
-
* @
|
|
41
|
+
* @param missingAlt - under `'warn'`, a source with no fallback `alt` is copied with no `role` and no `aria-label`.
|
|
42
|
+
* @throws {Error} when the source has no fallback `alt`, under `'refuse'`.
|
|
38
43
|
* @remarks The element has no child until {@link draw} gives it its canvas (*a facsimile's copy holds its box off screen*, CHANGELOG 2026-10-28). An `<object>` with a child and no `data` decides at its first layout to render the child, which is a reattach of its layout tree, and on a page the browser skips (`content-visibility: auto`, *a page is contained*) a read from script lays the subtree out for the length of the read, drops the reattach when it puts the subtree back, and leaves the copy with no box at all. Measured 2026-10-28: the same copy with no child keeps its box on every read.
|
|
39
44
|
*/
|
|
40
|
-
export declare function make(source: Element): HTMLObjectElement;
|
|
45
|
+
export declare function make(source: Element, missingAlt?: MissingAlt): HTMLObjectElement;
|
|
41
46
|
/** Let go of the observer, which the next element to connect makes again: the element's class is the window's and outlives the client that defined it. */
|
|
42
47
|
export declare function dispose(): void;
|
|
43
48
|
/**
|
|
44
49
|
* Define the element, read the size of every page the document embeds and register the presenter — before the first run (the intake calls this first, and awaits it), because a presenter is a condition of every copy and a registration after a run is a whole run.
|
|
45
50
|
*
|
|
46
|
-
* Every source is checked for its alt text here, so a document that would print a figure with none is refused at the door and not after the cuts. A PDF that cannot be read is TOLD and not refused: the box has no ratio from the cache, nothing is drawn in it, and its size, if the file turns up later, arrives through the rim; a missing page number is the same — except that it is the author's mistake, and the message says so.
|
|
51
|
+
* Every source is checked for its alt text here, so under the default a document that would print a figure with none is refused at the door and not after the cuts. A PDF that cannot be read is TOLD and not refused: the box has no ratio from the cache, nothing is drawn in it, and its size, if the file turns up later, arrives through the rim; a missing page number is the same — except that it is the author's mistake, and the message says so.
|
|
47
52
|
*
|
|
48
53
|
* @param present - `window.__beltypeCicero.present`, handed in so this module imports nothing from the engine.
|
|
49
|
-
* @
|
|
54
|
+
* @param options - `missingAlt: 'warn'` tells each source with no fallback `alt` once, at intake, and copies it with no alternative text, so the document is drawn and the print refuses it.
|
|
55
|
+
* @throws {Error} when a source has no fallback `alt`, under the default `missingAlt: 'refuse'`.
|
|
50
56
|
*/
|
|
51
|
-
export declare function install(doc: Document, present: (selector: string, make: (source: Element) => Element) => void
|
|
57
|
+
export declare function install(doc: Document, present: (selector: string, make: (source: Element) => Element) => void, options?: {
|
|
58
|
+
missingAlt?: MissingAlt;
|
|
59
|
+
}): Promise<void>;
|
package/dist/seam/facsimile.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* **`make` runs often** — for every copy, each page that shows one, each hollow shell, each measuring copy — and so it is cheap and synchronous and starts no work: the drawing is deferred to when the element is connected AND seen (an `IntersectionObserver`), and what it draws is cached per (url, page, width, device pixel ratio). An element in the measure container does not draw at all.
|
|
13
13
|
*
|
|
14
|
+
* **A source with no alt is refused at intake by default**; `install`'s `missingAlt: 'warn'`, for an editor that must keep drawing the document, tells it once instead and copies it with no `role` and no `aria-label`, which Caster refuses at print by name (Egildo's ruling of 2026-11-08).
|
|
15
|
+
*
|
|
14
16
|
* Loaded only when the document holds a matching element (`seam/client.js`, at intake): a document without a PDF pays nothing, and pdf.js is not fetched. pdf.js is vendored — `pdfjs-dist`, pinned exactly, its module and worker published by `vendorAssets()` at `cicero/seam/pdfjs.js` and `cicero/seam/pdfjs-worker.js` (the one entries of that table that are not the file of their own name).
|
|
15
17
|
*
|
|
16
18
|
* @packageDocumentation
|
|
@@ -73,12 +75,15 @@ export function locate(source) {
|
|
|
73
75
|
return { url: href.href, page: pageGiven ? wanted : 1, pageGiven };
|
|
74
76
|
}
|
|
75
77
|
/**
|
|
76
|
-
* The alternative text a source carries — its fallback image's `alt`. A PDF page with none is a figure Caster would refuse (PDF/UA wants one), so the refusal is made here, at intake, by name.
|
|
78
|
+
* The alternative text a source carries — its fallback image's `alt`. A PDF page with none is a figure Caster would refuse (PDF/UA wants one), so under `'refuse'` the refusal is made here, at intake, by name.
|
|
77
79
|
*
|
|
78
|
-
* @
|
|
80
|
+
* @returns `null` under `'warn'` when there is none; the caller decides what to tell.
|
|
81
|
+
* @throws {Error} naming the element, under `'refuse'`, when there is no fallback `<img>` with a non-blank `alt`.
|
|
79
82
|
*/
|
|
80
|
-
function altOf(source) {
|
|
83
|
+
function altOf(source, missingAlt = 'refuse') {
|
|
81
84
|
const alt = source.querySelector('img')?.getAttribute('alt')?.trim();
|
|
85
|
+
if (!alt && missingAlt === 'warn')
|
|
86
|
+
return null;
|
|
82
87
|
if (!alt)
|
|
83
88
|
throw new Error(`BelType/Cicero: <object type="application/pdf" data="${source.getAttribute('data')}"> has no fallback <img alt="…"> — the page it embeds would be a figure with no alternative text, and the print refuses that. Write the fallback inside the object: <object …><img src="…" alt="what the page shows"></object>.`);
|
|
84
89
|
return alt;
|
|
@@ -87,10 +92,11 @@ function altOf(source) {
|
|
|
87
92
|
* The element a copy of `source` is: an `<object is="beltype-facsimile">`, in the contract the header states, with no child yet. Cheap and synchronous, and it starts nothing.
|
|
88
93
|
*
|
|
89
94
|
* @param source - a `<object type="application/pdf">`, parked or placed; its specified lengths are readable, its used width and height are not (measured: a parked source answers `50%` for a percentage width).
|
|
90
|
-
* @
|
|
95
|
+
* @param missingAlt - under `'warn'`, a source with no fallback `alt` is copied with no `role` and no `aria-label`.
|
|
96
|
+
* @throws {Error} when the source has no fallback `alt`, under `'refuse'`.
|
|
91
97
|
* @remarks The element has no child until {@link draw} gives it its canvas (*a facsimile's copy holds its box off screen*, CHANGELOG 2026-10-28). An `<object>` with a child and no `data` decides at its first layout to render the child, which is a reattach of its layout tree, and on a page the browser skips (`content-visibility: auto`, *a page is contained*) a read from script lays the subtree out for the length of the read, drops the reattach when it puts the subtree back, and leaves the copy with no box at all. Measured 2026-10-28: the same copy with no child keeps its box on every read.
|
|
92
98
|
*/
|
|
93
|
-
export function make(source) {
|
|
99
|
+
export function make(source, missingAlt = 'refuse') {
|
|
94
100
|
const doc = source.ownerDocument;
|
|
95
101
|
const el = doc.createElement('object', { is: TAG });
|
|
96
102
|
for (const { name, value } of source.attributes)
|
|
@@ -103,8 +109,11 @@ export function make(source) {
|
|
|
103
109
|
el.setAttribute('data-page', String(page));
|
|
104
110
|
else
|
|
105
111
|
el.removeAttribute('data-page');
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
const alt = altOf(source, missingAlt);
|
|
113
|
+
if (alt) {
|
|
114
|
+
el.setAttribute('role', 'img');
|
|
115
|
+
el.setAttribute('aria-label', alt);
|
|
116
|
+
}
|
|
108
117
|
el.setAttribute('contenteditable', 'false');
|
|
109
118
|
const size = sizes.get(keyOf(url, page));
|
|
110
119
|
if (size) {
|
|
@@ -207,12 +216,14 @@ async function draw(el) {
|
|
|
207
216
|
/**
|
|
208
217
|
* Define the element, read the size of every page the document embeds and register the presenter — before the first run (the intake calls this first, and awaits it), because a presenter is a condition of every copy and a registration after a run is a whole run.
|
|
209
218
|
*
|
|
210
|
-
* Every source is checked for its alt text here, so a document that would print a figure with none is refused at the door and not after the cuts. A PDF that cannot be read is TOLD and not refused: the box has no ratio from the cache, nothing is drawn in it, and its size, if the file turns up later, arrives through the rim; a missing page number is the same — except that it is the author's mistake, and the message says so.
|
|
219
|
+
* Every source is checked for its alt text here, so under the default a document that would print a figure with none is refused at the door and not after the cuts. A PDF that cannot be read is TOLD and not refused: the box has no ratio from the cache, nothing is drawn in it, and its size, if the file turns up later, arrives through the rim; a missing page number is the same — except that it is the author's mistake, and the message says so.
|
|
211
220
|
*
|
|
212
221
|
* @param present - `window.__beltypeCicero.present`, handed in so this module imports nothing from the engine.
|
|
213
|
-
* @
|
|
222
|
+
* @param options - `missingAlt: 'warn'` tells each source with no fallback `alt` once, at intake, and copies it with no alternative text, so the document is drawn and the print refuses it.
|
|
223
|
+
* @throws {Error} when a source has no fallback `alt`, under the default `missingAlt: 'refuse'`.
|
|
214
224
|
*/
|
|
215
|
-
export async function install(doc, present) {
|
|
225
|
+
export async function install(doc, present, options = {}) {
|
|
226
|
+
const missingAlt = options.missingAlt ?? 'refuse';
|
|
216
227
|
if (!customElements.get(TAG)) {
|
|
217
228
|
customElements.define(TAG, class extends HTMLObjectElement {
|
|
218
229
|
connectedCallback() { watcher().observe(this); }
|
|
@@ -221,7 +232,12 @@ export async function install(doc, present) {
|
|
|
221
232
|
}
|
|
222
233
|
const reads = [];
|
|
223
234
|
for (const source of doc.querySelectorAll(SOURCE)) {
|
|
224
|
-
|
|
235
|
+
// Once per source URL, as `told` keys a drawing failure: a host whose document holds two copies of one embed, or that installs again per slice, is told once.
|
|
236
|
+
const altKey = `alt:${source.getAttribute('data')}`;
|
|
237
|
+
if (altOf(source, missingAlt) === null && !told.has(altKey)) {
|
|
238
|
+
told.add(altKey);
|
|
239
|
+
console.warn(`BelType/Cicero: <object type="application/pdf" data="${source.getAttribute('data')}"> has no fallback <img alt="…">: the page it embeds is a figure with no alternative text, which the editor draws and the print will refuse. Write the fallback inside the object: <object …><img src="…" alt="what the page shows"></object>.`);
|
|
240
|
+
}
|
|
225
241
|
const { url, page } = locate(source);
|
|
226
242
|
reads.push(readSize(url, page).catch((error) => {
|
|
227
243
|
const key = keyOf(url, page);
|
|
@@ -232,5 +248,5 @@ export async function install(doc, present) {
|
|
|
232
248
|
}));
|
|
233
249
|
}
|
|
234
250
|
await Promise.all(reads);
|
|
235
|
-
present(SOURCE, make);
|
|
251
|
+
present(SOURCE, missingAlt === 'refuse' ? make : (source) => make(source, missingAlt));
|
|
236
252
|
}
|
package/dist/seam/rim.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THE RIM — the system's edge, where a fact the engine measured before it was true announces that it is (*late truth is an edit*, 2026-10-13; `.claude/decisions/cicero/late-truth-is-an-edit.md`, Decisions three and four; *a box announces its own size*, 2026-10-21; `.claude/decisions/cicero/a-box-announces-its-own-size.md`).
|
|
3
3
|
*
|
|
4
|
-
* It listens on the top-level boxes the intake took, and on nothing else: an `<img>` inside one (`load` and `error`, caught in the capture phase because they do not bubble
|
|
4
|
+
* It listens on the top-level boxes the intake took, and on nothing else: an `<img>` inside one (`load` and `error`, caught in the capture phase because they do not bubble: a picture still pending at intake, which the first run no longer waits for, and a picture whose `src` changes later are the same event), a custom element whose name is not yet defined (`customElements.whenDefined`, then one animation frame for its `connectedCallback` to have run), and `document.fonts` (`loadingdone`, a fact about the conditions and so about no box).
|
|
5
5
|
*
|
|
6
6
|
* **And the one cause that needs no name: a box's own size.** One `ResizeObserver` over the same boxes reports each one's border box; the report is read only while the box is drawn as itself on a page (the root's `isPresented`, and a reading above zero, since a parked box and a box on a page the browser skips read zero), and it is news only where it stands more than a quarter pixel from the height the last run measured for that box (the driver's table, which the client hands in as `measured`). The ear never measures: the observation is a trigger, and the resume measures. It is never paused; what it hears while a run is called or under way is judged when the runs have ended, against the table they leave, which is what keeps the engine's own presentations silent with no flag on its writes.
|
|
7
7
|
*
|
|
8
8
|
* **And the pieces a built page draws a block through** (*a fragment announces its own size*, 2026-10-22; `.claude/decisions/cicero/a-fragment-announces-its-own-size.md`). A divided block's original is parked, and so is a composed block's and one holding a nested fixed box, so the root names to the rim, through the client, every piece it slots into a built page's flow and every node a presentation takes out (`pieces`), and the same observer watches each from the frame it is built in. **A piece's expected height is its first reading above zero**, the presentation's own; a later reading more than the quarter pixel from it, judged under the same gate as an original's, is a `size` signal naming the piece's source box, the page the piece stands on beside it. The engine's own rebuild of a page makes new pieces with new first readings, so its writes pass with no flag. **Its limits**: a change of the original's tree after the copy was taken reaches no piece and is heard by nothing; a box on a page far from the reader is heard when the page comes near (the hearing probe, P40e), and a piece on a page never yet near has read nothing, so a change made before then, like one landing in the presentation's own frame, is its first reading and passes; and a reflow that keeps a box's height and moves its cut passes unheard, since a cut is a word address and the gate a height.
|
|
9
9
|
*
|
|
10
|
+
* **A picture is news only where its arrival changed its box** (amended 2026-11-08, on Egildo's ruling of that day). A `load` or an `error` is judged when the runs have ended, as a size is: where the box is drawn as itself and the observer's last reading of it stands within the quarter pixel of the height the last run measured, the picture moved nothing and is no signal, which is what an image that declares its `width` and `height` gives; where the observer has read the box as nothing (a page the browser skips), the box's own `offsetHeight` is read instead. A box not drawn as itself while some page is not built is judged again when the observer next reports, since it may stand on that page. Where the reading differs, or the box is not drawn as itself once every page is built (parked, drawn through pieces) or was never measured whole, the rim cannot compare, and the picture is an `image` signal as before. A reading taken before the arrival was laid out passes, and the observer's own `size` hears the box a frame later.
|
|
11
|
+
*
|
|
10
12
|
* **Every signal is one cancelable `beltype:change` on the document**, `detail: { kind, boxes }` with `kind` one of `image`, `element`, `font`, `size` and `boxes` the top-level boxes in document order (empty for a font); a `size` adds `pages`, beside each box the page it was heard on through a piece, or `null` where it was heard as itself. The same name is the one the live-values store dispatches for its writes (`kind: 'store'`, stage 2): the engine learns one name for every late truth and the host cancels one event to own the pace. A host that calls `preventDefault()` has taken it; otherwise `onSignal` runs with the same detail, and the client answers with the resumable run. **The default action is the engine's for an event the rim did not dispatch as well** (the amendment of 2026-10-13): the store's write (`kind: 'store'`) is dispatched by the host's module, which writes its fields synchronously when the dispatch returns un-cancelled, so the rim waits one animation frame and then, if nobody took the pace, hands `{ kind, boxes }` to the same `onSignal`.
|
|
11
13
|
*
|
|
12
14
|
* This module knows no run: it hears, tells, and hands the default action back. The `originals` array is the client's own and is read live — `patch` replaces an entry in it — so a box that left it is never named.
|
|
@@ -37,10 +39,11 @@ export declare function bringsNewFace(event: Event): boolean;
|
|
|
37
39
|
* @param p.pageHolding - the page a piece is slotted into (`root.js`' `pageHolding`), asked when a piece is judged changed.
|
|
38
40
|
* @param p.measured - the box's border-box height as the last run measured it, or undefined where it did not.
|
|
39
41
|
* @param p.idle - resolves when no run is under way and nothing the rim told is still being answered: a size is judged then, against the table the run leaves.
|
|
42
|
+
* @param p.unbuilt - whether some page of the presentation is not built yet: a picture's box that is not drawn as itself may stand on it, and is judged again when the observer next reports. Absent, every page is built.
|
|
40
43
|
* @param p.busy - whether a run is under way or a signal is being answered now: asked again just before a size is judged, since a named signal may have come while the ear waited for the frame.
|
|
41
44
|
* @returns `add` subscribes to a box, releasing what the box it replaced (a patched edit) and the box itself already held, so one load is one signal; `pieces` observes the pieces a page was just built with, each beside its source box, and lets go of the ones a presentation took out; `stop` releases everything.
|
|
42
45
|
*/
|
|
43
|
-
export declare function watchLateTruth({ document: doc, fonts, originals, onSignal, presented, pageHolding, measured, idle, busy }: {
|
|
46
|
+
export declare function watchLateTruth({ document: doc, fonts, originals, onSignal, presented, pageHolding, measured, idle, busy, unbuilt }: {
|
|
44
47
|
document: Document;
|
|
45
48
|
fonts?: FontFaceSet;
|
|
46
49
|
originals: Element[];
|
|
@@ -54,6 +57,7 @@ export declare function watchLateTruth({ document: doc, fonts, originals, onSign
|
|
|
54
57
|
measured?: (box: Element) => number | undefined;
|
|
55
58
|
idle?: () => Promise<void>;
|
|
56
59
|
busy?: () => boolean;
|
|
60
|
+
unbuilt?: () => boolean;
|
|
57
61
|
}): {
|
|
58
62
|
stop: () => void;
|
|
59
63
|
add: (box: Element, replaced?: Element) => void;
|