Skip to content
Anjunar/ DOCS

Document model

A document is an immutable tree. Nodes reference their children by ID, so an edit deep inside costs a few entries, not a copy of every ancestor.

Tree

Nodes, IDs and types

Every node has a stable NodeId and a type from the schema. Element nodes list their children as IDs; text runs carry text and marks.

The tree behind the text

Edit the document and watch the tree: typing changes one text run, formatting splits runs, Enter adds a paragraph.

A small document

A paragraph with strong and emphasized text, a link and inline code.

  1. First item

  2. Second item

20 words · 21 nodes
ember.core.root/1 [document] ember.rich-text.heading/1 [document2] ember.core.text/1 [document1] "A small document" ember.rich-text.paragraph/1 [document13] ember.core.text/1 [document3] "A paragraph with " ember.core.text/1 [document4] "strong" ember.core.text/1 [document5] " and " ember.core.text/1 [document6] "emphasized" ember.core.text/1 [document7] " text, a " ember.link.link/1 [document9] ember.core.text/1 [document8] "link" ember.core.text/1 [document10] " and " ember.core.text/1 [document11] "inline code" ember.core.text/1 [document12] "." ember.list.list/1 [document20] ember.list.item/1 [document16] ember.rich-text.paragraph/1 [document15] ember.core.text/1 [document14] "First item" ember.list.item/1 [document19] ember.rich-text.paragraph/1 [document18] ember.core.text/1 [document17] "Second item"
Building

Valid by construction

Document.build checks five stages in order: identity, references, cycles, reachability and schema. It stops at the first failing stage, so one dangling reference does not drown in follow-up errors.

scala
val root = NodeId("root") Document.build(schema, root, Vector(RootNode(root, Vector(NodeId("ghost"))))) // Left(Vector(Violation(...)))
Output
<root>#root.children[0]: `root` references the unknown node `ghost` at position 0.
Operations

Seven primitives

Insert, Remove, Move, Replace, SpliceText, SplitText and MergeText. Each returns the new document, a ChangeSet and a PositionMapping together, so they can never drift apart. On failure the starting document is unchanged.

scala
document.applyOperation(Operation.SpliceText(NodeId("t1"), 5, 0, "!")) // Either[OperationError, OperationResult]
Positions survive edits

A PositionMapping carries a point across an edit and says whether it was preserved or displaced. An upload bookmark whose text vanished is displaced, never silently moved elsewhere.