CHAPTER 02

The model is the truth

Type in the editor below, then look at the panel. The tree and the log are not a debug view added on the side: they read the Loro document, the same object the editor renders. The page you are looking at is a picture of that document, and the button under the editor proves it, because it changes the picture without touching it.

A tree, in Loro

The document is a Loro tree named blocks. One node per block. The node's data holds the block's type and its props, and a text block keeps its text in a LoroText under text. A block inside another block is a child node.
JavaScript
// src/lib/blocks/nabu.svelte.js
this.tree = this.doc.getTree("blocks");
Loro is a CRDT, so two copies of this tree can be edited apart and merged later without a server deciding who wins (ADR 000 says why a CRDT, and why this one). We use that for tabs today (chapter 06). Nothing in the editor depends on a network.

Blocks are Svelte classes over it

ADR 002 chose classes in .svelte.js files for the engine's entities: Nabu, Block, MegaBlock (a block with children), TextBlock. A block wraps its Loro node and keeps its reactive state in runes. For text, that is a copy of the Loro text, refreshed whenever Loro says it changed (text.behavior.svelte.js):
JavaScript
this.text = $state(this.container.toString());
this.delta = $state(this.container.toDelta());
this.container.subscribe(() => {
  this.text = this.container.toString();
  this.delta = this.container.toDelta();
  // …
});
A component reads block.delta, and Svelte re-renders what depends on it. The rule that follows from this is the title of the chapter: nothing writes to the DOM first. A key press becomes a Loro operation, the operation becomes an event, the event updates a rune, the rune updates the page. Undo, a tab that syncs, a document loaded from disk and your own script all take the same road. That is why the button in the widget works: it inserts text into the first paragraph's LoroText with no key press and no DOM event, and the paragraph changes anyway.

Rich text is marks on one text

ADR 004 settled a question that looks small. Is a paragraph with a bold word one block or three? We said one text with marks. Bold is a range on the LoroText, and the renderer (RichText.svelte) draws the ranges as spans. The reasons in the ADR are the ones you would expect: the caret does not cross component boundaries, a style change does not delete and recreate blocks, and mapping a DOM position to a text offset stays linear. Chapter 04 depends on that last one.

What you can check

The model is also the format we save. serialize('json') is the tree, and roundtrip-structure.test.js exports, re-imports and compares a kitchen-sink document: nested lists, to-dos, code, toggles, regions with every field type. It compares the Loro nodes, not a serialization, so a field the serializer forgets cannot hide. Markdown is lossy by nature, and we say so in the clipboard guide. JSON is not meant to be.
Persistence goes one level lower. ADR 010 stores the binary Loro snapshot, history included, not a JSON we derive from it. What we save is the truth itself.