@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 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["message"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The Matra documentation, served over the Model Context Protocol.\n *\n * MCP is JSON-RPC 2.0 with a handshake and a small vocabulary — tools,\n * resources, prompts — that Claude, Cursor, Codex and the rest all speak. This\n * module is the protocol half: it takes one message and returns one reply,\n * and knows nothing about where either came from. The transports — stdio and\n * HTTP — are in `cli.ts`, and a test drives this without either.\n *\n * Written against the spec directly, with no SDK, because every other package\n * here has zero runtime dependencies and the one that exists to be installed\n * with `npx` should not be the exception.\n */\n\n/** One page of documentation. */\nexport interface Doc {\n /** URL-safe, unique: `installation`, `engine`, `changelog`. */\n slug: string\n title: string\n description: string\n /** Where it came from — a path in the repository, or a page on the site. */\n source: string\n /** Markdown. */\n text: string\n}\n\nexport interface ServerOptions {\n name?: string\n version?: string\n /** What a client is told about this server when it connects. */\n instructions?: string\n}\n\n// --- JSON-RPC ---------------------------------------------------------------\n\nexport type JsonRpcId = string | number | null\n\nexport interface JsonRpcRequest {\n jsonrpc: '2.0'\n id?: JsonRpcId\n method: string\n params?: Record<string, unknown>\n}\n\nexport interface JsonRpcResponse {\n jsonrpc: '2.0'\n id: JsonRpcId\n result?: unknown\n error?: { code: number; message: string; data?: unknown }\n}\n\nconst PARSE_ERROR = -32700\nconst INVALID_REQUEST = -32600\nconst METHOD_NOT_FOUND = -32601\nconst INVALID_PARAMS = -32602\n\nclass RpcError extends Error {\n constructor(\n readonly code: number,\n message: string,\n ) {\n super(message)\n }\n}\n\n/** Protocol revisions this server can speak, newest first. */\nexport const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'] as const\n\nconst LATEST = PROTOCOL_VERSIONS[0]\n\n// --- searching ----------------------------------------------------------------\n\nconst tokens = (text: string): string[] =>\n text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter((word) => word.length > 1)\n\ninterface Hit {\n doc: Doc\n score: number\n snippet: string\n}\n\n/**\n * Rank pages for a query.\n *\n * Term frequency, with the title worth more than the body and every term\n * required to appear somewhere. Small enough to read, good enough for twenty\n * pages of documentation, and it needs no index built in advance.\n */\nexport function searchDocs(docs: readonly Doc[], query: string, limit = 5): Hit[] {\n const terms = tokens(query)\n if (!terms.length) return []\n const hits: Hit[] = []\n for (const doc of docs) {\n const title = doc.title.toLowerCase()\n const body = doc.text.toLowerCase()\n let score = 0\n let missing = false\n for (const term of terms) {\n const inTitle = title.includes(term) ? 3 : 0\n let inBody = 0\n let at = body.indexOf(term)\n while (at !== -1 && inBody < 20) {\n inBody++\n at = body.indexOf(term, at + term.length)\n }\n if (!inTitle && !inBody) {\n missing = true\n break\n }\n score += inTitle + inBody\n }\n if (missing) continue\n hits.push({ doc, score, snippet: snippetFor(doc.text, terms[0] as string) })\n }\n hits.sort((a, b) => b.score - a.score || a.doc.title.localeCompare(b.doc.title))\n return hits.slice(0, Math.max(1, Math.min(limit, 20)))\n}\n\n/** A line or two around the first place a term appears. */\nfunction snippetFor(text: string, term: string): string {\n const lower = text.toLowerCase()\n const at = lower.indexOf(term)\n if (at === -1) return text.slice(0, 200).replace(/\\s+/g, ' ').trim()\n const start = Math.max(0, at - 120)\n const end = Math.min(text.length, at + 200)\n return `${start > 0 ? '…' : ''}${text.slice(start, end).replace(/\\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`\n}\n\n// --- the server --------------------------------------------------------------\n\nconst URI_PREFIX = 'matra://docs/'\n\nconst TOOLS = [\n {\n name: 'list_docs',\n title: 'List the documentation',\n description:\n 'Every page of the Matra documentation, with its slug, title and a one-line description. Call this first to see what exists, then read_doc for a page.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n {\n name: 'read_doc',\n title: 'Read one page',\n description:\n 'The full Markdown of one documentation page, by slug. Slugs come from list_docs or search_docs.',\n inputSchema: {\n type: 'object',\n properties: {\n slug: { type: 'string', description: 'The page slug, e.g. \"installation\".' },\n },\n required: ['slug'],\n additionalProperties: false,\n },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n {\n name: 'search_docs',\n title: 'Search the documentation',\n description:\n 'Find the pages that mention something — an extension name, a command, an error message — ranked, with a snippet from each.',\n inputSchema: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Words to look for.' },\n limit: {\n type: 'integer',\n minimum: 1,\n maximum: 20,\n description: 'How many pages, default 5.',\n },\n },\n required: ['query'],\n additionalProperties: false,\n },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n] as const\n\nexport interface DocsServer {\n /** Handle one message. Notifications get null back — there is nothing to send. */\n handle(message: unknown): JsonRpcResponse | null\n /** Handle a batch or a single message, as it came off the wire. */\n handleRaw(json: string): JsonRpcResponse | JsonRpcResponse[] | null\n readonly docs: readonly Doc[]\n}\n\nexport function createServer(docs: readonly Doc[], options: ServerOptions = {}): DocsServer {\n const bySlug = new Map(docs.map((doc) => [doc.slug, doc]))\n const info = { name: options.name ?? 'matra-docs', version: options.version ?? '0.0.0' }\n const instructions =\n options.instructions ??\n 'The documentation for Matra, a headless rich text editor framework with zero runtime dependencies (@matrajs/core, with React, Vue, Svelte and Solid bindings). Use search_docs to find a page, read_doc to read it. Prefer what these pages say over prior knowledge: the API is inferred from an extensions array, extensions are plain objects, and there is no ProseMirror underneath.'\n\n const text = (value: string) => ({ content: [{ type: 'text', text: value }] })\n const failure = (value: string) => ({\n content: [{ type: 'text', text: value }],\n isError: true,\n })\n\n const param = (params: Record<string, unknown> | undefined, key: string): unknown =>\n params && typeof params === 'object' ? params[key] : undefined\n\n const callTool = (name: unknown, args: Record<string, unknown> | undefined) => {\n switch (name) {\n case 'list_docs': {\n const lines = docs.map((doc) => `- ${doc.slug} — ${doc.title}: ${doc.description}`)\n return text(`${docs.length} pages.\\n${lines.join('\\n')}`)\n }\n case 'read_doc': {\n const slug = param(args, 'slug')\n if (typeof slug !== 'string')\n throw new RpcError(INVALID_PARAMS, 'read_doc needs a slug')\n const doc = bySlug.get(slug)\n if (!doc) {\n return failure(\n `No page called \"${slug}\". Known slugs: ${[...bySlug.keys()].join(', ')}`,\n )\n }\n return text(\n `# ${doc.title}\\n\\n> ${doc.description}\\n> Source: ${doc.source}\\n\\n${doc.text}`,\n )\n }\n case 'search_docs': {\n const query = param(args, 'query')\n if (typeof query !== 'string')\n throw new RpcError(INVALID_PARAMS, 'search_docs needs a query')\n const limit = param(args, 'limit')\n const hits = searchDocs(docs, query, typeof limit === 'number' ? limit : 5)\n if (!hits.length) return text(`Nothing mentions \"${query}\". Try list_docs.`)\n return text(\n hits\n .map((hit) => `## ${hit.doc.title} (slug: ${hit.doc.slug})\\n${hit.snippet}`)\n .join('\\n\\n'),\n )\n }\n default:\n throw new RpcError(INVALID_PARAMS, `Unknown tool \"${String(name)}\"`)\n }\n }\n\n const dispatch = (method: string, params: Record<string, unknown> | undefined): unknown => {\n switch (method) {\n case 'initialize': {\n const asked = param(params, 'protocolVersion')\n const protocolVersion =\n typeof asked === 'string' && (PROTOCOL_VERSIONS as readonly string[]).includes(asked)\n ? asked\n : LATEST\n return {\n protocolVersion,\n capabilities: { tools: { listChanged: false }, resources: { listChanged: false } },\n serverInfo: { ...info, title: 'Matra docs' },\n instructions,\n }\n }\n case 'ping':\n return {}\n case 'tools/list':\n return { tools: TOOLS }\n case 'tools/call':\n return callTool(param(params, 'name'), param(params, 'arguments') as never)\n case 'resources/list':\n return {\n resources: docs.map((doc) => ({\n uri: `${URI_PREFIX}${doc.slug}`,\n name: doc.slug,\n title: doc.title,\n description: doc.description,\n mimeType: 'text/markdown',\n })),\n }\n case 'resources/templates/list':\n return { resourceTemplates: [] }\n case 'resources/read': {\n const uri = param(params, 'uri')\n if (typeof uri !== 'string' || !uri.startsWith(URI_PREFIX)) {\n throw new RpcError(INVALID_PARAMS, `Not a matra:// documentation URI: ${String(uri)}`)\n }\n const doc = bySlug.get(uri.slice(URI_PREFIX.length))\n if (!doc) throw new RpcError(INVALID_PARAMS, `No page at ${uri}`)\n return { contents: [{ uri, mimeType: 'text/markdown', text: doc.text }] }\n }\n case 'prompts/list':\n return { prompts: [] }\n case 'logging/setLevel':\n return {}\n default:\n throw new RpcError(METHOD_NOT_FOUND, `Method not found: ${method}`)\n }\n }\n\n const handle = (message: unknown): JsonRpcResponse | null => {\n const request = message as Partial<JsonRpcRequest> | null\n const id: JsonRpcId =\n request && typeof request === 'object' && 'id' in request ? (request.id ?? null) : null\n if (\n !request ||\n typeof request !== 'object' ||\n request.jsonrpc !== '2.0' ||\n typeof request.method !== 'string'\n ) {\n return {\n jsonrpc: '2.0',\n id,\n error: { code: INVALID_REQUEST, message: 'Not a JSON-RPC 2.0 request' },\n }\n }\n // A notification carries no id and gets no reply.\n const isNotification = !('id' in request) || request.id === undefined\n if (request.method.startsWith('notifications/')) return null\n try {\n const result = dispatch(request.method, request.params)\n return isNotification ? null : { jsonrpc: '2.0', id, result }\n } catch (error) {\n if (isNotification) return null\n const code = error instanceof RpcError ? error.code : -32603\n const message = error instanceof Error ? error.message : String(error)\n return { jsonrpc: '2.0', id, error: { code, message } }\n }\n }\n\n const handleRaw = (json: string): JsonRpcResponse | JsonRpcResponse[] | null => {\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch {\n return { jsonrpc: '2.0', id: null, error: { code: PARSE_ERROR, message: 'Parse error' } }\n }\n if (Array.isArray(parsed)) {\n const replies = parsed\n .map(handle)\n .filter((reply): reply is JsonRpcResponse => reply !== null)\n return replies.length ? replies : null\n }\n return handle(parsed)\n }\n\n return { handle, handleRaw, docs }\n}\n"]}
@@ -0,0 +1,207 @@
1
+ # Benchmarks
2
+
3
+ Run them yourself: `node bench/bench.mjs`.
4
+
5
+ ## Bundle
6
+
7
+ An app importing the editor and the starter kit, bundled with esbuild,
8
+ minified, gzipped:
9
+
10
+ | | minified | gzipped |
11
+ |---|---|---|
12
+ | **Matra** | 92.2 kB | **30 kB** |
13
+ | Tiptap 3.30 | 370.5 kB | 117.2 kB |
14
+
15
+ **3.9× smaller.** Matra has no runtime dependencies; Tiptap brings ProseMirror,
16
+ which is 51 packages in `node_modules`. The figure was 25 kB at 0.16; 1.0
17
+ spent 3.6 kB on the engine doing more — attributes one extension adds to
18
+ another's nodes, paste and drop hooks, files and text dropped from outside,
19
+ blocks inserted into the middle of a paragraph, decorations compared after
20
+ mapping, paragraphs patched in place, positions found by bisection — and
21
+ every one of those is on the runtime side of this file.
22
+
23
+ ## Speed, in a browser
24
+
25
+ Four editors mounted in the same page, in the same run, in WebKit. Each cell is
26
+ the median of three runs of a median of seven samples. Milliseconds, lower is
27
+ better. `bench/browser` builds and runs this.
28
+
29
+ **No editor** is the same paragraphs built by hand into a `contenteditable`
30
+ div, with nothing else in the page — the floor none of these can go under.
31
+
32
+ | operation | No editor | Matra | Tiptap | Lexical | Slate |
33
+ |---|---|---|---|---|---|
34
+ | parse a document, 2000 ¶ | — | **8.3** | 15.6 | 77.6 | — |
35
+ | `getHTML()`, 2000 ¶ | — | **1.5** | 3.6 | 7.1 | — |
36
+ | keystroke, 200 ¶ | — | **0.127** | 0.275 | 0.197 | — |
37
+ | keystroke, 2000 ¶ | — | **0.847** | 1.188 | 1.160 | — |
38
+ | mount + first render, 200 ¶ | 1.1 | **2.0** | 4.3 | 3.2 | 8.0 |
39
+ | mount + first render, 2000 ¶ | 10.9 | **22.1** | 37.7 | 26.0 | 93.0 |
40
+
41
+ **Absolute milliseconds only mean anything within one run.** Same harness, same
42
+ browser, a different day: Lexical parsed the same document in 34.7 ms one week
43
+ and 71.6 ms the next, without a line of it changing. That is why all four are
44
+ mounted in one page and measured in one pass, and why comparing a number here
45
+ against a number from an older copy of this file is comparing two machines.
46
+
47
+ **Every mount is checked, not trusted.** The harness mounts once more outside
48
+ the timing and looks for the last paragraph's text on screen. A reconciler that
49
+ returns before its DOM exists is the cheapest possible way to win the mount row,
50
+ and an earlier version of this harness reported a number for Slate when nothing
51
+ had been drawn at all.
52
+
53
+ **The keystroke row changed hands.** It used to be the row Matra lost. Measured
54
+ back to back against the same rivals in the same session:
55
+
56
+ | keystroke | before | after |
57
+ |---|---|---|
58
+ | 200 paragraphs | 0.246 | **0.127** |
59
+ | 2000 paragraphs | 1.465 | **0.847** |
60
+
61
+ What was costing it, all of it found by profiling rather than by reading:
62
+
63
+ - **Every ancestor of the edit was rebuilt by cutting.** A paragraph changes and
64
+ each ancestor up to the document is rebuilt around it — by cutting the run of
65
+ children in two, appending the replacement, appending the rest, and re-adding
66
+ every child's size to get a total that differed from the old one by exactly
67
+ one child. On a 2000-block document that is four walks of two thousand
68
+ children per character. `Fragment.replaceChild` swaps the one child that moved
69
+ and does the arithmetic in one subtraction.
70
+ - **The diff reached into the DOM for blocks it had already decided to skip.**
71
+ The loop read `childNodes[i]` before asking whether child `i` was inside the
72
+ edit at all. `childNodes` is a live list, and for 1999 of 2000 blocks the
73
+ answer was thrown away. Asking first, indexing second.
74
+ - **Every sixty-fourth keystroke threw the rendered document away.** The
75
+ position map absorbs each edit's mapping instead of rewriting itself, and
76
+ capped the backlog at 64 — past which the renderer rebuilt the entire
77
+ document's DOM. It was the backlog that had gone stale, not the DOM, so now
78
+ only the backlog is dropped and the positions are recorded again in place. A
79
+ rebuild also silently dropped every mounted node view's state, which is a
80
+ correctness bug wearing a performance bug's clothes.
81
+ - **The position map allocated an entry per node per edit** to record a position
82
+ it already held. It reuses the entry now.
83
+
84
+ In Node, against happy-dom, where none of the browser's layout cost is in the
85
+ way, those take a keystroke on a 2000-paragraph document from 0.464 ms to
86
+ 0.062 ms, and the cost stops tracking the length of the document: 0.045 ms at
87
+ 20 blocks against 0.062 ms at 2000.
88
+
89
+ **The mount row was wrong, and the harness was why.** This file used to say
90
+ Lexical put an editor on screen faster. It does not. Teardown ran *inside* the
91
+ timed function and the layout read came after it, so an editor whose teardown
92
+ detaches its DOM had taken its document off screen before the browser was asked
93
+ to lay anything out. Lexical's teardown does that; Matra's drops listeners and
94
+ leaves the document where it is. Matra was paying for two thousand paragraphs of
95
+ layout and Lexical was not, on a row where layout is most of the number.
96
+ Teardown now runs after the clock stops and both mount rows changed hands. Same
97
+ class of mistake the harness already refused to make for Slate, one level up.
98
+
99
+ The first render did get faster while this was being chased — 0.77 ms to 0.59 ms
100
+ for two hundred blocks in Node — by building into a document fragment and
101
+ attaching it once rather than appending block by block into a live tree,
102
+ dropping the mark stack's three arrays per child for marks that blocks never
103
+ have, and taking a direct path for the `['p', 0]` shape most nodes render as.
104
+
105
+ **What is missing and why.** Slate's keystroke goes through a React render that
106
+ has not happened by the time the timer stops, and the harness checks whether the
107
+ text on screen changed during the measurement — when it did not, it reports
108
+ `NOT MEASURED` instead of a number. An earlier version of the harness cheerfully
109
+ reported Slate at 0.02 ms per keystroke, which was the model update with nothing
110
+ drawn behind it.
111
+
112
+ **What is not like-for-like.** Each editor is driven through its own idiomatic
113
+ API. Lexical and Slate carry the rich-text behaviour their own quick-starts
114
+ prescribe, which is not the same feature set as Matra's or Tiptap's starter kit.
115
+
116
+ ## Speed, in Node
117
+
118
+ happy-dom, same document. Useful for the parts that never touch a DOM:
119
+
120
+ | operation | Matra | Tiptap | |
121
+ |---|---|---|---|
122
+ | create + parse a document | **0.70** | 21.62 | 31× faster |
123
+ | `getJSON()` | **0.16** | 0.11 | 1.5× slower |
124
+
125
+ ## 1.0, in Node
126
+
127
+ The ratchet's own figures, before and after, in the calibrated units
128
+ `bench/bench.mjs` prints — the same machine, the same run, the floor of three
129
+ passes each:
130
+
131
+ | figure | 0.16 | 1.0 | |
132
+ |---|---|---|---|
133
+ | create editor, 50 ¶ | 17.5 | **3.7** | 4.7× |
134
+ | setContent JSON, 2000 ¶ | 127.2 | **80.4** | 1.6× |
135
+ | `getHTML()`, 2000 ¶ | 89.6 | **30.3** | 3.0× |
136
+ | `getJSON()`, 2000 ¶ | 11.3 | **9.7** | 1.2× |
137
+ | `getText()`, 2000 ¶ | 27.6 | **7.5** | 3.7× |
138
+ | insert one character | 2.73 | **0.22** | 12× |
139
+ | toggle bold over a range | 4.46 | **0.26** | 17× |
140
+ | keystroke, mounted, 500 ¶ | 5.04 | **2.17** | 2.3× |
141
+
142
+ And the ones the ratchet did not measure, in microseconds, because they are
143
+ where the time actually went:
144
+
145
+ | operation, 2000 ¶ | 0.16 | 1.0 | |
146
+ |---|---|---|---|
147
+ | keystroke at the **end** of the document, mounted | 180 | **34** | 5.3× |
148
+ | keystroke at the start, mounted | 77 | **48** | 1.6× |
149
+ | toggle bold on a word near the end | 453 | **20** | 23× |
150
+ | `isActive('bold')` + `isActive('heading')` | 1.47 | **0.12** | 12× |
151
+ | `createEditor`, empty | 85 | **16** | 5.3× |
152
+ | parse 2000 ¶ of HTML with marks (ms) | 77 | **55** | 1.4× |
153
+
154
+ What was costing it:
155
+
156
+ - **Every position was found by walking.** Resolving a position walked the
157
+ document's children from the first, adding sizes until it passed the point,
158
+ and a keystroke resolves a dozen positions. Typing at the end of a
159
+ two-thousand-block document cost twelve times what typing at the top did,
160
+ and the benchmark only ever typed at the top. A fragment past twenty-four
161
+ children now keeps a prefix index and bisects it.
162
+ - **A mark on one word rebuilt the whole document.** Asking "is this bold"
163
+ visited every node in the document to find the ones in the selection, and
164
+ applying the mark rebuilt every level from the first child to the last.
165
+ Both now walk only what the range touches, and only the children that
166
+ changed are swapped into their parent.
167
+ - **`toDOM` was handed a full JSON serialisation of the node.** Rendering a
168
+ paragraph serialised its text; rendering the document serialised the
169
+ document, once per level, so that a function returning `['p', 0]` could read
170
+ an attribute. It now gets an object that carries the type and the attributes
171
+ and builds the rest on demand.
172
+ - **Every command built a fresh object of twenty closures**, and every
173
+ `isActive` started a transaction to answer a question about the state. The
174
+ context is a class now and the transaction starts on first write, so
175
+ asking is free.
176
+ - **The character counter re-serialised the document on every click.**
177
+ Any extension reading `ctx.doc` in its reducer paid for the whole document
178
+ on every transaction, caret moves included. The counter reads the engine's
179
+ text and only when the document changed; `ctx.doc` is cached per document
180
+ within a command.
181
+ - **The drag handle asked the browser for every block's rectangle on every
182
+ mouse move.** Blocks stack, so the one under the pointer is found by
183
+ bisection: eleven rectangles on a two-thousand-block page, not two thousand.
184
+ - **The undo history copied the whole entry to add each keystroke to it.**
185
+ Inverses are appended and replayed from the end.
186
+ - **Every keystroke wrote the browser selection**, even when it was already
187
+ where it was about to be put, and every write came straight back as a
188
+ `selectionchange` event to be read and found identical.
189
+ - **A decoration anywhere threw the narrowed redraw away.** Last render's
190
+ decorations are now mapped through the edit before being compared, so a
191
+ search hit that merely moved is the same hit, and only the span where the
192
+ decorations really differ is added to what gets redrawn.
193
+
194
+ ## Where the time went, the first time
195
+
196
+ Two earlier rounds, kept because both are the kind of thing that grows back:
197
+
198
+ - **The position map was rebuilt on every keystroke.** Every node was
199
+ re-recorded, so a 4000-paragraph document did eight thousand map writes per
200
+ character — all of them to say the same thing shifted by one. The map now
201
+ absorbs the transaction's mapping and translates positions when asked.
202
+ - **The diff visited every block.** It now skips any block the edit's span did
203
+ not touch: 3999 of 4000 children on a keystroke.
204
+
205
+ And one that was pure waste: `toJSON` called `Object.keys(attrs).length` to ask
206
+ whether a node had attributes, allocating an array per node to answer a
207
+ question about emptiness.
@@ -0,0 +1,222 @@
1
+ # Changelog
2
+
3
+ All packages share one version number and are released together.
4
+
5
+ ## 1.0.0 — 2026-09-04
6
+
7
+ The engine got faster everywhere it was measured, thirty-six extensions
8
+ arrived, the documentation gained a server any AI tool can read it through,
9
+ and every package is now installed with plain npm into a fresh app for each
10
+ framework and built there before it ships.
11
+
12
+ **Faster.** Typing at the end of a long document cost twelve times what
13
+ typing at the top did, because every position was found by walking the
14
+ document from its first block; it is found by bisection now, and the two are
15
+ the same. Marking a word rebuilt the whole document; it rebuilds the block.
16
+ `toDOM` was handed a JSON serialisation of the node, and its children, and
17
+ their children, so that it could read one attribute; it gets a lazy view.
18
+ `isActive` started a transaction to read the state. The character counter
19
+ serialised the document on every click. The drag handle measured every block
20
+ on every mouse move. The undo history copied itself to add a keystroke. The
21
+ browser selection was rewritten to where it already was on every keystroke.
22
+ Decorations threw the narrowed redraw away. Each of these is in
23
+ BENCHMARKS.md with a number beside it; in the ratchet's units a character
24
+ insert went from 2.73 to 0.22, a mark from 4.46 to 0.26, `getHTML` from 89.6
25
+ to 30.3, and creating an editor from 17.5 to 3.7.
26
+
27
+ **Fixed.** `commands.insert` left the caret one position past its own text,
28
+ because the selection was mapped through the change twice. Changing a node's
29
+ attributes — aligning a paragraph, ticking a task — was a replacement of the
30
+ node, so every position inside it, the caret included, was mapped to its end;
31
+ it is its own kind of step now, and nothing inside the node moves. Pasting a
32
+ table, a list or two paragraphs into the middle of a paragraph threw, because
33
+ a block cannot go inside a paragraph; the paragraph is split around blocks,
34
+ and pasted paragraphs join the halves so two paragraphs pasted into a third
35
+ make three. Three lines of plain text pasted from a text file became one
36
+ paragraph with the breaks collapsed to spaces; they are three paragraphs, and
37
+ inside a code block the line breaks stay put. A mention's label was passed to
38
+ the renderer as a tag name — `createElement('@Nahim')` — which a real browser
39
+ refuses. `insertHorizontalRule` and the `---` shortcut were refused with the
40
+ caret inside a paragraph. Code blocks loaded from HTML lost their line breaks.
41
+ `textAlign` did nothing on the stock paragraph. A node decoration that moved
42
+ on stayed on the element it left. Something dropped from outside the editor
43
+ was written into the DOM behind the document's back. A selection dragged
44
+ leftwards was written to the browser the right way round. The undo entry
45
+ order under a burst of typing is pinned by a test. The site's directory
46
+ promised commands that did not exist; the rows now say what the editor has.
47
+
48
+ **New extensions**, all MIT, all in `@matrajs/core`, none in the bundle until
49
+ it is in the array:
50
+
51
+ - `textStyle` — colour, background, font family and size, as one mark.
52
+ - `search()` — find and replace, incremental: typing rescans one paragraph.
53
+ - `autolink()` — URLs become links as they are typed and pasted.
54
+ - `detailsKit` — a collapsible toggle, rendered as a real `<details>`.
55
+ - `callout` — a Notion-style callout with a type and an emoji.
56
+ - `emoji()` — `:tada:` as you type, and a table for a picker.
57
+ - `clearFormatting` — every mark off, every block a paragraph, one undo step.
58
+ - `focus()` — a class on the block the caret is in.
59
+ - `trailingNode()` — always a paragraph after whatever ends the document.
60
+ - `youtube` — an embed built from the video id on the privacy domain.
61
+ - `embed()` — any embed page in a sandboxed frame, from an allowlist of hosts.
62
+ - `codeHighlight()` — syntax highlighting as decorations, with a built-in
63
+ tokeniser or yours.
64
+ - `indent()` — Tab and Shift-Tab on paragraphs and headings.
65
+ - `fileHandler()` — files dropped or pasted, with a marker that keeps the
66
+ drop position right while the upload runs.
67
+ - `imageResize()` — a drag handle on every image, and a `width` the HTML keeps.
68
+ - `locked()` — blocks that refuse every change: a keystroke, a paste, a drop, a
69
+ drag and a command alike. A template with fixed clauses.
70
+ - `field` — a blank in a template, filled in the editor with `fillFields` or
71
+ in JSON on a server with `fillFieldsIn`. A mail merge with no editor.
72
+ - `snippets()` — words that expand as they are typed, into text, nodes or
73
+ whole blocks.
74
+ - `columnsKit` — two to six columns, and back to blocks without losing anything.
75
+ - `pageBreak` — a labelled line on screen, a real page break in print.
76
+ - `lineHeight()` — line height on a block, as a checked style.
77
+ - `textDirection()` — `dir` on a block, and right-to-left detected from the text
78
+ when it is unset.
79
+ - `footnotesKit()` — a marker in the text and a note at the end, numbered by
80
+ position as decorations, so moving a paragraph renumbers everything.
81
+ - `mathKit()` — inline and display formulas; KaTeX or MathJax plug in, and
82
+ without them the source shows.
83
+ - `textTransform` — upper, lower, title and sentence case on the selection or
84
+ the word under the caret, keeping every mark.
85
+ - `invisibleCharacters()` — a dot on every space, a pilcrow on every block,
86
+ drawn and never stored.
87
+ - `selectionHighlight()` — every other occurrence of the selected word.
88
+ - `typewriter()` — the line being written stays put; the page moves under it.
89
+ - `autosave()` — saves once typing pauses, and before the page goes away.
90
+ - `smartPaste()` — tab-separated text becomes a table, Markdown becomes blocks.
91
+ - `hashtag()` — a tag as a node, listable from the JSON with `hashtagsIn`.
92
+ - `kbd` — a key name, as `<kbd>`.
93
+ - `bubbleMenu()` and `floatingMenu()` — your element, shown over the selection
94
+ or on an empty line.
95
+ - `ghostText()` — inline completion from any source: grey text after the caret,
96
+ Tab to take it, a word at a time if you like.
97
+ - `dictation()` — speak, and the words arrive at the caret, through the
98
+ browser's own recogniser.
99
+ - Tables: `addRowBefore`, `addRowAfter`, `deleteRow`, `addColumnBefore`,
100
+ `addColumnAfter`, `deleteColumn`, `toggleHeaderRow`, `goToNextCell`,
101
+ `goToPreviousCell`, and Tab between cells. A cell that spans the boundary a
102
+ new row or column crosses is widened rather than split.
103
+
104
+ **New in the extension API.** `attributes` on an extension adds attributes to
105
+ nodes and marks defined elsewhere, rendered onto the element and read back on
106
+ parse. `handlePaste(ctx, data)` and `handleDrop(ctx, data)` let an extension
107
+ claim a paste or a drop before the editor parses it. `filterChange(ctx)` lets
108
+ an extension veto a change before it lands, and `editor.can` asks it too.
109
+ `nodeViews` on an extension renders nodes defined elsewhere. `code: true` on a
110
+ node keeps whitespace literal inside it. `ctx.insert` and `ctx.replace` accept
111
+ blocks at a caret inside a paragraph. A command whose change a filter refused
112
+ returns false.
113
+
114
+ **`@matrajs/mcp`** — a Model Context Protocol server, zero dependencies,
115
+ that serves this documentation to any AI tool that speaks MCP over stdio or
116
+ HTTP. `npx @matrajs/mcp` and point Claude, Cursor or Codex at it.
117
+
118
+ **Bundle.** The starter kit is 30 kB gzipped, from 25. The budget in
119
+ `scripts/size.mjs` is 30, and the comment there says what the bytes bought.
120
+ Seventy-nine extensions ship in the package; the bundle carries the ones in
121
+ the array.
122
+
123
+ **Checked before release.** `pnpm install:matrix` packs every package, installs
124
+ it with plain npm into a fresh React, Vue, Svelte, Solid and vanilla Vite app,
125
+ builds each and runs the built app in a DOM. `pnpm facts` writes the counts
126
+ the site prints, so a number on the landing page is one a script produced.
127
+
128
+ **Docs.** The engine is called the Matra engine. Every extension has a
129
+ step-by-step recipe on the site, the README says what an extension may
130
+ declare, and `editor.can` is documented beside every command it answers for.
131
+
132
+ The repository carries no git tags, and only three release commits, so version
133
+ boundaries below `0.15.0` are not recoverable exactly. Those releases are
134
+ grouped and dated from the history rather than invented — where a date matters
135
+ legally, the licence boundary at `0.6.0`, it is stated on its own.
136
+
137
+ ## 0.16.0 — 2026-08-28
138
+
139
+ - **`getHTML()` answers without a DOM.** Serialising a document on a server no
140
+ longer needs a polyfill, which puts it alongside `toMarkdown` as something
141
+ that runs in Node, in a worker and at the edge.
142
+ - The server path and the published packages are covered by tests, after
143
+ `0.14.0` shipped from an earlier state of the source and the npm copy was
144
+ missing `isActive` and `can`.
145
+ - Bindings are tested in CI at both edges of every peer range, so a Vue 3.4 and
146
+ a Vue 3.5 user are both covered by something other than optimism.
147
+ - A page under `harness/ime` for checking composition on a real device.
148
+ - **`versionList` accepts a real editor.** Its parameter had been typed so that
149
+ nothing satisfied it, and every caller had to cast.
150
+ - The commercial licence names every MIT binding. It had listed `core`, `react`
151
+ and `vue` and omitted `svelte` and `solid`, contradicting the README.
152
+ - Documentation: a page each for AI, collaboration and version history; every
153
+ package covered in detail in the README; and the fact that loading an HTML
154
+ string needs a DOM while JSON does not, which the tests asserted and nothing
155
+ said out loud.
156
+
157
+ ## 0.15.0 — 2026-08-28
158
+
159
+ - **Node and mark names are typed from the extension array.** `isActive('bold')`
160
+ is checked against what you actually passed, so a renamed or absent extension
161
+ is a compile error rather than a button that silently never lights up.
162
+ - **`editor.can`** — every command, asking instead of doing. A toolbar button
163
+ can be disabled rather than enabled-and-inert.
164
+ - **A performance ratchet.** `pnpm bench:check` measures against a recorded
165
+ baseline and fails CI on a regression, on a harness steady enough to mean it.
166
+ - `pnpm size` refuses to measure a bundle older than its source.
167
+ - Loading a document starts its history there, so the first undo cannot empty
168
+ the editor.
169
+ - A node declares which marks it accepts, and the schema now asks.
170
+ - Documentation: the packages, sizes and snippets describe what actually ships.
171
+
172
+ ## 0.6.0 – 0.14.0 — 2026-08-25 to 2026-08-27
173
+
174
+ **The licence split happened at `0.6.0`.** `@matrajs/ai`, `@matrajs/collab` and
175
+ `@matrajs/versions` carry the [Matra Commercial License](./packages/ai/LICENSE)
176
+ from this version onward. Everything through `0.5.0` was MIT, including `ai` and
177
+ `collab`, and **that grant cannot be withdrawn** — anyone already on `0.5.0` may
178
+ stay there under MIT forever.
179
+
180
+ - **`@matrajs/svelte` and `@matrajs/solid`**, so every framework has a
181
+ first-class binding rather than a community one.
182
+ - **`@matrajs/versions`** — snapshots, a real block-and-word diff between them,
183
+ and restore as one undo step.
184
+ - **Drag and drop**, with the block handle Tiptap charges for: a line shows
185
+ where the block will land, and the move is one undo step.
186
+ - **Task lists, typography, table of contents, unique ids and Markdown** — the
187
+ last of these as pure string work, so it needs no DOM.
188
+ - **Mentions and slash commands**, detection only · the popup stays yours.
189
+ - Typing stopped costing the size of the document, twice: the view diff was
190
+ narrowed to the region an edit touched, and IME users stopped paying for the
191
+ whole document on every character.
192
+ - Distribution settled: three schemes for gating the paid packages were built
193
+ and discarded before the obvious question got asked. The source is public, so
194
+ the licence is the boundary rather than the download.
195
+ - The site was rebuilt around the idea that the page is the product — every
196
+ text on it is a live editor.
197
+
198
+ ## 0.2.0 – 0.5.0 — 2026-08-24
199
+
200
+ - **Comments, collaborative editing and remote cursors.**
201
+ - **Node views and a renderer that patches instead of rebuilding**, plus
202
+ decorations.
203
+ - **Thirteen security holes closed by attacking the rendering gate**, eight in
204
+ one pass and five more in a second. Document JSON, pasted HTML and
205
+ collaborative steps are all treated as hostile.
206
+ - Vue bindings and the full extension set.
207
+
208
+ ## 0.1.0 — 2026-08-23/24
209
+
210
+ The engine, written from scratch and taken off ProseMirror entirely.
211
+
212
+ - Document model, content expression parser, resolved positions and the DOM
213
+ layer.
214
+ - Position mapping, steps and transforms · rebasing, selections, transactions
215
+ and plugins.
216
+ - Keymap, input rules, history and list commands.
217
+ - `@matrajs/core` with inferred command types, `@matrajs/react`, `@matrajs/ai`,
218
+ and the starter kit.
219
+ - **Zero runtime dependencies**, which has held since.
220
+
221
+ Published under the `@matrajs` scope from the start · the `@matra` scope
222
+ belongs to an unrelated project.
@@ -0,0 +1,87 @@
1
+ # Contributing
2
+
3
+ Thanks for looking. This is a small project with a strong opinion about its own
4
+ API, so the most useful thing you can send is usually a failing test.
5
+
6
+ ## Getting set up
7
+
8
+ ```bash
9
+ pnpm install
10
+ pnpm dev # playground at localhost:5173
11
+ ```
12
+
13
+ pnpm, not npm or yarn — the workspace uses `workspace:` protocol ranges and
14
+ `pnpm-workspace.yaml` carries the overrides. The version is pinned by
15
+ `packageManager`, so `corepack enable` is enough.
16
+
17
+ ## Before you open a pull request
18
+
19
+ ```bash
20
+ pnpm test # vitest
21
+ pnpm typecheck # tsc, including the type-level tests
22
+ pnpm check # biome, and prettier for .astro
23
+ pnpm build # tsup, all packages
24
+ ```
25
+
26
+ CI runs those plus `pnpm size`, `pnpm bench:check`, `pnpm links`,
27
+ `pnpm wiring`, `pnpm packaging`, `pnpm facts --check` and, in its own job,
28
+ `pnpm install:matrix` — every package packed and installed with plain npm
29
+ into a fresh React, Vue, Svelte, Solid and vanilla Vite app, built and run.
30
+ Running the first four locally catches almost everything. Run the benchmark
31
+ on a quiet machine: a build or an install in another terminal reads as a
32
+ twenty per cent regression.
33
+
34
+ ## What gets merged quickly
35
+
36
+ - **A failing test for a bug.** Even without a fix. A reproduction in
37
+ `packages/core/src/*.test.ts` is worth more than a paragraph describing the
38
+ problem, and it is the thing that stops the bug coming back.
39
+ - **A fix with the test that would have caught it.**
40
+ - **Documentation that corrects something wrong.** The site is Astro under
41
+ `apps/site`; every docs page has an *Edit this page on GitHub* link.
42
+
43
+ ## What to discuss first
44
+
45
+ - **A new extension in `packages/core`.** Everything in the box is in the
46
+ bundle everyone downloads, so an extension has to earn its bytes. Open an
47
+ issue and say who needs it.
48
+ - **Anything that changes a public signature.** The API is the product here.
49
+ - **A new runtime dependency.** There are none, in any package, and that is a
50
+ deliberate constraint rather than an accident. A pull request that adds one
51
+ will be asked what it would take to write instead.
52
+
53
+ ## House rules
54
+
55
+ - **Conventional commits**, lowercase after the colon, saying what the change
56
+ achieves rather than what was added. `fix(view): backspace at the start of a
57
+ block` beats `fix: update view.ts`.
58
+ - **Biome** formats and lints. Do not add ESLint or Prettier config; `.astro`
59
+ is the one exception and is already wired.
60
+ - **No `this`, no classes, no inheritance** in the public API. Extensions are
61
+ plain objects and commands are plain functions — see
62
+ [DESIGN.md](./DESIGN.md) for why.
63
+ - **No engine type in a public signature.** If a change would put one there,
64
+ it needs a different shape.
65
+ - Tests live next to what they test, as `*.test.ts`. Type-level assertions go
66
+ in `*.test-d.ts` or `*.type-test.ts` and run under `pnpm typecheck`.
67
+
68
+ ## Security
69
+
70
+ Do not open a public issue for a vulnerability. [SECURITY.md](./SECURITY.md)
71
+ has the process. The rendering path is the gate that pasted HTML, document JSON
72
+ and collaborative steps all pass through, and reports about it are taken
73
+ seriously and answered.
74
+
75
+ ## The paid packages
76
+
77
+ `packages/ai`, `packages/collab` and `packages/versions` are in this repository
78
+ and readable by anyone, but they are not MIT — see
79
+ [the licence](./packages/ai/LICENSE). Contributions to them are welcome on the
80
+ same terms as everything else, and the licence on them does not change what you
81
+ may do with your own contribution to the MIT packages.
82
+
83
+ ## Licensing your contribution
84
+
85
+ By opening a pull request you agree that your contribution is licensed under
86
+ the licence of the package you changed: MIT for `core` and the four framework
87
+ bindings, the Matra Commercial License for `ai`, `collab` and `versions`.