Document Model
The persistent tree the pipeline transforms.
An editor needs a document that can survive a thousand tiny edits per second, show two versions of itself for a diff view, and let subscribers notice exactly which subtrees changed. A mutable tree cannot do any of those cleanly. A persistent tree can.
The document is an immutable structure of nodes. Every transaction produces a new root; unchanged subtrees share memory with the previous one. Observers hold references to old roots without pinning deep copies.
Worked example
A document with one paragraph containing a mark run:
doc (branch)
└── paragraph (branch)
├── text "Hello " (leaf)
└── text "world" (leaf, marks: [strong])
Paths into that tree:
[]— the root.[0]— the paragraph.[0, 1]— the bold"world"leaf.
Positions into that tree (Pos, UTF-16 offsets) count across the whole document: the caret "between the space and the w" is pos(6).
Nodes and marks
RopeNode— either aBranchNodeor aTextLeafNode. Branch nodes carry aNodeTypeName, anAttrsbag, child nodes, cached text length, and cached height. Text leaves carry a string, marks, text length, and height0.- Marks — inline annotations identified by
MarkTypeNamewith anAttrsbag. Marks are flat on a text leaf; they do not nest as tree wrappers. SpineEditor ships eleven mark types:strong,em,underline,strikethrough,subscript,superscript,link,fontFamily,fontSize,textColor,highlightColor.
Fragments and slices
Fragment is a contiguous run of document content used by paste and insertFragment. Slicing helpers live in packages/core/src/doc/ (doc-fragment.ts, doc-slice.ts). A paste payload converts to a Fragment before it enters the planner.
Why persistent
- Structural sharing. A one-character insert in a 10,000-block document rebuilds one block and one path up to the root, not 10,000 blocks.
- Safe observation. Subscribers receive
(nextState, previousState)and can compare references for unchanged subtrees. - History is cheap.
HistoryEntryholds references tostateBefore.docandstateAfter.doc— not deep copies.
API
Construction and traversal live in @spine-editor/core/doc. Most consumers do not build documents directly; they build them with schema-aware helpers (createSchema, repairDoc) and then let the pipeline transform them.
const resolved = resolve(doc, pos(6));
resolve walks a document position to the enclosing path and text leaf. See packages/core/src/doc/ for the full surface.