prosemirror-fragview 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Egildo Tagliareni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,11 @@
1
- # Temporary Holding Version
1
+ # prosemirror-fragview
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A fragmented view for ProseMirror. `FragmentedView` holds an ordinary `EditorState`, speaks the same protocol to the same plugins as `prosemirror-view`'s `EditorView`, and renders the document into a chain of FRAGMENTAINERS: components that pull content from a position and give back the position where they had to stop. The basic fragmentainer, `OverflowBox`, reports the browser's own overflow. A paginator's page is a fragmentainer that reports the best cut under its own rules, with every paged-media feature behind it, and still, in the end, a position.
4
+
5
+ The design and its phases are in [docs/plan.md](docs/plan.md).
6
+
7
+ Phases 1, 2, 3 and 4a are built. Phase 1 is the package, and the proof that a `FragmentedView` with one infinite `OverflowBox` behaves as an `EditorView`: 18 of 19 parity acts equal on document, selection, undo and redo, the nineteenth showing `EditorView` reading a stale selection under keys pressed back to back. Phase 2 is fixed-height boxes and the chain. The chain is held in plugin state and mapped through every transaction, and only dirty fragments are refilled, in order: one character typed refills one box and dirties none, a new line near the top ripples through all five boxes of the harness document, and undo restores the chain. A fragment the edit did not touch keeps its DOM, so a key costs about a box's worth of work whatever the document's length. The selection is painted across fragments as one highlight, geometry answers across the chain, a box shorter than its first line still takes it, a `near` window policy builds only the boxes around the viewport, and `moved` refills from late truth. Phase 3 is the slice renderer the paginator's CSS expects, measured on the studio's schema lab under its generic schema. Shallow siblings are put away by index, so `:nth-child` and `+` match in every fragment as in one rendering of the whole. A divided box carries the words of a continuation (`split`, `start`, `end`, `fragment`, `adjoin`), with the two sheets a host adopts. An opened `ol` continues its numbering through `value`: 13 of 13 markers equal at every cut. Structured marks are rebuilt from their instances and stamped with `data-txt-i`. Under 320 × 480 boxes the lab writes back with only its 6 known differences, every element a fragment shows is in the file or one of the parse's three repairs, and the generic-schema round's acts come out as expected, 8 of 8. Phase 4a opens the two doors a paginator's package needs of this one: the renderer's attribute and words are a registry, passed as `sliceRenderer(words)` or as the view's `words` prop, whose default is this package's own `data-fragview`, so a paginator brings its own spellings and the sweep that `src/` names no engine passes. And `invalidate()` releases every container and refills the chain from the first when the conditions of every fill change, a page size, a sheet or a late face, which the demo's page-size menu now calls instead of mounting a new view, with every count of phase 3 unchanged. What was built, the numbers and what is not done yet are in [docs/results.md](docs/results.md); `npm test`, `npm run parity`, `npm run chain`, `npm run lossless`, `npm run acts`, `npm run fragments` and `npm run compose` run it again. A composition is applied as the text it committed, at the selection held since `compositionstart`, and nothing is read back from the DOM ([results](docs/results.md#compositions-as-committed-text-the-results)). Written with BelType's studio as the venue. The first paginator to implement the fragmentainer is BelType's Cicero, in a package of its own, [prosemirror-beltype](https://codeberg.org/egildo/prosemirror-beltype), which imports this one and the engine; this package knows no engine, by the rule [*the engine knows no view*](https://github.com/egildo/beltype/blob/main/.claude/decisions/project/the-engine-knows-no-view.md), and a sweep test over `src/` fails on the words `beltype` and `cicero`.
8
+
9
+ ## Tooling
10
+
11
+ The repository is indexed by CodeGraph: `codegraph sync` refreshes the index in `.codegraph/` (gitignored) after a change, `codegraph index` rebuilds it from scratch, and `codegraph explore "<question>"` asks it where something lives. GitHub Spec Kit is initialized for Claude Code with `sh` scripts: its templates and scripts are in `.specify/`, its commands are installed as skills under `.claude/skills/speckit-*` (invoked as `/speckit-specify`, `/speckit-plan` and so on), and its constitution in `.specify/memory/constitution.md` is not yet written.
package/docs/plan.md ADDED
@@ -0,0 +1,126 @@
1
+ # prosemirror-fragview — a fragmented view for ProseMirror, and the seam to the paginator
2
+
3
+ Written 2026-10-24 from a conversation with Egildo, after the generic-schema round ([notes/generic-schema-results.md](generic-schema-results.md), the companion [*The Model Is The Document*](schema-companion.html)). A plan, not a record: nothing here is built. It names the seam between ProseMirror's mechanics and a paginator, the component on each side of it, the proof that the seam is clean, and the phases that build it. The studio is the venue; the package leaves for a home of its own when phase 4 holds.
4
+
5
+ ## The idea in one paragraph
6
+
7
+ ProseMirror is four pure libraries and one that touches the DOM. The model (nodes, marks, slices, the schema), the transform (steps and their mappings) and the state (document, selection, plugins, transactions) never see a browser; the view (`EditorView`) renders the state into one contenteditable tree it owns, reads the browser's changes back, and exposes geometry (`coordsAtPos`, `posAtCoords`). A paginator cannot use that view: it assumes one DOM tree per document and reverts any DOM it did not produce, and a page engine draws a node on several pages, as the original and as copies. So the view is replaced, and the replacement is `FragmentedView`: it holds the same state, speaks the same protocol to the same plugins, and renders the document into a chain of FRAGMENTAINERS, each a component that pulls content from a position and gives back the position where it had to stop. The basic fragmentainer is a box that reports the browser's own overflow. BelType's page is a fragmentainer that reports the best cut under the make-up order, with every paged-media feature behind it, and still, in the end, a position. The canon is Houdini's Layout API, where a layout receives a break token and returns a fragment and the next break token: this is that contract in userland, with a ProseMirror position as the break token.
8
+
9
+ ## The seam
10
+
11
+ **Below the seam, the fragmentainer.** A component that owns a geometry and a foot and knows nothing of selection, input, plugins or transactions. It receives a document, a position and a slice renderer it does not own, and answers where it ended.
12
+
13
+ ```ts
14
+ interface Fragmentainer {
15
+ /** The component's root element, placed by the view. */
16
+ readonly dom: HTMLElement;
17
+ /**
18
+ * Render the slice of `doc` from `from` under my geometry and decide where I end.
19
+ * `to` is the position my content stops at, `null` when everything from `from` fit.
20
+ * `next` is a hint about the container that should follow (a named page, a blank page); the basic box gives none.
21
+ */
22
+ fill(doc: Node, from: number, render: SliceRenderer): Promise<{ to: number | null; next?: ContainerHint }>;
23
+ /** Registration, both ways, inside me: the DOM point a position is shown at (null when I do not show it), and the position a DOM point stands for. */
24
+ pointAt(pos: number): { node: globalThis.Node; offset: number } | null;
25
+ posAt(node: globalThis.Node, offset: number): number | null;
26
+ /** Late truth: my content changed height after I was filled (an image arrived, a face loaded, a size signal). The view refills from my `from`. */
27
+ readonly moved: EventTarget; // dispatches 'moved'
28
+ release(): void;
29
+ }
30
+
31
+ /** What the view hands every fragmentainer: the fragment of the document from `from` to `to` as DOM, open ancestors kept, shallow siblings by index before the live child at each depth (marked `away`), instances stamped on inline elements. */
32
+ type SliceRenderer = (doc: Node, from: number, to: number | null) => DocumentFragment;
33
+
34
+ /** The factory the view asks for the container after a cut. The basic factory returns another box; BelType's returns the page the hint and its own rules name. */
35
+ type ContainerFactory = (index: number, hint: ContainerHint | undefined, chain: ReadonlyArray<Entry>) => Fragmentainer;
36
+ ```
37
+
38
+ **Above the seam, the view.** `FragmentedView` is constructed as `EditorView` is and exposes its methods, so every plugin written against ProseMirror's view runs unchanged.
39
+
40
+ ```ts
41
+ class FragmentedView {
42
+ constructor(place: HTMLElement, props: FragmentedViewProps);
43
+ readonly state: EditorState;
44
+ dispatch(tr: Transaction): void;
45
+ updateState(state: EditorState): void;
46
+ setProps(props: Partial<FragmentedViewProps>): void;
47
+ someProp<N extends keyof EditorProps>(name: N, f?: (value: EditorProps[N]) => any): any;
48
+ hasFocus(): boolean; focus(): void;
49
+ coordsAtPos(pos: number): { left: number; right: number; top: number; bottom: number };
50
+ posAtCoords(coords: { left: number; top: number }): { pos: number; inside: number } | null;
51
+ domAtPos(pos: number): { node: globalThis.Node; offset: number };
52
+ posAtDOM(node: globalThis.Node, offset: number): number;
53
+ nodeDOM(pos: number): globalThis.Node | null;
54
+ endOfTextblock(dir: 'up' | 'down' | 'left' | 'right' | 'forward' | 'backward'): boolean;
55
+ destroy(): void;
56
+ /** Beyond EditorView: the chain and its containers. */
57
+ readonly fragments: ReadonlyArray<Entry>;
58
+ fragmentAt(pos: number): Entry | null;
59
+ }
60
+
61
+ interface FragmentedViewProps extends DirectEditorProps {
62
+ containers: ContainerFactory;
63
+ /** Which fragments to build now and which to estimate from measured heights; the default builds what is near the viewport and one beyond. */
64
+ window?: WindowPolicy;
65
+ }
66
+
67
+ /** One link of the chain, held as plugin state and mapped through every transaction. */
68
+ interface Entry { key: number; from: number; to: number | null; container: Fragmentainer; dirty: boolean }
69
+ ```
70
+
71
+ The view's own duties, none of them forked from `prosemirror-view` but written once for the fragmented case: the props protocol (`someProp` walks the plugins and calls `handleKeyDown`, `handleTextInput`, `handlePaste` and the rest at the moment the input layer reaches them); the model selection, drawn across fragments; input from `keydown` and `beforeinput`; the clipboard; decorations as paint; node views as presenters; the DOM read-back; the window policy.
72
+
73
+ **The chain is plugin state.** `fragmentsPlugin()` holds the entries in `EditorState`. Its `apply(tr, value)` maps every `from` and `to` through `tr.mapping` and marks dirty the entries whose range a step touched (`tr.mapping.maps[i].forEach` gives the changed ranges). The view reads that state after each transaction and refills the dirty entries only: an entry whose `to` lands where it was ends the refill; one whose `to` moved dirties the next. Undo, redo and collaboration map the chain for free, as they map decoration sets and the history. This is the ledger as mapped positions: what `patch`, `resumeAt` and the forcing memo do today by observing the DOM, the transaction says.
74
+
75
+ ## What ProseMirror gives, looked at from its side
76
+
77
+ 1. **A Slice is already the continuation.** `doc.slice(from, to)` keeps its open ancestors as partial nodes with `openStart` and `openEnd`; serializing its content with the schema's `DOMSerializer` gives `<ol><li>…` cut where the fragment begins, which is the hollow-ancestor copy a page engine otherwise builds by hand. The slice renderer adds shallow siblings before the live child at each open depth, as many as `$from.index(d)`, so `:nth-child`, `+`, `~` and list numbering read as in the source, and stamps each inline element with its mark instance so a reparse recovers identity.
78
+ 2. **Decorations without DOM.** The CSS Custom Highlight API (`CSS.highlights`) paints a set of `Range`s with no element inserted and no layout moved. Inline decorations, the search highlight, a collaborator's caret and the view's own selection become highlights, at no cost to the engine; the studio's `src/selection.js` already paints the selection this way. Widget decorations are refused, or shown in a gutter beside the fragment (the notes idea: comments and lint findings at the y of their position).
79
+ 3. **Mapping is the resume.** A transaction's mapping says which fragments moved and which were touched; nothing observes the DOM to learn it.
80
+
81
+ ## The basic fragmentainer: OverflowBox
82
+
83
+ A box of fixed width and height. `fill` renders the slice, lays it out, reads the first line whose rect passes the box's content foot, and answers its position as `to`: the browser's own overflow, greedy at line granularity. It is not a toy. It is a column's filling ("the browser fills, the engine reads"), a table cell's interior, and candidate zero of the Stonehand's retreat walk, which starts from the overflow and moves up to the best candidate. The rich fragmentainer is this one plus judgement. An infinite OverflowBox never cuts: `to` is always `null`.
84
+
85
+ ## The BelType fragmentainer
86
+
87
+ Cicero's page: `@page` geometry, margin boxes, named pages, the make-up order over candidates, forced breaks and avoids, counters and strings, demands when footnotes come ([*a competing flow bargains the foot of the main flow*](../../beltype/.claude/decisions/cicero/a-competing-flow-bargains-the-foot-of-the-main-flow.md)). Same `fill`, same `to`. Two things it needs that the box does not: the chain, since page numbers, `target-counter()` and strings are reads over every entry, not one; and a say in the container that follows (`next: { name: 'wide' }`, a blank page after `break-before: right`). So the BelType side is a `ContainerFactory` that reads the chain plus the page component, and the reads loop that settles in a few runs lives there and asks the view to refill.
88
+
89
+ **Corrected 2026-10-24** ([*the engine knows no view*](../../beltype/.claude/decisions/project/the-engine-knows-no-view.md)). The first draft said that `stonehand.js` and `chase.js` become the component, `driver.js` the view's chain, and that `root.js` stops owning the DOM. None of that holds: Cicero stays a function from a document and its sheets to cuts, runnable as one round from the CLI with no editor state, and `root.js` stays its presenter. The BelType fragmentainer lives in a third package, `prosemirror-beltype`, which imports this one and the engine and implements `Fragmentainer` and `ContainerFactory` over the engine's objects: the geometry capture, the sheet rewrite, the measure context and the report, the adjudication, the page element, the counter state over supplied readers, the reads loop over an abstract chain. This package never learns the engine's name: a sweep over `src/` for `beltype` and `cicero` fails the build.
90
+
91
+ ## The proof that the seam is clean
92
+
93
+ A `FragmentedView` with one infinite `OverflowBox` must behave as an `EditorView` on the schema lab, under the generic schema with structured marks: typing, selection, Backspace and Delete across blocks, Enter, undo and redo, the keymap and inputrules as plugins, collaboration through the studio's authority (`src/collab/`), the structure pane as a client. One fragment, `to` always `null`. Then two boxes of fixed height: the chain in plugin state, an edit mapping both entries, only the touched fragment refilled, the selection drawn across the two. If both hold, the seam is clean and the page component can be built against it.
94
+
95
+ ## Phases
96
+
97
+ Each phase is a branch of the studio, measured by a Playwright script, recorded in `notes/fragview-results.md`, and merged when green.
98
+
99
+ 1. **The package and the parity.** `fragview/` in the studio: the `Fragmentainer` interface, `OverflowBox`, `fragmentsPlugin`, `FragmentedView` with the geometry methods, the props protocol, input from `keydown` and `beforeinput`, the clipboard from `src/clipboard.js`. The lab under one infinite box: the parity acts above, pass or fail each, and `npm run typing` style sessions against the authority. Green means `EditorView` could be swapped out of any ProseMirror app built on these plugins.
100
+ 2. **Fixed-height boxes.** The chain mapped through transactions, refill of dirty entries only, the selection across fragments as highlights, the window policy building near the viewport. Measured: an edit on page one refills one fragment and dirties none when `to` lands where it was; an edit that moves a cut refills two; undo restores the chain.
101
+ 3. **The slice renderer from the generic schema.** Shallow siblings by index, instance stamps, the engine's `away` word on the siblings; the lab under two boxes lossless (the saved file byte for byte) and editable (the four acts of the generic-schema round).
102
+ 4. **The BelType fragmentainer**, moved to `prosemirror-beltype` on 2026-10-24 and phased there. What stays here are the two doors it needs of this package, both built as phase 4a ([results](results.md)): the words registry (the attribute and the six words) as a parameter of the renderer, `sliceRenderer(words)` and the view's `words` prop, with this package's own `data-fragview` as the default, so the sweep that this package names no engine passes and the BelType package passes `{ attribute: 'data-beltype' }`; and `invalidate()`, which releases every container and refills the chain from the first when conditions change (a `@page` edit dirties every entry and every page's geometry), found by the demo of 2026-10-24, where a change of box size needed a fresh mount because the view keeps a container whose chain key survives a new state. The proof moves with it: not the studio's orchestrator but the CLI's own round under Cicero, the same showcase cut twice, same cuts, same words, same folios.
103
+ 5. **The read-back.** DOM changes read back at the text level, CodeMirror 6's way, through the two hops (`sourcePoint` to the source, `posAt` to the position) and the instance stamps: dead keys for accents, autocorrect's word replacement, spellcheck's suggestion, each one act with the real input events and the model serialized after. IME composition left the read-back on 2026-10-27, by Egildo's ruling: it is applied as the text it committed, at the selection held since `compositionstart`, and nothing is read back ([results](results.md#compositions-as-committed-text-the-results)).
104
+
105
+ ## Refused, with the reason
106
+
107
+ - **A plugin that replaces the view.** A plugin lives in the state and supplies props; it cannot own the DOM. The chain is a plugin; the view is a view.
108
+ - **One EditorView per fragment.** A view needs a whole state; a slice as a state breaks positions, mapping and history (the footnote sub-editor trick, and its limits).
109
+ - **Forking prosemirror-view.** Model, transform and state are reused untouched; the view is written for the fragmented case, as Lexical and Slate wrote theirs. Its read-back code is the part to write once and carefully, with CodeMirror 6's text-level reading as the template, not prosemirror-view's node-level reparse; a composition is applied as the text it committed, as Slate and Lexical apply it.
110
+ - **Widget decorations in the flow.** They are layout; a page engine would re-cut for them. The gutter is their place.
111
+
112
+ ## Open questions
113
+
114
+ Answered 2026-10-24, in the round that produced [*the engine knows no view*](../../beltype/.claude/decisions/project/the-engine-knows-no-view.md):
115
+
116
+ - `fill` is asynchronous for what the container can await (fonts and images already in the DOM), and `moved` carries what arrives after. Both, as `OverflowBox` already does.
117
+ - The window estimate stays the view's mean span. The engine's heights table joins only if the placeholders visibly jump on the showcase.
118
+ - An entry carries positions and the `next` hint the previous fill answered, never geometry: `pageOf(pos)` is an index lookup over the chain, and sizes stay on the page element.
119
+ - The names stay: `prosemirror-fragview`, `FragmentedView`; the BelType package is `prosemirror-beltype`.
120
+
121
+ ## Where things are
122
+
123
+ - The studio's present orchestrator, which this generalizes: `src/positions.js` (geometry both ways over Cicero's pages), `src/patching.js` (a transaction as a Cicero patch), `src/input.js`, `src/selection.js`, `src/caret.js`, `src/clipboard.js`, `src/collab/`.
124
+ - The generic schema the renderer builds from: `src/schema-generic.js`, [notes/generic-schema-results.md](generic-schema-results.md).
125
+ - The engine's side of the seam, as it stands: `../beltype/packages/engine-cicero/src/stonehand.js`, `chase.js`, `driver.js`, `root.js`; its decision records in `../beltype/.claude/decisions/cicero/`.
126
+ - The canon: Houdini Layout API (break tokens in and out, unshipped); Chromium's break tokens (a path plus an inline offset; a ProseMirror position is that token as one integer with a mapping); Lexical and Slate (a view of their own over contenteditable); CodeMirror 6 (the viewport and height map, the text-level read-back); the CSS Custom Highlight API.
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "prosemirror-fragview",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A fragmented view for ProseMirror: the editor state rendered into a chain of fragmentainers, each a component that pulls content from a position and gives back the position where it had to stop.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Egildo Tagliareni",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/egildo/prosemirror-fragview.git"
11
+ },
12
+ "homepage": "https://codeberg.org/egildo/prosemirror-fragview",
13
+ "bugs": {
14
+ "url": "https://codeberg.org/egildo/prosemirror-fragview/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "main": "src/index.js",
20
+ "exports": {
21
+ ".": "./src/index.js"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "docs/plan.md",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "test": "node --test test/*.test.js",
30
+ "parity": "node scripts/parity.mjs",
31
+ "chain": "node scripts/chain.mjs",
32
+ "lossless": "node scripts/lossless.mjs",
33
+ "acts": "node scripts/acts.mjs",
34
+ "fragments": "node scripts/fragments.mjs",
35
+ "compose": "node scripts/compose.mjs",
36
+ "pointer": "node scripts/pointer.mjs",
37
+ "scroll": "node scripts/scroll.mjs",
38
+ "copy": "node scripts/copy.mjs",
39
+ "atoms": "node scripts/atoms.mjs"
40
+ },
41
+ "peerDependencies": {
42
+ "prosemirror-model": "^1.25.0",
43
+ "prosemirror-state": "^1.4.0",
44
+ "prosemirror-transform": "^1.12.0"
45
+ },
46
+ "devDependencies": {
47
+ "beltype-studio-experiment": "file:../../../../Users/egildo/Privata/dev/beltype-studio-experiment",
48
+ "playwright": "1.63.0",
49
+ "prosemirror-collab": "1.3.1",
50
+ "prosemirror-commands": "1.7.2",
51
+ "prosemirror-history": "1.5.1",
52
+ "prosemirror-inputrules": "1.5.1",
53
+ "prosemirror-keymap": "1.2.3",
54
+ "prosemirror-model": "1.25.12",
55
+ "prosemirror-schema-basic": "1.2.5",
56
+ "prosemirror-schema-list": "1.5.1",
57
+ "prosemirror-state": "1.4.4",
58
+ "prosemirror-test-builder": "1.1.1",
59
+ "prosemirror-transform": "1.12.2",
60
+ "prosemirror-view": "1.42.6",
61
+ "vite": "8.3.4"
62
+ }
63
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The seam's types: the `Fragmentainer` a view fills, the `SliceRenderer` it hands every fragmentainer, the `ContainerFactory` it asks for the next one, and the `ContainerHint` a fill may leave for it.
3
+ *
4
+ * @remarks
5
+ * A FRAGMENTAINER (the CSS Fragmentation word for a box that holds one fragment of a flow: a page, a column, a region) owns a geometry and a foot and knows nothing of selection, input, plugins or transactions. It receives a document, a position and a renderer it does not own, and answers where it ended. The contract is Houdini's Layout API in userland: a break token in, a fragment and the next break token out, where the break token is a ProseMirror position. See docs/plan.md, "The seam".
6
+ *
7
+ * One departure from the plan's sketch, measured while building phase 1: the plan's `SliceRenderer` returned a bare `DocumentFragment`, but the fragmentainer's `pointAt` and `posAt` need the positions the renderer knew while it wrote the DOM, so the renderer returns them with it (`RenderedSlice`).
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ /** @import { Node as PMNode } from 'prosemirror-model' */
13
+ /** @import { Entry } from './fragments-plugin.js' */
14
+
15
+ /**
16
+ * What a fill may say about the container that should follow it: a named page, a blank page. The basic box gives none.
17
+ * @typedef {object} ContainerHint
18
+ * @property {string} [name] The name of the container the next fragment wants, as CSS's named pages name a page.
19
+ * @property {boolean} [blank] The next container is to be left empty, as a blank page after `break-before: right`.
20
+ */
21
+
22
+ /**
23
+ * The model range one rendered DOM node stands for, clamped to the slice that was rendered.
24
+ * @typedef {object} Span
25
+ * @property {number} start The position the DOM node starts at: before the node for a node's element, the first character for a text node, the first content for a mark's element.
26
+ * @property {number} end The position the DOM node ends at.
27
+ * @property {number} [contentStart] Present when the element holds a node's content (its `contentDOM`): where that content starts.
28
+ * @property {number} [contentEnd] Where that content ends.
29
+ * @property {boolean} [mark] The element is a mark's wrapper, standing for no node of its own.
30
+ */
31
+
32
+ /**
33
+ * A slice rendered as DOM, with the position every rendered DOM node starts at.
34
+ * @typedef {object} RenderedSlice
35
+ * @property {DocumentFragment} fragment The slice's DOM, open ancestors kept as partial elements.
36
+ * @property {WeakMap<globalThis.Node, Span>} positions For every DOM node the renderer wrote and that stands for model content, the range it stands for; a node the renderer added for display only (an empty textblock's trailing break) has no entry.
37
+ * @property {number} from The first position the slice shows.
38
+ * @property {number} to The last position the slice shows.
39
+ */
40
+
41
+ /**
42
+ * What the view hands every fragmentainer: the document from `from` to `to` (to the end when `to` is null) rendered as DOM.
43
+ * @typedef {(doc: PMNode, from: number, to: number | null) => RenderedSlice} SliceRenderer
44
+ */
45
+
46
+ /**
47
+ * The answer of a fill.
48
+ * @typedef {object} FillResult
49
+ * @property {number | null} to The position the fragmentainer's content stops at, null when everything from `from` fit.
50
+ * @property {ContainerHint} [next] A hint about the container that should follow.
51
+ */
52
+
53
+ /**
54
+ * A component that pulls content from a position and gives back the position where it had to stop.
55
+ * @typedef {object} Fragmentainer
56
+ * @property {HTMLElement} dom The component's root element, placed by the view.
57
+ * @property {(doc: PMNode, from: number, render: SliceRenderer) => Promise<FillResult>} fill Render the slice of `doc` from `from` under the component's geometry and decide where it ends; the DOM is written before the promise's first wait, so a caller that reads the DOM straight after the call sees the new content.
58
+ * @property {(pos: number) => { node: globalThis.Node, offset: number } | null} pointAt The DOM point a position is shown at inside the component, null when the component does not show it.
59
+ * @property {(node: globalThis.Node, offset: number) => number | null} posAt The position a DOM point inside the component stands for, null when the point is not the component's.
60
+ * @property {EventTarget} moved Late truth: dispatches `moved` when the content changed height after a fill (an image arrived, a face loaded), so the view refills from the component's `from`.
61
+ * @property {() => void} release Let go of observers and listeners; the view removes `dom`.
62
+ */
63
+
64
+ /**
65
+ * The factory the view asks for the container after a cut, and for the first one. The basic factory returns another box; a paginator's returns the page the hint and its own rules name.
66
+ * @typedef {(index: number, hint: ContainerHint | undefined, chain: ReadonlyArray<Entry>) => Fragmentainer} ContainerFactory
67
+ */
68
+
69
+ export {};