@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.
package/docs/engine.md ADDED
@@ -0,0 +1,315 @@
1
+ # The Matra engine
2
+
3
+ Matra runs on its own engine, called the Matra engine the way Tiptap's is
4
+ called ProseMirror and Lexical's is Lexical: the document model, content
5
+ expressions, transforms, position mapping, editor state, history, and the
6
+ editable view, all under `packages/core/src/engine/`. Nothing in it depends on
7
+ another editor framework, and no type from it appears in a public signature.
8
+
9
+ This file is its history and its notes. It began as a plan to remove
10
+ ProseMirror layer by layer, and the plan is kept as written because the
11
+ reasoning still holds.
12
+
13
+ ## Removing ProseMirror
14
+
15
+ Decision: Matra owns its engine. ProseMirror is being replaced layer by layer,
16
+ not ripped out — the public API leaks no engine type, so each layer can be
17
+ swapped without users noticing.
18
+
19
+ ## Method
20
+
21
+ Phase 1 was a true strangler: keymap, input rules, history and list commands
22
+ each replaced a package and each dropped a dependency, with the suite green
23
+ throughout.
24
+
25
+ **Phases 2–5 cannot work that way.** The remaining packages are mutually coupled
26
+ through the document model:
27
+
28
+ prosemirror-transform → prosemirror-model
29
+ prosemirror-state → prosemirror-model, transform, view
30
+ prosemirror-view → prosemirror-model, state, transform
31
+
32
+ PM's transform builds and consumes PM `Node` instances, so our model cannot be
33
+ handed to it. Model, transform, state and view therefore land together as a
34
+ parallel engine and flip in one cutover.
35
+
36
+ That means no dependency count moves until the whole thing is done, and the
37
+ cutover is the risky moment rather than a series of small ones. To keep it
38
+ honest:
39
+
40
+ 1. Build the parallel engine under `packages/core/src/engine/`.
41
+ 2. Test each layer directly, in isolation, as it is written.
42
+ 3. Before the flip, run the entire existing suite against the new engine behind
43
+ a switch — both engines pass, or the flip does not happen.
44
+ 4. Keep the ProseMirror view available behind a flag until the new view has
45
+ survived real users on iOS Safari and Android Chrome.
46
+
47
+ ## Phases
48
+
49
+ | Phase | Layer | Lines | gz | Status |
50
+ |---|---|---|---|---|
51
+ | 1 | keymap, input rules, history, list commands | ~1,700 | 21 kB | **done** |
52
+ | 2 | model — nodes, marks, fragments, schema, content expressions, DOM parse/serialize | ~3,500 | 30 kB | **done** |
53
+ | 3 | transform — steps, position mapping, rebasing | ~2,200 | 19 kB | **done** |
54
+ | 4 | state — transactions, selection, plugins | ~1,000 | 9 kB | **done** |
55
+ | 5 | view — contenteditable, IME, selection sync | ~6,000 | 59 kB | **done** |
56
+
57
+ ## Phase 2 notes
58
+
59
+ `engine/model/content-expression.ts` is done: a tokenizer and parser for the
60
+ content language (`paragraph block*`, `(text | image)+`, `heading{1,3}`), an NFA
61
+ compiler, and a subset construction to a DFA of `ContentMatch` states.
62
+
63
+ `fillBefore` is the piece worth pointing at. When a match cannot legally end, it
64
+ breadth-first searches for the shortest run of fillable types that would close
65
+ it — which is how the editor repairs a document instead of refusing an edit. A
66
+ type marked `fillable: false` is never used to repair, so a node that needs real
67
+ attributes is never invented out of nothing.
68
+
69
+ Written since: `mark.ts` (mark sets, rank ordering, exclusion), `fragment.ts`
70
+ (immutable runs, text joining, cutting, boundary-correct `findIndex`),
71
+ `node.ts` (sizes, classification, text extraction, descendant walking) and
72
+ `schema.ts` (NodeType, content compilation, `createAndFill`).
73
+
74
+ Two decisions worth remembering:
75
+
76
+ - **Text nodes are canonicalised on construction.** Adjacent text carrying
77
+ identical marks is merged and empty text is dropped, so two documents that
78
+ mean the same thing compare equal.
79
+ - **`createAndFill` returns null rather than guessing.** If closing a content
80
+ gap would need a node whose attributes have no defaults, the caller is told
81
+ the edit is impossible instead of receiving a malformed document.
82
+
83
+ Phase 2 is complete: `resolved-pos.ts` (ancestor chains, neighbours, marks at a
84
+ position, shared depth, block ranges) and the DOM layer.
85
+
86
+ Two behaviours in the DOM layer are deliberate and worth keeping:
87
+
88
+ - **An unrecognised element is transparent.** The parser descends into it rather
89
+ than dropping it, so pasting from a word processor keeps the text instead of
90
+ losing it to a `<div>` nobody wrote a rule for.
91
+ - **Loose inline content gets wrapped.** Pasting bare text produces inline nodes
92
+ with no parent block; they are wrapped in the default textblock rather than
93
+ discarded, because discarding them loses the paste.
94
+
95
+ Next: phase 3, transform — steps and position mapping.
96
+
97
+ ## Phase 3 notes
98
+
99
+ `step-map.ts` is the crown jewel: flat `[start, oldSize, newSize]` triples, an
100
+ `assoc` argument deciding which side of an insertion point a position lands on,
101
+ and `deleted` reporting when a position was inside a span that no longer exists.
102
+
103
+ It is fuzzed, not just sampled — 500 deterministic seeds asserting that mapping
104
+ never moves a position backwards past an earlier one, that inverting returns
105
+ every position outside a change exactly, and that a chain of maps equals
106
+ applying them one at a time.
107
+
108
+ **A property the fuzz forced us to state honestly:** a deletion collapses both
109
+ edges of its span onto one point. Deleting `[5,6)` sends both 5 and 6 to 5, and
110
+ inverting cannot know which it came from — that information is genuinely gone.
111
+ Round-trip is exact only for positions strictly outside the changed span; on the
112
+ boundary, `assoc` picks a side. The first version of the test asserted a
113
+ stronger property than reality allows and had to be corrected, not the code.
114
+
115
+ `step.ts` covers replace, addMark and removeMark, each able to invert itself.
116
+ The replacement planner handles all four shapes a cross-block range can take —
117
+ both ends inside blocks (the blocks join, which is what backspace at a boundary
118
+ means), one end inside, or both on boundaries. Anything deeper than one level
119
+ of nesting returns null so the step fails loudly rather than producing a
120
+ malformed document.
121
+
122
+ Rebasing is done. `Step.map` moves a step over changes made underneath it and
123
+ returns null when there is nothing left to act on. One rule there is worth
124
+ keeping: a step that meant *replace this text* whose text has since been
125
+ deleted must not degrade into *insert this text here*. Without that check, a
126
+ rebased AI rewrite pastes itself into a paragraph the user already deleted.
127
+
128
+ ## Phase 4 notes
129
+
130
+ `selection.ts`, `transaction.ts`, `state.ts` and `plugin.ts`.
131
+
132
+ - Selections snap to positions text can actually occupy, so nothing downstream
133
+ has to re-check. A NodeSelection whose node is deleted degrades to a caret
134
+ rather than pointing at nothing.
135
+ - A transaction remaps its own selection after every step, so the caret stays
136
+ where the user would expect as the document moves under it.
137
+ - Setting the selection clears stored marks: typing after moving the caret
138
+ should not inherit bold from somewhere else.
139
+ - `state.apply` returns *the same state object* when a plugin vetoes, so callers
140
+ can compare by identity to know whether anything happened.
141
+
142
+ ## Phase 5 notes — and the cutover
143
+
144
+ The view is built on `beforeinput` rather than mutation reconciliation. The
145
+ browser announces what it is about to do, the view cancels it, applies the
146
+ equivalent change to the model, and re-renders. The DOM is therefore a
147
+ projection of the document rather than a second source of truth that has to be
148
+ diffed back.
149
+
150
+ Composition is the deliberate exception. While an IME candidate window is open
151
+ the browser is left completely alone — cancelling input mid-composition breaks
152
+ Japanese, Chinese and Korean entry outright — and the affected content is read
153
+ back when composition ends.
154
+
155
+ **One behavioural difference from ProseMirror:** the view takes over the element
156
+ it is given rather than creating a child. `editor.mount(el)` makes `el` itself
157
+ the editable surface.
158
+
159
+ ### Cutover, done
160
+
161
+ dependencies: {}
162
+
163
+ All four ProseMirror packages are gone and the entire suite passes on our
164
+ engine: 178 tests, unchanged in intent from when they ran against ProseMirror.
165
+ That was the contract, and it held.
166
+
167
+ Measured at the cutover:
168
+
169
+ | | before | after |
170
+ |---|---|---|
171
+ | runtime dependencies | 9 | **0** |
172
+ | `@matrajs/core` gzipped | 5.9 kB | 25.9 kB |
173
+ | full app bundle gzipped | 66.4 kB | **18.4 kB** |
174
+
175
+ The core package grew because the engine is now inside it. What matters to a
176
+ user is the last row: an app ships a fraction of what it did, because nothing
177
+ is pulled in that the editor does not use.
178
+
179
+ Extensions have landed since, so that last number is not today's. The current
180
+ figure is whatever `pnpm size` prints — **30 kB** for the starter kit as of
181
+ 1.0 — and it is checked in CI rather than quoted from here.
182
+
183
+ ## What 1.0 changed underneath
184
+
185
+ Measured first, then changed; the numbers are in BENCHMARKS.md. The shape of
186
+ each change, and why it is that shape:
187
+
188
+ - **Fragments past twenty-four children keep a prefix index.** `findIndex`
189
+ bisects it, and `replaceChild` carries it across by shifting the tail rather
190
+ than rebuilding it. Resolving a position near the end of a long document
191
+ stopped costing the document.
192
+ - **`nodesBetween` walks only what a range touches**, jumping to the first
193
+ child with the index and stopping at the last. `hasMark`, `removeMark`,
194
+ `setBlockType` and every extension that used to `descendants` its way
195
+ through the document to find a selected word use it.
196
+ - **Mark steps rebuild locally.** A block level swaps only the run of children
197
+ that changed, via `Fragment.replaceRange`; a textblock is rebuilt in
198
+ canonical form because text merges. Untouched blocks stay the same object,
199
+ which is what lets the renderer skip them.
200
+ - **The command context is a class with a lazy transaction.** `isActive`,
201
+ `can` and decoration hooks read the state and never start one.
202
+ - **The transaction's selection is taken as it is.** It was mapped through the
203
+ whole mapping a second time in `EditorState.apply`, after the transaction
204
+ had already moved it step by step — so `insert('X')` left the caret one
205
+ past its own text. Found by a probe, pinned by a test.
206
+ - **A block inserted at a caret inside a paragraph splits the paragraph.**
207
+ The rule button and the `---` shortcut both asked for that and were refused.
208
+ - **Decorations are compared after mapping**, and the span where they differ
209
+ joins the span the edit touched. A textblock whose runs kept their shape has
210
+ its text nodes updated in place. A node decoration that moved rebuilds the
211
+ element it left as well as the one it reached.
212
+ - **The parser compiles each selector once** and indexes rules by tag. Inside
213
+ `<pre>`, and inside any node that says `code`, whitespace is literal.
214
+ - **Two editors from one extension array share the compiled schema**, the
215
+ parser, the serializer, the command table and the input rules.
216
+ - **Position markers are held weakly**, and the mapping log is swept every
217
+ 256 changes for what no live marker still needs.
218
+ - **Attributes can be added to nodes another extension owns**, rendered onto
219
+ the element and read back on parse. `textAlign` now works on the paragraph
220
+ in the box; before, it silently did nothing unless you wrote your own.
221
+
222
+ ## What typing costs
223
+
224
+ A keystroke is the operation everything else is measured against, and it took
225
+ three rounds of profiling to stop it costing the length of the document. The
226
+ current shape, and why each piece is that shape:
227
+
228
+ - **The document is rebuilt around one child, not by cutting.** An edit inside a
229
+ paragraph rebuilds every ancestor between it and the root. Doing that by
230
+ cutting the ancestor's children in two and appending the replacement back into
231
+ the middle walks the whole run three times and re-adds every child's size to
232
+ reach a total that differs from the old one by exactly one child.
233
+ `Fragment.replaceChild` copies the array once and does the size arithmetic in
234
+ a subtraction. Text is the exception — text nodes merge with their neighbours,
235
+ so the canonical form still has to be rebuilt when either side is text.
236
+ - **The diff asks before it touches the DOM.** `childNodes` is a live list, and
237
+ the patch loop used to index it for every child before deciding whether that
238
+ child was inside the edit at all. On two thousand blocks, 1999 of those reads
239
+ were thrown away.
240
+ - **The position map reuses its entries.** Re-recording is what a patch does to
241
+ every node whose subtree it kept, and a fresh entry object per node per edit is
242
+ garbage generated to say what the old object already said.
243
+ - **A full position-map backlog drops the backlog, not the document.** The map
244
+ absorbs each edit's mapping rather than rewriting every entry, and replays the
245
+ backlog when a cold entry is read. Past sixty-four pending edits the replay
246
+ costs more than saying where everything is again — which used to mean
247
+ rebuilding the whole document's DOM, at the cost of a rebuild every
248
+ sixty-fourth keystroke and the silent loss of every mounted node view's state.
249
+ The re-record happens after the patch, because before it the positions are
250
+ still in the coordinates the edit moved away from.
251
+
252
+ Measured in Node against happy-dom, that takes a keystroke on a
253
+ 2000-paragraph document from 0.464 ms to 0.062 ms, and stops it tracking the
254
+ document's length: 0.045 ms at 20 blocks against 0.062 ms at 2000. In a browser
255
+ it is what put Matra ahead of Lexical on the row it used to lose.
256
+
257
+ The first render is a different problem with a different answer. It is within
258
+ about 15% of the floor — the cost of the browser creating the same elements with
259
+ no editor involved — so there is very little of it that is ours to remove. What
260
+ was ours: building into a document fragment and attaching it once rather than
261
+ appending block by block into a live tree, skipping the mark stack for children
262
+ that have no marks, and taking a direct path for the `[tag, 0]` shape most nodes
263
+ render as. Together, 0.77 ms to 0.59 ms for two hundred blocks in Node.
264
+
265
+ ## What is deliberately not built yet
266
+
267
+ Honesty about the gaps, since "no dependencies" can read as "complete":
268
+
269
+ - ~~**Collaborative editing.**~~ Done in `@matrajs/collab`: an authority, step
270
+ exchange, rebasing of unsent work over remote edits, and remote cursors drawn
271
+ as decorations, each one mapped through local steps rather than clamped.
272
+ - ~~**Node views.**~~ Done. A node type may declare `nodeView`, returning its
273
+ own DOM plus an optional `contentDOM` for children. `stopEvent` keeps the
274
+ editor's hands off interactions inside the view.
275
+
276
+ Node views forced a real fix underneath: the renderer used to call
277
+ `replaceChildren()` on every keystroke, which is O(document) per character and
278
+ would have destroyed a view's focus, scroll position and any half-finished
279
+ interaction. It now patches. Because nodes are immutable, an edit inside one
280
+ paragraph leaves every other paragraph as literally the same object, so
281
+ identity alone skips most of the tree. Inline content inside a textblock is
282
+ still rebuilt whole — it is small, and mark wrappers make its DOM shape
283
+ diverge from the fragment.
284
+ - **Decorations.** No inline highlights or widgets independent of the document.
285
+ - **Drag and drop**, and **tables**.
286
+ - **Deep nesting in replace.** A cross-block range nested more than one level
287
+ deep returns null rather than guessing; it fails loudly, but it fails.
288
+
289
+ ## Where the risk actually is
290
+
291
+ **Phase 3 is the correctness risk.** Position mapping is what makes a late AI
292
+ edit land on the right words. A subtle bug there corrupts documents silently,
293
+ which is the exact failure Matra is sold against. It needs property-based tests:
294
+ invert-and-reapply round-trips, mapping associativity, and fuzzed step sequences
295
+ compared against a reference implementation.
296
+
297
+ **Phase 5 remains the schedule risk, and shipping it does not end that.** The
298
+ view passes its tests in happy-dom, which is not a browser. IME composition for
299
+ CJK input, Android GBoard's after-the-fact corrections, spellcheck and
300
+ autocorrect mutating the DOM, and browser-specific selection bugs are found by
301
+ real users on real devices, not by unit tests. Treat the current view as
302
+ working-but-unproven until it has survived iOS Safari and Android Chrome, and
303
+ expect a tail of fixes there rather than a clean finish.
304
+
305
+ `harness/ime` is where that gets checked: a page to open on a real phone that
306
+ watches the document and the screen for the moment they disagree, logs the
307
+ composition events the browser actually sent, and walks a checklist of the cases
308
+ that break editors. Deliberately manual — the value is in the keyboards a device
309
+ farm does not have installed.
310
+
311
+ ## Rules while this is in progress
312
+
313
+ - No ProseMirror type may enter a public signature. `types.ts` stays clean.
314
+ - Every phase keeps the full suite green; no phase lands with skipped tests.
315
+ - Bundle size is measured at each phase and recorded here, not estimated.
@@ -0,0 +1,205 @@
1
+ [
2
+ {
3
+ "slug": "readme",
4
+ "title": "Matra README",
5
+ "description": "What Matra is, every package, the extension API, and how it compares.",
6
+ "source": "README.md",
7
+ "file": "readme.md"
8
+ },
9
+ {
10
+ "slug": "engine",
11
+ "title": "The Matra engine",
12
+ "description": "How the engine is built, what typing costs, and what changed in each release.",
13
+ "source": "ENGINE.md",
14
+ "file": "engine.md"
15
+ },
16
+ {
17
+ "slug": "design",
18
+ "title": "API design",
19
+ "description": "The principles behind the API and what differs from Tiptap.",
20
+ "source": "DESIGN.md",
21
+ "file": "design.md"
22
+ },
23
+ {
24
+ "slug": "benchmarks-full",
25
+ "title": "Benchmarks",
26
+ "description": "Bundle size and speed, measured, with the method and the caveats.",
27
+ "source": "BENCHMARKS.md",
28
+ "file": "benchmarks-full.md"
29
+ },
30
+ {
31
+ "slug": "security",
32
+ "title": "Security",
33
+ "description": "What is treated as hostile and where the rendering gate is.",
34
+ "source": "SECURITY.md",
35
+ "file": "security.md"
36
+ },
37
+ {
38
+ "slug": "changelog",
39
+ "title": "Changelog",
40
+ "description": "What changed in each version.",
41
+ "source": "CHANGELOG.md",
42
+ "file": "changelog.md"
43
+ },
44
+ {
45
+ "slug": "contributing",
46
+ "title": "Contributing",
47
+ "description": "How to set up, what gets merged, house rules.",
48
+ "source": "CONTRIBUTING.md",
49
+ "file": "contributing.md"
50
+ },
51
+ {
52
+ "slug": "releasing",
53
+ "title": "Releasing",
54
+ "description": "How a release is cut.",
55
+ "source": "RELEASING.md",
56
+ "file": "releasing.md"
57
+ },
58
+ {
59
+ "slug": "ai",
60
+ "title": "AI",
61
+ "description": "Streaming edits that survive concurrent typing: the stream you supply, askAi, and what cancel, accept and reject each do.",
62
+ "source": "https://matrajs.com/docs/ai",
63
+ "file": "docs-ai.md"
64
+ },
65
+ {
66
+ "slug": "api",
67
+ "title": "API",
68
+ "description": "The editor object, the core commands, and the command context, in one page.",
69
+ "source": "https://matrajs.com/docs/api",
70
+ "file": "docs-api.md"
71
+ },
72
+ {
73
+ "slug": "benchmarks",
74
+ "title": "Benchmarks",
75
+ "description": "How the numbers on the landing page were measured, what they leave out, and how to run the harness yourself.",
76
+ "source": "https://matrajs.com/docs/benchmarks",
77
+ "file": "docs-benchmarks.md"
78
+ },
79
+ {
80
+ "slug": "collab",
81
+ "title": "Collaboration",
82
+ "description": "Step exchange, rebasing and remote cursors over a central authority — with no CRDT and no dependency.",
83
+ "source": "https://matrajs.com/docs/collab",
84
+ "file": "docs-collab.md"
85
+ },
86
+ {
87
+ "slug": "commands",
88
+ "title": "Commands",
89
+ "description": "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.",
90
+ "source": "https://matrajs.com/docs/commands",
91
+ "file": "docs-commands.md"
92
+ },
93
+ {
94
+ "slug": "document-model",
95
+ "title": "Document model",
96
+ "description": "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.",
97
+ "source": "https://matrajs.com/docs/document-model",
98
+ "file": "docs-document-model.md"
99
+ },
100
+ {
101
+ "slug": "extensions",
102
+ "title": "Writing an extension",
103
+ "description": "An extension is a plain object. Nodes, marks and behaviour, with command types inferred rather than declared.",
104
+ "source": "https://matrajs.com/docs/extensions",
105
+ "file": "docs-extensions.md"
106
+ },
107
+ {
108
+ "slug": "first-editor",
109
+ "title": "Your first editor",
110
+ "description": "A working editor in ten lines, a toolbar wired to real commands, and the four mistakes everybody makes on the first afternoon.",
111
+ "source": "https://matrajs.com/docs/first-editor",
112
+ "file": "docs-first-editor.md"
113
+ },
114
+ {
115
+ "slug": "frameworks",
116
+ "title": "Frameworks",
117
+ "description": "React, Vue, Svelte and Solid have bindings. Angular, Qwik and plain JavaScript need three calls, and here they are.",
118
+ "source": "https://matrajs.com/docs/frameworks",
119
+ "file": "docs-frameworks.md"
120
+ },
121
+ {
122
+ "slug": "docs",
123
+ "title": "Introduction",
124
+ "description": "Matra is a headless rich text editor framework. It gives you a document model, an extension API and a command system, and stays out of your interface.",
125
+ "source": "https://matrajs.com/docs/",
126
+ "file": "docs-index.md"
127
+ },
128
+ {
129
+ "slug": "installation",
130
+ "title": "Installation",
131
+ "description": "Which packages exist, which are MIT, and which need a subscription.",
132
+ "source": "https://matrajs.com/docs/installation",
133
+ "file": "docs-installation.md"
134
+ },
135
+ {
136
+ "slug": "mcp",
137
+ "title": "Docs for AI tools",
138
+ "description": "An MCP server that serves this documentation to Claude, Cursor, Codex and anything else that speaks the protocol. Zero dependencies, one command.",
139
+ "source": "https://matrajs.com/docs/mcp",
140
+ "file": "docs-mcp.md"
141
+ },
142
+ {
143
+ "slug": "position-mapping",
144
+ "title": "Position mapping",
145
+ "description": "A position is an integer into the document. The moment anything changes, that integer points somewhere else. Mapping is how a position survives an edit it did not expect.",
146
+ "source": "https://matrajs.com/docs/position-mapping",
147
+ "file": "docs-position-mapping.md"
148
+ },
149
+ {
150
+ "slug": "react",
151
+ "title": "React",
152
+ "description": "useEditor, useEditorState and EditorContent: a toolbar whose active states stay honest.",
153
+ "source": "https://matrajs.com/docs/react",
154
+ "file": "docs-react.md"
155
+ },
156
+ {
157
+ "slug": "recipes",
158
+ "title": "Recipes",
159
+ "description": "Autosave, read-only, Markdown, a table of contents, search highlighting, and the things people ask for on day one.",
160
+ "source": "https://matrajs.com/docs/recipes",
161
+ "file": "docs-recipes.md"
162
+ },
163
+ {
164
+ "slug": "shortcuts",
165
+ "title": "Keyboard shortcuts",
166
+ "description": "Every shortcut and input rule the starter kit binds.",
167
+ "source": "https://matrajs.com/docs/shortcuts",
168
+ "file": "docs-shortcuts.md"
169
+ },
170
+ {
171
+ "slug": "solid",
172
+ "title": "Solid",
173
+ "description": "createMatra: the editor, a ref, and a signal that re-runs only what reads it.",
174
+ "source": "https://matrajs.com/docs/solid",
175
+ "file": "docs-solid.md"
176
+ },
177
+ {
178
+ "slug": "styling",
179
+ "title": "Styling",
180
+ "description": "Matra ships no appearance. What you have to style, what comes with a stylesheet you can paste, and what is state rather than UI.",
181
+ "source": "https://matrajs.com/docs/styling",
182
+ "file": "docs-styling.md"
183
+ },
184
+ {
185
+ "slug": "svelte",
186
+ "title": "Svelte",
187
+ "description": "A use: action, the editor, and a store that keeps a toolbar honest. Svelte 4 and 5 from one package.",
188
+ "source": "https://matrajs.com/docs/svelte",
189
+ "file": "docs-svelte.md"
190
+ },
191
+ {
192
+ "slug": "versions",
193
+ "title": "Version history",
194
+ "description": "Snapshots, a real word-level diff between them, and restore as one undo step.",
195
+ "source": "https://matrajs.com/docs/versions",
196
+ "file": "docs-versions.md"
197
+ },
198
+ {
199
+ "slug": "vue",
200
+ "title": "Vue",
201
+ "description": "useEditor, useEditorState, useEditorFocus and EditorContent for Vue 3 — first-class rather than community-maintained.",
202
+ "source": "https://matrajs.com/docs/vue",
203
+ "file": "docs-vue.md"
204
+ }
205
+ ]