satteri 0.9.5 → 0.10.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 (60) hide show
  1. package/README.md +11 -1
  2. package/dist/binding.browser.d.ts +1 -1
  3. package/dist/binding.browser.js +1 -1
  4. package/dist/binding.d.ts +1 -1
  5. package/dist/binding.js +1 -1
  6. package/dist/byte-writer.d.ts +4 -0
  7. package/dist/byte-writer.js +13 -4
  8. package/dist/child-stub.d.ts +7 -3
  9. package/dist/child-stub.js +18 -7
  10. package/dist/command-buffer.d.ts +36 -50
  11. package/dist/command-buffer.js +102 -49
  12. package/dist/compile.d.ts +100 -32
  13. package/dist/compile.js +294 -69
  14. package/dist/freeze.d.ts +2 -0
  15. package/dist/freeze.js +10 -0
  16. package/dist/generated/wire-constants.d.ts +3 -2
  17. package/dist/generated/wire-constants.js +4 -2
  18. package/dist/hast/child-stub.d.ts +2 -3
  19. package/dist/hast/child-stub.js +18 -11
  20. package/dist/hast/element-props.d.ts +1 -1
  21. package/dist/hast/element-props.js +14 -6
  22. package/dist/hast/hast-materializer.d.ts +6 -2
  23. package/dist/hast/hast-materializer.js +48 -134
  24. package/dist/hast/hast-reader.d.ts +7 -13
  25. package/dist/hast/hast-reader.js +76 -21
  26. package/dist/hast/hast-visitor.d.ts +48 -4
  27. package/dist/hast/hast-visitor.js +318 -154
  28. package/dist/index.d.ts +6 -6
  29. package/dist/index.js +1 -1
  30. package/dist/lazy-child-resolver.d.ts +20 -13
  31. package/dist/lazy-child-resolver.js +66 -38
  32. package/dist/materializer-cache.d.ts +42 -0
  33. package/dist/materializer-cache.js +192 -0
  34. package/dist/mdast/child-stub.d.ts +6 -3
  35. package/dist/mdast/child-stub.js +71 -8
  36. package/dist/mdast/generated/layout.d.ts +2 -3
  37. package/dist/mdast/generated/layout.js +24 -28
  38. package/dist/mdast/generated/node-types.d.ts +4 -0
  39. package/dist/mdast/generated/node-types.js +25 -0
  40. package/dist/mdast/mdast-materializer.d.ts +12 -3
  41. package/dist/mdast/mdast-materializer.js +83 -78
  42. package/dist/mdast/mdast-reader.d.ts +15 -0
  43. package/dist/mdast/mdast-reader.js +55 -10
  44. package/dist/mdast/mdast-visitor.d.ts +73 -32
  45. package/dist/mdast/mdast-visitor.js +298 -122
  46. package/dist/op-stream.d.ts +5 -12
  47. package/dist/op-stream.js +6 -21
  48. package/dist/plugin.d.ts +36 -10
  49. package/dist/plugin.js +50 -2
  50. package/dist/string-pool.d.ts +13 -0
  51. package/dist/string-pool.js +91 -0
  52. package/dist/types.d.ts +38 -1
  53. package/dist/visitor-shared.d.ts +27 -4
  54. package/dist/visitor-shared.js +36 -10
  55. package/dist/wire-read.js +3 -1
  56. package/index.d.ts +133 -7
  57. package/index.js +89 -68
  58. package/package.json +23 -15
  59. package/dist/lazy-props.d.ts +0 -10
  60. package/dist/lazy-props.js +0 -50
@@ -1,37 +1,39 @@
1
- import { materializeNode } from "./mdast-materializer.js";
1
+ import { LEAF_TYPES, isCustomLeaf, materializeNode } from "./mdast-materializer.js";
2
2
  import { MdastReader } from "./mdast-reader.js";
3
- import { CommandBuffer, classifyReturn } from "../command-buffer.js";
3
+ import { acquireCommandBuffer, releaseCommandBuffer, classifyReturn, CommandBuffer, STRUCTURAL_CMD, } from "../command-buffer.js";
4
+ import { CMD_SET_CHILDREN } from "../generated/wire-constants.js";
4
5
  import { ru32, rstr, readPosition } from "../wire-read.js";
5
6
  import { decodeMdastTypeData } from "./generated/layout.js";
6
7
  import { TYPE_NAMES, NAME_TO_TYPE, VISITOR_KEYS, MDAST_OPSTREAM_TYPES, } from "./generated/node-types.js";
7
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";
8
9
  import { walkMdastHandle, mdastTextContentHandle } from "#binding";
9
- import { asArray, makeRequireNid, mergeAndReset, unencodableContentError, } from "../visitor-shared.js";
10
- import { LazyChildResolver } from "../lazy-child-resolver.js";
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";
11
12
  import { MdastChildStub } from "./child-stub.js";
12
- /** Maps MdastNode objects to their arena node IDs without Object.defineProperty overhead. */
13
- const mdastNodeIdMap = new WeakMap();
14
- function nid(node) {
13
+ function nid(node, refs) {
15
14
  // Genuine stubs carry their id as a plain field; a spread copy is not
16
15
  // `instanceof` and has no `_nodeId`, so it correctly reads as new content.
17
16
  if (node instanceof MdastChildStub)
18
- return node._id;
19
- const id = mdastNodeIdMap.get(node);
17
+ return node._refs === refs ? node._id : FOREIGN_REF;
18
+ const id = refs.get(node);
20
19
  if (id !== undefined)
21
20
  return id;
22
- // Plain objects are trusted only via the WeakMap or a NON-enumerable
21
+ // Plain objects are trusted only via this tree's refs or a NON-enumerable
23
22
  // `_nodeId` (the materializer's convention, which spread cannot copy) — an
24
23
  // enumerable one rode in on a copy and must read as new content.
25
24
  const d = Object.getOwnPropertyDescriptor(node, "_nodeId");
26
- return d !== undefined && !d.enumerable ? d.value : undefined;
25
+ if (d !== undefined && !d.enumerable)
26
+ return FOREIGN_REF;
27
+ return crossPipelineForeign(node);
27
28
  }
28
29
  const requireNid = makeRequireNid(nid);
29
30
  export class MdastVisitorContext {
30
- #commandBuffer = new CommandBuffer();
31
- #diagnostics = [];
31
+ #commandBuffer = acquireCommandBuffer();
32
+ #diagnostics;
32
33
  #handle;
33
34
  #getSource;
34
35
  #resolver;
36
+ #refs;
35
37
  /** One canonical object per parent id, so visitors can dedupe by identity.
36
38
  * Null until the first `parent()` call; most passes never make one. */
37
39
  #parentsById = null;
@@ -55,13 +57,15 @@ export class MdastVisitorContext {
55
57
  * pipelines branch on which it is handling.
56
58
  */
57
59
  sourceFormat;
58
- constructor(handle, getSource, fileURL, resolver, data, sourceFormat) {
60
+ constructor(handle, getSource, fileURL, resolver, data, sourceFormat, diagnostics) {
59
61
  this.#handle = handle;
60
62
  this.#getSource = getSource;
61
63
  this.fileURL = fileURL;
62
64
  this.#resolver = resolver;
65
+ this.#refs = resolver.refs;
63
66
  this.data = data;
64
67
  this.sourceFormat = sourceFormat;
68
+ this.#diagnostics = diagnostics;
65
69
  }
66
70
  get source() {
67
71
  const value = this.#getSource();
@@ -69,39 +73,44 @@ export class MdastVisitorContext {
69
73
  return value;
70
74
  }
71
75
  removeNode(node) {
72
- this.#commandBuffer.removeNode(requireNid(node, "removeNode"));
76
+ this.#commandBuffer.removeNode(requireNid(node, "removeNode", this.#refs));
73
77
  }
74
78
  insertBefore(node, newNode) {
75
- const id = requireNid(node, "insertBefore");
79
+ const id = requireNid(node, "insertBefore", this.#refs);
76
80
  for (const n of asArray(newNode))
77
- emitMdastTree(this.#commandBuffer, "insertBefore", id, n);
81
+ emitMdastTree(this.#commandBuffer, "insertBefore", id, n, false, this.#refs);
78
82
  }
79
83
  insertAfter(node, newNode) {
80
- const id = requireNid(node, "insertAfter");
84
+ const id = requireNid(node, "insertAfter", this.#refs);
81
85
  for (const n of asArray(newNode))
82
- emitMdastTree(this.#commandBuffer, "insertAfter", id, n);
86
+ emitMdastTree(this.#commandBuffer, "insertAfter", id, n, false, this.#refs);
83
87
  }
84
88
  /**
85
89
  * Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any
86
- * children `parentNode` declares are kept after it.
90
+ * children `parentNode` declares are kept after it. `parentNode` must be a
91
+ * node type that can hold children, or a raw string parsing to exactly one
92
+ * such block (`{ raw: "> " }` wraps in a blockquote); to surround a node
93
+ * with raw HTML tags, use `replaceNode(node, [openTag, node, closeTag])`
94
+ * instead.
87
95
  */
88
96
  wrapNode(node, parentNode) {
89
- const id = requireNid(node, "wrapNode");
90
- emitMdastTree(this.#commandBuffer, "wrapNode", id, parentNode);
97
+ const id = requireNid(node, "wrapNode", this.#refs);
98
+ assertMdastWrapParent(parentNode);
99
+ emitMdastTree(this.#commandBuffer, "wrapNode", id, parentNode, false, this.#refs);
91
100
  }
92
101
  prependChild(node, childNode) {
93
- const id = requireNid(node, "prependChild");
102
+ const id = requireNid(node, "prependChild", this.#refs);
94
103
  for (const n of asArray(childNode))
95
- emitMdastTree(this.#commandBuffer, "prependChild", id, n);
104
+ emitMdastTree(this.#commandBuffer, "prependChild", id, n, false, this.#refs);
96
105
  }
97
106
  appendChild(node, childNode) {
98
- const id = requireNid(node, "appendChild");
107
+ const id = requireNid(node, "appendChild", this.#refs);
99
108
  for (const n of asArray(childNode))
100
- emitMdastTree(this.#commandBuffer, "appendChild", id, n);
109
+ emitMdastTree(this.#commandBuffer, "appendChild", id, n, false, this.#refs);
101
110
  }
102
111
  /** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */
103
112
  insertChildAt(node, index, childNode) {
104
- const children = "children" in node ? node.children : [];
113
+ const children = ("children" in node ? node.children : undefined) ?? [];
105
114
  if (index <= 0 || children.length === 0) {
106
115
  this.prependChild(node, childNode);
107
116
  }
@@ -114,38 +123,70 @@ export class MdastVisitorContext {
114
123
  }
115
124
  /** Remove the `index`-th child of `node`; a no-op when there is no such child. */
116
125
  removeChildAt(node, index) {
117
- const child = "children" in node ? node.children[index] : undefined;
126
+ const child = "children" in node ? node.children?.[index] : undefined;
118
127
  if (child)
119
128
  this.removeNode(child);
120
129
  }
130
+ /**
131
+ * Swap `node` for one node, or for an array of nodes placed in order at its
132
+ * position. An empty array drops the node, the same as `removeNode`.
133
+ * The document root takes a `root`, the one place a `root` is accepted as
134
+ * content, or a raw string, which parses to a root of its own.
135
+ */
121
136
  replaceNode(node, newNode) {
122
- const id = requireNid(node, "replaceNode");
123
- emitMdastTree(this.#commandBuffer, "replace", id, newNode, true);
137
+ const id = requireNid(node, "replaceNode", this.#refs);
138
+ if (Array.isArray(newNode)) {
139
+ if (id === ROOT_NODE_ID && newNode.length > 1)
140
+ throw rootReplacementError(newNode);
141
+ // The last node carries the `replace` so refs back to the target still splice.
142
+ let previous;
143
+ for (const n of newNode) {
144
+ if (previous !== undefined) {
145
+ emitMdastTree(this.#commandBuffer, "insertBefore", id, previous, false, this.#refs);
146
+ }
147
+ previous = n;
148
+ }
149
+ if (previous === undefined) {
150
+ // Replacing with nothing drops the node, like removeNode.
151
+ this.removeNode(node);
152
+ }
153
+ else if (id === ROOT_NODE_ID && !isRawMdastContent(previous)) {
154
+ emitMdastRootReplace(this.#commandBuffer, requireRootReplacement(previous), this.#refs);
155
+ }
156
+ else {
157
+ emitMdastTree(this.#commandBuffer, "replace", id, previous, true, this.#refs);
158
+ }
159
+ return;
160
+ }
161
+ if (id === ROOT_NODE_ID && !isRawMdastContent(newNode)) {
162
+ emitMdastRootReplace(this.#commandBuffer, requireRootReplacement(newNode), this.#refs);
163
+ return;
164
+ }
165
+ emitMdastTree(this.#commandBuffer, "replace", id, newNode, true, this.#refs);
124
166
  }
125
167
  setProperty(node, key, value) {
126
168
  if (key === "children") {
127
169
  // children is structural: set-children keeps the node and swaps only its
128
170
  // child list (reused children keep their id).
129
- const id = requireNid(node, "setProperty");
130
- const ops = compileMdastChildrenToOpstream(value);
131
- if (!ops)
171
+ const id = requireNid(node, "setProperty", this.#refs);
172
+ if (!emitMdastChildrenCommand(this.#commandBuffer, id, value, this.#refs)) {
132
173
  throw unencodableContentError(value);
133
- this.#commandBuffer.setChildrenOpstream(id, ops);
174
+ }
134
175
  return;
135
176
  }
136
177
  if (key === "data") {
137
178
  // data is stored as JSON in the arena, serialize it for the command buffer
138
- this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value != null ? JSON.stringify(value) : null);
179
+ this.#commandBuffer.setProperty(requireNid(node, "setProperty", this.#refs), key, value != null ? JSON.stringify(value) : null);
139
180
  return;
140
181
  }
141
- this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value);
182
+ this.#commandBuffer.setProperty(requireNid(node, "setProperty", this.#refs), key, value);
142
183
  }
143
184
  /** Collect the concatenated text of all descendant text nodes (like mdast-util-to-string). */
144
185
  textContent(node, options) {
145
- return mdastTextContentHandle(this.#handle, requireNid(node, "textContent"), options);
186
+ return mdastTextContentHandle(this.#handle, requireNid(node, "textContent", this.#refs), options);
146
187
  }
147
188
  parent(node) {
148
- const parentId = this.#resolver.parentIdOf(requireNid(node, "parent"));
189
+ const parentId = this.#resolver.parentIdOf(requireNid(node, "parent", this.#refs));
149
190
  if (parentId === undefined)
150
191
  return undefined;
151
192
  const byId = (this.#parentsById ??= new Map());
@@ -161,12 +202,13 @@ export class MdastVisitorContext {
161
202
  * Use this rather than `parent.children.indexOf(node)`, which won't find it.
162
203
  */
163
204
  indexOf(node) {
164
- return this.#resolver.indexInParent(requireNid(node, "indexOf"));
205
+ return this.#resolver.indexInParent(requireNid(node, "indexOf", this.#refs));
165
206
  }
166
207
  report({ message, node, severity = "error", }) {
208
+ const id = node ? nid(node, this.#refs) : undefined;
167
209
  this.#diagnostics.push({
168
210
  message,
169
- nodeId: node ? nid(node) : undefined,
211
+ nodeId: id === FOREIGN_REF ? undefined : id,
170
212
  position: node?.position,
171
213
  severity,
172
214
  });
@@ -179,7 +221,24 @@ export class MdastVisitorContext {
179
221
  return this.#diagnostics;
180
222
  }
181
223
  }
224
+ const mdastSubscriptionCache = new WeakMap();
182
225
  export function resolveMdastSubscriptions(plugin) {
226
+ const cached = mdastSubscriptionCache.get(plugin);
227
+ if (cached !== undefined)
228
+ return cached.subs;
229
+ const built = buildMdastSubscriptions(plugin);
230
+ mdastSubscriptionCache.set(plugin, built);
231
+ return built.subs;
232
+ }
233
+ function getMdastRustSubs(plugin) {
234
+ const cached = mdastSubscriptionCache.get(plugin);
235
+ if (cached !== undefined)
236
+ return cached.rustSubs;
237
+ const built = buildMdastSubscriptions(plugin);
238
+ mdastSubscriptionCache.set(plugin, built);
239
+ return built.rustSubs;
240
+ }
241
+ function buildMdastSubscriptions(plugin) {
183
242
  const subs = [];
184
243
  for (const [name, fn] of Object.entries(plugin)) {
185
244
  if (VISITOR_KEYS.has(name) && typeof fn === "function") {
@@ -192,14 +251,19 @@ export function resolveMdastSubscriptions(plugin) {
192
251
  }
193
252
  }
194
253
  }
195
- return subs;
254
+ const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: [] }));
255
+ return { subs, rustSubs };
196
256
  }
257
+ const MDAST_EPOCH_CACHE = registerEpochCacheSlot(new WeakMap());
197
258
  class MdastLazyChildResolver extends LazyChildResolver {
259
+ cacheSlot() {
260
+ return MDAST_EPOCH_CACHE;
261
+ }
198
262
  createReader(wire) {
199
263
  return new MdastReader(wire);
200
264
  }
201
- materializeNode(reader, nodeId) {
202
- return materializeNode(reader, nodeId);
265
+ materializeNode(reader, nodeId, refs) {
266
+ return materializeNode(reader, nodeId, true, refs);
203
267
  }
204
268
  readParentId(reader, nodeId) {
205
269
  return reader.getParentId(nodeId);
@@ -209,11 +273,18 @@ class MdastLazyChildResolver extends LazyChildResolver {
209
273
  }
210
274
  }
211
275
  /** Build the child-stub list for a matched node from the wire's `[child_ids]
212
- * [child_types]` blocks — no arena snapshot. The seal check still applies:
213
- * post-pass ids are stale, and a stub built from them could later splice the
214
- * wrong node as a ref. */
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. */
215
279
  function readMdastChildStubs(view, buf, idsPos, typesPos, count, resolver) {
216
- resolver.assertUnsealed();
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
+ }
217
288
  const stubs = new Array(count);
218
289
  for (let i = 0; i < count; i++) {
219
290
  stubs[i] = new MdastChildStub(resolver, ru32(view, idsPos + i * 4), buf[typesPos + i]);
@@ -300,82 +371,124 @@ function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver)
300
371
  node.spread = buf[pos + 1] !== 0;
301
372
  break;
302
373
  }
374
+ case 37: {
375
+ // descriptionDetails
376
+ node.spread = buf[pos] !== 0;
377
+ break;
378
+ }
303
379
  // table (21), directives (30/31/32) and mdxJsx elements (100/101) are
304
380
  // decoded by the generated `decodeMdastTypeData` from their tails.
305
381
  // root(0), paragraph(1), thematicBreak(3), blockquote(4), emphasis(11),
306
382
  // strong(12), break(14), tableRow(22), tableCell(23), delete(24): no extra data
307
383
  }
308
384
  }
309
- mdastNodeIdMap.set(node, nodeId);
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);
310
400
  if (initialData) {
311
401
  node.data = initialData;
312
402
  }
313
403
  return node;
314
404
  }
405
+ 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
+ const MDAST_CUSTOM = NAME_TO_TYPE.custom;
315
410
  /** The arena id of a node if it is an existing (materialized) node, else
316
411
  * undefined for a freshly-built one. */
317
- function reusedId(node) {
412
+ function reusedId(node, refs) {
318
413
  if (node === null || typeof node !== "object")
319
414
  return undefined;
320
- const id = nid(node);
321
- return typeof id === "number" ? id : undefined;
322
- }
323
- // Reused across every replacement in a pass: compile is synchronous and its
324
- // result is copied into the command buffer before the next call, so a single
325
- // writer is safe and avoids a 512-byte allocation per built node.
326
- const mdastWriter = new OpWriter();
327
- /**
328
- * Compile a declarative MDAST replacement tree to the op-stream — the only
329
- * structural encoding. Reused nodes (those still carrying an arena id) become
330
- * `ref`s so the rebuild splices the original back in place. Returns null when
331
- * the replay can't reproduce the tree identically (unsupported node type or
332
- * out-of-range numeric field); the caller turns that into a hard error.
333
- */
334
- function compileMdastToOpstream(root, forReplace = false) {
335
- mdastWriter.begin();
336
- try {
337
- if (!emitMdastOp(mdastWriter, root, true, forReplace))
338
- return null;
339
- return mdastWriter.take();
340
- }
341
- finally {
342
- mdastWriter.end();
343
- }
415
+ const id = nid(node, refs);
416
+ return id !== undefined && id !== FOREIGN_REF ? id : undefined;
344
417
  }
345
- /** Compile a set-children payload: a root-wrapped child list, the shape
418
+ /** Emit a set-children command in place: a root-wrapped child list, the shape
346
419
  * `Patch::SetChildren` splices in. Reused children become refs. */
347
- function compileMdastChildrenToOpstream(children) {
420
+ function emitMdastChildrenCommand(buffer, id, children, refs) {
348
421
  if (!Array.isArray(children))
349
- return null;
350
- mdastWriter.begin();
351
- try {
352
- mdastWriter.open(NAME_TO_TYPE.root);
422
+ return false;
423
+ return buffer.emitOpstreamCommand(CMD_SET_CHILDREN, id, () => {
424
+ buffer.open(MDAST_ROOT);
353
425
  for (const c of children) {
354
- if (!emitMdastOp(mdastWriter, c, false, false))
355
- return null;
426
+ if (!emitMdastOp(buffer, c, false, false, refs))
427
+ return false;
356
428
  }
357
- mdastWriter.close();
358
- return mdastWriter.take();
429
+ buffer.close();
430
+ return true;
431
+ });
432
+ }
433
+ /** Separate from the per-node encoder, which rejects a `root` payload. */
434
+ function emitMdastRootReplace(buffer, root, refs) {
435
+ const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD.replace, ROOT_NODE_ID, () => emitMdastRootOp(buffer, root, refs));
436
+ if (!ok)
437
+ throw unencodableContentError(root);
438
+ }
439
+ function emitMdastRootOp(w, n, refs) {
440
+ w.open(MDAST_ROOT);
441
+ if (n.data != null)
442
+ w.data(n.data);
443
+ if (n._keepChildren === true) {
444
+ w.keepChildren();
359
445
  }
360
- finally {
361
- mdastWriter.end();
446
+ else {
447
+ const children = n.children;
448
+ if (Array.isArray(children)) {
449
+ for (const c of children)
450
+ if (!emitMdastOp(w, c, false, true, refs))
451
+ return false;
452
+ }
362
453
  }
454
+ w.close();
455
+ return true;
363
456
  }
364
- function emitMdastOp(w, node, isRoot, forReplace) {
457
+ function emitMdastOp(w, node, isRoot, forReplace, refs) {
365
458
  if (node === null || typeof node !== "object")
366
459
  return false;
367
460
  if (!isRoot) {
368
- const id = reusedId(node);
461
+ const id = reusedId(node, refs);
369
462
  if (id !== undefined) {
370
463
  w.ref(id);
371
464
  return true;
372
465
  }
373
466
  }
374
467
  const n = node;
375
- const type = MDAST_OPSTREAM_TYPES[n.type];
376
- if (type === undefined)
377
- return false;
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
+ let type = MDAST_OPSTREAM_TYPES[n.type];
471
+ let isCustom = false;
472
+ if (type === undefined) {
473
+ if (typeof n.type !== "string" || n.type.length === 0)
474
+ 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.
478
+ if (NAME_TO_TYPE[n.type] !== undefined)
479
+ return false;
480
+ type = MDAST_CUSTOM;
481
+ isCustom = true;
482
+ }
483
+ 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
+ isCustom = true;
488
+ }
378
489
  w.open(type);
490
+ if (isCustom)
491
+ w.str(OF_NAME, n.type);
379
492
  if (typeof n.value === "string")
380
493
  w.str(OF_VALUE, n.value);
381
494
  if (typeof n.url === "string")
@@ -412,7 +525,7 @@ function emitMdastOp(w, node, isRoot, forReplace) {
412
525
  w.bool(OF_ORDERED, n.ordered);
413
526
  if (typeof n.spread === "boolean")
414
527
  w.bool(OF_SPREAD, n.spread);
415
- if (typeof n.name === "string")
528
+ if (!isCustom && typeof n.name === "string")
416
529
  w.str(OF_NAME, n.name);
417
530
  const attrs = n.attributes;
418
531
  if (Array.isArray(attrs)) {
@@ -446,7 +559,7 @@ function emitMdastOp(w, node, isRoot, forReplace) {
446
559
  const children = n.children;
447
560
  if (Array.isArray(children)) {
448
561
  for (const c of children)
449
- if (!emitMdastOp(w, c, false, forReplace))
562
+ if (!emitMdastOp(w, c, false, forReplace, refs))
450
563
  return false;
451
564
  }
452
565
  }
@@ -470,7 +583,7 @@ function isRawMdastContent(content) {
470
583
  * op-stream is the only declarative encoding. The switches stay inline so the
471
584
  * buffer calls are monomorphic (computed method names defeat inline caches on
472
585
  * this warm path). */
473
- function emitMdastTree(buffer, op, id, content, forReplace = false) {
586
+ function emitMdastTree(buffer, op, id, content, forReplace, refs) {
474
587
  if (isRawMdastContent(content)) {
475
588
  switch (op) {
476
589
  case "replace":
@@ -487,27 +600,65 @@ function emitMdastTree(buffer, op, id, content, forReplace = false) {
487
600
  return buffer.wrapNode(id, content);
488
601
  }
489
602
  }
490
- const ops = compileMdastToOpstream(content, forReplace);
491
- if (ops === null)
603
+ const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD[op], id, () => emitMdastOp(buffer, content, true, forReplace, refs));
604
+ if (!ok)
492
605
  throw unencodableContentError(content);
493
- switch (op) {
494
- case "replace":
495
- return buffer.replaceOpstream(id, ops);
496
- case "insertBefore":
497
- return buffer.insertBeforeOpstream(id, ops);
498
- case "insertAfter":
499
- return buffer.insertAfterOpstream(id, ops);
500
- case "prependChild":
501
- return buffer.prependChildOpstream(id, ops);
502
- case "appendChild":
503
- return buffer.appendChildOpstream(id, ops);
504
- case "wrapNode":
505
- return buffer.wrapNodeOpstream(id, ops);
606
+ }
607
+ /** A leaf wrapper would make the patch engine drop or displace the wrapped node. */
608
+ function assertMdastWrapParent(parentNode) {
609
+ const sandwich = 'replaceNode(node, [{ type: "html", value: "<div>" }, node, { type: "html", value: "</div>" }])';
610
+ // A raw wrapper is only a tree once Rust has parsed it, so it is checked there.
611
+ if (isRawMdastContent(parentNode))
612
+ return;
613
+ const type = parentNode.type;
614
+ const tag = typeof type === "string" ? NAME_TO_TYPE[type] : undefined;
615
+ if (tag === undefined) {
616
+ // User-defined type: only the declared shape distinguishes a parent from
617
+ // a text leaf.
618
+ if (Array.isArray(parentNode.children))
619
+ return;
620
+ throw new Error(`wrapNode: a user-defined "${String(type)}" wrapper must declare a children array. ` +
621
+ "A leaf-shaped custom node renders as text and cannot hold the wrapped node.");
506
622
  }
623
+ if (!LEAF_TYPES.has(tag))
624
+ return;
625
+ throw new Error(`wrapNode: "${String(type)}" nodes cannot hold children, so they cannot wrap a node. ` +
626
+ 'Pass a parent node such as { type: "blockquote", children: [] }, a raw wrapper ' +
627
+ `such as { raw: "> " }, or surround the node with tags: ${sandwich}.`);
628
+ }
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. */
632
+ const MDAST_VALUE_ONLY_TYPES = new Set([
633
+ "text",
634
+ "html",
635
+ "inlineCode",
636
+ "yaml",
637
+ "toml",
638
+ "inlineMath",
639
+ ]);
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. */
643
+ function isMdastTextValueSwap(result, original) {
644
+ if (original === undefined)
645
+ return false;
646
+ if (result.type !== original.type)
647
+ return false;
648
+ if (!MDAST_VALUE_ONLY_TYPES.has(result.type))
649
+ return false;
650
+ const r = result;
651
+ if (typeof r.value !== "string")
652
+ return false;
653
+ return (r.children === undefined &&
654
+ r.position === undefined &&
655
+ r.data === undefined &&
656
+ r.lang === undefined &&
657
+ r.meta === undefined);
507
658
  }
508
659
  /** A result that is the same object as the input node is a no-op, so context
509
660
  * mutations (e.g. setProperty) are not clobbered. */
510
- function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
661
+ function applyMdastVisitResult(result, nodeId, returnBuffer, refs, originalNode) {
511
662
  if (result === undefined || result === null)
512
663
  return;
513
664
  if (result === originalNode)
@@ -520,9 +671,15 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
520
671
  case "raw_html":
521
672
  returnBuffer.replace(nodeId, result);
522
673
  break;
523
- case "structured_node":
524
- emitMdastTree(returnBuffer, "replace", nodeId, result, true);
674
+ case "structured_node": {
675
+ const node = result;
676
+ if (isMdastTextValueSwap(node, originalNode)) {
677
+ returnBuffer.setProperty(nodeId, "value", node.value);
678
+ break;
679
+ }
680
+ emitMdastTree(returnBuffer, "replace", nodeId, node, true, refs);
525
681
  break;
682
+ }
526
683
  }
527
684
  }
528
685
  /**
@@ -532,12 +689,12 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
532
689
  * Returns MdastVisitResult synchronously if all visitors are sync,
533
690
  * or Promise<MdastVisitResult> if any visitor is async.
534
691
  */
535
- export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {}, sourceFormat = "markdown") {
692
+ export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
536
693
  const getSource = typeof source === "function" ? source : () => source;
537
694
  const resolver = new MdastLazyChildResolver(handle);
538
- const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data, sourceFormat);
539
- const returnBuffer = new CommandBuffer();
540
- const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: [] }));
695
+ const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data, sourceFormat, diagnostics);
696
+ const returnBuffer = acquireCommandBuffer();
697
+ const rustSubs = getMdastRustSubs(plugin);
541
698
  const matchBuf = walkMdastHandle(handle, rustSubs);
542
699
  const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
543
700
  const matchCount = ru32(matchView, 0);
@@ -555,25 +712,44 @@ export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {
555
712
  deferred.push({ nodeId, promise: result, originalNode: node });
556
713
  }
557
714
  else {
558
- applyMdastVisitResult(result, nodeId, returnBuffer, node);
715
+ applyMdastVisitResult(result, nodeId, returnBuffer, resolver.refs, node);
559
716
  }
560
717
  }
561
718
  if (deferred) {
562
719
  return Promise.all(deferred.map((d) => d.promise.then((r) => ({ nodeId: d.nodeId, result: r, originalNode: d.originalNode })))).then((results) => {
563
720
  for (const { nodeId, result, originalNode } of results) {
564
- applyMdastVisitResult(result, nodeId, returnBuffer, originalNode);
721
+ applyMdastVisitResult(result, nodeId, returnBuffer, resolver.refs, originalNode);
565
722
  }
566
- // End of the pass — the caller applies the returned command buffer next,
567
- // renumbering the arena, so later snapshots would resolve match-time
568
- // child ids against wrong nodes. This is the last point we control.
569
- resolver.seal();
570
723
  return finalizeMdastVisit(handle, context, returnBuffer);
571
724
  });
572
725
  }
573
- resolver.seal();
726
+ return finalizeMdastVisit(handle, context, returnBuffer);
727
+ }
728
+ const MDAST_ROOT_SUBS = [
729
+ { nodeType: MDAST_ROOT, tagFilter: [] },
730
+ ];
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. */
733
+ export function visitMdastHook(handle, plugin, hook, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
734
+ const getSource = typeof source === "function" ? source : () => source;
735
+ const resolver = new MdastLazyChildResolver(handle);
736
+ const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data, sourceFormat, diagnostics);
737
+ const returnBuffer = acquireCommandBuffer();
738
+ const matchBuf = walkMdastHandle(handle, MDAST_ROOT_SUBS);
739
+ const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
740
+ if (ru32(matchView, 0) === 0)
741
+ return finalizeMdastVisit(handle, context, returnBuffer);
742
+ const root = readMdastMatchedNode(matchView, matchBuf, ru32(matchView, 10), ru32(matchView, 4), MDAST_ROOT, resolver);
743
+ const result = hook.call(plugin, root, context);
744
+ if (result instanceof Promise) {
745
+ return result.then(() => finalizeMdastVisit(handle, context, returnBuffer));
746
+ }
574
747
  return finalizeMdastVisit(handle, context, returnBuffer);
575
748
  }
576
749
  function finalizeMdastVisit(handle, context, returnBuffer) {
577
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());
578
754
  return { commandBuffer: merged, diagnostics: context.getDiagnostics(), hasMutations };
579
755
  }