{
  "markdown": "# Matra\n\nA headless rich text editor framework with a first-class extension API.\n\n- **No engine leakage** — the document model is plain JSON; no ProseMirror type appears in a public signature\n- **Plain objects, plain functions** — no `this`, no classes, no inheritance chains\n- **Inferred types** — adding an extension adds its commands, fully typed, with no module augmentation\n- **Async-safe** — position mapping is built in, so a late AI response cannot corrupt the document\n\nSee [DESIGN.md](./DESIGN.md) for the API rationale, [CHANGELOG.md](./CHANGELOG.md)\nfor what changed when, and [CONTRIBUTING.md](./CONTRIBUTING.md) before a pull\nrequest.\n\n## Packages\n\n| Package | Purpose | Licence |\n|---|---|---|\n| `@matrajs/core` | Engine, document model, extension API, starter kit | MIT |\n| `@matrajs/react` | `useEditor`, `useEditorState`, `useEditorFocus`, `EditorContent` | MIT |\n| `@matrajs/vue` | `useEditor`, `useEditorState`, `useEditorFocus`, `EditorContent` | MIT |\n| `@matrajs/svelte` | `matra` — a `use:` action, the editor, and a state store | MIT |\n| `@matrajs/solid` | `createMatra` — the editor, a `mount` ref, and a state signal | MIT |\n| `@matrajs/ai` | Streaming edits that survive concurrent typing | Commercial |\n| `@matrajs/collab` | Authority, step rebasing, remote cursors | Commercial |\n| `@matrajs/versions` | Snapshots, a real diff between them, restore as one undo step | Commercial |\n\nMatra is মাত্রা — the horizontal line that runs across the top of Bengali\nscript and holds a word together. Packages live under the `@matrajs` scope,\nmatching matrajs.com.\n\n**Installing a binding installs the engine with it.** For a React application\n`pnpm add @matrajs/react` is the entire install: one package, and no\nthird-party dependency arrives behind it.\n\n## Quick start\n\n```ts\nimport { createEditor, starterKit } from '@matrajs/core'\n\nconst editor = createEditor({\n  extensions: starterKit,\n  content: '<p>Hello</p>',\n})\n\neditor.mount(document.querySelector('#editor')!)\neditor.commands.toggleBold()\n```\n\nEvery command comes from the array you passed. Nothing else is on `editor.commands`,\nand calling something that is not there is a compile error.\n\n## The packages in detail\n\nEight packages, one version number, released together. Every other package\ndepends on `@matrajs/core` and on nothing else, so installing a binding\ninstalls the whole editor — there is no second package to remember, no\n`@matrajs/pm` to keep in step, and no peer range to resolve by hand.\n\n---\n\n### `@matrajs/core` — MIT\n\nThe engine, and the only package that is not optional. The document model,\ntransforms, position mapping, editor state and the editable view are written\nhere, with **zero runtime dependencies**.\n\n```sh\npnpm add @matrajs/core\n```\n\n**Entry points**\n\n| Export | What it is |\n|---|---|\n| `createEditor(options)` | Builds an editor. The `extensions` array decides everything else about it. |\n| `buildSchema(extensions)` | The schema alone, for validating a document with no view and no DOM. |\n| `pos(…)`, `range(…)` | Constructors for the two position types. |\n| `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. |\n| 79 named extensions | Every entry in [Extensions](#extensions), each importable on its own. |\n| 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. |\n| `toMarkdown`, `fromMarkdown` | Pure string work, so they run in Node, in a worker and at the edge. |\n| `…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. |\n\n**`EditorOptions`**\n\n| Field | Type | Notes |\n|---|---|---|\n| `extensions` | `readonly AnyDef[]` | Declare it `as const`. The tuple is what makes the commands infer. |\n| `content` | `DocNode \\| string` | Document JSON, or HTML to parse. |\n| `editable` | `boolean` | |\n| `autofocus` | `boolean \\| 'start' \\| 'end'` | |\n| `element` | `HTMLElement` | Mount as soon as the editor exists, instead of calling `mount` yourself. |\n\n**The editor**\n\n| Member | Signature | |\n|---|---|---|\n| `commands` | `CommandsOf<T> & CoreCommands` | Only what the extensions you passed provide. Anything else is a compile error. |\n| `can` | same shape | Asks instead of does, so a button can be disabled rather than dead. |\n| `batch(run)` | `=> boolean` | Several commands, one undo step. Rolls back entirely if any returns `false`. |\n| `isActive(name, attrs?)` | `=> boolean` | Marks first, then nodes · `isActive('heading', { level: 2 })` reads naturally. |\n| `getJSON()` | `=> DocNode` | |\n| `getHTML()` | `=> string` | Answers without a DOM. |\n| `getText()` | `=> string` | |\n| `setContent(content)` | `=> void` | |\n| `selection` | `Selection` | |\n| `editable` / `setEditable(v)` | | |\n| `on(event, fn)` | `=> () => void` | `change`, `focus`, `blur`, `selectionChange`. Returns its own unsubscribe. |\n| `extensionState<S>(name)` | `=> S \\| undefined` | How a toolbar reads a character count or a collab version without a global. |\n| `mount(el)` / `destroy()` | | |\n| `unsafe` | `{ view, state, schema }` | Excluded from semver. Needing it means the public API has a gap — open an issue. |\n\n**Core commands**, present whatever you pass: `select`, `insert`, `replace`,\n`remove`, `moveBlock`, `focus`. `insert` and `replace` accept blocks at a\ncaret inside a paragraph and split the paragraph around them, which is what\na rule or a table asked for at the caret means.\n\n**What an extension may declare**, beyond commands, keys and input rules:\n\n| Field | On | What it does |\n|---|---|---|\n| `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. |\n| `handlePaste(ctx, { html, text, files })` | extension | Claim a paste before the editor parses it. Return `true` to keep it. |\n| `handleDrop(ctx, { html, text, files, pos })` | extension | The same for something dropped from outside. Block drags inside the editor never reach it. |\n| `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. |\n| `nodeViews` | extension | Render nodes defined elsewhere with your own DOM · `{ image: ({ node, getPos, editor }) => … }`. How `imageResize()` puts a handle on the stock image. |\n| `decorations(ctx)` | extension | Draw over the document · highlights, widgets, a class on the current block. |\n| `state` | extension | Reduced on every transaction · read with `editor.extensionState(name)`. |\n| `code` | node | Whitespace inside is literal, so a pasted function keeps its line breaks. |\n| `listItem` | node | Enter splits, Tab nests, Backspace at the start lifts. |\n| `marks` | node | Which marks the text may carry · `''` for none. |\n| `nodeView` | node | Render with your own DOM and keep it across edits. |\n\n---\n\n### `@matrajs/react` — MIT\n\n```sh\npnpm add @matrajs/react\n```\n\n| Export | Signature |\n|---|---|\n| `useEditor(options)` | `Editor<T>` — created lazily on first render, destroyed on unmount. |\n| `useEditorState(editor, select)` | `S` — a `useSyncExternalStore` subscription to `change` and `selectionChange`. |\n| `useEditorFocus(editor)` | `boolean` |\n| `EditorContent` | `{ editor }` plus every `div` attribute. |\n\n```tsx\nimport { starterKit } from '@matrajs/core'\nimport { EditorContent, useEditor, useEditorState } from '@matrajs/react'\n\nexport function Notes() {\n  const editor = useEditor({ extensions: starterKit, content: '<p>Hello</p>' })\n  const bold = useEditorState(editor, (e) => e.isActive('bold'))\n\n  return (\n    <>\n      <button onClick={() => editor.commands.toggleBold()} aria-pressed={bold}>\n        Bold\n      </button>\n      <EditorContent editor={editor} className=\"prose\" />\n    </>\n  )\n}\n```\n\nOptions are read once. Changing them later does not recreate the editor,\nbecause tearing down a live document on a prop change loses the user's work —\nuse the commands instead. The mount is guarded on `unsafe.view`, so StrictMode's\ndouble invoke cannot leave two views fighting over one element.\n\n---\n\n### `@matrajs/vue` — MIT\n\nThe same four names as React, returning refs.\n\n```sh\npnpm add @matrajs/vue\n```\n\n| Export | Signature |\n|---|---|\n| `useEditor(options)` | `Editor<T>`, `markRaw`ped · works in a component or a bare effect scope. |\n| `useEditorState(editor, select)` | `Readonly<Ref<S>>` |\n| `useEditorFocus(editor)` | `Readonly<Ref<boolean>>` |\n| `EditorContent` | Component with an `editor` prop. |\n\n```vue\n<script setup lang=\"ts\">\nimport { starterKit } from '@matrajs/core'\nimport { EditorContent, useEditor, useEditorState } from '@matrajs/vue'\n\nconst editor = useEditor({ extensions: starterKit })\nconst bold = useEditorState(editor, (e) => e.isActive('bold'))\n</script>\n\n<template>\n  <button :aria-pressed=\"bold\" @click=\"editor.commands.toggleBold()\">Bold</button>\n  <EditorContent :editor=\"editor\" />\n</template>\n```\n\nThe mount is guarded, so a `<KeepAlive>` remount does not attach a second view.\n\n---\n\n### `@matrajs/svelte` — MIT\n\nSvelte already has the right shape — an action runs when the element exists and\nis told when it goes away — so the binding is thin on purpose. Written with\nstores rather than runes, so it behaves identically on Svelte 4 and 5.\n\n```sh\npnpm add @matrajs/svelte\n```\n\n| Export | Signature |\n|---|---|\n| `matra(options)` | `{ action, editor, state }` |\n| `editorState(editor)` | `Readable<Editor<T>>` — republishes on change and selection. |\n\n```svelte\n<script>\n  import { starterKit } from '@matrajs/core'\n  import { matra } from '@matrajs/svelte'\n\n  const { action, editor, state } = matra({ extensions: starterKit })\n</script>\n\n<button aria-pressed={$state.isActive('bold')} onclick={() => editor.commands.toggleBold()}>\n  Bold\n</button>\n<div use:action></div>\n```\n\nThe editor exists before the element does, so commands, `content` and\n`getJSON()` all work before anything is on screen — which is what a server\nrender and a test both need.\n\n---\n\n### `@matrajs/solid` — MIT\n\nSolid's reactivity is not a render loop, so there is no `useSyncExternalStore`\nshape to reach for: a signal that bumps on every change is enough.\n\n```sh\npnpm add @matrajs/solid\n```\n\n| Export | Signature |\n|---|---|\n| `createMatra(options)` | `{ editor, mount, state }` — bound to the component's lifetime. |\n\n```tsx\nimport { starterKit } from '@matrajs/core'\nimport { createMatra } from '@matrajs/solid'\n\nconst { editor, mount, state } = createMatra({ extensions: starterKit })\n\nreturn (\n  <>\n    <button aria-pressed={state().isActive('bold')} onClick={() => editor.commands.toggleBold()}>\n      Bold\n    </button>\n    <div ref={mount} />\n  </>\n)\n```\n\n`state()` returns the editor itself rather than a copy: a toolbar asks\n`isActive` at render time, and cloning a document to answer that would be the\nexpensive way to do nothing.\n\n---\n\n### `@matrajs/ai` — Commercial\n\nStreaming edits that survive concurrent typing. The range being rewritten is\nre-resolved against the current document on every chunk, so a user who keeps\ntyping while the model streams does not end up with a corrupted paragraph.\n\n```sh\npnpm add @matrajs/ai\n```\n\n| Export | What it is |\n|---|---|\n| `ai(options)` | The extension. `{ stream, onStatus? }`. |\n| `AiStream` | `(request: AiRequest) => AsyncIterable<string>` — yours to implement. |\n| `AiRequest` | `{ text, instruction, signal }` |\n| `AiSession` | `{ id, status, range, received, error? }` |\n| `AiStatus` | `'idle' \\| 'streaming' \\| 'done' \\| 'error' \\| 'cancelled'` |\n\nCommands: `askAi(instruction)`, `cancelAi()`, `acceptAi()`, `rejectAi()`.\n\n```ts\nimport { createEditor, starterKit } from '@matrajs/core'\nimport { ai } from '@matrajs/ai'\n\nconst editor = createEditor({\n  extensions: [\n    ...starterKit,\n    ai({\n      async *stream({ text, instruction, signal }) {\n        const response = await fetch('/api/rewrite', {\n          method: 'POST',\n          body: JSON.stringify({ text, instruction }),\n          signal,\n        })\n        for await (const chunk of response.body!.pipeThrough(new TextDecoderStream())) yield chunk\n      },\n      onStatus: (session) => setSpinner(session.status === 'streaming'),\n    }),\n  ] as const,\n})\n\neditor.commands.askAi('make this shorter')\n```\n\n`stream` runs in your application, so the model key stays on your server. The\nextension never talks to us.\n\n---\n\n### `@matrajs/collab` — Commercial\n\nStep exchange, rebasing and presence, with **no CRDT dependency**. Another\nclient's work rebases over unsent local work without either being lost.\n\n```sh\npnpm add @matrajs/collab\n```\n\n| Export | What it is |\n|---|---|\n| `collab(options)` | The extension. `{ clientId, version? }`. |\n| `Authority` | The server side · `receive(version, steps)` and `since(version)`. Transport-agnostic. |\n| `sendableSteps(editor)` | `Sendable \\| null` — what to put on the wire. |\n| `getVersion(editor)` | `number` |\n| `remoteCursors()` | The presence extension. |\n| `colorFor(clientId)` | A stable colour per client. |\n| `remoteCursorCSS` | The stylesheet the cursor decorations expect. |\n| `CollabStep`, `Presence`, `Sendable`, `CollabState` | Wire types. |\n\nCommand: `receiveCollabSteps(steps)` — steps this client sent are skipped, and a\nstep that no longer applies is dropped rather than thrown, because one bad\nmessage from a peer must not take the editor down.\n\n```ts\nimport { createEditor, starterKit } from '@matrajs/core'\nimport { collab, remoteCursors, sendableSteps } from '@matrajs/collab'\n\nconst editor = createEditor({\n  extensions: [...starterKit, collab({ clientId: 'me' }), remoteCursors()] as const,\n})\n\neditor.on('change', () => {\n  const sendable = sendableSteps(editor)\n  if (sendable) socket.send(JSON.stringify(sendable))\n})\n\nsocket.onmessage = (event) => editor.commands.receiveCollabSteps(JSON.parse(event.data))\n```\n\n`Authority` is a plain class with no server attached — run it in a WebSocket\nhandler, a Durable Object, or a test.\n\n---\n\n### `@matrajs/versions` — Commercial\n\nSnapshots, a real diff between them, and restore as one undo step.\n\n```sh\npnpm add @matrajs/versions\n```\n\n| Export | What it is |\n|---|---|\n| `versions(options)` | The extension. `{ now?, idleMs?, keep?, onChange?, store? }`. |\n| `versionList(editor)` | `Version[]` |\n| `localVersionStore(key)` | A `VersionStore` on `localStorage`. |\n| `diffDocs(a, b)` | `DocDiff` — block-level changes between two documents. |\n| `diffWords(a, b)` | `WordRun[]` |\n| `blockStarts`, `sizeOf`, `textOf` | The primitives the diff is built from. |\n| `versionClasses`, `versionDiffCSS` | Class names and the stylesheet for preview decorations. |\n| `Version` | `{ id, label, at, doc, size }` |\n\nCommands: `snapshotVersion(label?)`, `restoreVersion(id)`,\n`previewVersion(id | null)`, `forgetVersion(id)`.\n\n```ts\nimport { createEditor, starterKit } from '@matrajs/core'\nimport { localVersionStore, versionList, versions } from '@matrajs/versions'\n\nconst editor = createEditor({\n  extensions: [\n    ...starterKit,\n    versions({\n      idleMs: 30_000,\n      keep: 50,\n      store: localVersionStore('doc-42'),\n      onChange: (state) => render(state.versions, state.diff),\n    }),\n  ] as const,\n})\n\neditor.commands.snapshotVersion('before the rewrite')\neditor.commands.previewVersion(versionList(editor)[0].id)\n```\n\n`idleMs: null` turns automatic snapshots off and leaves them to\n`snapshotVersion`. A version per keystroke is not history, it is a keylogger\nwith a nicer name. `now` is injected rather than reached for, so a test does not\nhave to sleep to make two versions differ.\n\n\n---\n\n## Security\n\nDocument JSON, pasted HTML and collaborative steps are all treated as hostile,\nand the rendering path is the gate they all pass through: executable attributes\nare never set, URL attributes are scheme-checked, undeclared attributes are\ndropped, and commands report failure rather than throwing. See\n[SECURITY.md](./SECURITY.md).\n\n## Development\n\n```bash\npnpm install\npnpm dev         # playground at localhost:5173\npnpm test        # vitest\npnpm typecheck   # tsc, including the type-level tests\npnpm check       # biome, and prettier for .astro\npnpm build       # tsup, all packages\npnpm size        # the bundle ladder the site quotes\npnpm bench:check # the performance ratchet, against the recorded baseline\npnpm links       # no dead internal links on the site\npnpm packaging   # every built package imports and requires (run after build)\npnpm wiring      # every script on the site finds the markup it asks for\npnpm exercise        # drive every extension through the built package in a DOM: every command, rule and paste\npnpm install:matrix  # pack every package, npm-install it into fresh Vite apps for each framework, build and run them\npnpm facts       # the counts the site prints — tests, adversarial tests, extensions\n```\n\n## Status\n\n1.0 — the Matra engine, end to end. Document model, transforms, position\nmapping, editor state and the editable view are written from scratch, with\n**zero runtime dependencies**. 779 tests, 68 of them adversarial, and every\npackage is installed with plain npm into a fresh React, Vue, Svelte, Solid\nand vanilla Vite app and built there before a release (`pnpm install:matrix`).\n\nAn app on the starter kit bundles **31 kB gzipped**, because nothing arrives\nthat the editor does not use — seventy-nine extensions ship in the package\nand none of them is in the bundle until it is in the array. The whole ladder,\nfrom an empty extension array upwards, is measured by `pnpm size` and checked\nin CI. It was 25 kB at 0.16; what the five kilobytes bought is listed in\n[CHANGELOG.md](./CHANGELOG.md).\n\nDrag and drop landed in 0.9.0: blocks drag with a handle, a line shows where\nthey will land, and the move is one undo step.\n\nThe view passes its tests but has not yet met real IME users on iOS Safari or\nAndroid Chrome. See [ENGINE.md](./ENGINE.md) for where the risk actually sits,\nand [harness/ime](./harness/ime) for the page that checks it on a real device.\n\n## Extensions\n\nEverything in the box, and everything free unless marked.\n\n| | | |\n|---|---|---|\n| **Text** | bold, italic, strike, code, underline, highlight, subscript, superscript, link, **text style**, **kbd** | colour, background, font family and size, as one mark |\n| **Blocks** | paragraph, heading, blockquote, code block, horizontal rule, hard break, image, **callout**, **details** | a Notion callout and a collapsible toggle |\n| **Embeds** | **YouTube**, **any embed page** in a sandboxed frame, **image resize** with a handle | allowlisted hosts only; the width lands in the HTML |\n| **Templates** | **locked blocks**, **fields**, **snippets** | a contract with fixed clauses, a mail merge with no editor, words that expand as typed |\n| **Layout** | **columns**, **page break**, **line height**, **text direction** | two to six columns, a real break in print, right-to-left detected from the text |\n| **Scholarly** | **footnotes**, **math** inline and display | numbered by position; KaTeX or MathJax plug in, or the source shows |\n| **Lists** | bulleted, ordered, **task lists** with real checkboxes | |\n| **Tables** | insert, delete, header rows, colspan and rowspan, **add and remove rows and columns, Tab between cells** | spanning cells widen rather than split |\n| **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 |\n| **Finding** | **search and replace** | incremental: typing rescans one paragraph |\n| **Code** | **syntax highlighting** as decorations | a built-in tokeniser, or plug in Shiki, Prism or lowlight |\n| **Structure** | **table of contents**, **unique block ids**, **focus class**, **trailing node** | derived from the document, never stored beside it |\n| **Interchange** | **Markdown in and out**, with no DOM | runs on a server |\n| **Dragging** | block drag and drop, **drag handle**, drop cursor, **files dropped or pasted** | the drop cursor is in the engine, not an extension |\n| **Review** | threaded comments anchored to ranges | free here · Tiptap's Comments needs a subscription |\n| **Menus** | `@` mentions and `/` commands, detection only, **bubble and floating menus** for your element | the popup is yours |\n| **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 |\n| **Paid** | AI streaming, collaboration with remote cursors, version history | |\n\nTiptap 3 moved most of its old Pro extensions to MIT — a table of contents,\nunique ids, the drag handle, the file handler, emoji, details, invisible\ncharacters and mathematics are all free there now, and it is worth saying so\nrather than repeating a comparison that was true of Tiptap 2. What is still\nbehind a Tiptap subscription is comments, snapshots and version history, the\nAI toolkit, track changes, DOCX import and export, and pagination.\n\nOf those, comments are free here. Version history, collaboration and AI are\nthe three packages this project charges for, and the shape is deliberate: the\nthings that take a week are free and drive adoption, and the ones that took\nmonths are what you pay for.\n\n### Adding one, step by step\n\nEvery extension follows the same four steps. Search and replace, as the\nexample:\n\n1. **Import it** from `@matrajs/core` — the binding you installed already\n   depends on it, so there is nothing to add to `package.json`.\n2. **Put it in the array.** Extensions that take options are functions;\n   the rest are plain objects.\n3. **Call its commands.** They are on `editor.commands`, typed from the\n   array, so a typo is a compile error.\n4. **Paste its CSS** if it has any. Extensions that draw something export a\n   `…CSS` string; the editor ships no appearance of its own.\n\n```ts\nimport { createEditor, search, searchCSS, starterKit } from '@matrajs/core'\n\nconst editor = createEditor({ extensions: [...starterKit, search()] as const })\n\neditor.commands.setSearch({ query: 'colour', wholeWord: true })\neditor.commands.nextMatch()              // selects it, so the view scrolls there\neditor.commands.replaceMatch('color')\neditor.commands.replaceAllMatches('color')   // one undo step\neditor.extensionState('search')          // { matches, current, query, … } for a panel\n\ndocument.head.appendChild(Object.assign(document.createElement('style'), { textContent: searchCSS }))\n```\n\nThe same shape for the rest: `textStyle` then `editor.commands.setColor('#c00')`;\n`callout` then `toggleCallout('warning')`; `...detailsKit` then\n`insertDetails()`; `youtube` then `insertYoutube({ src: url })`;\n`fileHandler({ accept: ['image/'], onDrop })` then upload in `onDrop` and\ninsert at `marker.map(pos)`; `...tableKit` then `insertTable(3, 3)` and\n`addRowAfter()`. Each is one row in the directory on\n[matrajs.com/extensions](https://matrajs.com/extensions), with the line you\nwould write.\n\n`toMarkdown` and `fromMarkdown` are pure string work rather than a trip through\nHTML, so they run in Node, in a worker, and at the edge. Turning a document into\nMarkdown on a server does not need a DOM polyfill.\n\n## Against the alternatives\n\nMeasured, not asserted — see [BENCHMARKS.md](./BENCHMARKS.md) for the method and\nwhat the numbers are not.\n\nPackage counts are what npm resolves for a React install of each, measured by\n[`scripts/rivals.mjs`](./scripts/rivals.mjs) on 2026-09-07 against Tiptap\n3.31.3, Lexical 0.50.0 and Slate 0.126.2, with React and `@types/*` left out\nof the count.\n\n| | Matra | Tiptap | Lexical | Slate |\n|---|---|---|---|---|\n| Bundle, gzipped | **31 kB** | 117 kB | ~35 kB | ~50 kB |\n| Packages installed | **2** | 50 | 34 | 12 |\n| Of those, third-party | **0** | 22 | 10 | 8 |\n| Engine types in your code | **none** | ProseMirror | Lexical | Slate |\n| Command types | **inferred** | module augmentation | manual | manual |\n| Async position safety | **built in** | manual | manual | manual |\n| Vue binding | first-class | first-class | community | community |\n| Svelte and Solid bindings | **first-class** | community | community | community |\n| Comments | **free** | subscription | build it | build it |\n| Runtime licence check or phone-home | **never** | none | n/a | n/a |\n\nRows that used to be here and are no longer true: Tiptap 3 publishes its table\nof contents, unique ids, drag handle, file handler, emoji, details, invisible\ncharacters and mathematics extensions as MIT, and `@tiptap/markdown` parses and\nserialises Markdown in bare Node. Tiptap also ships an official Vue binding,\nwhich an earlier version of this table called community.\n\nWhere the alternatives win, and it is worth saying so: ProseMirror's ecosystem\nis a decade deep and Tiptap inherits all of it, Lexical has been hardened by\nMeta's traffic, and both have met far more real IME users than this has. If you\nneed a mature extension for something exotic today, they have it and this does\nnot.\n\n## Releasing\n\nOne registry, and an order that matters. See [RELEASING.md](./RELEASING.md).\nEvery release is recorded in [CHANGELOG.md](./CHANGELOG.md).\n\n## Licence\n\n**The core is MIT and stays that way.** `@matrajs/core` and every framework\nbinding — `@matrajs/react`, `@matrajs/vue`, `@matrajs/svelte` and\n`@matrajs/solid` — the engine, the document model, the extension API, the\nstarter kit, tables, comments, every mark and node that ships in the box. No\nopen-core asterisk on any of it, no feature removed later to sell back.\n\n**AI, collaboration and version history are paid.** `@matrajs/ai`,\n`@matrajs/collab` and `@matrajs/versions` are source-available under the\n[Matra Commercial License](./packages/ai/LICENSE):\nfree to evaluate, develop against, test, teach with, and use in personal\nprojects and small internal tools; paid per developer in production. They are\nthe things here that took months rather than days — streaming edits that\nsurvive concurrent typing, rebasing another client's work over unsent local\nwork without losing either, and a real diff between two snapshots of a\ndocument.\n\n**Nothing phones home and there is no runtime licence check.** Your editor\nnever talks to us, in development or in production, and a lapsed subscription\ncannot switch anything off in an app you already shipped.\n\nThere is no download gate either. The source is in this repository and the\npackages install from public npm — the licence is the boundary, as with the\nBusiness Source Licence. What a subscription buys is the right to run them in\nproduction, plus updates and support.\n\n**Versions up to 0.5.0 shipped under MIT, including `ai` and `collab`, and that\ngrant cannot be withdrawn.** Anyone already on 0.5.0 may stay there under MIT\nforever. The commercial licence starts at 0.6.0.\n",
  "bytes": 27565,
  "sha": "1ab6cb2a21509493d6d9079aacbad2de1888ecfbc3d3278d9005cb1a5b941eb9",
  "repo_slug": "amrelaco/matra",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_com_matrajs_matra_e2def106/readme"
}