satteri 0.8.1 → 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 -26
  7. package/dist/command-buffer.js +64 -132
  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 +35 -13
  28. package/dist/hast/hast-visitor.js +395 -302
  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 +52 -14
  47. package/dist/mdast/mdast-visitor.js +348 -427
  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,78 +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;
57
- }
58
- function asArray(value) {
59
- return Array.isArray(value) ? value : [value];
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;
60
27
  }
28
+ const requireNid = makeRequireNid(nid);
61
29
  export class MdastVisitorContext {
62
30
  #commandBuffer = new CommandBuffer();
63
31
  #diagnostics = [];
64
32
  #handle;
65
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;
66
38
  /**
67
39
  * The URL of the document being processed (the compile `fileURL` option),
68
40
  * or `undefined` when none was given. Use `fileURLToPath(ctx.fileURL)` for a
69
41
  * decoded filesystem path.
70
42
  */
71
43
  fileURL;
72
- 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) {
73
53
  this.#handle = handle;
74
54
  this.#getSource = getSource;
75
55
  this.fileURL = fileURL;
56
+ this.#resolver = resolver;
57
+ this.data = data;
76
58
  }
77
59
  get source() {
78
60
  const value = this.#getSource();
@@ -80,34 +62,35 @@ export class MdastVisitorContext {
80
62
  return value;
81
63
  }
82
64
  removeNode(node) {
83
- this.#commandBuffer.removeNode(nid(node));
65
+ this.#commandBuffer.removeNode(requireNid(node, "removeNode"));
84
66
  }
85
67
  insertBefore(node, newNode) {
86
- const id = nid(node);
68
+ const id = requireNid(node, "insertBefore");
87
69
  for (const n of asArray(newNode))
88
- this.#commandBuffer.insertBefore(id, n);
70
+ emitMdastTree(this.#commandBuffer, "insertBefore", id, n);
89
71
  }
90
72
  insertAfter(node, newNode) {
91
- const id = nid(node);
73
+ const id = requireNid(node, "insertAfter");
92
74
  for (const n of asArray(newNode))
93
- this.#commandBuffer.insertAfter(id, n);
75
+ emitMdastTree(this.#commandBuffer, "insertAfter", id, n);
94
76
  }
95
77
  /**
96
78
  * Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any
97
79
  * children `parentNode` declares are kept after it.
98
80
  */
99
81
  wrapNode(node, parentNode) {
100
- this.#commandBuffer.wrapNode(nid(node), parentNode);
82
+ const id = requireNid(node, "wrapNode");
83
+ emitMdastTree(this.#commandBuffer, "wrapNode", id, parentNode);
101
84
  }
102
85
  prependChild(node, childNode) {
103
- const id = nid(node);
86
+ const id = requireNid(node, "prependChild");
104
87
  for (const n of asArray(childNode))
105
- this.#commandBuffer.prependChild(id, n);
88
+ emitMdastTree(this.#commandBuffer, "prependChild", id, n);
106
89
  }
107
90
  appendChild(node, childNode) {
108
- const id = nid(node);
91
+ const id = requireNid(node, "appendChild");
109
92
  for (const n of asArray(childNode))
110
- this.#commandBuffer.appendChild(id, n);
93
+ emitMdastTree(this.#commandBuffer, "appendChild", id, n);
111
94
  }
112
95
  /** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */
113
96
  insertChildAt(node, index, childNode) {
@@ -129,26 +112,49 @@ export class MdastVisitorContext {
129
112
  this.removeNode(child);
130
113
  }
131
114
  replaceNode(node, newNode) {
132
- this.#commandBuffer.replace(nid(node), newNode);
115
+ const id = requireNid(node, "replaceNode");
116
+ emitMdastTree(this.#commandBuffer, "replace", id, newNode, true);
133
117
  }
134
118
  setProperty(node, key, value) {
135
119
  if (key === "children") {
136
120
  // children is structural: set-children keeps the node and swaps only its
137
121
  // child list (reused children keep their id).
138
- const wrapper = refifyReusedNodes({ type: "root", children: value }, true);
139
- this.#commandBuffer.setChildren(nid(node), JSON.stringify(wrapper));
122
+ const id = requireNid(node, "setProperty");
123
+ const ops = compileMdastChildrenToOpstream(value);
124
+ if (!ops)
125
+ throw unencodableContentError(value);
126
+ this.#commandBuffer.setChildrenOpstream(id, ops);
140
127
  return;
141
128
  }
142
129
  if (key === "data") {
143
130
  // data is stored as JSON in the arena, serialize it for the command buffer
144
- 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);
145
132
  return;
146
133
  }
147
- this.#commandBuffer.setProperty(nid(node), key, value);
134
+ this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value);
148
135
  }
149
136
  /** Collect the concatenated text of all descendant text nodes (like mdast-util-to-string). */
150
137
  textContent(node, options) {
151
- 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"));
152
158
  }
153
159
  report({ message, node, severity = "error", }) {
154
160
  this.#diagnostics.push({
@@ -166,32 +172,6 @@ export class MdastVisitorContext {
166
172
  return this.#diagnostics;
167
173
  }
168
174
  }
169
- /** Merge return-value + context command buffers and release internals. */
170
- function mergeAndReset(returnBuffer, ctx) {
171
- const ctxCmdBuf = ctx.getCommandBuffer();
172
- const ctxBuf = ctxCmdBuf.getBuffer();
173
- const retBuf = returnBuffer.getBuffer();
174
- const totalLen = retBuf.length + ctxBuf.length;
175
- let merged;
176
- if (totalLen === 0) {
177
- merged = new Uint8Array(0);
178
- }
179
- else {
180
- merged = new Uint8Array(totalLen);
181
- merged.set(retBuf, 0);
182
- merged.set(ctxBuf, retBuf.length);
183
- }
184
- returnBuffer.reset();
185
- ctxCmdBuf.reset();
186
- return { merged, hasMutations: totalLen > 0 };
187
- }
188
- const textDecoder = new TextDecoder("utf-8");
189
- /** Build name→nodeType map from TYPE_NAMES (reverse of TYPE_NAMES). */
190
- const NAME_TO_TYPE = {};
191
- for (const [num, name] of Object.entries(TYPE_NAMES)) {
192
- NAME_TO_TYPE[name] = Number(num);
193
- }
194
- /** Resolve subscriptions from a plugin instance. */
195
175
  export function resolveMdastSubscriptions(plugin) {
196
176
  const subs = [];
197
177
  for (const [name, fn] of Object.entries(plugin)) {
@@ -207,69 +187,61 @@ export function resolveMdastSubscriptions(plugin) {
207
187
  }
208
188
  return subs;
209
189
  }
210
- /** Read a u16 from buf at offset (LE). */
211
- function ru16(view, off) {
212
- return view.getUint16(off, true);
213
- }
214
- /** Read a u32 from buf at offset (LE). */
215
- function ru32(view, off) {
216
- return view.getUint32(off, true);
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
+ }
217
203
  }
218
- /** Read a utf8 string from buf. */
219
- function rstr(buf, off, len) {
220
- return len === 0 ? "" : textDecoder.decode(buf.subarray(off, off + len));
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]);
213
+ }
214
+ return stubs;
221
215
  }
222
- /**
223
- * Lazy child materializer for the MDAST handle walk path.
224
- * Serializes the handle once on first child access, then materializes
225
- * children via MdastReader + materializeNode.
226
- */
227
- class MdastLazyChildResolver {
228
- #handle;
229
- #reader = null;
230
- constructor(handle) {
231
- this.#handle = handle;
232
- }
233
- #ensure() {
234
- if (!this.#reader) {
235
- this.#reader = new MdastReader(serializeHandle(this.#handle));
236
- }
237
- return this.#reader;
238
- }
239
- materializeChildren(childIds) {
240
- const reader = this.#ensure();
241
- const handle = this.#handle;
242
- return childIds.map((id) => {
243
- const node = materializeNode(reader, id);
244
- Object.defineProperty(node, "data", {
245
- get() {
246
- const json = napiGetNodeData(handle, id);
247
- const val = json ? JSON.parse(json) : null;
248
- Object.defineProperty(this, "data", {
249
- value: val,
250
- writable: true,
251
- enumerable: true,
252
- configurable: true,
253
- });
254
- return val;
255
- },
256
- configurable: true,
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,
257
227
  enumerable: true,
228
+ configurable: true,
258
229
  });
259
- return node;
260
- });
261
- }
230
+ return val;
231
+ },
232
+ enumerable: true,
233
+ configurable: true,
234
+ });
262
235
  }
263
236
  /**
264
237
  * Read an MDAST node from the inline data in a match buffer entry.
265
238
  *
266
239
  * Inline format (from Rust serialize_mdast_node_inline):
267
- * [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]
268
242
  */
269
- const encoder = new TextEncoder();
270
243
  function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver) {
271
244
  let pos = dataOffset;
272
- // Node data (JSON bytes), always first
273
245
  const dataJsonLen = ru32(view, pos);
274
246
  pos += 4;
275
247
  let initialData = null;
@@ -278,272 +250,54 @@ function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver)
278
250
  try {
279
251
  initialData = JSON.parse(jsonStr);
280
252
  }
281
- catch {
282
- /* ignore */
253
+ catch (err) {
254
+ if (process.env.NODE_ENV !== "production") {
255
+ console.warn(`readMdastMatchedNode: malformed node_data for nodeId=${nodeId}`, err);
256
+ }
283
257
  }
284
258
  pos += dataJsonLen;
285
259
  }
286
- // Position
287
- const position = {
288
- start: { offset: ru32(view, pos), line: ru32(view, pos + 8), column: ru32(view, pos + 12) },
289
- end: { offset: ru32(view, pos + 4), line: ru32(view, pos + 16), column: ru32(view, pos + 20) },
290
- };
260
+ const position = readPosition(view, pos);
291
261
  pos += 24;
292
- // Children, read IDs, materialize lazily via resolver
293
- const childCount = ru16(view, pos);
294
- pos += 2;
295
- const childIds = [];
296
- for (let i = 0; i < childCount; i++) {
297
- childIds.push(ru32(view, pos));
298
- pos += 4;
299
- }
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;
300
269
  const typeName = TYPE_NAMES[nodeType] ?? `unknown(${nodeType})`;
301
- // Build node with type-specific fields
302
- const node = { type: typeName, position };
270
+ const node = { type: typeName };
271
+ if (position !== undefined)
272
+ node.position = position;
303
273
  if (childCount > 0) {
304
- Object.defineProperty(node, "children", {
305
- get() {
306
- const val = resolver.materializeChildren(childIds);
307
- Object.defineProperty(this, "children", {
308
- value: val,
309
- writable: true,
310
- enumerable: true,
311
- configurable: true,
312
- });
313
- return val;
314
- },
315
- configurable: true,
316
- enumerable: true,
317
- });
318
- }
319
- switch (nodeType) {
320
- case 2: {
321
- // heading
322
- node.depth = buf[pos];
323
- break;
324
- }
325
- case 10:
326
- case 13:
327
- case 7:
328
- case 25:
329
- case 26: {
330
- // text, inlineCode, html, yaml, toml
331
- const vlen = ru32(view, pos);
332
- node.value = rstr(buf, pos + 4, vlen);
333
- break;
334
- }
335
- case 8: {
336
- // code
337
- const langLen = ru16(view, pos);
338
- pos += 2;
339
- node.lang = langLen > 0 ? rstr(buf, pos, langLen) : null;
340
- pos += langLen;
341
- const metaLen = ru16(view, pos);
342
- pos += 2;
343
- node.meta = metaLen > 0 ? rstr(buf, pos, metaLen) : null;
344
- pos += metaLen;
345
- const valLen = ru32(view, pos);
346
- pos += 4;
347
- node.value = rstr(buf, pos, valLen);
348
- break;
349
- }
350
- case 27:
351
- case 28: {
352
- // math, inlineMath
353
- const metaLen = ru16(view, pos);
354
- pos += 2;
355
- node.meta = metaLen > 0 ? rstr(buf, pos, metaLen) : null;
356
- pos += metaLen;
357
- const valLen = ru32(view, pos);
358
- pos += 4;
359
- node.value = rstr(buf, pos, valLen);
360
- break;
361
- }
362
- case 15: {
363
- // link
364
- const urlLen = ru16(view, pos);
365
- pos += 2;
366
- node.url = rstr(buf, pos, urlLen);
367
- pos += urlLen;
368
- const titleLen = ru16(view, pos);
369
- pos += 2;
370
- node.title = titleLen > 0 ? rstr(buf, pos, titleLen) : null;
371
- break;
372
- }
373
- case 16: {
374
- // image
375
- const urlLen = ru16(view, pos);
376
- pos += 2;
377
- node.url = rstr(buf, pos, urlLen);
378
- pos += urlLen;
379
- const altLen = ru16(view, pos);
380
- pos += 2;
381
- node.alt = rstr(buf, pos, altLen);
382
- pos += altLen;
383
- const titleLen = ru16(view, pos);
384
- pos += 2;
385
- node.title = titleLen > 0 ? rstr(buf, pos, titleLen) : null;
386
- break;
387
- }
388
- case 9: {
389
- // definition
390
- const urlLen = ru16(view, pos);
391
- pos += 2;
392
- node.url = rstr(buf, pos, urlLen);
393
- pos += urlLen;
394
- const titleLen = ru16(view, pos);
395
- pos += 2;
396
- node.title = titleLen > 0 ? rstr(buf, pos, titleLen) : null;
397
- pos += titleLen;
398
- const idLen = ru16(view, pos);
399
- pos += 2;
400
- node.identifier = rstr(buf, pos, idLen);
401
- pos += idLen;
402
- const labelLen = ru16(view, pos);
403
- pos += 2;
404
- node.label = rstr(buf, pos, labelLen);
405
- break;
406
- }
407
- case 5: {
408
- // list
409
- node.start = ru32(view, pos);
410
- node.ordered = buf[pos + 4] !== 0;
411
- node.spread = buf[pos + 5] !== 0;
412
- if (!node.ordered)
413
- node.start = null;
414
- break;
415
- }
416
- case 6: {
417
- // listItem
418
- const checked = buf[pos];
419
- node.checked = checked === 2 ? null : checked === 1;
420
- node.spread = buf[pos + 1] !== 0;
421
- break;
422
- }
423
- case 17:
424
- case 18:
425
- case 20: {
426
- // linkReference, imageReference, footnoteReference
427
- const idLen = ru16(view, pos);
428
- pos += 2;
429
- node.identifier = rstr(buf, pos, idLen);
430
- pos += idLen;
431
- const labelLen = ru16(view, pos);
432
- pos += 2;
433
- node.label = rstr(buf, pos, labelLen);
434
- pos += labelLen;
435
- const kind = buf[pos];
436
- pos += 1;
437
- // Only link/image references carry `referenceType`; the mdast spec
438
- // defines it for those two, not for `footnoteReference`.
439
- if (nodeType !== 20) {
440
- node.referenceType = ["shortcut", "collapsed", "full"][kind] ?? "shortcut";
441
- }
442
- // imageReference also carries `alt` (the serializer appends it).
443
- if (nodeType === 18) {
444
- const altLen = ru16(view, pos);
445
- pos += 2;
446
- node.alt = rstr(buf, pos, altLen);
447
- }
448
- break;
449
- }
450
- case 19: {
451
- // footnoteDefinition
452
- const idLen = ru16(view, pos);
453
- pos += 2;
454
- node.identifier = rstr(buf, pos, idLen);
455
- pos += idLen;
456
- const labelLen = ru16(view, pos);
457
- pos += 2;
458
- node.label = rstr(buf, pos, labelLen);
459
- break;
460
- }
461
- case 21: {
462
- // table
463
- const count = ru16(view, pos);
464
- pos += 2;
465
- const alignNames = [null, "left", "right", "center"];
466
- node.align = Array.from({ length: count }, (_, i) => alignNames[buf[pos + i]] ?? null);
467
- break;
468
- }
469
- case 30:
470
- case 31:
471
- case 32: {
472
- // containerDirective, leafDirective, textDirective
473
- const nameLen = ru16(view, pos);
474
- pos += 2;
475
- node.name = rstr(buf, pos, nameLen);
476
- pos += nameLen;
477
- const attrCount = ru16(view, pos);
478
- pos += 2;
479
- const attributes = {};
480
- for (let i = 0; i < attrCount; i++) {
481
- const keyLen = ru16(view, pos);
482
- pos += 2;
483
- const key = rstr(buf, pos, keyLen);
484
- pos += keyLen;
485
- const valLen = ru16(view, pos);
486
- pos += 2;
487
- const val = rstr(buf, pos, valLen);
488
- pos += valLen;
489
- attributes[key] = val;
274
+ makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver);
275
+ }
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;
490
288
  }
491
- node.attributes = attributes;
492
- break;
493
- }
494
- case 100:
495
- case 101: {
496
- // mdxJsxFlowElement, mdxJsxTextElement
497
- const nameLen = ru16(view, pos);
498
- pos += 2;
499
- node.name = nameLen > 0 ? rstr(buf, pos, nameLen) : null;
500
- pos += nameLen;
501
- const attrCount = ru16(view, pos);
502
- pos += 2;
503
- const attributes = [];
504
- for (let i = 0; i < attrCount; i++) {
505
- const kind = buf[pos];
506
- pos += 1;
507
- const anLen = ru16(view, pos);
508
- pos += 2;
509
- const an = rstr(buf, pos, anLen);
510
- pos += anLen;
511
- const avLen = ru32(view, pos);
512
- pos += 4;
513
- const av = rstr(buf, pos, avLen);
514
- pos += avLen;
515
- switch (kind) {
516
- case 0:
517
- attributes.push({ type: "mdxJsxAttribute", name: an, value: null });
518
- break;
519
- case 1:
520
- attributes.push({ type: "mdxJsxAttribute", name: an, value: av });
521
- break;
522
- case 2:
523
- attributes.push({
524
- type: "mdxJsxAttribute",
525
- name: an,
526
- value: { type: "mdxJsxAttributeValueExpression", value: av },
527
- });
528
- break;
529
- case 3:
530
- attributes.push({ type: "mdxJsxExpressionAttribute", value: av });
531
- break;
532
- }
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;
533
295
  }
534
- node.attributes = attributes;
535
- break;
536
- }
537
- case 102:
538
- case 103:
539
- case 104: {
540
- // mdxFlowExpression, mdxTextExpression, mdxjsEsm
541
- const vlen = ru32(view, pos);
542
- node.value = rstr(buf, pos + 4, vlen);
543
- 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
544
300
  }
545
- // root(0), paragraph(1), thematicBreak(3), blockquote(4), emphasis(11),
546
- // strong(12), break(14), tableRow(22), tableCell(23), delete(24): no extra data
547
301
  }
548
302
  mdastNodeIdMap.set(node, nodeId);
549
303
  if (initialData) {
@@ -551,9 +305,6 @@ function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver)
551
305
  }
552
306
  return node;
553
307
  }
554
- /** Apply a sync visitor result to the return buffer.
555
- * If the result is the same object as the input node, treat it as a no-op
556
- * so that context mutations (e.g. setProperty) are not clobbered. */
557
308
  /** The arena id of a node if it is an existing (materialized) node, else
558
309
  * undefined for a freshly-built one. */
559
310
  function reusedId(node) {
@@ -562,28 +313,193 @@ function reusedId(node) {
562
313
  const id = nid(node);
563
314
  return typeof id === "number" ? id : undefined;
564
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();
565
320
  /**
566
- * Rewrite a returned replacement tree so every *reused* node (one that came
567
- * from the arena, at any depth) becomes a `{ _ref: id }` placeholder. The
568
- * rebuild splices those originals back in place, preserving their ids — so a
569
- * patch a nested visitor queued on a passed-through child still lands, in the
570
- * same pass. Freshly-built nodes serialize as before. The root is never reffed:
571
- * 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.
572
326
  */
573
- 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) {
574
358
  if (node === null || typeof node !== "object")
575
- return node;
359
+ return false;
576
360
  if (!isRoot) {
577
361
  const id = reusedId(node);
578
- if (id !== undefined)
579
- return { _ref: id };
362
+ if (id !== undefined) {
363
+ w.ref(id);
364
+ return true;
365
+ }
580
366
  }
581
- const children = node.children;
582
- if (Array.isArray(children)) {
583
- 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);
396
+ }
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";
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);
584
499
  }
585
- return node;
586
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. */
587
503
  function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
588
504
  if (result === undefined || result === null)
589
505
  return;
@@ -598,7 +514,7 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
598
514
  returnBuffer.replace(nodeId, result);
599
515
  break;
600
516
  case "structured_node":
601
- returnBuffer.replace(nodeId, refifyReusedNodes(result, true));
517
+ emitMdastTree(returnBuffer, "replace", nodeId, result, true);
602
518
  break;
603
519
  }
604
520
  }
@@ -609,18 +525,18 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
609
525
  * Returns MdastVisitResult synchronously if all visitors are sync,
610
526
  * or Promise<MdastVisitResult> if any visitor is async.
611
527
  */
612
- export function visitMdastHandle(handle, plugin, subs, source, fileURL) {
528
+ export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {}) {
613
529
  const getSource = typeof source === "function" ? source : () => source;
614
- const context = new MdastVisitorContext(handle, getSource, fileURL);
615
- const returnBuffer = new CommandBuffer();
616
530
  const resolver = new MdastLazyChildResolver(handle);
531
+ const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data);
532
+ const returnBuffer = new CommandBuffer();
617
533
  const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: [] }));
618
534
  const matchBuf = walkMdastHandle(handle, rustSubs);
619
535
  const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
620
536
  const matchCount = ru32(matchView, 0);
621
537
  let deferred = null;
622
538
  for (let i = 0; i < matchCount; i++) {
623
- const indexBase = 4 + i * 12;
539
+ const indexBase = 4 + i * 10;
624
540
  const nodeId = ru32(matchView, indexBase);
625
541
  const subIndex = matchBuf[indexBase + 4];
626
542
  const dataOffset = ru32(matchView, indexBase + 6);
@@ -640,9 +556,14 @@ export function visitMdastHandle(handle, plugin, subs, source, fileURL) {
640
556
  for (const { nodeId, result, originalNode } of results) {
641
557
  applyMdastVisitResult(result, nodeId, returnBuffer, originalNode);
642
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();
643
563
  return finalizeMdastVisit(handle, context, returnBuffer);
644
564
  });
645
565
  }
566
+ resolver.seal();
646
567
  return finalizeMdastVisit(handle, context, returnBuffer);
647
568
  }
648
569
  function finalizeMdastVisit(handle, context, returnBuffer) {