satteri 0.10.5 → 0.11.3

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 (90) 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 +101 -0
  16. package/dist/compile-types.d.ts +342 -0
  17. package/dist/compile-types.js +1 -0
  18. package/dist/compile.d.ts +22 -328
  19. package/dist/compile.js +133 -383
  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.d.ts +1 -0
  27. package/dist/generated/parse-options.js +1 -5
  28. package/dist/generated/wire-constants.d.ts +4 -0
  29. package/dist/generated/wire-constants.js +34 -42
  30. package/dist/handles.d.ts +0 -7
  31. package/dist/hast/child-stub.d.ts +1 -8
  32. package/dist/hast/child-stub.js +14 -13
  33. package/dist/hast/element-props.d.ts +13 -0
  34. package/dist/hast/element-props.js +52 -6
  35. package/dist/hast/generated/node-types.d.ts +1 -9
  36. package/dist/hast/generated/node-types.js +1 -9
  37. package/dist/hast/generated/walk-decode.d.ts +10 -0
  38. package/dist/hast/generated/walk-decode.js +61 -0
  39. package/dist/hast/hast-materializer.d.ts +3 -10
  40. package/dist/hast/hast-materializer.js +73 -47
  41. package/dist/hast/hast-reader.d.ts +9 -38
  42. package/dist/hast/hast-reader.js +82 -271
  43. package/dist/hast/hast-visitor.d.ts +29 -16
  44. package/dist/hast/hast-visitor.js +200 -508
  45. package/dist/hast/hast-walk.d.ts +18 -0
  46. package/dist/hast/hast-walk.js +264 -0
  47. package/dist/index.d.ts +5 -5
  48. package/dist/index.js +4 -12
  49. package/dist/lazy-child-resolver.d.ts +1 -21
  50. package/dist/lazy-child-resolver.js +11 -32
  51. package/dist/materializer-cache.d.ts +5 -29
  52. package/dist/materializer-cache.js +29 -101
  53. package/dist/mdast/child-stub.d.ts +1 -12
  54. package/dist/mdast/child-stub.js +18 -28
  55. package/dist/mdast/column-align.d.ts +1 -1
  56. package/dist/mdast/column-align.js +0 -2
  57. package/dist/mdast/generated/layout.d.ts +0 -16
  58. package/dist/mdast/generated/layout.js +6 -47
  59. package/dist/mdast/generated/node-types.d.ts +1 -9
  60. package/dist/mdast/generated/node-types.js +3 -9
  61. package/dist/mdast/mdast-materializer.d.ts +3 -11
  62. package/dist/mdast/mdast-materializer.js +91 -63
  63. package/dist/mdast/mdast-reader.d.ts +15 -63
  64. package/dist/mdast/mdast-reader.js +132 -269
  65. package/dist/mdast/mdast-visitor.d.ts +23 -15
  66. package/dist/mdast/mdast-visitor.js +189 -294
  67. package/dist/mdast/mdast-walk.d.ts +13 -0
  68. package/dist/mdast/mdast-walk.js +144 -0
  69. package/dist/mdx-attr.js +0 -3
  70. package/dist/mdx-types.js +1 -6
  71. package/dist/op-stream.d.ts +4 -15
  72. package/dist/op-stream.js +31 -20
  73. package/dist/phantom.d.ts +0 -1
  74. package/dist/phantom.js +1 -9
  75. package/dist/plugin-pipeline.d.ts +32 -0
  76. package/dist/plugin-pipeline.js +177 -0
  77. package/dist/plugin.d.ts +14 -8
  78. package/dist/plugin.js +5 -10
  79. package/dist/types.d.ts +0 -1
  80. package/dist/visitor-node.d.ts +12 -0
  81. package/dist/visitor-node.js +23 -0
  82. package/dist/visitor-shared.d.ts +26 -30
  83. package/dist/visitor-shared.js +34 -36
  84. package/dist/wire-read.d.ts +0 -22
  85. package/dist/wire-read.js +3 -24
  86. package/index.d.ts +19 -10
  87. package/index.js +56 -53
  88. package/package.json +6 -12
  89. package/dist/string-pool.d.ts +0 -13
  90. 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,39 +367,33 @@ 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);
396
+ w.position(n.position);
490
397
  if (isCustom)
491
398
  w.str(OF_NAME, n.type);
492
399
  if (typeof n.value === "string")
@@ -507,8 +414,7 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
507
414
  w.str(OF_LABEL, n.label);
508
415
  if (typeof n.referenceType === "string")
509
416
  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.
417
+ // Reject out-of-range values before integer writes can truncate them.
512
418
  if (typeof n.depth === "number") {
513
419
  if (!Number.isInteger(n.depth) || n.depth < 0 || n.depth > 255)
514
420
  return false;
@@ -533,8 +439,6 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
533
439
  emitMdxAttr(w, a);
534
440
  }
535
441
  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
442
  for (const key in attrs) {
539
443
  const v = attrs[key];
540
444
  if (typeof v === "string")
@@ -549,13 +453,10 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
549
453
  if (n.data != null)
550
454
  w.data(n.data);
551
455
  if (isRoot && forReplace && n._keepChildren === true) {
552
- // Replace splices the target's original children, discarding any the
553
- // replacement declares.
554
456
  w.keepChildren();
555
457
  }
556
458
  else {
557
- // `_keepChildren` only applies to replace; other ops ignore the marker
558
- // and emit the declared children.
459
+ // Only replacement can reuse the target’s original children.
559
460
  const children = n.children;
560
461
  if (Array.isArray(children)) {
561
462
  for (const c of children)
@@ -566,24 +467,23 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
566
467
  w.close();
567
468
  return true;
568
469
  }
569
- /** Map a table `align` entry to its arena code (none=0). */
570
470
  function alignCode(a) {
571
471
  return a === "left" ? 1 : a === "right" ? 2 : a === "center" ? 3 : 0;
572
472
  }
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
473
  function isRawMdastContent(content) {
577
474
  const c = content;
578
475
  return typeof c.raw === "string" || typeof c.rawHtml === "string";
579
476
  }
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) {
477
+ function isDirective(node) {
478
+ return (node.type === "containerDirective" ||
479
+ node.type === "leafDirective" ||
480
+ node.type === "textDirective");
481
+ }
482
+ function isMdxJsxElement(node) {
483
+ return ((node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && "attributes" in node);
484
+ }
485
+ // Keep direct method calls so structural command dispatch stays monomorphic.
486
+ function emitMdastTree(buffer, op, id, content, forReplace, refs, allowRootRef = false) {
587
487
  if (isRawMdastContent(content)) {
588
488
  switch (op) {
589
489
  case "replace":
@@ -600,7 +500,7 @@ function emitMdastTree(buffer, op, id, content, forReplace, refs) {
600
500
  return buffer.wrapNode(id, content);
601
501
  }
602
502
  }
603
- const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD[op], id, () => emitMdastOp(buffer, content, true, forReplace, refs));
503
+ const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD[op], id, () => emitMdastOp(buffer, content, true, forReplace, refs, allowRootRef));
604
504
  if (!ok)
605
505
  throw unencodableContentError(content);
606
506
  }
@@ -613,8 +513,6 @@ function assertMdastWrapParent(parentNode) {
613
513
  const type = parentNode.type;
614
514
  const tag = typeof type === "string" ? NAME_TO_TYPE[type] : undefined;
615
515
  if (tag === undefined) {
616
- // User-defined type: only the declared shape distinguishes a parent from
617
- // a text leaf.
618
516
  if (Array.isArray(parentNode.children))
619
517
  return;
620
518
  throw new Error(`wrapNode: a user-defined "${String(type)}" wrapper must declare a children array. ` +
@@ -626,9 +524,7 @@ function assertMdastWrapParent(parentNode) {
626
524
  'Pass a parent node such as { type: "blockquote", children: [] }, a raw wrapper ' +
627
525
  `such as { raw: "> " }, or surround the node with tags: ${sandwich}.`);
628
526
  }
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. */
527
+ // Updating only value avoids an arena rebuild for these node types.
632
528
  const MDAST_VALUE_ONLY_TYPES = new Set([
633
529
  "text",
634
530
  "html",
@@ -637,9 +533,7 @@ const MDAST_VALUE_ONLY_TYPES = new Set([
637
533
  "toml",
638
534
  "inlineMath",
639
535
  ]);
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. */
536
+ // Extra fields require full replacement so the value-only fast path cannot discard them.
643
537
  function isMdastTextValueSwap(result, original) {
644
538
  if (original === undefined)
645
539
  return false;
@@ -656,8 +550,7 @@ function isMdastTextValueSwap(result, original) {
656
550
  r.lang === undefined &&
657
551
  r.meta === undefined);
658
552
  }
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. */
553
+ // Returning the input node must not overwrite mutations queued through the context.
661
554
  function applyMdastVisitResult(result, nodeId, returnBuffer, refs, originalNode) {
662
555
  if (result === undefined || result === null)
663
556
  return;
@@ -677,14 +570,14 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, refs, originalNode)
677
570
  returnBuffer.setProperty(nodeId, "value", node.value);
678
571
  break;
679
572
  }
680
- emitMdastTree(returnBuffer, "replace", nodeId, node, true, refs);
573
+ emitMdastTree(returnBuffer, "replace", nodeId, node, true, refs, true);
681
574
  break;
682
575
  }
683
576
  }
684
577
  }
685
578
  /**
686
579
  * 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.
580
+ * and collect mutations for the caller to apply. No arena buffers cross NAPI.
688
581
  *
689
582
  * Returns MdastVisitResult synchronously if all visitors are sync,
690
583
  * or Promise<MdastVisitResult> if any visitor is async.
@@ -716,20 +609,21 @@ export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {
716
609
  }
717
610
  }
718
611
  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);
612
+ const visits = deferred;
613
+ return Promise.all(visits.map((visit) => visit.promise)).then((results) => {
614
+ for (let i = 0; i < visits.length; i++) {
615
+ const { nodeId, originalNode } = visits[i];
616
+ applyMdastVisitResult(results[i], nodeId, returnBuffer, resolver.refs, originalNode);
722
617
  }
723
- return finalizeMdastVisit(handle, context, returnBuffer);
618
+ return finalizeMdastVisit(context, returnBuffer);
724
619
  });
725
620
  }
726
- return finalizeMdastVisit(handle, context, returnBuffer);
621
+ return finalizeMdastVisit(context, returnBuffer);
727
622
  }
728
623
  const MDAST_ROOT_SUBS = [
729
624
  { nodeType: MDAST_ROOT, tagFilter: [] },
730
625
  ];
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. */
626
+ /** Hooks run in separate passes so each sees mutations from the preceding pass. */
733
627
  export function visitMdastHook(handle, plugin, hook, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
734
628
  const getSource = typeof source === "function" ? source : () => source;
735
629
  const resolver = new MdastLazyChildResolver(handle);
@@ -738,18 +632,19 @@ export function visitMdastHook(handle, plugin, hook, source, fileURL, data = {},
738
632
  const matchBuf = walkMdastHandle(handle, MDAST_ROOT_SUBS);
739
633
  const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
740
634
  if (ru32(matchView, 0) === 0)
741
- return finalizeMdastVisit(handle, context, returnBuffer);
635
+ return finalizeMdastVisit(context, returnBuffer);
742
636
  const root = readMdastMatchedNode(matchView, matchBuf, ru32(matchView, 10), ru32(matchView, 4), MDAST_ROOT, resolver);
743
637
  const result = hook.call(plugin, root, context);
744
638
  if (result instanceof Promise) {
745
- return result.then(() => finalizeMdastVisit(handle, context, returnBuffer));
639
+ return result.then(() => finalizeMdastVisit(context, returnBuffer));
746
640
  }
747
- return finalizeMdastVisit(handle, context, returnBuffer);
641
+ return finalizeMdastVisit(context, returnBuffer);
748
642
  }
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 };
643
+ function finalizeMdastVisit(context, returnBuffer) {
644
+ const commandBuffer = collectCommands(returnBuffer, context.getCommandBuffer());
645
+ return {
646
+ commandBuffer,
647
+ diagnostics: context.getDiagnostics(),
648
+ hasMutations: commandBuffer.length > 0,
649
+ };
755
650
  }