Concepts
Document model
Your document is plain JSON. Not a class, not an engine object · a tree of nodes you can log, diff, store and send over the wire.
{ type: 'doc', content: [
{ type: 'heading', attrs: { level: 1 },
content: [{ type: 'text', text: 'Title' }] },
{ type: 'paragraph', content: [
{ type: 'text', text: 'Some ' },
{ type: 'text', text: 'bold', marks: [{ type: 'bold' }] },
{ type: 'text', text: ' text.' },
] },
] }
That is the whole format. editor.getJSON() hands it to you and
editor.setContent() takes it back, and nothing in between is proprietary.
Nodes and marks
A node is a thing in the document: a paragraph, a heading, an image, a table cell. A mark is something applied to a range of text: bold, a link, a comment thread. Nodes nest; marks decorate.
The distinction matters when you write an extension, because it decides three things:
- Where it can go. A node lives in another node's
content; a mark lives on a text node'smarksarray. - What it costs in positions. A node has borders and takes space (below); a mark takes none at all.
- How it splits. Marks split and merge freely as you type through them. Nodes do not — splitting one is a deliberate operation.
Positions
Positions are integers into this tree, and the arithmetic is worth learning once because every command, every selection and every comment anchor is one.
- The document starts at
0. - Each character of text costs one.
- Each node border costs one — so a paragraph costs two, plus its inside.
- A leaf — a hard break, an image, a mention — costs exactly one, whatever it renders as. A five-letter mention is one position, not five.
- A mark costs nothing. Bold text is the same width as plain text.
<p>hi</p>
0 1 2 3
│ h i │
│ └─ 3 · after the closing border
│ └──── 2 · after "i", still inside
│ └─────────── 1 · the start of the text
└────────────── 0 · before the paragraph
So a document holding one paragraph of "hi" has a size of 4, and its content spans 0 to 3.
Selecting the word means { from: 1, to: 3 }.
Never do arithmetic on a position across an await. Take a marker with
ctx.mark() and map through it — see
position mapping, which is the page that explains why
this one is worth reading.
The schema decides what is possible
The extensions you pass build a schema, and the schema is enforced on every change. This is
not decoration: a transaction that would produce an invalid document is refused,
and the command that asked for it returns false.
const editor = createEditor({ extensions: [document, paragraph, text, bold] })
editor.setContent('<h1>A heading</h1><p>Some <em>italic</em>.</p>')
editor.getHTML()
// '<p>A heading</p><p>Some italic.</p>'
The heading became a paragraph and the emphasis was dropped, because there is no node or
mark in that schema to hold them. A node can also name the marks it accepts —
marks: '' is why the text in a code block stays literal, whatever you paste into
it. That is what makes a comment box a comment box: somebody pasting three pages of a Word document
into it gets three paragraphs of text, not three pages of headings and tables.
It is immutable
Every change produces a new document rather than editing the old one. Two consequences you will actually notice:
- An old document stays valid. Hold on to
getJSON()from ten seconds ago and it is still exactly what it was — which is how version history and collaborative rebasing work at all. - Unchanged parts are the same object. Editing one paragraph leaves every other paragraph as literally the same reference, which is what lets the renderer skip them by identity rather than by comparison.
Reading it without an editor
Because it is ordinary data, you can persist it anywhere, compare two revisions with any diff library, and read it on a server without loading an editor at all.
import { toMarkdown } from '@matrajs/core'
// On a server, in a worker, in a test · no DOM anywhere.
const markdown = toMarkdown(JSON.parse(row.body)) toMarkdown walks the JSON. It never touches a DOM, which is why it runs on a server
with no jsdom in sight and why it is in the benchmark rather than in a caveat.
Next
- Commands · how a change is made and why they return booleans.
- Position mapping · how a position survives an edit.
- Writing an extension · adding a node or a mark of your own.