@matrajs/mcp 1.0.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.
@@ -0,0 +1,162 @@
1
+ # Commands
2
+
3
+ Commands are functions that either change the document and return true, or change nothing and return false. How they compose, how they are typed, and why the boolean matters.
4
+
5
+ A command is a function that takes a context and returns a boolean: **true if it did something, false if it declined.** It never throws · one that does is caught, logged, and its half-built transaction discarded rather than applied.
6
+
7
+ ```
8
+ {`const toggleBold: Command = (ctx) => ctx.toggleMark('bold')`}
9
+ ```
10
+
11
+ ## The boolean is the interesting part
12
+
13
+ A command returning `false` is not an error. It is the answer to "can this happen here?", and it is how the schema, the selection and the document shape all get a say without anybody writing a check.
14
+
15
+ ```
16
+ {`// In a comment box with no heading in its schema:
17
+ editor.commands.toggleHeading(2) // undefined · not a command at all
18
+
19
+ // In a full editor, with the caret inside a code block:
20
+ editor.commands.toggleBold() // false · code blocks accept no marks
21
+
22
+ // With nothing selected:
23
+ editor.commands.askAi('shorten') // false · there is nothing to shorten`}
24
+ ```
25
+
26
+ A keyboard shortcut that returns `false` falls through to the browser's own handling rather than swallowing the key.
27
+
28
+ ## Names are checked too
29
+
30
+ The same inference covers the names you pass to `isActive`. An editor built without `heading` has no `toggleHeading` — and no node called "heading" either, so asking about one is a compile error rather than a permanent `false`.
31
+
32
+ ```
33
+ {`const editor = createEditor({ extensions: [document, paragraph, text, bold] })
34
+
35
+ editor.isActive('bold') // fine
36
+ editor.isActive('heading') // Argument of type '"heading"' is not assignable
37
+ editor.isActive('bould') // nor is a typo`}
38
+ ```
39
+
40
+ This is the failure that used to be hardest to see. A missing command is a `TypeError` the first time you press the button; a misspelt name in `isActive` is a toolbar button that simply never lights up, on a page where everything else works.
41
+
42
+ ## Asking without doing
43
+
44
+ The boolean above only arrives after the change has happened, which is no use to a button that wants to be disabled *before* it is pressed. `editor.can` is the same command surface, with the same names and the same arguments, that returns the answer and changes nothing.
45
+
46
+ ```
47
+ {`bold.disabled = !editor.can.toggleBold()
48
+ undo.disabled = !editor.can.undo()`}
49
+ ```
50
+
51
+ The command builds its transaction as usual and the transaction is thrown away, so asking costs about what pressing the button costs and leaves the document, the selection and the undo stack exactly as they were. No `change` event fires.
52
+
53
+ > Note: Repaint on `selectionChange` as well as `change`. Whether bold is possible depends on where the caret is, so moving it into a code block has to grey the button out even though nothing was edited.
54
+
55
+ ## Types are inferred, not declared
56
+
57
+ `editor.commands` is built from the extensions array you passed. Add an extension and its commands appear, typed, with their argument types intact. Leave it out and calling them is a compile error rather than a runtime surprise.
58
+
59
+ ```
60
+ {`const editor = createEditor({ extensions: [document, paragraph, text, bold] })
61
+
62
+ editor.commands.toggleBold() // fine
63
+ editor.commands.toggleItalic() // Property 'toggleItalic' does not exist`}
64
+ ```
65
+
66
+ There is no module augmentation and nothing to declare. This is the one thing here that other editors cannot copy without changing their architecture: Tiptap's commands live in a global interface, so every installed extension's commands appear on every editor whether you passed them or not.
67
+
68
+ ## Running several as one
69
+
70
+ ```
71
+ {`editor.batch((commands) => {
72
+ commands.toggleBold()
73
+ commands.insert(' and this')
74
+ })`}
75
+ ```
76
+
77
+ One undo step, and all or nothing · if any command returns `false` the whole batch rolls back and `batch` itself returns `false`. Nothing half-applied reaches the document.
78
+
79
+ ## Making a change its own undo step
80
+
81
+ Undo groups by time, which is right for typing and wrong for anything deliberate. A command that restores a template, applies a rewrite or accepts a suggestion should not disappear into the sentence somebody was writing a second earlier.
82
+
83
+ ```
84
+ {`const applyTemplate: Command = (ctx) => {
85
+ if (!ctx.replace({ from: 0, to: size }, template)) return false
86
+ return ctx.isolateUndo()
87
+ }`}
88
+ ```
89
+
90
+ It seals from both sides: nothing merges into it from before, and the next keystroke does not merge into it either.
91
+
92
+ ## The core commands
93
+
94
+ These exist whatever extensions you pass, because the engine provides them:
95
+
96
+ | Command | What it does |
97
+
98
+ | `select(range)` | Move the selection · a position or a range `insert(content, at?)` | JSON, an array of nodes, or an HTML string `replace(range, content)` | Swap a span for something else `remove(range?)` | Delete a span, or the selection |
99
+
100
+ | `moveBlock(from, to)` | What a drag handle does |
101
+
102
+ | `focus()` | Put the caret back in the editor |
103
+
104
+ ## What the context gives you
105
+
106
+ Inside a command, `ctx` is the whole surface. The document as JSON, the selection, and the operations that change them:
107
+
108
+ ```
109
+ {`ctx.doc // the document, as plain JSON
110
+ ctx.selection // { from, to, anchor, head, empty }
111
+
112
+ ctx.toggleMark('bold') // and addMark, removeMark
113
+ ctx.setBlockType('heading', { level: 2 })
114
+ ctx.setNodeAttrs('taskItem', { checked: true })
115
+ ctx.wrapIn('blockquote') // and lift()
116
+
117
+ ctx.insert(content, at?) // replace, delete, select, focus
118
+ ctx.mark() // a position marker · see position mapping
119
+ ctx.isolateUndo()`}
120
+ ```
121
+
122
+ > Note: `setBlockType` only applies to textblocks — nodes that hold text. For a node that holds *blocks*, like a checklist item or a table cell, use `setNodeAttrs`. Ticking a checkbox with `setBlockType` silently does nothing, which is a mistake worth making only once.
123
+
124
+ ## Writing a position down
125
+
126
+ `Pos` is branded, so arithmetic on one does not typecheck: `from + 5` is a plain `number`, and a plain `number` is not accepted back. That is deliberate · a position computed from a stale one is the bug this editor exists to prevent, and it is invisible at runtime.
127
+
128
+ It does mean a literal position needs saying out loud. `pos` and `range` are that, and nothing else:
129
+
130
+ ```
131
+ {`import { pos, range } from '@matrajs/core'
132
+
133
+ editor.commands.select(pos(0))
134
+ editor.commands.replace(range(1, 6), 'goodbye')`}
135
+ ```
136
+
137
+ Use them for positions you are writing down, never for one you are carrying across an `await` · that is `ctx.mark()`, which maps rather than guesses. See [position mapping](https://matrajs.com/docs/position-mapping).
138
+
139
+ ## Writing your own
140
+
141
+ A command is a plain function on an extension. Nothing registers it, nothing wraps it, and the name you give it is the name on `editor.commands`:
142
+
143
+ ```
144
+ {`import type { Command, ExtensionDef } from '@matrajs/core'
145
+
146
+ const shout: Command<[times?: number]> = (ctx, times = 1) => {
147
+ const { from, to } = ctx.selection
148
+ if (from === to) return false // nothing selected · decline
149
+ return ctx.insert('!'.repeat(times), to)
150
+ }
151
+
152
+ export const emphasis: ExtensionDef<{ shout: Command<[times?: number]> }> = {
153
+ kind: 'extension',
154
+ name: 'emphasis',
155
+ commands: { shout },
156
+ keys: { 'Mod-Alt-!': 'shout' },
157
+ }`}
158
+ ```
159
+
160
+ `editor.commands.shout(3)` now exists and is typed, because the array it came from says so. See [writing an extension](https://matrajs.com/docs/extensions) for the rest of the shape.
161
+
162
+ > Note: Positions that are not finite integers inside the document return `false` rather than throwing — `NaN` included. It slips past a naive range check, because both `NaN size` are false.
@@ -0,0 +1,103 @@
1
+ # Document model
2
+
3
+ Your document is plain JSON: a tree of nodes you can log, diff, store and send over the wire. What positions mean, what the schema refuses, and why it is immutable.
4
+
5
+ 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.
6
+
7
+ ```
8
+ {`{ type: 'doc', content: [
9
+ { type: 'heading', attrs: { level: 1 },
10
+ content: [{ type: 'text', text: 'Title' }] },
11
+ { type: 'paragraph', content: [
12
+ { type: 'text', text: 'Some ' },
13
+ { type: 'text', text: 'bold', marks: [{ type: 'bold' }] },
14
+ { type: 'text', text: ' text.' },
15
+ ] },
16
+ ] }`}
17
+ ```
18
+
19
+ That is the whole format. `editor.getJSON()` hands it to you and `editor.setContent()` takes it back, and nothing in between is proprietary.
20
+
21
+ ## Nodes and marks
22
+
23
+ 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.
24
+
25
+ The distinction matters when you write an extension, because it decides three things:
26
+
27
+ - **Where it can go.** A node lives in another node's `content`; a mark lives on a text node's `marks` array.
28
+
29
+ - **What it costs in positions.** A node has borders and takes space (below); a mark takes none at all.
30
+
31
+ - **How it splits.** Marks split and merge freely as you type through them. Nodes do not — splitting one is a deliberate operation.
32
+
33
+ ## Positions
34
+
35
+ Positions are integers into this tree, and the arithmetic is worth learning once because every command, every selection and every comment anchor is one.
36
+
37
+ - The document starts at `0`.
38
+
39
+ - Each character of text costs **one**.
40
+
41
+ - Each node border costs **one** — so a paragraph costs two, plus its inside.
42
+
43
+ - 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.
44
+
45
+ - A mark costs **nothing**. Bold text is the same width as plain text.
46
+
47
+ ```
48
+ {`<p>hi</p>
49
+
50
+ 0 1 2 3
51
+ │ h i │
52
+ │ └─ 3 · after the closing border
53
+ │ └──── 2 · after "i", still inside
54
+ │ └─────────── 1 · the start of the text
55
+ └────────────── 0 · before the paragraph`}
56
+ ```
57
+
58
+ So a document holding one paragraph of "hi" has a size of 4, and its content spans 0 to 3. Selecting the word means ``.
59
+
60
+ > Note: Never do arithmetic on a position across an `await`. Take a marker with `ctx.mark()` and map through it — see [position mapping](https://matrajs.com/docs/position-mapping), which is the page that explains why this one is worth reading.
61
+
62
+ ## The schema decides what is possible
63
+
64
+ 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`.
65
+
66
+ ```
67
+ {`const editor = createEditor({ extensions: [document, paragraph, text, bold] })
68
+
69
+ editor.setContent('<h1>A heading</h1><p>Some <em>italic</em>.</p>')
70
+ editor.getHTML()
71
+ // '<p>A heading</p><p>Some italic.</p>'`}
72
+ ```
73
+
74
+ 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.
75
+
76
+ ## It is immutable
77
+
78
+ Every change produces a new document rather than editing the old one. Two consequences you will actually notice:
79
+
80
+ - **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.
81
+
82
+ - **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.
83
+
84
+ ## Reading it without an editor
85
+
86
+ 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.
87
+
88
+ ```
89
+ {`import { toMarkdown } from '@matrajs/core'
90
+
91
+ // On a server, in a worker, in a test · no DOM anywhere.
92
+ const markdown = toMarkdown(JSON.parse(row.body))`}
93
+ ```
94
+
95
+ `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.
96
+
97
+ ## Next
98
+
99
+ - [Commands](https://matrajs.com/docs/commands) · how a change is made and why they return booleans.
100
+
101
+ - [Position mapping](https://matrajs.com/docs/position-mapping) · how a position survives an edit.
102
+
103
+ - [Writing an extension](https://matrajs.com/docs/extensions) · adding a node or a mark of your own.
@@ -0,0 +1,168 @@
1
+ # Writing an extension
2
+
3
+ An extension is a plain object. Nodes, marks and behaviour, with command types inferred rather than declared.
4
+
5
+ An extension is a plain object with the same power as the built-ins. There is no base class to extend, nothing to register, and no declaration merging · pass it to `createEditor` and its commands appear on `editor.commands`, typed.
6
+
7
+ ## A mark
8
+
9
+ ```
10
+ import type { Command, MarkDef } from '@matrajs/core'
11
+
12
+ export const spoiler: MarkDef<{ toggleSpoiler: Command }> = {
13
+ kind: 'mark',
14
+ name: 'spoiler',
15
+ parseDOM: [{ tag: 'span[data-spoiler]' }],
16
+ toDOM: () => ['span', { 'data-spoiler': '', class: 'spoiler' }, 0],
17
+ commands: {
18
+ toggleSpoiler: (ctx) => ctx.toggleMark('spoiler'),
19
+ },
20
+ keys: { 'Mod-Shift-S': 'toggleSpoiler' },
21
+ }
22
+ ```
23
+
24
+ The generic is what makes `editor.commands.toggleSpoiler` exist and be typed. It is the only ceremony, and it buys autocomplete on every call site.
25
+
26
+ ## A node
27
+
28
+ ```
29
+ export const callout: NodeDef<{ setCallout: Command<[tone: string]> }> = {
30
+ kind: 'node',
31
+ name: 'callout',
32
+ group: 'block',
33
+ content: 'block+',
34
+ attrs: { tone: { default: 'note' } },
35
+ parseDOM: [{
36
+ tag: 'div[data-callout]',
37
+ getAttrs: (dom) => ({ tone: dom.getAttribute('data-callout') }),
38
+ }],
39
+ toDOM: (node) => ['div', { 'data-callout': node.attrs?.tone }, 0],
40
+ commands: {
41
+ setCallout: (ctx, tone) => ctx.wrapIn('callout', { tone }),
42
+ },
43
+ }
44
+ ```
45
+
46
+ > Note: Declare every attribute you intend to keep. Undeclared attributes are dropped on the way in — that is a security property, not an oversight, and it is what stops a document supplying `onclick` to a node type that spreads its attrs.
47
+
48
+ ## Input rules
49
+
50
+ Text that becomes something else as it is typed. Each match is one undo step.
51
+
52
+ ```
53
+ inputRules: [{
54
+ match: /^>>\s$/,
55
+ handler: (ctx, _match, range) => ctx.delete(range) && ctx.wrapIn('callout'),
56
+ }]
57
+ ```
58
+
59
+ ## State
60
+
61
+ An extension can keep state, reduced on every transaction. This is how the character counter works without publishing a global.
62
+
63
+ ```
64
+ export const wordGoal = (target: number): ExtensionDef<Record<string, never>, boolean> => ({
65
+ kind: 'extension',
66
+ name: 'wordGoal',
67
+ state: {
68
+ init: (ctx) => count(ctx.doc) >= target,
69
+ apply: (ctx) => count(ctx.doc) >= target,
70
+ },
71
+ })
72
+
73
+ // anywhere
74
+ editor.extensionState<boolean>('wordGoal')
75
+ ```
76
+
77
+ ## Decorations
78
+
79
+ Drawing over the document without putting anything in it · search highlights, remote cursors, squiggles. Decorations never travel with a copy, an export or an undo.
80
+
81
+ ```
82
+ decorations: (ctx) => findMatches(ctx.doc, query).map((range) => ({
83
+ type: 'inline',
84
+ from: range.from,
85
+ to: range.to,
86
+ attrs: { class: 'search-hit' },
87
+ }))
88
+ ```
89
+
90
+ ## Attributes on somebody else's node
91
+
92
+ Alignment belongs on a paragraph, but the paragraph should not have to know about alignment. An extension may declare an attribute for nodes and marks defined elsewhere: it lands in the schema of every type named, is rendered onto the element, and is read back on parse. This is how `textAlign`, `indent` and `uniqueId` work.
93
+
94
+ ```
95
+ export const tone: ExtensionDef = {
96
+ kind: 'extension',
97
+ name: 'tone',
98
+ attributes: [{
99
+ types: ['paragraph', 'heading'],
100
+ attrs: {
101
+ tone: {
102
+ default: null,
103
+ render: (value) => ({ 'data-tone': String(value) }), // onto the element
104
+ parse: (dom) => dom.getAttribute('data-tone'), // and back off it
105
+ },
106
+ },
107
+ }],
108
+ commands: {
109
+ setTone: (ctx, tone) => ctx.setBlockType('paragraph', { tone }),
110
+ },
111
+ }
112
+ ```
113
+
114
+ Leave `render` and `parse` off and the value goes to `data-`. A `style` or `class` the render returns is composed with the node's own rather than replacing it. A node that declares the attribute itself keeps its own rendering, and the global is not applied twice.
115
+
116
+ ## Paste and drop
117
+
118
+ An extension can claim what arrives from the clipboard or by drag before the editor parses it. Return `true` and the editor leaves it alone; anything else and the next handler, then the editor, gets its turn. Files come through the same way — a screenshot pasted from the clipboard is a file — and a drop carries the position it landed on.
119
+
120
+ ```
121
+ handlePaste: (ctx, { html, text, files }) => {
122
+ if (text !== 'magic') return false
123
+ return ctx.insert('✨')
124
+ },
125
+ handleDrop: (ctx, { files, pos }) => {
126
+ const marker = ctx.mark() // survives the upload
127
+ upload(files[0]).then((src) =>
128
+ editor.commands.insert({ type: 'image', attrs: { src } }, pos && marker.map(pos)))
129
+ return true
130
+ }
131
+ ```
132
+
133
+ `fileHandler()` is this, packaged: an `accept` list and a callback with the editor, the files, the position and the marker already in it.
134
+
135
+ ## Whitespace in code
136
+
137
+ HTML collapses runs of whitespace and so does the parser — except inside a node that says `code: true`, and inside any ``, where every space and line break is content. The stock code block says it; a node of your own that holds code should too.
138
+
139
+ ## Refusing a change
140
+
141
+ An extension can veto a change before it lands. The context it gets is built on the document as it is, holding the change as it would be, so it can compare the two; return `false` and the document, the selection and the undo history stay exactly as they were. A keystroke, a paste, a drop, a drag and a command are all changes, so one filter covers them all — which is how `locked()` works — and `editor.can` asks the same question, so a button greys out rather than doing nothing.
142
+
143
+ ```
144
+ filterChange: (ctx) => {
145
+ const { state, tr } = engine(ctx) // the state before, the transaction as built
146
+ return !tr.docChanged || tr.doc.textContent.length <= 280
147
+ }
148
+ ```
149
+
150
+ Undo and redo, and `setContent`, carry a meta the filter can look for, so a change that was allowed in can always be allowed back out.
151
+
152
+ ## Rendering somebody else's node
153
+
154
+ A node view belongs to the node that needs one — except when the need belongs to another extension. Resize handles belong on an image, and the image should not have to know about them. An extension may render nodes defined elsewhere, keyed by name; its view wins over the node's own. This is how `imageResize()` works, alongside a `width` it adds with `attributes`.
155
+
156
+ ```
157
+ nodeViews: {
158
+ image: ({ node, getPos, editor }) => {
159
+ const dom = document.createElement('span')
160
+ // … an <img> and a handle
161
+ return { dom, update: (next) => next.type === 'image', stopEvent: (event) => event.target === handle }
162
+ },
163
+ }
164
+ ```
165
+
166
+ ## Ordering
167
+
168
+ Extensions load in array order, adjustable with `priority`. Later ones win a key binding conflict, and a more specific `parseDOM` rule needs a higher priority to beat a general one · which is how a task list beats a plain bulleted list for the same ``.
@@ -0,0 +1,136 @@
1
+ # Your first editor
2
+
3
+ A working editor in ten lines, a toolbar wired to real commands, and the four mistakes everybody makes on the first afternoon.
4
+
5
+ Ten lines, and the result is the editor on the front page.
6
+
7
+ ```
8
+ {`import { createEditor, starterKit } from '@matrajs/core'
9
+
10
+ const editor = createEditor({
11
+ extensions: starterKit,
12
+ content: '<p>Hello.</p>',
13
+ element: document.querySelector('#editor'),
14
+ })`}
15
+ ```
16
+
17
+ `element` mounts it there and then. Leave it out and you get an unmounted editor to `mount` yourself later, which is what the framework bindings do · in React the element does not exist until after the first render.
18
+
19
+ > Note: It will look like unstyled text, and that is correct. Matra ships no appearance at all — a heading looks like a heading because your stylesheet says so. [Styling](https://matrajs.com/docs/styling) is fifteen lines and worth reading now rather than after you have decided something is broken.
20
+
21
+ ## What is in the starter kit
22
+
23
+ Seventeen extensions: document, paragraph, text, heading, blockquote, codeBlock, bulletList, orderedList, listItem, horizontalRule, hardBreak, bold, italic, strike, code, link and history. Enough to write with, and nothing that needs configuring.
24
+
25
+ Not in it, on purpose: `typography` (curling quotes is a decision), `taskList`, tables, and everything that needs options. Add what you want.
26
+
27
+ ```
28
+ {`// Everything:
29
+ createEditor({ extensions: starterKit })
30
+
31
+ // Or exactly what you want, and nothing else:
32
+ createEditor({ extensions: [document, paragraph, text, bold, underline] })
33
+
34
+ // Or the kit plus something:
35
+ createEditor({ extensions: [...starterKit, taskList, taskItem, ...tableKit] })`}
36
+ ```
37
+
38
+ The array is the feature list. An editor built from the second line has no way to make a heading — no command, no shortcut, and pasting one flattens it to a paragraph. That is how you build a comment box that stays a comment box.
39
+
40
+ ## A toolbar
41
+
42
+ Every button is a command call. `editor.commands` is typed from the array you passed, so `toggleBold` exists because `bold` is in the kit — not because of a declaration merge somewhere.
43
+
44
+ ```
45
+ {`const bold = document.querySelector('#bold')
46
+
47
+ bold.addEventListener('mousedown', (event) => {
48
+ event.preventDefault() // keep the caret in the editor
49
+ editor.commands.toggleBold()
50
+ })`}
51
+ ```
52
+
53
+ > Note: `mousedown` with `preventDefault`, not `click`. A click moves focus to the button first, which collapses the selection — so the command runs on a caret rather than on the words the user chose, and bold appears to do nothing.
54
+
55
+ ## A toolbar that tells the truth
56
+
57
+ A button that looks the same whether or not the thing it does is already done is a button you have to check your text to use. Ask the document:
58
+
59
+ ```
60
+ {`const paint = () => {
61
+ // Is it on?
62
+ bold.setAttribute('aria-pressed', String(editor.isActive('bold')))
63
+ h2.setAttribute('aria-pressed', String(editor.isActive('heading', { level: 2 })))
64
+
65
+ // Is it even possible here? No, inside a code block.
66
+ bold.disabled = !editor.can.toggleBold()
67
+ undo.disabled = !editor.can.undo()
68
+ }
69
+
70
+ editor.on('change', paint)
71
+ editor.on('selectionChange', paint)
72
+ paint()`}
73
+ ```
74
+
75
+ Two different questions, and a toolbar needs both. `isActive` asks whether the thing is done; `editor.can` asks whether it could be. A button that answers only the first looks pressable over a code block, gets pressed, and does nothing.
76
+
77
+ `selectionChange` matters as much as `change`: moving the caret into bold text does not change the document, and the button still has to light up · or grey out.
78
+
79
+ ## Reading the document
80
+
81
+ ```
82
+ {`editor.getJSON() // plain JSON, no engine types
83
+ editor.getHTML() // a string
84
+ editor.getText() // block-separated text`}
85
+ ```
86
+
87
+ Store the JSON. It is ordinary data: you can diff two revisions with any library, log it, and read it in a language that has never heard of this editor. HTML is for display and for pasting somewhere else — round-tripping through it loses anything your schema expresses that HTML does not.
88
+
89
+ ## Reacting to changes
90
+
91
+ ```
92
+ {`const off = editor.on('change', () => save(editor.getJSON()))
93
+
94
+ // later
95
+ off()`}
96
+ ```
97
+
98
+ `on` returns its own unsubscribe, so there is nothing to name and nothing to match up. The events are `change`, `selectionChange`, `focus` and `blur`.
99
+
100
+ Do not save on every keystroke. [Recipes](https://matrajs.com/docs/recipes) has an autosave that waits for a pause and does not lose the last edit when the tab closes.
101
+
102
+ ## Setting content later
103
+
104
+ ```
105
+ {`editor.setContent(await load(id)) // JSON or an HTML string`}
106
+ ```
107
+
108
+ > Note: `setContent` replaces the document and **clears the undo history** — which is right when you are loading a different document, and wrong if you meant to change the one in front of the user. For that, use a command: `editor.commands.replace(range, content)`.
109
+
110
+ ## Cleaning up
111
+
112
+ ```
113
+ {`editor.destroy()`}
114
+ ```
115
+
116
+ Removes the listeners and the `contenteditable` attribute, so the element is safe to reuse. Every framework binding does it for you on unmount; if you are mounting by hand, a route change or a hot reload is where forgetting shows up.
117
+
118
+ ## Where people get stuck
119
+
120
+ - **"It looks like plain text."** It does. See [styling](https://matrajs.com/docs/styling).
121
+
122
+ - **"My toolbar button does nothing."** Almost always `click` instead of `mousedown` with `preventDefault`. If it is right and the button still does nothing, ask `editor.can` · the answer is usually that the caret is somewhere the command refuses.
123
+
124
+ - **"The command does not exist."** Its extension is not in your array. That is the design, and TypeScript is telling you before the browser would.
125
+
126
+ - **"Two carets appeared."** The element was mounted twice. Guard with `if (!editor.unsafe.view) editor.mount(element)` — see [frameworks](https://matrajs.com/docs/frameworks).
127
+
128
+ - **"Pasting loses my formatting."** The schema has no node or mark for it. Add the extension, or accept it — a comment box refusing headings is doing its job.
129
+
130
+ ## Next
131
+
132
+ - [Styling](https://matrajs.com/docs/styling) · because it will look wrong until you read it.
133
+
134
+ - [Frameworks](https://matrajs.com/docs/frameworks) · React, Vue, Svelte, Solid and everything else.
135
+
136
+ - [Commands](https://matrajs.com/docs/commands) · what else the editor can be told to do.