@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/readme.md ADDED
@@ -0,0 +1,604 @@
1
+ # Matra
2
+
3
+ A headless rich text editor framework with a first-class extension API.
4
+
5
+ - **No engine leakage** — the document model is plain JSON; no ProseMirror type appears in a public signature
6
+ - **Plain objects, plain functions** — no `this`, no classes, no inheritance chains
7
+ - **Inferred types** — adding an extension adds its commands, fully typed, with no module augmentation
8
+ - **Async-safe** — position mapping is built in, so a late AI response cannot corrupt the document
9
+
10
+ See [DESIGN.md](./DESIGN.md) for the API rationale, [CHANGELOG.md](./CHANGELOG.md)
11
+ for what changed when, and [CONTRIBUTING.md](./CONTRIBUTING.md) before a pull
12
+ request.
13
+
14
+ ## Packages
15
+
16
+ | Package | Purpose | Licence |
17
+ |---|---|---|
18
+ | `@matrajs/core` | Engine, document model, extension API, starter kit | MIT |
19
+ | `@matrajs/react` | `useEditor`, `useEditorState`, `useEditorFocus`, `EditorContent` | MIT |
20
+ | `@matrajs/vue` | `useEditor`, `useEditorState`, `useEditorFocus`, `EditorContent` | MIT |
21
+ | `@matrajs/svelte` | `matra` — a `use:` action, the editor, and a state store | MIT |
22
+ | `@matrajs/solid` | `createMatra` — the editor, a `mount` ref, and a state signal | MIT |
23
+ | `@matrajs/ai` | Streaming edits that survive concurrent typing | Commercial |
24
+ | `@matrajs/collab` | Authority, step rebasing, remote cursors | Commercial |
25
+ | `@matrajs/versions` | Snapshots, a real diff between them, restore as one undo step | Commercial |
26
+
27
+ Matra is মাত্রা — the horizontal line that runs across the top of Bengali
28
+ script and holds a word together. Packages live under the `@matrajs` scope,
29
+ matching matrajs.com.
30
+
31
+ **Installing a binding installs the engine with it.** For a React application
32
+ `pnpm add @matrajs/react` is the entire install: one package, and no
33
+ third-party dependency arrives behind it.
34
+
35
+ ## Quick start
36
+
37
+ ```ts
38
+ import { createEditor, starterKit } from '@matrajs/core'
39
+
40
+ const editor = createEditor({
41
+ extensions: starterKit,
42
+ content: '<p>Hello</p>',
43
+ })
44
+
45
+ editor.mount(document.querySelector('#editor')!)
46
+ editor.commands.toggleBold()
47
+ ```
48
+
49
+ Every command comes from the array you passed. Nothing else is on `editor.commands`,
50
+ and calling something that is not there is a compile error.
51
+
52
+ ## The packages in detail
53
+
54
+ Eight packages, one version number, released together. Every other package
55
+ depends on `@matrajs/core` and on nothing else, so installing a binding
56
+ installs the whole editor — there is no second package to remember, no
57
+ `@matrajs/pm` to keep in step, and no peer range to resolve by hand.
58
+
59
+ ---
60
+
61
+ ### `@matrajs/core` — MIT
62
+
63
+ The engine, and the only package that is not optional. The document model,
64
+ transforms, position mapping, editor state and the editable view are written
65
+ here, with **zero runtime dependencies**.
66
+
67
+ ```sh
68
+ pnpm add @matrajs/core
69
+ ```
70
+
71
+ **Entry points**
72
+
73
+ | Export | What it is |
74
+ |---|---|
75
+ | `createEditor(options)` | Builds an editor. The `extensions` array decides everything else about it. |
76
+ | `buildSchema(extensions)` | The schema alone, for validating a document with no view and no DOM. |
77
+ | `pos(…)`, `range(…)` | Constructors for the two position types. |
78
+ | `starterKit` | Seventeen extensions in one array — document, paragraph, text, heading, blockquote, code block, bullet/ordered/list item, horizontal rule, hard break, bold, italic, strike, code, link, history. |
79
+ | 79 named extensions | Every entry in [Extensions](#extensions), each importable on its own. |
80
+ | Helpers | `tableOfContents(doc)`, `assignIds(doc)`, `commentRanges(doc)`, `activeSuggestion(editor)`, `searchEmoji(query)`, `youtubeId(url)`, `normalizeUrl(text)`, `fieldsIn(doc)`, `fillFieldsIn(doc, values)`, `hashtagsIn(doc)`, `parseDelimited(text)`, `dictationSupported()` — plain functions, not extensions. |
81
+ | `toMarkdown`, `fromMarkdown` | Pure string work, so they run in Node, in a worker and at the edge. |
82
+ | `…CSS` helpers | `placeholderCSS`, `commentCSS`, `taskListCSS`, `dragHandleCSS`, `suggestionCSS`, `searchCSS`, `lockedCSS`, `fieldsCSS`, `columnsCSS`, `footnotesCSS` and the rest — stylesheets to paste into an app rather than a stylesheet to import. |
83
+
84
+ **`EditorOptions`**
85
+
86
+ | Field | Type | Notes |
87
+ |---|---|---|
88
+ | `extensions` | `readonly AnyDef[]` | Declare it `as const`. The tuple is what makes the commands infer. |
89
+ | `content` | `DocNode \| string` | Document JSON, or HTML to parse. |
90
+ | `editable` | `boolean` | |
91
+ | `autofocus` | `boolean \| 'start' \| 'end'` | |
92
+ | `element` | `HTMLElement` | Mount as soon as the editor exists, instead of calling `mount` yourself. |
93
+
94
+ **The editor**
95
+
96
+ | Member | Signature | |
97
+ |---|---|---|
98
+ | `commands` | `CommandsOf<T> & CoreCommands` | Only what the extensions you passed provide. Anything else is a compile error. |
99
+ | `can` | same shape | Asks instead of does, so a button can be disabled rather than dead. |
100
+ | `batch(run)` | `=> boolean` | Several commands, one undo step. Rolls back entirely if any returns `false`. |
101
+ | `isActive(name, attrs?)` | `=> boolean` | Marks first, then nodes · `isActive('heading', { level: 2 })` reads naturally. |
102
+ | `getJSON()` | `=> DocNode` | |
103
+ | `getHTML()` | `=> string` | Answers without a DOM. |
104
+ | `getText()` | `=> string` | |
105
+ | `setContent(content)` | `=> void` | |
106
+ | `selection` | `Selection` | |
107
+ | `editable` / `setEditable(v)` | | |
108
+ | `on(event, fn)` | `=> () => void` | `change`, `focus`, `blur`, `selectionChange`. Returns its own unsubscribe. |
109
+ | `extensionState<S>(name)` | `=> S \| undefined` | How a toolbar reads a character count or a collab version without a global. |
110
+ | `mount(el)` / `destroy()` | | |
111
+ | `unsafe` | `{ view, state, schema }` | Excluded from semver. Needing it means the public API has a gap — open an issue. |
112
+
113
+ **Core commands**, present whatever you pass: `select`, `insert`, `replace`,
114
+ `remove`, `moveBlock`, `focus`. `insert` and `replace` accept blocks at a
115
+ caret inside a paragraph and split the paragraph around them, which is what
116
+ a rule or a table asked for at the caret means.
117
+
118
+ **What an extension may declare**, beyond commands, keys and input rules:
119
+
120
+ | Field | On | What it does |
121
+ |---|---|---|
122
+ | `attributes` | extension | Add attributes to nodes and marks defined elsewhere · `[{ types: ['paragraph', 'heading'], attrs: { indent: { default: 0, render, parse } } }]`. How `textAlign`, `indent` and `uniqueId` work without the paragraph knowing about them. |
123
+ | `handlePaste(ctx, { html, text, files })` | extension | Claim a paste before the editor parses it. Return `true` to keep it. |
124
+ | `handleDrop(ctx, { html, text, files, pos })` | extension | The same for something dropped from outside. Block drags inside the editor never reach it. |
125
+ | `filterChange(ctx)` | extension | Veto a change before it lands. Return `false` and the document, the selection and the undo history stay as they were · how `locked()` refuses a keystroke, a paste and a drag alike. `editor.can` asks it too. |
126
+ | `nodeViews` | extension | Render nodes defined elsewhere with your own DOM · `{ image: ({ node, getPos, editor }) => … }`. How `imageResize()` puts a handle on the stock image. |
127
+ | `decorations(ctx)` | extension | Draw over the document · highlights, widgets, a class on the current block. |
128
+ | `state` | extension | Reduced on every transaction · read with `editor.extensionState(name)`. |
129
+ | `code` | node | Whitespace inside is literal, so a pasted function keeps its line breaks. |
130
+ | `listItem` | node | Enter splits, Tab nests, Backspace at the start lifts. |
131
+ | `marks` | node | Which marks the text may carry · `''` for none. |
132
+ | `nodeView` | node | Render with your own DOM and keep it across edits. |
133
+
134
+ ---
135
+
136
+ ### `@matrajs/react` — MIT
137
+
138
+ ```sh
139
+ pnpm add @matrajs/react
140
+ ```
141
+
142
+ | Export | Signature |
143
+ |---|---|
144
+ | `useEditor(options)` | `Editor<T>` — created lazily on first render, destroyed on unmount. |
145
+ | `useEditorState(editor, select)` | `S` — a `useSyncExternalStore` subscription to `change` and `selectionChange`. |
146
+ | `useEditorFocus(editor)` | `boolean` |
147
+ | `EditorContent` | `{ editor }` plus every `div` attribute. |
148
+
149
+ ```tsx
150
+ import { starterKit } from '@matrajs/core'
151
+ import { EditorContent, useEditor, useEditorState } from '@matrajs/react'
152
+
153
+ export function Notes() {
154
+ const editor = useEditor({ extensions: starterKit, content: '<p>Hello</p>' })
155
+ const bold = useEditorState(editor, (e) => e.isActive('bold'))
156
+
157
+ return (
158
+ <>
159
+ <button onClick={() => editor.commands.toggleBold()} aria-pressed={bold}>
160
+ Bold
161
+ </button>
162
+ <EditorContent editor={editor} className="prose" />
163
+ </>
164
+ )
165
+ }
166
+ ```
167
+
168
+ Options are read once. Changing them later does not recreate the editor,
169
+ because tearing down a live document on a prop change loses the user's work —
170
+ use the commands instead. The mount is guarded on `unsafe.view`, so StrictMode's
171
+ double invoke cannot leave two views fighting over one element.
172
+
173
+ ---
174
+
175
+ ### `@matrajs/vue` — MIT
176
+
177
+ The same four names as React, returning refs.
178
+
179
+ ```sh
180
+ pnpm add @matrajs/vue
181
+ ```
182
+
183
+ | Export | Signature |
184
+ |---|---|
185
+ | `useEditor(options)` | `Editor<T>`, `markRaw`ped · works in a component or a bare effect scope. |
186
+ | `useEditorState(editor, select)` | `Readonly<Ref<S>>` |
187
+ | `useEditorFocus(editor)` | `Readonly<Ref<boolean>>` |
188
+ | `EditorContent` | Component with an `editor` prop. |
189
+
190
+ ```vue
191
+ <script setup lang="ts">
192
+ import { starterKit } from '@matrajs/core'
193
+ import { EditorContent, useEditor, useEditorState } from '@matrajs/vue'
194
+
195
+ const editor = useEditor({ extensions: starterKit })
196
+ const bold = useEditorState(editor, (e) => e.isActive('bold'))
197
+ </script>
198
+
199
+ <template>
200
+ <button :aria-pressed="bold" @click="editor.commands.toggleBold()">Bold</button>
201
+ <EditorContent :editor="editor" />
202
+ </template>
203
+ ```
204
+
205
+ The mount is guarded, so a `<KeepAlive>` remount does not attach a second view.
206
+
207
+ ---
208
+
209
+ ### `@matrajs/svelte` — MIT
210
+
211
+ Svelte already has the right shape — an action runs when the element exists and
212
+ is told when it goes away — so the binding is thin on purpose. Written with
213
+ stores rather than runes, so it behaves identically on Svelte 4 and 5.
214
+
215
+ ```sh
216
+ pnpm add @matrajs/svelte
217
+ ```
218
+
219
+ | Export | Signature |
220
+ |---|---|
221
+ | `matra(options)` | `{ action, editor, state }` |
222
+ | `editorState(editor)` | `Readable<Editor<T>>` — republishes on change and selection. |
223
+
224
+ ```svelte
225
+ <script>
226
+ import { starterKit } from '@matrajs/core'
227
+ import { matra } from '@matrajs/svelte'
228
+
229
+ const { action, editor, state } = matra({ extensions: starterKit })
230
+ </script>
231
+
232
+ <button aria-pressed={$state.isActive('bold')} onclick={() => editor.commands.toggleBold()}>
233
+ Bold
234
+ </button>
235
+ <div use:action></div>
236
+ ```
237
+
238
+ The editor exists before the element does, so commands, `content` and
239
+ `getJSON()` all work before anything is on screen — which is what a server
240
+ render and a test both need.
241
+
242
+ ---
243
+
244
+ ### `@matrajs/solid` — MIT
245
+
246
+ Solid's reactivity is not a render loop, so there is no `useSyncExternalStore`
247
+ shape to reach for: a signal that bumps on every change is enough.
248
+
249
+ ```sh
250
+ pnpm add @matrajs/solid
251
+ ```
252
+
253
+ | Export | Signature |
254
+ |---|---|
255
+ | `createMatra(options)` | `{ editor, mount, state }` — bound to the component's lifetime. |
256
+
257
+ ```tsx
258
+ import { starterKit } from '@matrajs/core'
259
+ import { createMatra } from '@matrajs/solid'
260
+
261
+ const { editor, mount, state } = createMatra({ extensions: starterKit })
262
+
263
+ return (
264
+ <>
265
+ <button aria-pressed={state().isActive('bold')} onClick={() => editor.commands.toggleBold()}>
266
+ Bold
267
+ </button>
268
+ <div ref={mount} />
269
+ </>
270
+ )
271
+ ```
272
+
273
+ `state()` returns the editor itself rather than a copy: a toolbar asks
274
+ `isActive` at render time, and cloning a document to answer that would be the
275
+ expensive way to do nothing.
276
+
277
+ ---
278
+
279
+ ### `@matrajs/ai` — Commercial
280
+
281
+ Streaming edits that survive concurrent typing. The range being rewritten is
282
+ re-resolved against the current document on every chunk, so a user who keeps
283
+ typing while the model streams does not end up with a corrupted paragraph.
284
+
285
+ ```sh
286
+ pnpm add @matrajs/ai
287
+ ```
288
+
289
+ | Export | What it is |
290
+ |---|---|
291
+ | `ai(options)` | The extension. `{ stream, onStatus? }`. |
292
+ | `AiStream` | `(request: AiRequest) => AsyncIterable<string>` — yours to implement. |
293
+ | `AiRequest` | `{ text, instruction, signal }` |
294
+ | `AiSession` | `{ id, status, range, received, error? }` |
295
+ | `AiStatus` | `'idle' \| 'streaming' \| 'done' \| 'error' \| 'cancelled'` |
296
+
297
+ Commands: `askAi(instruction)`, `cancelAi()`, `acceptAi()`, `rejectAi()`.
298
+
299
+ ```ts
300
+ import { createEditor, starterKit } from '@matrajs/core'
301
+ import { ai } from '@matrajs/ai'
302
+
303
+ const editor = createEditor({
304
+ extensions: [
305
+ ...starterKit,
306
+ ai({
307
+ async *stream({ text, instruction, signal }) {
308
+ const response = await fetch('/api/rewrite', {
309
+ method: 'POST',
310
+ body: JSON.stringify({ text, instruction }),
311
+ signal,
312
+ })
313
+ for await (const chunk of response.body!.pipeThrough(new TextDecoderStream())) yield chunk
314
+ },
315
+ onStatus: (session) => setSpinner(session.status === 'streaming'),
316
+ }),
317
+ ] as const,
318
+ })
319
+
320
+ editor.commands.askAi('make this shorter')
321
+ ```
322
+
323
+ `stream` runs in your application, so the model key stays on your server. The
324
+ extension never talks to us.
325
+
326
+ ---
327
+
328
+ ### `@matrajs/collab` — Commercial
329
+
330
+ Step exchange, rebasing and presence, with **no CRDT dependency**. Another
331
+ client's work rebases over unsent local work without either being lost.
332
+
333
+ ```sh
334
+ pnpm add @matrajs/collab
335
+ ```
336
+
337
+ | Export | What it is |
338
+ |---|---|
339
+ | `collab(options)` | The extension. `{ clientId, version? }`. |
340
+ | `Authority` | The server side · `receive(version, steps)` and `since(version)`. Transport-agnostic. |
341
+ | `sendableSteps(editor)` | `Sendable \| null` — what to put on the wire. |
342
+ | `getVersion(editor)` | `number` |
343
+ | `remoteCursors()` | The presence extension. |
344
+ | `colorFor(clientId)` | A stable colour per client. |
345
+ | `remoteCursorCSS` | The stylesheet the cursor decorations expect. |
346
+ | `CollabStep`, `Presence`, `Sendable`, `CollabState` | Wire types. |
347
+
348
+ Command: `receiveCollabSteps(steps)` — steps this client sent are skipped, and a
349
+ step that no longer applies is dropped rather than thrown, because one bad
350
+ message from a peer must not take the editor down.
351
+
352
+ ```ts
353
+ import { createEditor, starterKit } from '@matrajs/core'
354
+ import { collab, remoteCursors, sendableSteps } from '@matrajs/collab'
355
+
356
+ const editor = createEditor({
357
+ extensions: [...starterKit, collab({ clientId: 'me' }), remoteCursors()] as const,
358
+ })
359
+
360
+ editor.on('change', () => {
361
+ const sendable = sendableSteps(editor)
362
+ if (sendable) socket.send(JSON.stringify(sendable))
363
+ })
364
+
365
+ socket.onmessage = (event) => editor.commands.receiveCollabSteps(JSON.parse(event.data))
366
+ ```
367
+
368
+ `Authority` is a plain class with no server attached — run it in a WebSocket
369
+ handler, a Durable Object, or a test.
370
+
371
+ ---
372
+
373
+ ### `@matrajs/versions` — Commercial
374
+
375
+ Snapshots, a real diff between them, and restore as one undo step.
376
+
377
+ ```sh
378
+ pnpm add @matrajs/versions
379
+ ```
380
+
381
+ | Export | What it is |
382
+ |---|---|
383
+ | `versions(options)` | The extension. `{ now?, idleMs?, keep?, onChange?, store? }`. |
384
+ | `versionList(editor)` | `Version[]` |
385
+ | `localVersionStore(key)` | A `VersionStore` on `localStorage`. |
386
+ | `diffDocs(a, b)` | `DocDiff` — block-level changes between two documents. |
387
+ | `diffWords(a, b)` | `WordRun[]` |
388
+ | `blockStarts`, `sizeOf`, `textOf` | The primitives the diff is built from. |
389
+ | `versionClasses`, `versionDiffCSS` | Class names and the stylesheet for preview decorations. |
390
+ | `Version` | `{ id, label, at, doc, size }` |
391
+
392
+ Commands: `snapshotVersion(label?)`, `restoreVersion(id)`,
393
+ `previewVersion(id | null)`, `forgetVersion(id)`.
394
+
395
+ ```ts
396
+ import { createEditor, starterKit } from '@matrajs/core'
397
+ import { localVersionStore, versionList, versions } from '@matrajs/versions'
398
+
399
+ const editor = createEditor({
400
+ extensions: [
401
+ ...starterKit,
402
+ versions({
403
+ idleMs: 30_000,
404
+ keep: 50,
405
+ store: localVersionStore('doc-42'),
406
+ onChange: (state) => render(state.versions, state.diff),
407
+ }),
408
+ ] as const,
409
+ })
410
+
411
+ editor.commands.snapshotVersion('before the rewrite')
412
+ editor.commands.previewVersion(versionList(editor)[0].id)
413
+ ```
414
+
415
+ `idleMs: null` turns automatic snapshots off and leaves them to
416
+ `snapshotVersion`. A version per keystroke is not history, it is a keylogger
417
+ with a nicer name. `now` is injected rather than reached for, so a test does not
418
+ have to sleep to make two versions differ.
419
+
420
+
421
+ ---
422
+
423
+ ## Security
424
+
425
+ Document JSON, pasted HTML and collaborative steps are all treated as hostile,
426
+ and the rendering path is the gate they all pass through: executable attributes
427
+ are never set, URL attributes are scheme-checked, undeclared attributes are
428
+ dropped, and commands report failure rather than throwing. See
429
+ [SECURITY.md](./SECURITY.md).
430
+
431
+ ## Development
432
+
433
+ ```bash
434
+ pnpm install
435
+ pnpm dev # playground at localhost:5173
436
+ pnpm test # vitest
437
+ pnpm typecheck # tsc, including the type-level tests
438
+ pnpm check # biome, and prettier for .astro
439
+ pnpm build # tsup, all packages
440
+ pnpm size # the bundle ladder the site quotes
441
+ pnpm bench:check # the performance ratchet, against the recorded baseline
442
+ pnpm links # no dead internal links on the site
443
+ pnpm packaging # every built package imports and requires (run after build)
444
+ pnpm wiring # every script on the site finds the markup it asks for
445
+ pnpm install:matrix # pack every package, npm-install it into fresh Vite apps for each framework, build and run them
446
+ pnpm facts # the counts the site prints — tests, adversarial tests, extensions
447
+ ```
448
+
449
+ ## Status
450
+
451
+ 1.0 — the Matra engine, end to end. Document model, transforms, position
452
+ mapping, editor state and the editable view are written from scratch, with
453
+ **zero runtime dependencies**. 779 tests, 68 of them adversarial, and every
454
+ package is installed with plain npm into a fresh React, Vue, Svelte, Solid
455
+ and vanilla Vite app and built there before a release (`pnpm install:matrix`).
456
+
457
+ An app on the starter kit bundles **30 kB gzipped**, because nothing arrives
458
+ that the editor does not use — seventy-nine extensions ship in the package
459
+ and none of them is in the bundle until it is in the array. The whole ladder,
460
+ from an empty extension array upwards, is measured by `pnpm size` and checked
461
+ in CI. It was 25 kB at 0.16; what the five kilobytes bought is listed in
462
+ [CHANGELOG.md](./CHANGELOG.md).
463
+
464
+ Drag and drop landed in 0.9.0: blocks drag with a handle, a line shows where
465
+ they will land, and the move is one undo step.
466
+
467
+ The view passes its tests but has not yet met real IME users on iOS Safari or
468
+ Android Chrome. See [ENGINE.md](./ENGINE.md) for where the risk actually sits,
469
+ and [harness/ime](./harness/ime) for the page that checks it on a real device.
470
+
471
+ ## Extensions
472
+
473
+ Everything in the box, and everything free unless marked.
474
+
475
+ | | | |
476
+ |---|---|---|
477
+ | **Text** | bold, italic, strike, code, underline, highlight, subscript, superscript, link, **text style**, **kbd** | colour, background, font family and size, as one mark |
478
+ | **Blocks** | paragraph, heading, blockquote, code block, horizontal rule, hard break, image, **callout**, **details** | a Notion callout and a collapsible toggle |
479
+ | **Embeds** | **YouTube**, **any embed page** in a sandboxed frame, **image resize** with a handle | allowlisted hosts only; the width lands in the HTML |
480
+ | **Templates** | **locked blocks**, **fields**, **snippets** | a contract with fixed clauses, a mail merge with no editor, words that expand as typed |
481
+ | **Layout** | **columns**, **page break**, **line height**, **text direction** | two to six columns, a real break in print, right-to-left detected from the text |
482
+ | **Scholarly** | **footnotes**, **math** inline and display | numbered by position; KaTeX or MathJax plug in, or the source shows |
483
+ | **Lists** | bulleted, ordered, **task lists** with real checkboxes | |
484
+ | **Tables** | insert, delete, header rows, colspan and rowspan, **add and remove rows and columns, Tab between cells** | spanning cells widen rather than split |
485
+ | **Writing** | placeholder, character count, text align, **indent**, **typography**, **emoji shortcodes**, **autolink**, **clear formatting**, **text case**, **invisible characters**, **selection highlight**, **typewriter scrolling**, **autosave**, **smart paste**, **hashtags** | smart quotes, dashes, arrows · `:tada:` · URLs link as you type · tab-separated text becomes a table |
486
+ | **Finding** | **search and replace** | incremental: typing rescans one paragraph |
487
+ | **Code** | **syntax highlighting** as decorations | a built-in tokeniser, or plug in Shiki, Prism or lowlight |
488
+ | **Structure** | **table of contents**, **unique block ids**, **focus class**, **trailing node** | Tiptap charges for the first two |
489
+ | **Interchange** | **Markdown in and out**, with no DOM | runs on a server |
490
+ | **Dragging** | block drag and drop, **drag handle**, drop cursor, **files dropped or pasted** | Tiptap charges for the handle and the file handler |
491
+ | **Review** | threaded comments anchored to ranges | Tiptap charges for these |
492
+ | **Menus** | `@` mentions and `/` commands, detection only, **bubble and floating menus** for your element | the popup is yours |
493
+ | **Assistance** | **ghost text** completion from any source, **dictation** through the browser's recogniser | Tab takes the suggestion; nothing is sent anywhere the browser does not already send it |
494
+ | **Paid** | AI streaming, collaboration with remote cursors, version history | |
495
+
496
+ Everything Tiptap puts behind its Pro tier that fits in a week — table of
497
+ contents, unique ids, the drag handle, comments, the file handler, emoji,
498
+ details — is free here. That is the deliberate shape of it: the things that
499
+ take a week are free and drive adoption, and the ones that took months are
500
+ what you pay for.
501
+
502
+ ### Adding one, step by step
503
+
504
+ Every extension follows the same four steps. Search and replace, as the
505
+ example:
506
+
507
+ 1. **Import it** from `@matrajs/core` — the binding you installed already
508
+ depends on it, so there is nothing to add to `package.json`.
509
+ 2. **Put it in the array.** Extensions that take options are functions;
510
+ the rest are plain objects.
511
+ 3. **Call its commands.** They are on `editor.commands`, typed from the
512
+ array, so a typo is a compile error.
513
+ 4. **Paste its CSS** if it has any. Extensions that draw something export a
514
+ `…CSS` string; the editor ships no appearance of its own.
515
+
516
+ ```ts
517
+ import { createEditor, search, searchCSS, starterKit } from '@matrajs/core'
518
+
519
+ const editor = createEditor({ extensions: [...starterKit, search()] as const })
520
+
521
+ editor.commands.setSearch({ query: 'colour', wholeWord: true })
522
+ editor.commands.nextMatch() // selects it, so the view scrolls there
523
+ editor.commands.replaceMatch('color')
524
+ editor.commands.replaceAllMatches('color') // one undo step
525
+ editor.extensionState('search') // { matches, current, query, … } for a panel
526
+
527
+ document.head.appendChild(Object.assign(document.createElement('style'), { textContent: searchCSS }))
528
+ ```
529
+
530
+ The same shape for the rest: `textStyle` then `editor.commands.setColor('#c00')`;
531
+ `callout` then `toggleCallout('warning')`; `...detailsKit` then
532
+ `insertDetails()`; `youtube` then `insertYoutube({ src: url })`;
533
+ `fileHandler({ accept: ['image/'], onDrop })` then upload in `onDrop` and
534
+ insert at `marker.map(pos)`; `...tableKit` then `insertTable(3, 3)` and
535
+ `addRowAfter()`. Each is one row in the directory on
536
+ [matrajs.com/extensions](https://matrajs.com/extensions), with the line you
537
+ would write.
538
+
539
+ `toMarkdown` and `fromMarkdown` are pure string work rather than a trip through
540
+ HTML, so they run in Node, in a worker, and at the edge. Turning a document into
541
+ Markdown on a server does not need a DOM polyfill.
542
+
543
+ ## Against the alternatives
544
+
545
+ Measured, not asserted — see [BENCHMARKS.md](./BENCHMARKS.md) for the method and
546
+ what the numbers are not.
547
+
548
+ | | Matra | Tiptap | Lexical | Slate |
549
+ |---|---|---|---|---|
550
+ | Bundle, gzipped | **30 kB** | 117 kB | ~35 kB | ~50 kB |
551
+ | Runtime dependencies | **0** | 51 packages | few | several |
552
+ | Engine types in your code | **none** | ProseMirror | Lexical | Slate |
553
+ | Command types | **inferred** | module augmentation | manual | manual |
554
+ | Async position safety | **built in** | manual | manual | manual |
555
+ | Vue | **first-class** | community | none | community |
556
+ | Svelte and Solid | **first-class** | community | none | community |
557
+ | Markdown without a DOM | **yes** | no | no | no |
558
+ | Table of contents | **free** | paid | build it | build it |
559
+ | Unique block ids | **free** | paid | build it | build it |
560
+ | Drag handle | **free** | paid | build it | build it |
561
+ | Comments | **free** | paid | build it | build it |
562
+ | Runtime licence check or phone-home | **never** | none | n/a | n/a |
563
+
564
+ Where the alternatives win, and it is worth saying so: ProseMirror's ecosystem
565
+ is a decade deep and Tiptap inherits all of it, Lexical has been hardened by
566
+ Meta's traffic, and both have met far more real IME users than this has. If you
567
+ need a mature extension for something exotic today, they have it and this does
568
+ not.
569
+
570
+ ## Releasing
571
+
572
+ One registry, and an order that matters. See [RELEASING.md](./RELEASING.md).
573
+ Every release is recorded in [CHANGELOG.md](./CHANGELOG.md).
574
+
575
+ ## Licence
576
+
577
+ **The core is MIT and stays that way.** `@matrajs/core` and every framework
578
+ binding — `@matrajs/react`, `@matrajs/vue`, `@matrajs/svelte` and
579
+ `@matrajs/solid` — the engine, the document model, the extension API, the
580
+ starter kit, tables, comments, every mark and node that ships in the box. No
581
+ open-core asterisk on any of it, no feature removed later to sell back.
582
+
583
+ **AI, collaboration and version history are paid.** `@matrajs/ai`,
584
+ `@matrajs/collab` and `@matrajs/versions` are source-available under the
585
+ [Matra Commercial License](./packages/ai/LICENSE):
586
+ free to evaluate, develop against, test, teach with, and use in personal
587
+ projects and small internal tools; paid per developer in production. They are
588
+ the things here that took months rather than days — streaming edits that
589
+ survive concurrent typing, rebasing another client's work over unsent local
590
+ work without losing either, and a real diff between two snapshots of a
591
+ document.
592
+
593
+ **Nothing phones home and there is no runtime licence check.** Your editor
594
+ never talks to us, in development or in production, and a lapsed subscription
595
+ cannot switch anything off in an app you already shipped.
596
+
597
+ There is no download gate either. The source is in this repository and the
598
+ packages install from public npm — the licence is the boundary, as with the
599
+ Business Source Licence. What a subscription buys is the right to run them in
600
+ production, plus updates and support.
601
+
602
+ **Versions up to 0.5.0 shipped under MIT, including `ai` and `collab`, and that
603
+ grant cannot be withdrawn.** Anyone already on 0.5.0 may stay there under MIT
604
+ forever. The commercial licence starts at 0.6.0.
@@ -0,0 +1,65 @@
1
+ # Releasing
2
+
3
+ One registry. Everything goes to public npm.
4
+
5
+ ```sh
6
+ pnpm build && pnpm test && pnpm check && pnpm typecheck
7
+
8
+ cd packages/core && pnpm publish --access public
9
+ # wait for it to actually exist before publishing anything that depends on it
10
+ until [ "$(npm view @matrajs/core version)" = "1.2.3" ]; do sleep 20; done
11
+
12
+ for p in react vue svelte solid ai collab versions; do (cd packages/$p && pnpm publish --access public); done
13
+ ```
14
+
15
+ Dependants pin `@matrajs/core` by range, so core has to be *available* — not
16
+ merely accepted — before they go out. npm processes a publish asynchronously:
17
+ `+@matrajs/core@1.2.3` means accepted, not installable. Publishing dependants
18
+ against a queued core has produced uninstallable packages here once already.
19
+
20
+ Always `pnpm publish`, never `npm publish`: npm does not convert `workspace:*`
21
+ and will ship a manifest nobody can install.
22
+
23
+ ## Why the paid packages are public too
24
+
25
+ They were nearly not. Three designs were built and discarded — a private npm
26
+ scope, GitHub Packages under a second organisation, a private repository
27
+ installed by git ref — before the obvious question got asked: what were they
28
+ protecting?
29
+
30
+ **The source is already public.** `packages/ai`, `packages/collab` and
31
+ `packages/versions` are in
32
+ this repository, and this repository is public. Anyone can read
33
+ `collab/src/collab.ts` in a browser, clone it, and build it. Every one of those
34
+ schemes guarded the npm door of a building with no walls, and each cost real
35
+ administration — an organisation to run, collaborators to add and remove, a
36
+ second repository to keep in step — to achieve nothing a determined non-payer
37
+ would even notice.
38
+
39
+ The alternative was making the repository private, which would trade the thing
40
+ that actually brings people in for a lock that a `git clone` opens.
41
+
42
+ So `@matrajs/ai`, `@matrajs/collab` and `@matrajs/versions` publish publicly,
43
+ and **the licence is
44
+ the boundary rather than the download**. That is the same arrangement as the
45
+ Business Source License and the Functional Source License: read it, build it,
46
+ run it in development, and pay when it goes to production beyond the free
47
+ threshold.
48
+
49
+ ## What actually produces revenue
50
+
51
+ Not a gate. A company with a legal department does not put an unlicensed
52
+ dependency in its build to save $99 a month — it surfaces in acquisition due
53
+ diligence and in every enterprise security review, and the exposure is thousands
54
+ of times the saving. Individuals might, and the licence already gives
55
+ individuals these packages free.
56
+
57
+ What a subscription buys is the right to use them in production, plus updates
58
+ and support. See [SELLING.md](./SELLING.md).
59
+
60
+ ## Versions
61
+
62
+ `ai` and `collab` were MIT through 0.5.0, and that grant cannot be withdrawn.
63
+ From 0.6.0 they carry the commercial licence. Nothing in either has ever checked
64
+ a licence at runtime, and nothing ever will: it would be patched out in an hour,
65
+ and until then it would sit in a customer's production waiting to fail.