satteri 0.10.4 → 0.11.1

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 (89) hide show
  1. package/README.md +11 -0
  2. package/dist/arena-reader.d.ts +13 -0
  3. package/dist/arena-reader.js +104 -0
  4. package/dist/binding.browser.d.ts +1 -1
  5. package/dist/binding.browser.js +1 -1
  6. package/dist/binding.d.ts +1 -1
  7. package/dist/binding.js +1 -1
  8. package/dist/byte-writer.d.ts +0 -18
  9. package/dist/byte-writer.js +6 -30
  10. package/dist/child-stub.d.ts +0 -17
  11. package/dist/child-stub.js +6 -22
  12. package/dist/command-buffer.d.ts +5 -26
  13. package/dist/command-buffer.js +28 -39
  14. package/dist/compile-options.d.ts +11 -0
  15. package/dist/compile-options.js +99 -0
  16. package/dist/compile-types.d.ts +330 -0
  17. package/dist/compile-types.js +1 -0
  18. package/dist/compile.d.ts +22 -328
  19. package/dist/compile.js +129 -379
  20. package/dist/directive-types.d.ts +1 -0
  21. package/dist/directive-types.js +1 -6
  22. package/dist/generated/arena-layout.d.ts +6 -4
  23. package/dist/generated/arena-layout.js +6 -7
  24. package/dist/generated/fused-wire.d.ts +3 -0
  25. package/dist/generated/fused-wire.js +229 -0
  26. package/dist/generated/parse-options.js +0 -5
  27. package/dist/generated/wire-constants.d.ts +3 -0
  28. package/dist/generated/wire-constants.js +33 -42
  29. package/dist/handles.d.ts +0 -7
  30. package/dist/hast/child-stub.d.ts +1 -8
  31. package/dist/hast/child-stub.js +14 -13
  32. package/dist/hast/element-props.d.ts +13 -0
  33. package/dist/hast/element-props.js +52 -6
  34. package/dist/hast/generated/node-types.d.ts +1 -9
  35. package/dist/hast/generated/node-types.js +1 -9
  36. package/dist/hast/generated/walk-decode.d.ts +10 -0
  37. package/dist/hast/generated/walk-decode.js +61 -0
  38. package/dist/hast/hast-materializer.d.ts +3 -10
  39. package/dist/hast/hast-materializer.js +73 -47
  40. package/dist/hast/hast-reader.d.ts +9 -38
  41. package/dist/hast/hast-reader.js +82 -271
  42. package/dist/hast/hast-visitor.d.ts +29 -16
  43. package/dist/hast/hast-visitor.js +197 -508
  44. package/dist/hast/hast-walk.d.ts +18 -0
  45. package/dist/hast/hast-walk.js +264 -0
  46. package/dist/index.d.ts +5 -5
  47. package/dist/index.js +4 -12
  48. package/dist/lazy-child-resolver.d.ts +1 -21
  49. package/dist/lazy-child-resolver.js +11 -32
  50. package/dist/materializer-cache.d.ts +5 -29
  51. package/dist/materializer-cache.js +29 -101
  52. package/dist/mdast/child-stub.d.ts +1 -12
  53. package/dist/mdast/child-stub.js +18 -28
  54. package/dist/mdast/column-align.d.ts +1 -1
  55. package/dist/mdast/column-align.js +0 -2
  56. package/dist/mdast/generated/layout.d.ts +0 -16
  57. package/dist/mdast/generated/layout.js +6 -47
  58. package/dist/mdast/generated/node-types.d.ts +1 -9
  59. package/dist/mdast/generated/node-types.js +3 -9
  60. package/dist/mdast/mdast-materializer.d.ts +3 -11
  61. package/dist/mdast/mdast-materializer.js +91 -63
  62. package/dist/mdast/mdast-reader.d.ts +15 -63
  63. package/dist/mdast/mdast-reader.js +132 -269
  64. package/dist/mdast/mdast-visitor.d.ts +23 -15
  65. package/dist/mdast/mdast-visitor.js +188 -294
  66. package/dist/mdast/mdast-walk.d.ts +13 -0
  67. package/dist/mdast/mdast-walk.js +144 -0
  68. package/dist/mdx-attr.js +0 -3
  69. package/dist/mdx-types.js +1 -6
  70. package/dist/op-stream.d.ts +1 -15
  71. package/dist/op-stream.js +3 -19
  72. package/dist/phantom.d.ts +0 -1
  73. package/dist/phantom.js +1 -9
  74. package/dist/plugin-pipeline.d.ts +32 -0
  75. package/dist/plugin-pipeline.js +177 -0
  76. package/dist/plugin.d.ts +8 -7
  77. package/dist/plugin.js +3 -9
  78. package/dist/types.d.ts +0 -1
  79. package/dist/visitor-node.d.ts +12 -0
  80. package/dist/visitor-node.js +23 -0
  81. package/dist/visitor-shared.d.ts +26 -30
  82. package/dist/visitor-shared.js +34 -36
  83. package/dist/wire-read.d.ts +0 -22
  84. package/dist/wire-read.js +3 -24
  85. package/index.d.ts +19 -10
  86. package/index.js +56 -53
  87. package/package.json +3 -12
  88. package/dist/string-pool.d.ts +0 -13
  89. package/dist/string-pool.js +0 -91
@@ -1,42 +1,23 @@
1
- import { LEAF_TYPES, isCustomLeaf, materializeNode } from "./mdast-materializer.js";
2
- import { MdastReader } from "./mdast-reader.js";
3
- import { acquireCommandBuffer, releaseCommandBuffer, classifyReturn, CommandBuffer, STRUCTURAL_CMD, } from "../command-buffer.js";
1
+ import { mdastTextContentHandle, walkMdastHandle } from "#binding";
2
+ import { acquireCommandBuffer, classifyReturn, CommandBuffer, STRUCTURAL_CMD, } from "../command-buffer.js";
4
3
  import { CMD_SET_CHILDREN } from "../generated/wire-constants.js";
5
- import { ru32, rstr, readPosition } from "../wire-read.js";
6
- import { decodeMdastTypeData } from "./generated/layout.js";
7
- import { TYPE_NAMES, NAME_TO_TYPE, VISITOR_KEYS, MDAST_OPSTREAM_TYPES, } from "./generated/node-types.js";
8
- 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";
9
- import { walkMdastHandle, mdastTextContentHandle } from "#binding";
10
- import { asArray, makeRequireNid, mergeAndReset, crossPipelineForeign, FOREIGN_REF, ROOT_NODE_ID, requireRootReplacement, rootReplacementError, unencodableContentError, } from "../visitor-shared.js";
11
- import { LazyChildResolver, registerEpochCacheSlot, } from "../lazy-child-resolver.js";
12
- import { MdastChildStub } from "./child-stub.js";
13
- function nid(node, refs) {
14
- // Genuine stubs carry their id as a plain field; a spread copy is not
15
- // `instanceof` and has no `_nodeId`, so it correctly reads as new content.
16
- if (node instanceof MdastChildStub)
17
- return node._refs === refs ? node._id : FOREIGN_REF;
18
- const id = refs.get(node);
19
- if (id !== undefined)
20
- return id;
21
- // Plain objects are trusted only via this tree's refs or a NON-enumerable
22
- // `_nodeId` (the materializer's convention, which spread cannot copy) — an
23
- // enumerable one rode in on a copy and must read as new content.
24
- const d = Object.getOwnPropertyDescriptor(node, "_nodeId");
25
- if (d !== undefined && !d.enumerable)
26
- return FOREIGN_REF;
27
- return crossPipelineForeign(node);
28
- }
29
- const requireNid = makeRequireNid(nid);
4
+ import { LazyChildResolver } from "../lazy-child-resolver.js";
5
+ import { emitMdxAttr, OF_ALT, OF_CHECKED, OF_DEPTH, OF_EXPLICIT, OF_IDENTIFIER, OF_LABEL, OF_LANG, OF_META, OF_NAME, OF_ORDERED, OF_REFERENCE_TYPE, OF_SPREAD, OF_START, OF_TITLE, OF_URL, OF_VALUE, OpWriter, PROP_STRING, } from "../op-stream.js";
6
+ import { asArray, collectCommands, FOREIGN_REF, foreignContentError, makeRequireNid, requireRootReplacement, ROOT_NODE_ID, rootReplacementError, unencodableContentError, withMdxJsxAttribute, } from "../visitor-shared.js";
7
+ import { ru32 } from "../wire-read.js";
8
+ import { MDAST_OPSTREAM_TYPES, NAME_TO_TYPE, VISITOR_KEYS } from "./generated/node-types.js";
9
+ import { LEAF_TYPES } from "./mdast-materializer.js";
10
+ import { MdastReader } from "./mdast-reader.js";
11
+ import { getNodeId, MdastLazyChildResolver, readMdastMatchedNode } from "./mdast-walk.js";
12
+ const requireNid = makeRequireNid(getNodeId);
30
13
  export class MdastVisitorContext {
31
14
  #commandBuffer = acquireCommandBuffer();
32
15
  #diagnostics;
16
+ #pendingNodes = new Map();
33
17
  #handle;
34
18
  #getSource;
35
19
  #resolver;
36
20
  #refs;
37
- /** One canonical object per parent id, so visitors can dedupe by identity.
38
- * Null until the first `parent()` call; most passes never make one. */
39
- #parentsById = null;
40
21
  /**
41
22
  * The URL of the document being processed (the compile `fileURL` option),
42
23
  * or `undefined` when none was given. Use `fileURLToPath(ctx.fileURL)` for a
@@ -73,17 +54,20 @@ export class MdastVisitorContext {
73
54
  return value;
74
55
  }
75
56
  removeNode(node) {
76
- this.#commandBuffer.removeNode(requireNid(node, "removeNode", this.#refs));
57
+ const id = requireNid(node, "removeNode", this.#refs);
58
+ this.#commandBuffer.removeNode(id);
59
+ this.#pendingNodes.delete(id);
77
60
  }
78
61
  insertBefore(node, newNode) {
79
- const id = requireNid(node, "insertBefore", this.#refs);
80
- for (const n of asArray(newNode))
81
- emitMdastTree(this.#commandBuffer, "insertBefore", id, n, false, this.#refs);
62
+ this.#splice(requireNid(node, "insertBefore", this.#refs), newNode, "insertBefore");
82
63
  }
83
64
  insertAfter(node, newNode) {
84
- const id = requireNid(node, "insertAfter", this.#refs);
85
- for (const n of asArray(newNode))
86
- emitMdastTree(this.#commandBuffer, "insertAfter", id, n, false, this.#refs);
65
+ this.#splice(requireNid(node, "insertAfter", this.#refs), newNode, "insertAfter");
66
+ }
67
+ #splice(anchorId, content, op) {
68
+ for (const n of asArray(content)) {
69
+ emitMdastTree(this.#commandBuffer, op, anchorId, n, false, this.#refs, true);
70
+ }
87
71
  }
88
72
  /**
89
73
  * Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any
@@ -99,27 +83,20 @@ export class MdastVisitorContext {
99
83
  emitMdastTree(this.#commandBuffer, "wrapNode", id, parentNode, false, this.#refs);
100
84
  }
101
85
  prependChild(node, childNode) {
102
- const id = requireNid(node, "prependChild", this.#refs);
103
- for (const n of asArray(childNode))
104
- emitMdastTree(this.#commandBuffer, "prependChild", id, n, false, this.#refs);
86
+ this.#splice(requireNid(node, "prependChild", this.#refs), childNode, "prependChild");
105
87
  }
106
88
  appendChild(node, childNode) {
107
- const id = requireNid(node, "appendChild", this.#refs);
108
- for (const n of asArray(childNode))
109
- emitMdastTree(this.#commandBuffer, "appendChild", id, n, false, this.#refs);
89
+ this.#splice(requireNid(node, "appendChild", this.#refs), childNode, "appendChild");
110
90
  }
111
91
  /** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */
112
92
  insertChildAt(node, index, childNode) {
113
93
  const children = ("children" in node ? node.children : undefined) ?? [];
114
- if (index <= 0 || children.length === 0) {
115
- this.prependChild(node, childNode);
116
- }
117
- else if (index >= children.length) {
118
- this.appendChild(node, childNode);
119
- }
120
- else {
121
- this.insertBefore(children[index], childNode);
122
- }
94
+ const [anchor, op] = index <= 0 || children.length === 0
95
+ ? [node, "prependChild"]
96
+ : index >= children.length
97
+ ? [node, "appendChild"]
98
+ : [children[index], "insertBefore"];
99
+ this.#splice(requireNid(anchor, "insertChildAt", this.#refs), childNode, op);
123
100
  }
124
101
  /** Remove the `index`-th child of `node`; a no-op when there is no such child. */
125
102
  removeChildAt(node, index) {
@@ -135,26 +112,46 @@ export class MdastVisitorContext {
135
112
  */
136
113
  replaceNode(node, newNode) {
137
114
  const id = requireNid(node, "replaceNode", this.#refs);
115
+ const onlyReplacement = Array.isArray(newNode)
116
+ ? newNode.length === 1
117
+ ? newNode[0]
118
+ : undefined
119
+ : newNode;
120
+ if (id === ROOT_NODE_ID &&
121
+ onlyReplacement !== undefined &&
122
+ reusedId(onlyReplacement, this.#refs) === id) {
123
+ return;
124
+ }
138
125
  if (Array.isArray(newNode)) {
139
126
  if (id === ROOT_NODE_ID && newNode.length > 1)
140
127
  throw rootReplacementError(newNode);
141
- // The last node carries the `replace` so refs back to the target still splice.
128
+ // One command, so the node's replacement is its whole slot and a ref back
129
+ // to it resolves to all of it rather than to the last element.
130
+ if (id !== ROOT_NODE_ID && newNode.length > 1 && newNode.every(isPlainReplacement)) {
131
+ emitMdastMultiReplace(this.#commandBuffer, id, newNode, this.#refs);
132
+ return;
133
+ }
142
134
  let previous;
143
135
  for (const n of newNode) {
144
136
  if (previous !== undefined) {
145
- emitMdastTree(this.#commandBuffer, "insertBefore", id, previous, false, this.#refs);
137
+ emitMdastTree(this.#commandBuffer, "insertBefore", id, previous, false, this.#refs, true);
146
138
  }
147
139
  previous = n;
148
140
  }
149
141
  if (previous === undefined) {
150
- // Replacing with nothing drops the node, like removeNode.
151
142
  this.removeNode(node);
152
143
  }
153
144
  else if (id === ROOT_NODE_ID && !isRawMdastContent(previous)) {
154
145
  emitMdastRootReplace(this.#commandBuffer, requireRootReplacement(previous), this.#refs);
155
146
  }
156
147
  else {
157
- emitMdastTree(this.#commandBuffer, "replace", id, previous, true, this.#refs);
148
+ emitMdastTree(this.#commandBuffer, "replace", id, previous, true, this.#refs, true);
149
+ }
150
+ if (previous !== undefined && !isRawMdastContent(previous)) {
151
+ this.#pendingNodes.set(id, previous);
152
+ }
153
+ else {
154
+ this.#pendingNodes.delete(id);
158
155
  }
159
156
  return;
160
157
  }
@@ -162,24 +159,84 @@ export class MdastVisitorContext {
162
159
  emitMdastRootReplace(this.#commandBuffer, requireRootReplacement(newNode), this.#refs);
163
160
  return;
164
161
  }
165
- emitMdastTree(this.#commandBuffer, "replace", id, newNode, true, this.#refs);
162
+ if (isRawMdastContent(newNode))
163
+ this.#pendingNodes.delete(id);
164
+ else
165
+ this.#pendingNodes.set(id, newNode);
166
+ emitMdastTree(this.#commandBuffer, "replace", id, newNode, true, this.#refs, true);
167
+ }
168
+ setField(node, key, value) {
169
+ const id = requireNid(node, "setField", this.#refs);
170
+ if (this.#foldPendingField(node, id, key, value))
171
+ return;
172
+ if (key === "children") {
173
+ if (!emitMdastChildrenCommand(this.#commandBuffer, id, value, this.#refs)) {
174
+ throw unencodableContentError(value);
175
+ }
176
+ return;
177
+ }
178
+ if ((key === "attributes" && (isMdxJsxElement(node) || isDirective(node))) ||
179
+ (key === "align" && node.type === "table")) {
180
+ // These are structured node fields, so re-encode the node rather than a scalar command.
181
+ this.replaceNode(node, { ...node, [key]: value });
182
+ return;
183
+ }
184
+ if (key === "data")
185
+ value = value != null ? JSON.stringify(value) : null;
186
+ this.#commandBuffer.setField(id, key, value);
187
+ }
188
+ setAttribute(node, name, value) {
189
+ const id = requireNid(node, "setAttribute", this.#refs);
190
+ const target = this.#pendingNodes.get(id) ?? node;
191
+ if (isDirective(target) && typeof value !== "string") {
192
+ throw new TypeError("setAttribute: directive attributes must be strings");
193
+ }
194
+ if (this.#foldPendingAttribute(node, id, name, value))
195
+ return;
196
+ this.#commandBuffer.setAttribute(id, name, value);
197
+ }
198
+ /** A queued replacement would discard the field, so fold it into a fresh replacement. */
199
+ #foldPendingField(node, id, key, value) {
200
+ const pending = this.#pendingNodes.get(id);
201
+ if (pending === undefined)
202
+ return false;
203
+ this.replaceNode(node, { ...pending, [key]: value });
204
+ return true;
205
+ }
206
+ /** A queued replacement would discard the attribute, so fold it in instead. */
207
+ #foldPendingAttribute(node, id, name, value) {
208
+ const pending = this.#pendingNodes.get(id);
209
+ if (pending === undefined)
210
+ return false;
211
+ if (isDirective(pending)) {
212
+ this.replaceNode(node, {
213
+ ...pending,
214
+ attributes: { ...pending.attributes, [name]: value },
215
+ });
216
+ return true;
217
+ }
218
+ if (isMdxJsxElement(pending)) {
219
+ this.replaceNode(node, withMdxJsxAttribute(pending, name, value));
220
+ return true;
221
+ }
222
+ return false;
166
223
  }
167
224
  setProperty(node, key, value) {
225
+ const id = requireNid(node, "setProperty", this.#refs);
168
226
  if (key === "children") {
169
- // children is structural: set-children keeps the node and swaps only its
170
- // child list (reused children keep their id).
171
- const id = requireNid(node, "setProperty", this.#refs);
172
227
  if (!emitMdastChildrenCommand(this.#commandBuffer, id, value, this.#refs)) {
173
228
  throw unencodableContentError(value);
174
229
  }
175
230
  return;
176
231
  }
177
- if (key === "data") {
178
- // data is stored as JSON in the arena, serialize it for the command buffer
179
- this.#commandBuffer.setProperty(requireNid(node, "setProperty", this.#refs), key, value != null ? JSON.stringify(value) : null);
232
+ const target = this.#pendingNodes.get(id) ?? node;
233
+ if (key === "attributes" && isMdxJsxElement(target)) {
234
+ this.replaceNode(node, { ...target, attributes: value });
180
235
  return;
181
236
  }
182
- this.#commandBuffer.setProperty(requireNid(node, "setProperty", this.#refs), key, value);
237
+ if (key === "data")
238
+ value = value != null ? JSON.stringify(value) : null;
239
+ this.#commandBuffer.setProperty(id, key, value);
183
240
  }
184
241
  /** Collect the concatenated text of all descendant text nodes (like mdast-util-to-string). */
185
242
  textContent(node, options) {
@@ -189,13 +246,7 @@ export class MdastVisitorContext {
189
246
  const parentId = this.#resolver.parentIdOf(requireNid(node, "parent", this.#refs));
190
247
  if (parentId === undefined)
191
248
  return undefined;
192
- const byId = (this.#parentsById ??= new Map());
193
- let parent = byId.get(parentId);
194
- if (parent === undefined) {
195
- parent = this.#resolver.materializeOne(parentId);
196
- byId.set(parentId, parent);
197
- }
198
- return parent;
249
+ return this.#resolver.materializeOne(parentId);
199
250
  }
200
251
  /**
201
252
  * Index of `node` within its parent's children, or `undefined` at the root.
@@ -205,7 +256,7 @@ export class MdastVisitorContext {
205
256
  return this.#resolver.indexInParent(requireNid(node, "indexOf", this.#refs));
206
257
  }
207
258
  report({ message, node, severity = "error", }) {
208
- const id = node ? nid(node, this.#refs) : undefined;
259
+ const id = node ? getNodeId(node, this.#refs) : undefined;
209
260
  this.#diagnostics.push({
210
261
  message,
211
262
  nodeId: id === FOREIGN_REF ? undefined : id,
@@ -213,7 +264,6 @@ export class MdastVisitorContext {
213
264
  severity,
214
265
  });
215
266
  }
216
- /** Get the binary command buffer for all mutations recorded via context methods. */
217
267
  getCommandBuffer() {
218
268
  return this.#commandBuffer;
219
269
  }
@@ -254,169 +304,14 @@ function buildMdastSubscriptions(plugin) {
254
304
  const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: [] }));
255
305
  return { subs, rustSubs };
256
306
  }
257
- const MDAST_EPOCH_CACHE = registerEpochCacheSlot(new WeakMap());
258
- class MdastLazyChildResolver extends LazyChildResolver {
259
- cacheSlot() {
260
- return MDAST_EPOCH_CACHE;
261
- }
262
- createReader(wire) {
263
- return new MdastReader(wire);
264
- }
265
- materializeNode(reader, nodeId, refs) {
266
- return materializeNode(reader, nodeId, true, refs);
267
- }
268
- readParentId(reader, nodeId) {
269
- return reader.getParentId(nodeId);
270
- }
271
- readChildIds(reader, nodeId) {
272
- return reader.getChildIds(nodeId);
273
- }
274
- }
275
- /** Build the child-stub list for a matched node from the wire's `[child_ids]
276
- * [child_types]` blocks, no arena snapshot. Stale ids are caught at
277
- * materialization: the resolver's epoch check refuses a snapshot once the
278
- * arena has mutated or been dropped. */
279
- function readMdastChildStubs(view, buf, idsPos, typesPos, count, resolver) {
280
- // With a hot snapshot a stub's deferral buys nothing; real nodes skip its per-field getters.
281
- if (resolver.hasHotSnapshot()) {
282
- const nodes = new Array(count);
283
- for (let i = 0; i < count; i++) {
284
- nodes[i] = resolver.materializeOne(ru32(view, idsPos + i * 4));
285
- }
286
- return nodes;
287
- }
288
- const stubs = new Array(count);
289
- for (let i = 0; i < count; i++) {
290
- stubs[i] = new MdastChildStub(resolver, ru32(view, idsPos + i * 4), buf[typesPos + i]);
291
- }
292
- return stubs;
293
- }
294
- /** Install `children` as an own enumerable getter (spread must carry it),
295
- * self-replacing with the one stable stub array on first read. One closure
296
- * and one define per node — installing the wire locals as hidden slots
297
- * instead measurably regressed every matching pipeline. */
298
- function makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver) {
299
- Object.defineProperty(node, "children", {
300
- get() {
301
- const val = readMdastChildStubs(view, buf, childIdsPos, childTypesPos, childCount, resolver);
302
- Object.defineProperty(this, "children", {
303
- value: val,
304
- writable: true,
305
- enumerable: true,
306
- configurable: true,
307
- });
308
- return val;
309
- },
310
- enumerable: true,
311
- configurable: true,
312
- });
313
- }
314
- /**
315
- * Read an MDAST node from the inline data in a match buffer entry.
316
- *
317
- * Inline format (from Rust serialize_mdast_node_inline):
318
- * [node_data: u32+bytes][position: 6×u32 = 24B][child_count: u32][child_ids: N×u32]
319
- * [child_types: N×u8][type-specific data]
320
- */
321
- function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver) {
322
- let pos = dataOffset;
323
- const dataJsonLen = ru32(view, pos);
324
- pos += 4;
325
- let initialData = null;
326
- if (dataJsonLen > 0) {
327
- const jsonStr = rstr(buf, pos, dataJsonLen);
328
- try {
329
- initialData = JSON.parse(jsonStr);
330
- }
331
- catch (err) {
332
- if (process.env.NODE_ENV !== "production") {
333
- console.warn(`readMdastMatchedNode: malformed node_data for nodeId=${nodeId}`, err);
334
- }
335
- }
336
- pos += dataJsonLen;
337
- }
338
- const position = readPosition(view, pos);
339
- pos += 24;
340
- const childCount = ru32(view, pos);
341
- pos += 4;
342
- // Ids/types decode lazily with `.children` — most matched nodes never read them.
343
- const childIdsPos = pos;
344
- pos += childCount * 4;
345
- const childTypesPos = pos;
346
- pos += childCount;
347
- const typeName = TYPE_NAMES[nodeType] ?? `unknown(${nodeType})`;
348
- const node = { type: typeName };
349
- if (position !== undefined)
350
- node.position = position;
351
- if (childCount > 0) {
352
- makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver);
353
- }
354
- // Fixed-field types decode from the generated layout table; the rest
355
- // (variable-length / cross-field) stay in the hand-written switch.
356
- if (!decodeMdastTypeData(view, buf, pos, nodeType, node)) {
357
- switch (nodeType) {
358
- case 5: {
359
- // list
360
- node.start = ru32(view, pos);
361
- node.ordered = buf[pos + 4] !== 0;
362
- node.spread = buf[pos + 5] !== 0;
363
- if (!node.ordered)
364
- node.start = null;
365
- break;
366
- }
367
- case 6: {
368
- // listItem
369
- const checked = buf[pos];
370
- node.checked = checked === 2 ? null : checked === 1;
371
- node.spread = buf[pos + 1] !== 0;
372
- break;
373
- }
374
- case 37: {
375
- // descriptionDetails
376
- node.spread = buf[pos] !== 0;
377
- break;
378
- }
379
- // table (21), directives (30/31/32) and mdxJsx elements (100/101) are
380
- // decoded by the generated `decodeMdastTypeData` from their tails.
381
- // root(0), paragraph(1), thematicBreak(3), blockquote(4), emphasis(11),
382
- // strong(12), break(14), tableRow(22), tableCell(23), delete(24): no extra data
383
- }
384
- }
385
- // User-defined node: the stored `name` field holds the author's public type
386
- // string. Surface it as `node.type` (open type string) instead of the
387
- // internal `"custom"`, drop the redundant `name`, and drop an empty `value`
388
- // so a parent node isn't given a spurious leaf field.
389
- if (nodeType === MDAST_CUSTOM) {
390
- node.type = node.name;
391
- delete node.name;
392
- if (node.value === "")
393
- delete node.value;
394
- }
395
- const leafCustom = nodeType === MDAST_CUSTOM && isCustomLeaf({ value: node.value, data: initialData }, childCount);
396
- if (childCount === 0 && !LEAF_TYPES.has(nodeType) && !leafCustom) {
397
- node.children = [];
398
- }
399
- resolver.refs.set(node, nodeId);
400
- if (initialData) {
401
- node.data = initialData;
402
- }
403
- return node;
404
- }
405
307
  const MDAST_ROOT = NAME_TO_TYPE.root;
406
- /** Internal tag for user-defined nodes. The stored `name` field holds the
407
- * author's public `type` string; the read paths surface it as `node.type`
408
- * and the emit path routes any unrecognized `type` here. */
409
308
  const MDAST_CUSTOM = NAME_TO_TYPE.custom;
410
- /** The arena id of a node if it is an existing (materialized) node, else
411
- * undefined for a freshly-built one. */
412
309
  function reusedId(node, refs) {
413
310
  if (node === null || typeof node !== "object")
414
311
  return undefined;
415
- const id = nid(node, refs);
312
+ const id = getNodeId(node, refs);
416
313
  return id !== undefined && id !== FOREIGN_REF ? id : undefined;
417
314
  }
418
- /** Emit a set-children command in place: a root-wrapped child list, the shape
419
- * `Patch::SetChildren` splices in. Reused children become refs. */
420
315
  function emitMdastChildrenCommand(buffer, id, children, refs) {
421
316
  if (!Array.isArray(children))
422
317
  return false;
@@ -430,7 +325,25 @@ function emitMdastChildrenCommand(buffer, id, children, refs) {
430
325
  return true;
431
326
  });
432
327
  }
433
- /** Separate from the per-node encoder, which rejects a `root` payload. */
328
+ /** True for content the op-stream can carry inside one root-wrapped payload. */
329
+ function isPlainReplacement(content) {
330
+ return (!isRawMdastContent(content) && content._keepChildren !== true);
331
+ }
332
+ /** Replace `id` with several nodes in one command, root-wrapped so the engine
333
+ * splices the children in place of the node. */
334
+ function emitMdastMultiReplace(buffer, id, nodes, refs) {
335
+ const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD.replace, id, () => {
336
+ buffer.open(MDAST_ROOT);
337
+ for (const n of nodes)
338
+ if (!emitMdastOp(buffer, n, false, true, refs))
339
+ return false;
340
+ buffer.close();
341
+ return true;
342
+ });
343
+ if (!ok)
344
+ throw unencodableContentError(nodes);
345
+ }
346
+ // Root replacement needs a separate encoder because per-node encoding rejects root payloads.
434
347
  function emitMdastRootReplace(buffer, root, refs) {
435
348
  const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD.replace, ROOT_NODE_ID, () => emitMdastRootOp(buffer, root, refs));
436
349
  if (!ok)
@@ -454,36 +367,29 @@ function emitMdastRootOp(w, n, refs) {
454
367
  w.close();
455
368
  return true;
456
369
  }
457
- function emitMdastOp(w, node, isRoot, forReplace, refs) {
370
+ function emitMdastOp(w, node, isRoot, forReplace, refs, allowRootRef = false) {
458
371
  if (node === null || typeof node !== "object")
459
372
  return false;
460
- if (!isRoot) {
461
- const id = reusedId(node, refs);
462
- if (id !== undefined) {
463
- w.ref(id);
464
- return true;
465
- }
373
+ const id = getNodeId(node, refs);
374
+ if (id === FOREIGN_REF)
375
+ throw foreignContentError();
376
+ if ((!isRoot || allowRootRef) && id !== undefined) {
377
+ w.ref(id);
378
+ return true;
466
379
  }
467
380
  const n = node;
468
- // Any `type` outside the built-in set is a user-defined node: route it to the
469
- // internal `custom` tag and carry the author's `type` string as the name.
470
381
  let type = MDAST_OPSTREAM_TYPES[n.type];
471
382
  let isCustom = false;
472
383
  if (type === undefined) {
473
384
  if (typeof n.type !== "string" || n.type.length === 0)
474
385
  return false;
475
- // A known built-in that just isn't op-stream-encodable (e.g. `root`) is a
476
- // real type used wrong, so fail loudly rather than reinterpreting it as a
477
- // user-defined node. Only genuinely-unknown type strings become custom.
386
+ // Unsupported built-in types must fail rather than silently becoming custom nodes.
478
387
  if (NAME_TO_TYPE[n.type] !== undefined)
479
388
  return false;
480
389
  type = MDAST_CUSTOM;
481
390
  isCustom = true;
482
391
  }
483
392
  else if (type === MDAST_CUSTOM) {
484
- // `"custom"` is the internal tag's own public name, so it resolves here
485
- // instead of falling through as unknown. Still a user-defined node, so carry
486
- // the `type` string as the name so it round-trips rather than vanishing.
487
393
  isCustom = true;
488
394
  }
489
395
  w.open(type);
@@ -507,8 +413,7 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
507
413
  w.str(OF_LABEL, n.label);
508
414
  if (typeof n.referenceType === "string")
509
415
  w.str(OF_REFERENCE_TYPE, n.referenceType);
510
- // Out-of-range numbers compile to null and the caller throws — a visible
511
- // error instead of silently masking the bits.
416
+ // Reject out-of-range values before integer writes can truncate them.
512
417
  if (typeof n.depth === "number") {
513
418
  if (!Number.isInteger(n.depth) || n.depth < 0 || n.depth > 255)
514
419
  return false;
@@ -533,8 +438,6 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
533
438
  emitMdxAttr(w, a);
534
439
  }
535
440
  else if (attrs !== null && typeof attrs === "object") {
536
- // Directive attributes: a string→string map; non-string values are
537
- // dropped, since the stored form holds only strings.
538
441
  for (const key in attrs) {
539
442
  const v = attrs[key];
540
443
  if (typeof v === "string")
@@ -549,13 +452,10 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
549
452
  if (n.data != null)
550
453
  w.data(n.data);
551
454
  if (isRoot && forReplace && n._keepChildren === true) {
552
- // Replace splices the target's original children, discarding any the
553
- // replacement declares.
554
455
  w.keepChildren();
555
456
  }
556
457
  else {
557
- // `_keepChildren` only applies to replace; other ops ignore the marker
558
- // and emit the declared children.
458
+ // Only replacement can reuse the target’s original children.
559
459
  const children = n.children;
560
460
  if (Array.isArray(children)) {
561
461
  for (const c of children)
@@ -566,24 +466,23 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
566
466
  w.close();
567
467
  return true;
568
468
  }
569
- /** Map a table `align` entry to its arena code (none=0). */
570
469
  function alignCode(a) {
571
470
  return a === "left" ? 1 : a === "right" ? 2 : a === "center" ? 3 : 0;
572
471
  }
573
- /** True for the `{raw}` / `{rawHtml}` escape hatches — re-parsed by Rust rather
574
- * than compiled to an op-stream, so they ride the RAW_MARKDOWN / RAW_HTML
575
- * payloads instead of the declarative encoder. */
576
472
  function isRawMdastContent(content) {
577
473
  const c = content;
578
474
  return typeof c.raw === "string" || typeof c.rawHtml === "string";
579
475
  }
580
- /** Encode `content` as the `op` structural command. Declarative nodes compile
581
- * to the op-stream; the `{raw}`/`{rawHtml}` escape hatches ride the raw
582
- * re-parse payloads. Anything that compiles to neither is a hard error — the
583
- * op-stream is the only declarative encoding. The switches stay inline so the
584
- * buffer calls are monomorphic (computed method names defeat inline caches on
585
- * this warm path). */
586
- function emitMdastTree(buffer, op, id, content, forReplace, refs) {
476
+ function isDirective(node) {
477
+ return (node.type === "containerDirective" ||
478
+ node.type === "leafDirective" ||
479
+ node.type === "textDirective");
480
+ }
481
+ function isMdxJsxElement(node) {
482
+ return ((node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && "attributes" in node);
483
+ }
484
+ // Keep direct method calls so structural command dispatch stays monomorphic.
485
+ function emitMdastTree(buffer, op, id, content, forReplace, refs, allowRootRef = false) {
587
486
  if (isRawMdastContent(content)) {
588
487
  switch (op) {
589
488
  case "replace":
@@ -600,7 +499,7 @@ function emitMdastTree(buffer, op, id, content, forReplace, refs) {
600
499
  return buffer.wrapNode(id, content);
601
500
  }
602
501
  }
603
- const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD[op], id, () => emitMdastOp(buffer, content, true, forReplace, refs));
502
+ const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD[op], id, () => emitMdastOp(buffer, content, true, forReplace, refs, allowRootRef));
604
503
  if (!ok)
605
504
  throw unencodableContentError(content);
606
505
  }
@@ -613,8 +512,6 @@ function assertMdastWrapParent(parentNode) {
613
512
  const type = parentNode.type;
614
513
  const tag = typeof type === "string" ? NAME_TO_TYPE[type] : undefined;
615
514
  if (tag === undefined) {
616
- // User-defined type: only the declared shape distinguishes a parent from
617
- // a text leaf.
618
515
  if (Array.isArray(parentNode.children))
619
516
  return;
620
517
  throw new Error(`wrapNode: a user-defined "${String(type)}" wrapper must declare a children array. ` +
@@ -626,9 +523,7 @@ function assertMdastWrapParent(parentNode) {
626
523
  'Pass a parent node such as { type: "blockquote", children: [] }, a raw wrapper ' +
627
524
  `such as { raw: "> " }, or surround the node with tags: ${sandwich}.`);
628
525
  }
629
- /** MDAST node types whose `value` Rust can set in place via setProperty. A
630
- * visitor returning one of these as `{type, value}` with no other fields
631
- * skips a full arena rebuild. */
526
+ // Updating only value avoids an arena rebuild for these node types.
632
527
  const MDAST_VALUE_ONLY_TYPES = new Set([
633
528
  "text",
634
529
  "html",
@@ -637,9 +532,7 @@ const MDAST_VALUE_ONLY_TYPES = new Set([
637
532
  "toml",
638
533
  "inlineMath",
639
534
  ]);
640
- /** True when the visitor returned a same-type MDAST node carrying only `type`
641
- * + `value`. Any other field present (children, position, data, lang, meta)
642
- * falls back to the full replace path so nothing is silently dropped. */
535
+ // Extra fields require full replacement so the value-only fast path cannot discard them.
643
536
  function isMdastTextValueSwap(result, original) {
644
537
  if (original === undefined)
645
538
  return false;
@@ -656,8 +549,7 @@ function isMdastTextValueSwap(result, original) {
656
549
  r.lang === undefined &&
657
550
  r.meta === undefined);
658
551
  }
659
- /** A result that is the same object as the input node is a no-op, so context
660
- * mutations (e.g. setProperty) are not clobbered. */
552
+ // Returning the input node must not overwrite mutations queued through the context.
661
553
  function applyMdastVisitResult(result, nodeId, returnBuffer, refs, originalNode) {
662
554
  if (result === undefined || result === null)
663
555
  return;
@@ -677,14 +569,14 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, refs, originalNode)
677
569
  returnBuffer.setProperty(nodeId, "value", node.value);
678
570
  break;
679
571
  }
680
- emitMdastTree(returnBuffer, "replace", nodeId, node, true, refs);
572
+ emitMdastTree(returnBuffer, "replace", nodeId, node, true, refs, true);
681
573
  break;
682
574
  }
683
575
  }
684
576
  }
685
577
  /**
686
578
  * Walk an MDAST handle in Rust, dispatch matched nodes to JS visitor functions,
687
- * and apply mutations back to the handle. No arena buffers cross NAPI.
579
+ * and collect mutations for the caller to apply. No arena buffers cross NAPI.
688
580
  *
689
581
  * Returns MdastVisitResult synchronously if all visitors are sync,
690
582
  * or Promise<MdastVisitResult> if any visitor is async.
@@ -716,20 +608,21 @@ export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {
716
608
  }
717
609
  }
718
610
  if (deferred) {
719
- return Promise.all(deferred.map((d) => d.promise.then((r) => ({ nodeId: d.nodeId, result: r, originalNode: d.originalNode })))).then((results) => {
720
- for (const { nodeId, result, originalNode } of results) {
721
- applyMdastVisitResult(result, nodeId, returnBuffer, resolver.refs, originalNode);
611
+ const visits = deferred;
612
+ return Promise.all(visits.map((visit) => visit.promise)).then((results) => {
613
+ for (let i = 0; i < visits.length; i++) {
614
+ const { nodeId, originalNode } = visits[i];
615
+ applyMdastVisitResult(results[i], nodeId, returnBuffer, resolver.refs, originalNode);
722
616
  }
723
- return finalizeMdastVisit(handle, context, returnBuffer);
617
+ return finalizeMdastVisit(context, returnBuffer);
724
618
  });
725
619
  }
726
- return finalizeMdastVisit(handle, context, returnBuffer);
620
+ return finalizeMdastVisit(context, returnBuffer);
727
621
  }
728
622
  const MDAST_ROOT_SUBS = [
729
623
  { nodeType: MDAST_ROOT, tagFilter: [] },
730
624
  ];
731
- /** Its own pass so the caller can apply what `before` queued before the
732
- * visitors walk, and the visitors' mutations before `after` reads the tree. */
625
+ /** Hooks run in separate passes so each sees mutations from the preceding pass. */
733
626
  export function visitMdastHook(handle, plugin, hook, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
734
627
  const getSource = typeof source === "function" ? source : () => source;
735
628
  const resolver = new MdastLazyChildResolver(handle);
@@ -738,18 +631,19 @@ export function visitMdastHook(handle, plugin, hook, source, fileURL, data = {},
738
631
  const matchBuf = walkMdastHandle(handle, MDAST_ROOT_SUBS);
739
632
  const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
740
633
  if (ru32(matchView, 0) === 0)
741
- return finalizeMdastVisit(handle, context, returnBuffer);
634
+ return finalizeMdastVisit(context, returnBuffer);
742
635
  const root = readMdastMatchedNode(matchView, matchBuf, ru32(matchView, 10), ru32(matchView, 4), MDAST_ROOT, resolver);
743
636
  const result = hook.call(plugin, root, context);
744
637
  if (result instanceof Promise) {
745
- return result.then(() => finalizeMdastVisit(handle, context, returnBuffer));
638
+ return result.then(() => finalizeMdastVisit(context, returnBuffer));
746
639
  }
747
- return finalizeMdastVisit(handle, context, returnBuffer);
640
+ return finalizeMdastVisit(context, returnBuffer);
748
641
  }
749
- function finalizeMdastVisit(handle, context, returnBuffer) {
750
- const { merged, hasMutations } = mergeAndReset(returnBuffer, context);
751
- // Return both buffers to the pool. Bytes were copied into `merged` above.
752
- releaseCommandBuffer(returnBuffer);
753
- releaseCommandBuffer(context.getCommandBuffer());
754
- return { commandBuffer: merged, diagnostics: context.getDiagnostics(), hasMutations };
642
+ function finalizeMdastVisit(context, returnBuffer) {
643
+ const commandBuffer = collectCommands(returnBuffer, context.getCommandBuffer());
644
+ return {
645
+ commandBuffer,
646
+ diagnostics: context.getDiagnostics(),
647
+ hasMutations: commandBuffer.length > 0,
648
+ };
755
649
  }