572 lines
23 KiB
JavaScript
572 lines
23 KiB
JavaScript
import { materializeNode } from "./mdast-materializer.js";
|
||
import { MdastReader } from "./mdast-reader.js";
|
||
import { CommandBuffer, classifyReturn } from "../command-buffer.js";
|
||
import { ru32, rstr, readPosition } from "../wire-read.js";
|
||
import { decodeMdastTypeData } from "./generated/layout.js";
|
||
import { TYPE_NAMES, NAME_TO_TYPE, VISITOR_KEYS, MDAST_OPSTREAM_TYPES, } from "./generated/node-types.js";
|
||
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";
|
||
import { walkMdastHandle, mdastTextContentHandle } from "#binding";
|
||
import { asArray, makeRequireNid, mergeAndReset, unencodableContentError, } from "../visitor-shared.js";
|
||
import { LazyChildResolver } from "../lazy-child-resolver.js";
|
||
import { MdastChildStub } from "./child-stub.js";
|
||
/** Maps MdastNode objects to their arena node IDs without Object.defineProperty overhead. */
|
||
const mdastNodeIdMap = new WeakMap();
|
||
function nid(node) {
|
||
// Genuine stubs carry their id as a plain field; a spread copy is not
|
||
// `instanceof` and has no `_nodeId`, so it correctly reads as new content.
|
||
if (node instanceof MdastChildStub)
|
||
return node._id;
|
||
const id = mdastNodeIdMap.get(node);
|
||
if (id !== undefined)
|
||
return id;
|
||
// Plain objects are trusted only via the WeakMap or a NON-enumerable
|
||
// `_nodeId` (the materializer's convention, which spread cannot copy) — an
|
||
// enumerable one rode in on a copy and must read as new content.
|
||
const d = Object.getOwnPropertyDescriptor(node, "_nodeId");
|
||
return d !== undefined && !d.enumerable ? d.value : undefined;
|
||
}
|
||
const requireNid = makeRequireNid(nid);
|
||
export class MdastVisitorContext {
|
||
#commandBuffer = new CommandBuffer();
|
||
#diagnostics = [];
|
||
#handle;
|
||
#getSource;
|
||
#resolver;
|
||
/** One canonical object per parent id, so visitors can dedupe by identity.
|
||
* Null until the first `parent()` call; most passes never make one. */
|
||
#parentsById = null;
|
||
/**
|
||
* The URL of the document being processed (the compile `fileURL` option),
|
||
* or `undefined` when none was given. Use `fileURLToPath(ctx.fileURL)` for a
|
||
* decoded filesystem path.
|
||
*/
|
||
fileURL;
|
||
/**
|
||
* Document-level data bag, shared across every plugin in the compile and
|
||
* across the mdast→hast phase boundary. Mutate keys directly
|
||
* (`ctx.data.foo = x`); the bag itself isn't reassignable. Values are kept
|
||
* on the JS side, so any value is allowed, including functions and class
|
||
* instances. Returned to the caller as `result.data`.
|
||
*/
|
||
data;
|
||
constructor(handle, getSource, fileURL, resolver, data) {
|
||
this.#handle = handle;
|
||
this.#getSource = getSource;
|
||
this.fileURL = fileURL;
|
||
this.#resolver = resolver;
|
||
this.data = data;
|
||
}
|
||
get source() {
|
||
const value = this.#getSource();
|
||
Object.defineProperty(this, "source", { value, writable: false, enumerable: true });
|
||
return value;
|
||
}
|
||
removeNode(node) {
|
||
this.#commandBuffer.removeNode(requireNid(node, "removeNode"));
|
||
}
|
||
insertBefore(node, newNode) {
|
||
const id = requireNid(node, "insertBefore");
|
||
for (const n of asArray(newNode))
|
||
emitMdastTree(this.#commandBuffer, "insertBefore", id, n);
|
||
}
|
||
insertAfter(node, newNode) {
|
||
const id = requireNid(node, "insertAfter");
|
||
for (const n of asArray(newNode))
|
||
emitMdastTree(this.#commandBuffer, "insertAfter", id, n);
|
||
}
|
||
/**
|
||
* Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any
|
||
* children `parentNode` declares are kept after it.
|
||
*/
|
||
wrapNode(node, parentNode) {
|
||
const id = requireNid(node, "wrapNode");
|
||
emitMdastTree(this.#commandBuffer, "wrapNode", id, parentNode);
|
||
}
|
||
prependChild(node, childNode) {
|
||
const id = requireNid(node, "prependChild");
|
||
for (const n of asArray(childNode))
|
||
emitMdastTree(this.#commandBuffer, "prependChild", id, n);
|
||
}
|
||
appendChild(node, childNode) {
|
||
const id = requireNid(node, "appendChild");
|
||
for (const n of asArray(childNode))
|
||
emitMdastTree(this.#commandBuffer, "appendChild", id, n);
|
||
}
|
||
/** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */
|
||
insertChildAt(node, index, childNode) {
|
||
const children = "children" in node ? node.children : [];
|
||
if (index <= 0 || children.length === 0) {
|
||
this.prependChild(node, childNode);
|
||
}
|
||
else if (index >= children.length) {
|
||
this.appendChild(node, childNode);
|
||
}
|
||
else {
|
||
this.insertBefore(children[index], childNode);
|
||
}
|
||
}
|
||
/** Remove the `index`-th child of `node`; a no-op when there is no such child. */
|
||
removeChildAt(node, index) {
|
||
const child = "children" in node ? node.children[index] : undefined;
|
||
if (child)
|
||
this.removeNode(child);
|
||
}
|
||
replaceNode(node, newNode) {
|
||
const id = requireNid(node, "replaceNode");
|
||
emitMdastTree(this.#commandBuffer, "replace", id, newNode, true);
|
||
}
|
||
setProperty(node, key, value) {
|
||
if (key === "children") {
|
||
// children is structural: set-children keeps the node and swaps only its
|
||
// child list (reused children keep their id).
|
||
const id = requireNid(node, "setProperty");
|
||
const ops = compileMdastChildrenToOpstream(value);
|
||
if (!ops)
|
||
throw unencodableContentError(value);
|
||
this.#commandBuffer.setChildrenOpstream(id, ops);
|
||
return;
|
||
}
|
||
if (key === "data") {
|
||
// data is stored as JSON in the arena, serialize it for the command buffer
|
||
this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value != null ? JSON.stringify(value) : null);
|
||
return;
|
||
}
|
||
this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value);
|
||
}
|
||
/** Collect the concatenated text of all descendant text nodes (like mdast-util-to-string). */
|
||
textContent(node, options) {
|
||
return mdastTextContentHandle(this.#handle, requireNid(node, "textContent"), options);
|
||
}
|
||
parent(node) {
|
||
const parentId = this.#resolver.parentIdOf(requireNid(node, "parent"));
|
||
if (parentId === undefined)
|
||
return undefined;
|
||
const byId = (this.#parentsById ??= new Map());
|
||
let parent = byId.get(parentId);
|
||
if (parent === undefined) {
|
||
parent = this.#resolver.materializeOne(parentId);
|
||
byId.set(parentId, parent);
|
||
}
|
||
return parent;
|
||
}
|
||
/**
|
||
* Index of `node` within its parent's children, or `undefined` at the root.
|
||
* Use this rather than `parent.children.indexOf(node)`, which won't find it.
|
||
*/
|
||
indexOf(node) {
|
||
return this.#resolver.indexInParent(requireNid(node, "indexOf"));
|
||
}
|
||
report({ message, node, severity = "error", }) {
|
||
this.#diagnostics.push({
|
||
message,
|
||
nodeId: node ? nid(node) : undefined,
|
||
position: node?.position,
|
||
severity,
|
||
});
|
||
}
|
||
/** Get the binary command buffer for all mutations recorded via context methods. */
|
||
getCommandBuffer() {
|
||
return this.#commandBuffer;
|
||
}
|
||
getDiagnostics() {
|
||
return this.#diagnostics;
|
||
}
|
||
}
|
||
export function resolveMdastSubscriptions(plugin) {
|
||
const subs = [];
|
||
for (const [name, fn] of Object.entries(plugin)) {
|
||
if (VISITOR_KEYS.has(name) && typeof fn === "function") {
|
||
const nodeType = NAME_TO_TYPE[name];
|
||
if (nodeType !== undefined) {
|
||
subs.push({
|
||
nodeType,
|
||
visitFn: fn,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
return subs;
|
||
}
|
||
class MdastLazyChildResolver extends LazyChildResolver {
|
||
createReader(wire) {
|
||
return new MdastReader(wire);
|
||
}
|
||
materializeNode(reader, nodeId) {
|
||
return materializeNode(reader, nodeId);
|
||
}
|
||
readParentId(reader, nodeId) {
|
||
return reader.getParentId(nodeId);
|
||
}
|
||
readChildIds(reader, nodeId) {
|
||
return reader.getChildIds(nodeId);
|
||
}
|
||
}
|
||
/** Build the child-stub list for a matched node from the wire's `[child_ids]
|
||
* [child_types]` blocks — no arena snapshot. The seal check still applies:
|
||
* post-pass ids are stale, and a stub built from them could later splice the
|
||
* wrong node as a ref. */
|
||
function readMdastChildStubs(view, buf, idsPos, typesPos, count, resolver) {
|
||
resolver.assertUnsealed();
|
||
const stubs = new Array(count);
|
||
for (let i = 0; i < count; i++) {
|
||
stubs[i] = new MdastChildStub(resolver, ru32(view, idsPos + i * 4), buf[typesPos + i]);
|
||
}
|
||
return stubs;
|
||
}
|
||
/** Install `children` as an own enumerable getter (spread must carry it),
|
||
* self-replacing with the one stable stub array on first read. One closure
|
||
* and one define per node — installing the wire locals as hidden slots
|
||
* instead measurably regressed every matching pipeline. */
|
||
function makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver) {
|
||
Object.defineProperty(node, "children", {
|
||
get() {
|
||
const val = readMdastChildStubs(view, buf, childIdsPos, childTypesPos, childCount, resolver);
|
||
Object.defineProperty(this, "children", {
|
||
value: val,
|
||
writable: true,
|
||
enumerable: true,
|
||
configurable: true,
|
||
});
|
||
return val;
|
||
},
|
||
enumerable: true,
|
||
configurable: true,
|
||
});
|
||
}
|
||
/**
|
||
* Read an MDAST node from the inline data in a match buffer entry.
|
||
*
|
||
* Inline format (from Rust serialize_mdast_node_inline):
|
||
* [node_data: u32+bytes][position: 6×u32 = 24B][child_count: u32][child_ids: N×u32]
|
||
* [child_types: N×u8][type-specific data]
|
||
*/
|
||
function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver) {
|
||
let pos = dataOffset;
|
||
const dataJsonLen = ru32(view, pos);
|
||
pos += 4;
|
||
let initialData = null;
|
||
if (dataJsonLen > 0) {
|
||
const jsonStr = rstr(buf, pos, dataJsonLen);
|
||
try {
|
||
initialData = JSON.parse(jsonStr);
|
||
}
|
||
catch (err) {
|
||
if (process.env.NODE_ENV !== "production") {
|
||
console.warn(`readMdastMatchedNode: malformed node_data for nodeId=${nodeId}`, err);
|
||
}
|
||
}
|
||
pos += dataJsonLen;
|
||
}
|
||
const position = readPosition(view, pos);
|
||
pos += 24;
|
||
const childCount = ru32(view, pos);
|
||
pos += 4;
|
||
// Ids/types decode lazily with `.children` — most matched nodes never read them.
|
||
const childIdsPos = pos;
|
||
pos += childCount * 4;
|
||
const childTypesPos = pos;
|
||
pos += childCount;
|
||
const typeName = TYPE_NAMES[nodeType] ?? `unknown(${nodeType})`;
|
||
const node = { type: typeName };
|
||
if (position !== undefined)
|
||
node.position = position;
|
||
if (childCount > 0) {
|
||
makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver);
|
||
}
|
||
// Fixed-field types decode from the generated layout table; the rest
|
||
// (variable-length / cross-field) stay in the hand-written switch.
|
||
if (!decodeMdastTypeData(view, buf, pos, nodeType, node)) {
|
||
switch (nodeType) {
|
||
case 5: {
|
||
// list
|
||
node.start = ru32(view, pos);
|
||
node.ordered = buf[pos + 4] !== 0;
|
||
node.spread = buf[pos + 5] !== 0;
|
||
if (!node.ordered)
|
||
node.start = null;
|
||
break;
|
||
}
|
||
case 6: {
|
||
// listItem
|
||
const checked = buf[pos];
|
||
node.checked = checked === 2 ? null : checked === 1;
|
||
node.spread = buf[pos + 1] !== 0;
|
||
break;
|
||
}
|
||
// table (21), directives (30/31/32) and mdxJsx elements (100/101) are
|
||
// decoded by the generated `decodeMdastTypeData` from their tails.
|
||
// root(0), paragraph(1), thematicBreak(3), blockquote(4), emphasis(11),
|
||
// strong(12), break(14), tableRow(22), tableCell(23), delete(24): no extra data
|
||
}
|
||
}
|
||
mdastNodeIdMap.set(node, nodeId);
|
||
if (initialData) {
|
||
node.data = initialData;
|
||
}
|
||
return node;
|
||
}
|
||
/** The arena id of a node if it is an existing (materialized) node, else
|
||
* undefined for a freshly-built one. */
|
||
function reusedId(node) {
|
||
if (node === null || typeof node !== "object")
|
||
return undefined;
|
||
const id = nid(node);
|
||
return typeof id === "number" ? id : undefined;
|
||
}
|
||
// Reused across every replacement in a pass: compile is synchronous and its
|
||
// result is copied into the command buffer before the next call, so a single
|
||
// writer is safe and avoids a 512-byte allocation per built node.
|
||
const mdastWriter = new OpWriter();
|
||
/**
|
||
* Compile a declarative MDAST replacement tree to the op-stream — the only
|
||
* structural encoding. Reused nodes (those still carrying an arena id) become
|
||
* `ref`s so the rebuild splices the original back in place. Returns null when
|
||
* the replay can't reproduce the tree identically (unsupported node type or
|
||
* out-of-range numeric field); the caller turns that into a hard error.
|
||
*/
|
||
function compileMdastToOpstream(root, forReplace = false) {
|
||
mdastWriter.begin();
|
||
try {
|
||
if (!emitMdastOp(mdastWriter, root, true, forReplace))
|
||
return null;
|
||
return mdastWriter.take();
|
||
}
|
||
finally {
|
||
mdastWriter.end();
|
||
}
|
||
}
|
||
/** Compile a set-children payload: a root-wrapped child list, the shape
|
||
* `Patch::SetChildren` splices in. Reused children become refs. */
|
||
function compileMdastChildrenToOpstream(children) {
|
||
if (!Array.isArray(children))
|
||
return null;
|
||
mdastWriter.begin();
|
||
try {
|
||
mdastWriter.open(NAME_TO_TYPE.root);
|
||
for (const c of children) {
|
||
if (!emitMdastOp(mdastWriter, c, false, false))
|
||
return null;
|
||
}
|
||
mdastWriter.close();
|
||
return mdastWriter.take();
|
||
}
|
||
finally {
|
||
mdastWriter.end();
|
||
}
|
||
}
|
||
function emitMdastOp(w, node, isRoot, forReplace) {
|
||
if (node === null || typeof node !== "object")
|
||
return false;
|
||
if (!isRoot) {
|
||
const id = reusedId(node);
|
||
if (id !== undefined) {
|
||
w.ref(id);
|
||
return true;
|
||
}
|
||
}
|
||
const n = node;
|
||
const type = MDAST_OPSTREAM_TYPES[n.type];
|
||
if (type === undefined)
|
||
return false;
|
||
w.open(type);
|
||
if (typeof n.value === "string")
|
||
w.str(OF_VALUE, n.value);
|
||
if (typeof n.url === "string")
|
||
w.str(OF_URL, n.url);
|
||
if (typeof n.title === "string")
|
||
w.str(OF_TITLE, n.title);
|
||
if (typeof n.alt === "string")
|
||
w.str(OF_ALT, n.alt);
|
||
if (typeof n.lang === "string")
|
||
w.str(OF_LANG, n.lang);
|
||
if (typeof n.meta === "string")
|
||
w.str(OF_META, n.meta);
|
||
if (typeof n.identifier === "string")
|
||
w.str(OF_IDENTIFIER, n.identifier);
|
||
if (typeof n.label === "string")
|
||
w.str(OF_LABEL, n.label);
|
||
if (typeof n.referenceType === "string")
|
||
w.str(OF_REFERENCE_TYPE, n.referenceType);
|
||
// Out-of-range numbers compile to null and the caller throws — a visible
|
||
// error instead of silently masking the bits.
|
||
if (typeof n.depth === "number") {
|
||
if (!Number.isInteger(n.depth) || n.depth < 0 || n.depth > 255)
|
||
return false;
|
||
w.u8(OF_DEPTH, n.depth);
|
||
}
|
||
if (typeof n.checked === "boolean")
|
||
w.u8(OF_CHECKED, n.checked ? 1 : 0);
|
||
if (typeof n.start === "number") {
|
||
if (!Number.isInteger(n.start) || n.start < 0 || n.start > 4294967295)
|
||
return false;
|
||
w.u32(OF_START, n.start);
|
||
}
|
||
if (typeof n.ordered === "boolean")
|
||
w.bool(OF_ORDERED, n.ordered);
|
||
if (typeof n.spread === "boolean")
|
||
w.bool(OF_SPREAD, n.spread);
|
||
if (typeof n.name === "string")
|
||
w.str(OF_NAME, n.name);
|
||
const attrs = n.attributes;
|
||
if (Array.isArray(attrs)) {
|
||
for (const a of attrs)
|
||
emitMdxAttr(w, a);
|
||
}
|
||
else if (attrs !== null && typeof attrs === "object") {
|
||
// Directive attributes: a string→string map; non-string values are
|
||
// dropped, since the stored form holds only strings.
|
||
for (const key in attrs) {
|
||
const v = attrs[key];
|
||
if (typeof v === "string")
|
||
w.prop(key, PROP_STRING, v);
|
||
}
|
||
}
|
||
if (Array.isArray(n.align))
|
||
w.align(n.align.map(alignCode));
|
||
if (n.data?._mdxExplicitJsx === true) {
|
||
w.bool(OF_EXPLICIT, true);
|
||
}
|
||
if (n.data != null)
|
||
w.data(n.data);
|
||
if (isRoot && forReplace && n._keepChildren === true) {
|
||
// Replace splices the target's original children, discarding any the
|
||
// replacement declares.
|
||
w.keepChildren();
|
||
}
|
||
else {
|
||
// `_keepChildren` only applies to replace; other ops ignore the marker
|
||
// and emit the declared children.
|
||
const children = n.children;
|
||
if (Array.isArray(children)) {
|
||
for (const c of children)
|
||
if (!emitMdastOp(w, c, false, forReplace))
|
||
return false;
|
||
}
|
||
}
|
||
w.close();
|
||
return true;
|
||
}
|
||
/** Map a table `align` entry to its arena code (none=0). */
|
||
function alignCode(a) {
|
||
return a === "left" ? 1 : a === "right" ? 2 : a === "center" ? 3 : 0;
|
||
}
|
||
/** True for the `{raw}` / `{rawHtml}` escape hatches — re-parsed by Rust rather
|
||
* than compiled to an op-stream, so they ride the RAW_MARKDOWN / RAW_HTML
|
||
* payloads instead of the declarative encoder. */
|
||
function isRawMdastContent(content) {
|
||
const c = content;
|
||
return typeof c.raw === "string" || typeof c.rawHtml === "string";
|
||
}
|
||
/** Encode `content` as the `op` structural command. Declarative nodes compile
|
||
* to the op-stream; the `{raw}`/`{rawHtml}` escape hatches ride the raw
|
||
* re-parse payloads. Anything that compiles to neither is a hard error — the
|
||
* op-stream is the only declarative encoding. The switches stay inline so the
|
||
* buffer calls are monomorphic (computed method names defeat inline caches on
|
||
* this warm path). */
|
||
function emitMdastTree(buffer, op, id, content, forReplace = false) {
|
||
if (isRawMdastContent(content)) {
|
||
switch (op) {
|
||
case "replace":
|
||
return buffer.replace(id, content);
|
||
case "insertBefore":
|
||
return buffer.insertBefore(id, content);
|
||
case "insertAfter":
|
||
return buffer.insertAfter(id, content);
|
||
case "prependChild":
|
||
return buffer.prependChild(id, content);
|
||
case "appendChild":
|
||
return buffer.appendChild(id, content);
|
||
case "wrapNode":
|
||
return buffer.wrapNode(id, content);
|
||
}
|
||
}
|
||
const ops = compileMdastToOpstream(content, forReplace);
|
||
if (ops === null)
|
||
throw unencodableContentError(content);
|
||
switch (op) {
|
||
case "replace":
|
||
return buffer.replaceOpstream(id, ops);
|
||
case "insertBefore":
|
||
return buffer.insertBeforeOpstream(id, ops);
|
||
case "insertAfter":
|
||
return buffer.insertAfterOpstream(id, ops);
|
||
case "prependChild":
|
||
return buffer.prependChildOpstream(id, ops);
|
||
case "appendChild":
|
||
return buffer.appendChildOpstream(id, ops);
|
||
case "wrapNode":
|
||
return buffer.wrapNodeOpstream(id, ops);
|
||
}
|
||
}
|
||
/** A result that is the same object as the input node is a no-op, so context
|
||
* mutations (e.g. setProperty) are not clobbered. */
|
||
function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
|
||
if (result === undefined || result === null)
|
||
return;
|
||
if (result === originalNode)
|
||
return;
|
||
const cls = classifyReturn(result);
|
||
switch (cls) {
|
||
case "raw_markdown":
|
||
returnBuffer.replace(nodeId, result);
|
||
break;
|
||
case "raw_html":
|
||
returnBuffer.replace(nodeId, result);
|
||
break;
|
||
case "structured_node":
|
||
emitMdastTree(returnBuffer, "replace", nodeId, result, true);
|
||
break;
|
||
}
|
||
}
|
||
/**
|
||
* Walk an MDAST handle in Rust, dispatch matched nodes to JS visitor functions,
|
||
* and apply mutations back to the handle. No arena buffers cross NAPI.
|
||
*
|
||
* Returns MdastVisitResult synchronously if all visitors are sync,
|
||
* or Promise<MdastVisitResult> if any visitor is async.
|
||
*/
|
||
export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {}) {
|
||
const getSource = typeof source === "function" ? source : () => source;
|
||
const resolver = new MdastLazyChildResolver(handle);
|
||
const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data);
|
||
const returnBuffer = new CommandBuffer();
|
||
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: [] }));
|
||
const matchBuf = walkMdastHandle(handle, rustSubs);
|
||
const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
|
||
const matchCount = ru32(matchView, 0);
|
||
let deferred = null;
|
||
for (let i = 0; i < matchCount; i++) {
|
||
const indexBase = 4 + i * 10;
|
||
const nodeId = ru32(matchView, indexBase);
|
||
const subIndex = matchBuf[indexBase + 4];
|
||
const dataOffset = ru32(matchView, indexBase + 6);
|
||
const sub = subs[subIndex];
|
||
const node = readMdastMatchedNode(matchView, matchBuf, dataOffset, nodeId, sub.nodeType, resolver);
|
||
const result = sub.visitFn.call(plugin, node, context);
|
||
if (result instanceof Promise) {
|
||
deferred ??= [];
|
||
deferred.push({ nodeId, promise: result, originalNode: node });
|
||
}
|
||
else {
|
||
applyMdastVisitResult(result, nodeId, returnBuffer, node);
|
||
}
|
||
}
|
||
if (deferred) {
|
||
return Promise.all(deferred.map((d) => d.promise.then((r) => ({ nodeId: d.nodeId, result: r, originalNode: d.originalNode })))).then((results) => {
|
||
for (const { nodeId, result, originalNode } of results) {
|
||
applyMdastVisitResult(result, nodeId, returnBuffer, originalNode);
|
||
}
|
||
// End of the pass — the caller applies the returned command buffer next,
|
||
// renumbering the arena, so later snapshots would resolve match-time
|
||
// child ids against wrong nodes. This is the last point we control.
|
||
resolver.seal();
|
||
return finalizeMdastVisit(handle, context, returnBuffer);
|
||
});
|
||
}
|
||
resolver.seal();
|
||
return finalizeMdastVisit(handle, context, returnBuffer);
|
||
}
|
||
function finalizeMdastVisit(handle, context, returnBuffer) {
|
||
const { merged, hasMutations } = mergeAndReset(returnBuffer, context);
|
||
return { commandBuffer: merged, diagnostics: context.getDiagnostics(), hasMutations };
|
||
}
|