satteri 0.8.0 → 0.9.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.
Files changed (60) hide show
  1. package/README.md +35 -0
  2. package/dist/byte-writer.d.ts +31 -0
  3. package/dist/byte-writer.js +106 -0
  4. package/dist/child-stub.d.ts +17 -0
  5. package/dist/child-stub.js +57 -0
  6. package/dist/command-buffer.d.ts +23 -24
  7. package/dist/command-buffer.js +65 -128
  8. package/dist/compile.d.ts +5 -1
  9. package/dist/compile.js +40 -26
  10. package/dist/directive-types.d.ts +4 -2
  11. package/dist/generated/arena-layout.d.ts +37 -0
  12. package/dist/generated/arena-layout.js +41 -0
  13. package/dist/generated/wire-constants.d.ts +51 -0
  14. package/dist/generated/wire-constants.js +63 -0
  15. package/dist/handles.d.ts +21 -0
  16. package/dist/handles.js +1 -0
  17. package/dist/hast/child-stub.d.ts +18 -0
  18. package/dist/hast/child-stub.js +43 -0
  19. package/dist/hast/element-props.d.ts +2 -0
  20. package/dist/hast/element-props.js +23 -0
  21. package/dist/hast/generated/node-types.d.ts +14 -0
  22. package/dist/hast/generated/node-types.js +64 -0
  23. package/dist/hast/hast-materializer.d.ts +2 -1
  24. package/dist/hast/hast-materializer.js +9 -45
  25. package/dist/hast/hast-reader.d.ts +18 -23
  26. package/dist/hast/hast-reader.js +48 -115
  27. package/dist/hast/hast-visitor.d.ts +38 -12
  28. package/dist/hast/hast-visitor.js +417 -281
  29. package/dist/index.d.ts +10 -4
  30. package/dist/index.js +23 -1
  31. package/dist/lazy-child-resolver.d.ts +36 -0
  32. package/dist/lazy-child-resolver.js +95 -0
  33. package/dist/lazy-props.d.ts +1 -1
  34. package/dist/mdast/child-stub.d.ts +18 -0
  35. package/dist/mdast/child-stub.js +46 -0
  36. package/dist/mdast/column-align.d.ts +1 -0
  37. package/dist/mdast/column-align.js +6 -0
  38. package/dist/mdast/generated/layout.d.ts +19 -0
  39. package/dist/mdast/generated/layout.js +272 -0
  40. package/dist/mdast/generated/node-types.d.ts +58 -0
  41. package/dist/mdast/generated/node-types.js +266 -0
  42. package/dist/mdast/mdast-materializer.d.ts +3 -2
  43. package/dist/mdast/mdast-materializer.js +9 -93
  44. package/dist/mdast/mdast-reader.d.ts +4 -120
  45. package/dist/mdast/mdast-reader.js +45 -272
  46. package/dist/mdast/mdast-visitor.d.ts +55 -13
  47. package/dist/mdast/mdast-visitor.js +375 -410
  48. package/dist/mdx-attr.d.ts +2 -0
  49. package/dist/mdx-attr.js +20 -0
  50. package/dist/op-stream.d.ts +38 -0
  51. package/dist/op-stream.js +125 -0
  52. package/dist/phantom.d.ts +2 -0
  53. package/dist/phantom.js +13 -0
  54. package/dist/types.d.ts +34 -4
  55. package/dist/visitor-shared.d.ts +28 -0
  56. package/dist/visitor-shared.js +52 -0
  57. package/dist/wire-read.d.ts +27 -0
  58. package/dist/wire-read.js +59 -0
  59. package/index.js +52 -52
  60. package/package.json +6 -6
@@ -1,75 +1,60 @@
1
- import { materializeNode, TYPE_NAMES } from "./mdast-materializer.js";
1
+ import { materializeNode } from "./mdast-materializer.js";
2
2
  import { MdastReader } from "./mdast-reader.js";
3
3
  import { CommandBuffer, classifyReturn } from "../command-buffer.js";
4
- import { walkMdastHandle, serializeHandle, getNodeData as napiGetNodeData, mdastTextContentHandle, } from "#binding";
5
- const MutationType = {
6
- Replace: "replace",
7
- Remove: "remove",
8
- InsertBefore: "insertBefore",
9
- InsertAfter: "insertAfter",
10
- Wrap: "wrap",
11
- PrependChild: "prependChild",
12
- AppendChild: "appendChild",
13
- SetProperty: "setProperty",
14
- };
15
- const VISITOR_KEYS = new Set([
16
- "paragraph",
17
- "heading",
18
- "thematicBreak",
19
- "blockquote",
20
- "list",
21
- "listItem",
22
- "html",
23
- "code",
24
- "definition",
25
- "text",
26
- "emphasis",
27
- "strong",
28
- "inlineCode",
29
- "break",
30
- "link",
31
- "image",
32
- "linkReference",
33
- "imageReference",
34
- "footnoteDefinition",
35
- "footnoteReference",
36
- "table",
37
- "tableRow",
38
- "tableCell",
39
- "delete",
40
- "yaml",
41
- "toml",
42
- "math",
43
- "inlineMath",
44
- "containerDirective",
45
- "leafDirective",
46
- "textDirective",
47
- "mdxJsxFlowElement",
48
- "mdxJsxTextElement",
49
- "mdxFlowExpression",
50
- "mdxTextExpression",
51
- "mdxjsEsm",
52
- ]);
4
+ import { ru32, rstr, readPosition } from "../wire-read.js";
5
+ import { decodeMdastTypeData } from "./generated/layout.js";
6
+ import { TYPE_NAMES, NAME_TO_TYPE, VISITOR_KEYS, MDAST_OPSTREAM_TYPES, } from "./generated/node-types.js";
7
+ import { OpWriter, OF_VALUE, OF_URL, OF_TITLE, OF_ALT, OF_LANG, OF_META, OF_IDENTIFIER, OF_LABEL, OF_NAME, OF_REFERENCE_TYPE, OF_DEPTH, OF_CHECKED, OF_START, OF_ORDERED, OF_SPREAD, OF_EXPLICIT, PROP_STRING, emitMdxAttr, } from "../op-stream.js";
8
+ import { walkMdastHandle, mdastTextContentHandle } from "#binding";
9
+ import { asArray, makeRequireNid, mergeAndReset, unencodableContentError, } from "../visitor-shared.js";
10
+ import { LazyChildResolver } from "../lazy-child-resolver.js";
11
+ import { MdastChildStub } from "./child-stub.js";
53
12
  /** Maps MdastNode objects to their arena node IDs without Object.defineProperty overhead. */
54
13
  const mdastNodeIdMap = new WeakMap();
55
14
  function nid(node) {
56
- return mdastNodeIdMap.get(node) ?? node._nodeId;
15
+ // Genuine stubs carry their id as a plain field; a spread copy is not
16
+ // `instanceof` and has no `_nodeId`, so it correctly reads as new content.
17
+ if (node instanceof MdastChildStub)
18
+ return node._id;
19
+ const id = mdastNodeIdMap.get(node);
20
+ if (id !== undefined)
21
+ return id;
22
+ // Plain objects are trusted only via the WeakMap or a NON-enumerable
23
+ // `_nodeId` (the materializer's convention, which spread cannot copy) — an
24
+ // enumerable one rode in on a copy and must read as new content.
25
+ const d = Object.getOwnPropertyDescriptor(node, "_nodeId");
26
+ return d !== undefined && !d.enumerable ? d.value : undefined;
57
27
  }
28
+ const requireNid = makeRequireNid(nid);
58
29
  export class MdastVisitorContext {
59
30
  #commandBuffer = new CommandBuffer();
60
31
  #diagnostics = [];
61
32
  #handle;
62
33
  #getSource;
34
+ #resolver;
35
+ /** One canonical object per parent id, so visitors can dedupe by identity.
36
+ * Null until the first `parent()` call; most passes never make one. */
37
+ #parentsById = null;
63
38
  /**
64
39
  * The URL of the document being processed (the compile `fileURL` option),
65
40
  * or `undefined` when none was given. Use `fileURLToPath(ctx.fileURL)` for a
66
41
  * decoded filesystem path.
67
42
  */
68
43
  fileURL;
69
- constructor(handle, getSource, fileURL) {
44
+ /**
45
+ * Document-level data bag, shared across every plugin in the compile and
46
+ * across the mdast→hast phase boundary. Mutate keys directly
47
+ * (`ctx.data.foo = x`); the bag itself isn't reassignable. Values are kept
48
+ * on the JS side, so any value is allowed, including functions and class
49
+ * instances. Returned to the caller as `result.data`.
50
+ */
51
+ data;
52
+ constructor(handle, getSource, fileURL, resolver, data) {
70
53
  this.#handle = handle;
71
54
  this.#getSource = getSource;
72
55
  this.fileURL = fileURL;
56
+ this.#resolver = resolver;
57
+ this.data = data;
73
58
  }
74
59
  get source() {
75
60
  const value = this.#getSource();
@@ -77,41 +62,99 @@ export class MdastVisitorContext {
77
62
  return value;
78
63
  }
79
64
  removeNode(node) {
80
- this.#commandBuffer.removeNode(nid(node));
65
+ this.#commandBuffer.removeNode(requireNid(node, "removeNode"));
81
66
  }
82
67
  insertBefore(node, newNode) {
83
- this.#commandBuffer.insertBefore(nid(node), newNode);
68
+ const id = requireNid(node, "insertBefore");
69
+ for (const n of asArray(newNode))
70
+ emitMdastTree(this.#commandBuffer, "insertBefore", id, n);
84
71
  }
85
72
  insertAfter(node, newNode) {
86
- this.#commandBuffer.insertAfter(nid(node), newNode);
73
+ const id = requireNid(node, "insertAfter");
74
+ for (const n of asArray(newNode))
75
+ emitMdastTree(this.#commandBuffer, "insertAfter", id, n);
87
76
  }
88
77
  /**
89
78
  * Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any
90
79
  * children `parentNode` declares are kept after it.
91
80
  */
92
81
  wrapNode(node, parentNode) {
93
- this.#commandBuffer.wrapNode(nid(node), parentNode);
82
+ const id = requireNid(node, "wrapNode");
83
+ emitMdastTree(this.#commandBuffer, "wrapNode", id, parentNode);
94
84
  }
95
85
  prependChild(node, childNode) {
96
- this.#commandBuffer.prependChild(nid(node), childNode);
86
+ const id = requireNid(node, "prependChild");
87
+ for (const n of asArray(childNode))
88
+ emitMdastTree(this.#commandBuffer, "prependChild", id, n);
97
89
  }
98
90
  appendChild(node, childNode) {
99
- this.#commandBuffer.appendChild(nid(node), childNode);
91
+ const id = requireNid(node, "appendChild");
92
+ for (const n of asArray(childNode))
93
+ emitMdastTree(this.#commandBuffer, "appendChild", id, n);
94
+ }
95
+ /** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */
96
+ insertChildAt(node, index, childNode) {
97
+ const children = "children" in node ? node.children : [];
98
+ if (index <= 0 || children.length === 0) {
99
+ this.prependChild(node, childNode);
100
+ }
101
+ else if (index >= children.length) {
102
+ this.appendChild(node, childNode);
103
+ }
104
+ else {
105
+ this.insertBefore(children[index], childNode);
106
+ }
107
+ }
108
+ /** Remove the `index`-th child of `node`; a no-op when there is no such child. */
109
+ removeChildAt(node, index) {
110
+ const child = "children" in node ? node.children[index] : undefined;
111
+ if (child)
112
+ this.removeNode(child);
100
113
  }
101
114
  replaceNode(node, newNode) {
102
- this.#commandBuffer.replace(nid(node), newNode);
115
+ const id = requireNid(node, "replaceNode");
116
+ emitMdastTree(this.#commandBuffer, "replace", id, newNode, true);
103
117
  }
104
118
  setProperty(node, key, value) {
119
+ if (key === "children") {
120
+ // children is structural: set-children keeps the node and swaps only its
121
+ // child list (reused children keep their id).
122
+ const id = requireNid(node, "setProperty");
123
+ const ops = compileMdastChildrenToOpstream(value);
124
+ if (!ops)
125
+ throw unencodableContentError(value);
126
+ this.#commandBuffer.setChildrenOpstream(id, ops);
127
+ return;
128
+ }
105
129
  if (key === "data") {
106
130
  // data is stored as JSON in the arena, serialize it for the command buffer
107
- this.#commandBuffer.setProperty(nid(node), key, value != null ? JSON.stringify(value) : null);
131
+ this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value != null ? JSON.stringify(value) : null);
108
132
  return;
109
133
  }
110
- this.#commandBuffer.setProperty(nid(node), key, value);
134
+ this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value);
111
135
  }
112
136
  /** Collect the concatenated text of all descendant text nodes (like mdast-util-to-string). */
113
137
  textContent(node, options) {
114
- return mdastTextContentHandle(this.#handle, nid(node), options);
138
+ return mdastTextContentHandle(this.#handle, requireNid(node, "textContent"), options);
139
+ }
140
+ parent(node) {
141
+ const parentId = this.#resolver.parentIdOf(requireNid(node, "parent"));
142
+ if (parentId === undefined)
143
+ return undefined;
144
+ const byId = (this.#parentsById ??= new Map());
145
+ let parent = byId.get(parentId);
146
+ if (parent === undefined) {
147
+ parent = this.#resolver.materializeOne(parentId);
148
+ byId.set(parentId, parent);
149
+ }
150
+ return parent;
151
+ }
152
+ /**
153
+ * Index of `node` within its parent's children, or `undefined` at the root.
154
+ * Use this rather than `parent.children.indexOf(node)`, which won't find it.
155
+ */
156
+ indexOf(node) {
157
+ return this.#resolver.indexInParent(requireNid(node, "indexOf"));
115
158
  }
116
159
  report({ message, node, severity = "error", }) {
117
160
  this.#diagnostics.push({
@@ -129,32 +172,6 @@ export class MdastVisitorContext {
129
172
  return this.#diagnostics;
130
173
  }
131
174
  }
132
- /** Merge return-value + context command buffers and release internals. */
133
- function mergeAndReset(returnBuffer, ctx) {
134
- const ctxCmdBuf = ctx.getCommandBuffer();
135
- const ctxBuf = ctxCmdBuf.getBuffer();
136
- const retBuf = returnBuffer.getBuffer();
137
- const totalLen = retBuf.length + ctxBuf.length;
138
- let merged;
139
- if (totalLen === 0) {
140
- merged = new Uint8Array(0);
141
- }
142
- else {
143
- merged = new Uint8Array(totalLen);
144
- merged.set(retBuf, 0);
145
- merged.set(ctxBuf, retBuf.length);
146
- }
147
- returnBuffer.reset();
148
- ctxCmdBuf.reset();
149
- return { merged, hasMutations: totalLen > 0 };
150
- }
151
- const textDecoder = new TextDecoder("utf-8");
152
- /** Build name→nodeType map from TYPE_NAMES (reverse of TYPE_NAMES). */
153
- const NAME_TO_TYPE = {};
154
- for (const [num, name] of Object.entries(TYPE_NAMES)) {
155
- NAME_TO_TYPE[name] = Number(num);
156
- }
157
- /** Resolve subscriptions from a plugin instance. */
158
175
  export function resolveMdastSubscriptions(plugin) {
159
176
  const subs = [];
160
177
  for (const [name, fn] of Object.entries(plugin)) {
@@ -170,69 +187,61 @@ export function resolveMdastSubscriptions(plugin) {
170
187
  }
171
188
  return subs;
172
189
  }
173
- /** Read a u16 from buf at offset (LE). */
174
- function ru16(view, off) {
175
- return view.getUint16(off, true);
176
- }
177
- /** Read a u32 from buf at offset (LE). */
178
- function ru32(view, off) {
179
- return view.getUint32(off, true);
180
- }
181
- /** Read a utf8 string from buf. */
182
- function rstr(buf, off, len) {
183
- return len === 0 ? "" : textDecoder.decode(buf.subarray(off, off + len));
190
+ class MdastLazyChildResolver extends LazyChildResolver {
191
+ createReader(wire) {
192
+ return new MdastReader(wire);
193
+ }
194
+ materializeNode(reader, nodeId) {
195
+ return materializeNode(reader, nodeId);
196
+ }
197
+ readParentId(reader, nodeId) {
198
+ return reader.getParentId(nodeId);
199
+ }
200
+ readChildIds(reader, nodeId) {
201
+ return reader.getChildIds(nodeId);
202
+ }
184
203
  }
185
- /**
186
- * Lazy child materializer for the MDAST handle walk path.
187
- * Serializes the handle once on first child access, then materializes
188
- * children via MdastReader + materializeNode.
189
- */
190
- class MdastLazyChildResolver {
191
- #handle;
192
- #reader = null;
193
- constructor(handle) {
194
- this.#handle = handle;
204
+ /** Build the child-stub list for a matched node from the wire's `[child_ids]
205
+ * [child_types]` blocks — no arena snapshot. The seal check still applies:
206
+ * post-pass ids are stale, and a stub built from them could later splice the
207
+ * wrong node as a ref. */
208
+ function readMdastChildStubs(view, buf, idsPos, typesPos, count, resolver) {
209
+ resolver.assertUnsealed();
210
+ const stubs = new Array(count);
211
+ for (let i = 0; i < count; i++) {
212
+ stubs[i] = new MdastChildStub(resolver, ru32(view, idsPos + i * 4), buf[typesPos + i]);
195
213
  }
196
- #ensure() {
197
- if (!this.#reader) {
198
- this.#reader = new MdastReader(serializeHandle(this.#handle));
199
- }
200
- return this.#reader;
201
- }
202
- materializeChildren(childIds) {
203
- const reader = this.#ensure();
204
- const handle = this.#handle;
205
- return childIds.map((id) => {
206
- const node = materializeNode(reader, id);
207
- Object.defineProperty(node, "data", {
208
- get() {
209
- const json = napiGetNodeData(handle, id);
210
- const val = json ? JSON.parse(json) : null;
211
- Object.defineProperty(this, "data", {
212
- value: val,
213
- writable: true,
214
- enumerable: true,
215
- configurable: true,
216
- });
217
- return val;
218
- },
219
- configurable: true,
214
+ return stubs;
215
+ }
216
+ /** Install `children` as an own enumerable getter (spread must carry it),
217
+ * self-replacing with the one stable stub array on first read. One closure
218
+ * and one define per node — installing the wire locals as hidden slots
219
+ * instead measurably regressed every matching pipeline. */
220
+ function makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver) {
221
+ Object.defineProperty(node, "children", {
222
+ get() {
223
+ const val = readMdastChildStubs(view, buf, childIdsPos, childTypesPos, childCount, resolver);
224
+ Object.defineProperty(this, "children", {
225
+ value: val,
226
+ writable: true,
220
227
  enumerable: true,
228
+ configurable: true,
221
229
  });
222
- return node;
223
- });
224
- }
230
+ return val;
231
+ },
232
+ enumerable: true,
233
+ configurable: true,
234
+ });
225
235
  }
226
236
  /**
227
237
  * Read an MDAST node from the inline data in a match buffer entry.
228
238
  *
229
239
  * Inline format (from Rust serialize_mdast_node_inline):
230
- * [node_data: u32+bytes][position: 6×u32 = 24B][child_count: u16][child_ids: N×u32][type-specific data]
240
+ * [node_data: u32+bytes][position: 6×u32 = 24B][child_count: u32][child_ids: N×u32]
241
+ * [child_types: N×u8][type-specific data]
231
242
  */
232
- const encoder = new TextEncoder();
233
243
  function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver) {
234
244
  let pos = dataOffset;
235
- // Node data (JSON bytes), always first
236
245
  const dataJsonLen = ru32(view, pos);
237
246
  pos += 4;
238
247
  let initialData = null;
@@ -241,265 +250,54 @@ function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver)
241
250
  try {
242
251
  initialData = JSON.parse(jsonStr);
243
252
  }
244
- catch {
245
- /* ignore */
253
+ catch (err) {
254
+ if (process.env.NODE_ENV !== "production") {
255
+ console.warn(`readMdastMatchedNode: malformed node_data for nodeId=${nodeId}`, err);
256
+ }
246
257
  }
247
258
  pos += dataJsonLen;
248
259
  }
249
- // Position
250
- const position = {
251
- start: { offset: ru32(view, pos), line: ru32(view, pos + 8), column: ru32(view, pos + 12) },
252
- end: { offset: ru32(view, pos + 4), line: ru32(view, pos + 16), column: ru32(view, pos + 20) },
253
- };
260
+ const position = readPosition(view, pos);
254
261
  pos += 24;
255
- // Children, read IDs, materialize lazily via resolver
256
- const childCount = ru16(view, pos);
257
- pos += 2;
258
- const childIds = [];
259
- for (let i = 0; i < childCount; i++) {
260
- childIds.push(ru32(view, pos));
261
- pos += 4;
262
- }
262
+ const childCount = ru32(view, pos);
263
+ pos += 4;
264
+ // Ids/types decode lazily with `.children` — most matched nodes never read them.
265
+ const childIdsPos = pos;
266
+ pos += childCount * 4;
267
+ const childTypesPos = pos;
268
+ pos += childCount;
263
269
  const typeName = TYPE_NAMES[nodeType] ?? `unknown(${nodeType})`;
264
- // Build node with type-specific fields
265
- const node = { type: typeName, position };
270
+ const node = { type: typeName };
271
+ if (position !== undefined)
272
+ node.position = position;
266
273
  if (childCount > 0) {
267
- Object.defineProperty(node, "children", {
268
- get() {
269
- const val = resolver.materializeChildren(childIds);
270
- Object.defineProperty(this, "children", {
271
- value: val,
272
- writable: true,
273
- enumerable: true,
274
- configurable: true,
275
- });
276
- return val;
277
- },
278
- configurable: true,
279
- enumerable: true,
280
- });
274
+ makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver);
281
275
  }
282
- switch (nodeType) {
283
- case 2: {
284
- // heading
285
- node.depth = buf[pos];
286
- break;
287
- }
288
- case 10:
289
- case 13:
290
- case 7:
291
- case 25:
292
- case 26: {
293
- // text, inlineCode, html, yaml, toml
294
- const vlen = ru32(view, pos);
295
- node.value = rstr(buf, pos + 4, vlen);
296
- break;
297
- }
298
- case 8: {
299
- // code
300
- const langLen = ru16(view, pos);
301
- pos += 2;
302
- node.lang = langLen > 0 ? rstr(buf, pos, langLen) : null;
303
- pos += langLen;
304
- const metaLen = ru16(view, pos);
305
- pos += 2;
306
- node.meta = metaLen > 0 ? rstr(buf, pos, metaLen) : null;
307
- pos += metaLen;
308
- const valLen = ru32(view, pos);
309
- pos += 4;
310
- node.value = rstr(buf, pos, valLen);
311
- break;
312
- }
313
- case 27:
314
- case 28: {
315
- // math, inlineMath
316
- const metaLen = ru16(view, pos);
317
- pos += 2;
318
- node.meta = metaLen > 0 ? rstr(buf, pos, metaLen) : null;
319
- pos += metaLen;
320
- const valLen = ru32(view, pos);
321
- pos += 4;
322
- node.value = rstr(buf, pos, valLen);
323
- break;
324
- }
325
- case 15: {
326
- // link
327
- const urlLen = ru16(view, pos);
328
- pos += 2;
329
- node.url = rstr(buf, pos, urlLen);
330
- pos += urlLen;
331
- const titleLen = ru16(view, pos);
332
- pos += 2;
333
- node.title = titleLen > 0 ? rstr(buf, pos, titleLen) : null;
334
- break;
335
- }
336
- case 16: {
337
- // image
338
- const urlLen = ru16(view, pos);
339
- pos += 2;
340
- node.url = rstr(buf, pos, urlLen);
341
- pos += urlLen;
342
- const altLen = ru16(view, pos);
343
- pos += 2;
344
- node.alt = rstr(buf, pos, altLen);
345
- pos += altLen;
346
- const titleLen = ru16(view, pos);
347
- pos += 2;
348
- node.title = titleLen > 0 ? rstr(buf, pos, titleLen) : null;
349
- break;
350
- }
351
- case 9: {
352
- // definition
353
- const urlLen = ru16(view, pos);
354
- pos += 2;
355
- node.url = rstr(buf, pos, urlLen);
356
- pos += urlLen;
357
- const titleLen = ru16(view, pos);
358
- pos += 2;
359
- node.title = titleLen > 0 ? rstr(buf, pos, titleLen) : null;
360
- pos += titleLen;
361
- const idLen = ru16(view, pos);
362
- pos += 2;
363
- node.identifier = rstr(buf, pos, idLen);
364
- pos += idLen;
365
- const labelLen = ru16(view, pos);
366
- pos += 2;
367
- node.label = rstr(buf, pos, labelLen);
368
- break;
369
- }
370
- case 5: {
371
- // list
372
- node.start = ru32(view, pos);
373
- node.ordered = buf[pos + 4] !== 0;
374
- node.spread = buf[pos + 5] !== 0;
375
- if (!node.ordered)
376
- node.start = null;
377
- break;
378
- }
379
- case 6: {
380
- // listItem
381
- const checked = buf[pos];
382
- node.checked = checked === 2 ? null : checked === 1;
383
- node.spread = buf[pos + 1] !== 0;
384
- break;
385
- }
386
- case 17:
387
- case 18:
388
- case 20: {
389
- // linkReference, imageReference, footnoteReference
390
- const idLen = ru16(view, pos);
391
- pos += 2;
392
- node.identifier = rstr(buf, pos, idLen);
393
- pos += idLen;
394
- const labelLen = ru16(view, pos);
395
- pos += 2;
396
- node.label = rstr(buf, pos, labelLen);
397
- pos += labelLen;
398
- const kind = buf[pos];
399
- // Only link/image references carry `referenceType`; the mdast spec
400
- // defines it for those two, not for `footnoteReference`.
401
- if (nodeType !== 20) {
402
- node.referenceType = ["shortcut", "collapsed", "full"][kind] ?? "shortcut";
403
- }
404
- break;
405
- }
406
- case 19: {
407
- // footnoteDefinition
408
- const idLen = ru16(view, pos);
409
- pos += 2;
410
- node.identifier = rstr(buf, pos, idLen);
411
- pos += idLen;
412
- const labelLen = ru16(view, pos);
413
- pos += 2;
414
- node.label = rstr(buf, pos, labelLen);
415
- break;
416
- }
417
- case 21: {
418
- // table
419
- const count = ru16(view, pos);
420
- pos += 2;
421
- const alignNames = [null, "left", "right", "center"];
422
- node.align = Array.from({ length: count }, (_, i) => alignNames[buf[pos + i]] ?? null);
423
- break;
424
- }
425
- case 30:
426
- case 31:
427
- case 32: {
428
- // containerDirective, leafDirective, textDirective
429
- const nameLen = ru16(view, pos);
430
- pos += 2;
431
- node.name = rstr(buf, pos, nameLen);
432
- pos += nameLen;
433
- const attrCount = ru16(view, pos);
434
- pos += 2;
435
- const attributes = {};
436
- for (let i = 0; i < attrCount; i++) {
437
- const keyLen = ru16(view, pos);
438
- pos += 2;
439
- const key = rstr(buf, pos, keyLen);
440
- pos += keyLen;
441
- const valLen = ru16(view, pos);
442
- pos += 2;
443
- const val = rstr(buf, pos, valLen);
444
- pos += valLen;
445
- attributes[key] = val;
276
+ // Fixed-field types decode from the generated layout table; the rest
277
+ // (variable-length / cross-field) stay in the hand-written switch.
278
+ if (!decodeMdastTypeData(view, buf, pos, nodeType, node)) {
279
+ switch (nodeType) {
280
+ case 5: {
281
+ // list
282
+ node.start = ru32(view, pos);
283
+ node.ordered = buf[pos + 4] !== 0;
284
+ node.spread = buf[pos + 5] !== 0;
285
+ if (!node.ordered)
286
+ node.start = null;
287
+ break;
446
288
  }
447
- node.attributes = attributes;
448
- break;
449
- }
450
- case 100:
451
- case 101: {
452
- // mdxJsxFlowElement, mdxJsxTextElement
453
- const nameLen = ru16(view, pos);
454
- pos += 2;
455
- node.name = nameLen > 0 ? rstr(buf, pos, nameLen) : null;
456
- pos += nameLen;
457
- const attrCount = ru16(view, pos);
458
- pos += 2;
459
- const attributes = [];
460
- for (let i = 0; i < attrCount; i++) {
461
- const kind = buf[pos];
462
- pos += 1;
463
- const anLen = ru16(view, pos);
464
- pos += 2;
465
- const an = rstr(buf, pos, anLen);
466
- pos += anLen;
467
- const avLen = ru32(view, pos);
468
- pos += 4;
469
- const av = rstr(buf, pos, avLen);
470
- pos += avLen;
471
- switch (kind) {
472
- case 0:
473
- attributes.push({ type: "mdxJsxAttribute", name: an, value: null });
474
- break;
475
- case 1:
476
- attributes.push({ type: "mdxJsxAttribute", name: an, value: av });
477
- break;
478
- case 2:
479
- attributes.push({
480
- type: "mdxJsxAttribute",
481
- name: an,
482
- value: { type: "mdxJsxAttributeValueExpression", value: av },
483
- });
484
- break;
485
- case 3:
486
- attributes.push({ type: "mdxJsxExpressionAttribute", value: av });
487
- break;
488
- }
289
+ case 6: {
290
+ // listItem
291
+ const checked = buf[pos];
292
+ node.checked = checked === 2 ? null : checked === 1;
293
+ node.spread = buf[pos + 1] !== 0;
294
+ break;
489
295
  }
490
- node.attributes = attributes;
491
- break;
492
- }
493
- case 102:
494
- case 103:
495
- case 104: {
496
- // mdxFlowExpression, mdxTextExpression, mdxjsEsm
497
- const vlen = ru32(view, pos);
498
- node.value = rstr(buf, pos + 4, vlen);
499
- break;
296
+ // table (21), directives (30/31/32) and mdxJsx elements (100/101) are
297
+ // decoded by the generated `decodeMdastTypeData` from their tails.
298
+ // root(0), paragraph(1), thematicBreak(3), blockquote(4), emphasis(11),
299
+ // strong(12), break(14), tableRow(22), tableCell(23), delete(24): no extra data
500
300
  }
501
- // root(0), paragraph(1), thematicBreak(3), blockquote(4), emphasis(11),
502
- // strong(12), break(14), tableRow(22), tableCell(23), delete(24): no extra data
503
301
  }
504
302
  mdastNodeIdMap.set(node, nodeId);
505
303
  if (initialData) {
@@ -507,9 +305,6 @@ function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver)
507
305
  }
508
306
  return node;
509
307
  }
510
- /** Apply a sync visitor result to the return buffer.
511
- * If the result is the same object as the input node, treat it as a no-op
512
- * so that context mutations (e.g. setProperty) are not clobbered. */
513
308
  /** The arena id of a node if it is an existing (materialized) node, else
514
309
  * undefined for a freshly-built one. */
515
310
  function reusedId(node) {
@@ -518,28 +313,193 @@ function reusedId(node) {
518
313
  const id = nid(node);
519
314
  return typeof id === "number" ? id : undefined;
520
315
  }
316
+ // Reused across every replacement in a pass: compile is synchronous and its
317
+ // result is copied into the command buffer before the next call, so a single
318
+ // writer is safe and avoids a 512-byte allocation per built node.
319
+ const mdastWriter = new OpWriter();
521
320
  /**
522
- * Rewrite a returned replacement tree so every *reused* node (one that came
523
- * from the arena, at any depth) becomes a `{ _ref: id }` placeholder. The
524
- * rebuild splices those originals back in place, preserving their ids — so a
525
- * patch a nested visitor queued on a passed-through child still lands, in the
526
- * same pass. Freshly-built nodes serialize as before. The root is never reffed:
527
- * it is the new shape replacing the visited node.
321
+ * Compile a declarative MDAST replacement tree to the op-stream — the only
322
+ * structural encoding. Reused nodes (those still carrying an arena id) become
323
+ * `ref`s so the rebuild splices the original back in place. Returns null when
324
+ * the replay can't reproduce the tree identically (unsupported node type or
325
+ * out-of-range numeric field); the caller turns that into a hard error.
528
326
  */
529
- function refifyReusedNodes(node, isRoot) {
327
+ function compileMdastToOpstream(root, forReplace = false) {
328
+ mdastWriter.begin();
329
+ try {
330
+ if (!emitMdastOp(mdastWriter, root, true, forReplace))
331
+ return null;
332
+ return mdastWriter.take();
333
+ }
334
+ finally {
335
+ mdastWriter.end();
336
+ }
337
+ }
338
+ /** Compile a set-children payload: a root-wrapped child list, the shape
339
+ * `Patch::SetChildren` splices in. Reused children become refs. */
340
+ function compileMdastChildrenToOpstream(children) {
341
+ if (!Array.isArray(children))
342
+ return null;
343
+ mdastWriter.begin();
344
+ try {
345
+ mdastWriter.open(NAME_TO_TYPE.root);
346
+ for (const c of children) {
347
+ if (!emitMdastOp(mdastWriter, c, false, false))
348
+ return null;
349
+ }
350
+ mdastWriter.close();
351
+ return mdastWriter.take();
352
+ }
353
+ finally {
354
+ mdastWriter.end();
355
+ }
356
+ }
357
+ function emitMdastOp(w, node, isRoot, forReplace) {
530
358
  if (node === null || typeof node !== "object")
531
- return node;
359
+ return false;
532
360
  if (!isRoot) {
533
361
  const id = reusedId(node);
534
- if (id !== undefined)
535
- return { _ref: id };
362
+ if (id !== undefined) {
363
+ w.ref(id);
364
+ return true;
365
+ }
536
366
  }
537
- const children = node.children;
538
- if (Array.isArray(children)) {
539
- return { ...node, children: children.map((c) => refifyReusedNodes(c, false)) };
367
+ const n = node;
368
+ const type = MDAST_OPSTREAM_TYPES[n.type];
369
+ if (type === undefined)
370
+ return false;
371
+ w.open(type);
372
+ if (typeof n.value === "string")
373
+ w.str(OF_VALUE, n.value);
374
+ if (typeof n.url === "string")
375
+ w.str(OF_URL, n.url);
376
+ if (typeof n.title === "string")
377
+ w.str(OF_TITLE, n.title);
378
+ if (typeof n.alt === "string")
379
+ w.str(OF_ALT, n.alt);
380
+ if (typeof n.lang === "string")
381
+ w.str(OF_LANG, n.lang);
382
+ if (typeof n.meta === "string")
383
+ w.str(OF_META, n.meta);
384
+ if (typeof n.identifier === "string")
385
+ w.str(OF_IDENTIFIER, n.identifier);
386
+ if (typeof n.label === "string")
387
+ w.str(OF_LABEL, n.label);
388
+ if (typeof n.referenceType === "string")
389
+ w.str(OF_REFERENCE_TYPE, n.referenceType);
390
+ // Out-of-range numbers compile to null and the caller throws — a visible
391
+ // error instead of silently masking the bits.
392
+ if (typeof n.depth === "number") {
393
+ if (!Number.isInteger(n.depth) || n.depth < 0 || n.depth > 255)
394
+ return false;
395
+ w.u8(OF_DEPTH, n.depth);
540
396
  }
541
- return node;
397
+ if (typeof n.checked === "boolean")
398
+ w.u8(OF_CHECKED, n.checked ? 1 : 0);
399
+ if (typeof n.start === "number") {
400
+ if (!Number.isInteger(n.start) || n.start < 0 || n.start > 4294967295)
401
+ return false;
402
+ w.u32(OF_START, n.start);
403
+ }
404
+ if (typeof n.ordered === "boolean")
405
+ w.bool(OF_ORDERED, n.ordered);
406
+ if (typeof n.spread === "boolean")
407
+ w.bool(OF_SPREAD, n.spread);
408
+ if (typeof n.name === "string")
409
+ w.str(OF_NAME, n.name);
410
+ const attrs = n.attributes;
411
+ if (Array.isArray(attrs)) {
412
+ for (const a of attrs)
413
+ emitMdxAttr(w, a);
414
+ }
415
+ else if (attrs !== null && typeof attrs === "object") {
416
+ // Directive attributes: a string→string map; non-string values are
417
+ // dropped, since the stored form holds only strings.
418
+ for (const key in attrs) {
419
+ const v = attrs[key];
420
+ if (typeof v === "string")
421
+ w.prop(key, PROP_STRING, v);
422
+ }
423
+ }
424
+ if (Array.isArray(n.align))
425
+ w.align(n.align.map(alignCode));
426
+ if (n.data?._mdxExplicitJsx === true) {
427
+ w.bool(OF_EXPLICIT, true);
428
+ }
429
+ if (n.data != null)
430
+ w.data(n.data);
431
+ if (isRoot && forReplace && n._keepChildren === true) {
432
+ // Replace splices the target's original children, discarding any the
433
+ // replacement declares.
434
+ w.keepChildren();
435
+ }
436
+ else {
437
+ // `_keepChildren` only applies to replace; other ops ignore the marker
438
+ // and emit the declared children.
439
+ const children = n.children;
440
+ if (Array.isArray(children)) {
441
+ for (const c of children)
442
+ if (!emitMdastOp(w, c, false, forReplace))
443
+ return false;
444
+ }
445
+ }
446
+ w.close();
447
+ return true;
448
+ }
449
+ /** Map a table `align` entry to its arena code (none=0). */
450
+ function alignCode(a) {
451
+ return a === "left" ? 1 : a === "right" ? 2 : a === "center" ? 3 : 0;
452
+ }
453
+ /** True for the `{raw}` / `{rawHtml}` escape hatches — re-parsed by Rust rather
454
+ * than compiled to an op-stream, so they ride the RAW_MARKDOWN / RAW_HTML
455
+ * payloads instead of the declarative encoder. */
456
+ function isRawMdastContent(content) {
457
+ const c = content;
458
+ return typeof c.raw === "string" || typeof c.rawHtml === "string";
542
459
  }
460
+ /** Encode `content` as the `op` structural command. Declarative nodes compile
461
+ * to the op-stream; the `{raw}`/`{rawHtml}` escape hatches ride the raw
462
+ * re-parse payloads. Anything that compiles to neither is a hard error — the
463
+ * op-stream is the only declarative encoding. The switches stay inline so the
464
+ * buffer calls are monomorphic (computed method names defeat inline caches on
465
+ * this warm path). */
466
+ function emitMdastTree(buffer, op, id, content, forReplace = false) {
467
+ if (isRawMdastContent(content)) {
468
+ switch (op) {
469
+ case "replace":
470
+ return buffer.replace(id, content);
471
+ case "insertBefore":
472
+ return buffer.insertBefore(id, content);
473
+ case "insertAfter":
474
+ return buffer.insertAfter(id, content);
475
+ case "prependChild":
476
+ return buffer.prependChild(id, content);
477
+ case "appendChild":
478
+ return buffer.appendChild(id, content);
479
+ case "wrapNode":
480
+ return buffer.wrapNode(id, content);
481
+ }
482
+ }
483
+ const ops = compileMdastToOpstream(content, forReplace);
484
+ if (ops === null)
485
+ throw unencodableContentError(content);
486
+ switch (op) {
487
+ case "replace":
488
+ return buffer.replaceOpstream(id, ops);
489
+ case "insertBefore":
490
+ return buffer.insertBeforeOpstream(id, ops);
491
+ case "insertAfter":
492
+ return buffer.insertAfterOpstream(id, ops);
493
+ case "prependChild":
494
+ return buffer.prependChildOpstream(id, ops);
495
+ case "appendChild":
496
+ return buffer.appendChildOpstream(id, ops);
497
+ case "wrapNode":
498
+ return buffer.wrapNodeOpstream(id, ops);
499
+ }
500
+ }
501
+ /** A result that is the same object as the input node is a no-op, so context
502
+ * mutations (e.g. setProperty) are not clobbered. */
543
503
  function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
544
504
  if (result === undefined || result === null)
545
505
  return;
@@ -554,7 +514,7 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
554
514
  returnBuffer.replace(nodeId, result);
555
515
  break;
556
516
  case "structured_node":
557
- returnBuffer.replace(nodeId, refifyReusedNodes(result, true));
517
+ emitMdastTree(returnBuffer, "replace", nodeId, result, true);
558
518
  break;
559
519
  }
560
520
  }
@@ -565,18 +525,18 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
565
525
  * Returns MdastVisitResult synchronously if all visitors are sync,
566
526
  * or Promise<MdastVisitResult> if any visitor is async.
567
527
  */
568
- export function visitMdastHandle(handle, plugin, subs, source, fileURL) {
528
+ export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {}) {
569
529
  const getSource = typeof source === "function" ? source : () => source;
570
- const context = new MdastVisitorContext(handle, getSource, fileURL);
571
- const returnBuffer = new CommandBuffer();
572
530
  const resolver = new MdastLazyChildResolver(handle);
531
+ const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data);
532
+ const returnBuffer = new CommandBuffer();
573
533
  const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: [] }));
574
534
  const matchBuf = walkMdastHandle(handle, rustSubs);
575
535
  const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
576
536
  const matchCount = ru32(matchView, 0);
577
537
  let deferred = null;
578
538
  for (let i = 0; i < matchCount; i++) {
579
- const indexBase = 4 + i * 12;
539
+ const indexBase = 4 + i * 10;
580
540
  const nodeId = ru32(matchView, indexBase);
581
541
  const subIndex = matchBuf[indexBase + 4];
582
542
  const dataOffset = ru32(matchView, indexBase + 6);
@@ -596,9 +556,14 @@ export function visitMdastHandle(handle, plugin, subs, source, fileURL) {
596
556
  for (const { nodeId, result, originalNode } of results) {
597
557
  applyMdastVisitResult(result, nodeId, returnBuffer, originalNode);
598
558
  }
559
+ // End of the pass — the caller applies the returned command buffer next,
560
+ // renumbering the arena, so later snapshots would resolve match-time
561
+ // child ids against wrong nodes. This is the last point we control.
562
+ resolver.seal();
599
563
  return finalizeMdastVisit(handle, context, returnBuffer);
600
564
  });
601
565
  }
566
+ resolver.seal();
602
567
  return finalizeMdastVisit(handle, context, returnBuffer);
603
568
  }
604
569
  function finalizeMdastVisit(handle, context, returnBuffer) {