satteri 0.10.4 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/arena-reader.d.ts +13 -0
- package/dist/arena-reader.js +104 -0
- package/dist/binding.browser.d.ts +1 -1
- package/dist/binding.browser.js +1 -1
- package/dist/binding.d.ts +1 -1
- package/dist/binding.js +1 -1
- package/dist/byte-writer.d.ts +0 -18
- package/dist/byte-writer.js +6 -30
- package/dist/child-stub.d.ts +0 -17
- package/dist/child-stub.js +6 -22
- package/dist/command-buffer.d.ts +5 -26
- package/dist/command-buffer.js +28 -39
- package/dist/compile-options.d.ts +11 -0
- package/dist/compile-options.js +99 -0
- package/dist/compile-types.d.ts +330 -0
- package/dist/compile-types.js +1 -0
- package/dist/compile.d.ts +22 -328
- package/dist/compile.js +129 -379
- package/dist/directive-types.d.ts +1 -0
- package/dist/directive-types.js +1 -6
- package/dist/generated/arena-layout.d.ts +6 -4
- package/dist/generated/arena-layout.js +6 -7
- package/dist/generated/fused-wire.d.ts +3 -0
- package/dist/generated/fused-wire.js +229 -0
- package/dist/generated/parse-options.js +0 -5
- package/dist/generated/wire-constants.d.ts +3 -0
- package/dist/generated/wire-constants.js +33 -42
- package/dist/handles.d.ts +0 -7
- package/dist/hast/child-stub.d.ts +1 -8
- package/dist/hast/child-stub.js +14 -13
- package/dist/hast/element-props.d.ts +13 -0
- package/dist/hast/element-props.js +52 -6
- package/dist/hast/generated/node-types.d.ts +1 -9
- package/dist/hast/generated/node-types.js +1 -9
- package/dist/hast/generated/walk-decode.d.ts +10 -0
- package/dist/hast/generated/walk-decode.js +61 -0
- package/dist/hast/hast-materializer.d.ts +3 -10
- package/dist/hast/hast-materializer.js +73 -47
- package/dist/hast/hast-reader.d.ts +9 -38
- package/dist/hast/hast-reader.js +82 -271
- package/dist/hast/hast-visitor.d.ts +29 -16
- package/dist/hast/hast-visitor.js +197 -508
- package/dist/hast/hast-walk.d.ts +18 -0
- package/dist/hast/hast-walk.js +264 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -12
- package/dist/lazy-child-resolver.d.ts +1 -21
- package/dist/lazy-child-resolver.js +11 -32
- package/dist/materializer-cache.d.ts +5 -29
- package/dist/materializer-cache.js +29 -101
- package/dist/mdast/child-stub.d.ts +1 -12
- package/dist/mdast/child-stub.js +18 -28
- package/dist/mdast/column-align.d.ts +1 -1
- package/dist/mdast/column-align.js +0 -2
- package/dist/mdast/generated/layout.d.ts +0 -16
- package/dist/mdast/generated/layout.js +6 -47
- package/dist/mdast/generated/node-types.d.ts +1 -9
- package/dist/mdast/generated/node-types.js +3 -9
- package/dist/mdast/mdast-materializer.d.ts +3 -11
- package/dist/mdast/mdast-materializer.js +91 -63
- package/dist/mdast/mdast-reader.d.ts +15 -63
- package/dist/mdast/mdast-reader.js +132 -269
- package/dist/mdast/mdast-visitor.d.ts +23 -15
- package/dist/mdast/mdast-visitor.js +188 -294
- package/dist/mdast/mdast-walk.d.ts +13 -0
- package/dist/mdast/mdast-walk.js +144 -0
- package/dist/mdx-attr.js +0 -3
- package/dist/mdx-types.js +1 -6
- package/dist/op-stream.d.ts +1 -15
- package/dist/op-stream.js +3 -19
- package/dist/phantom.d.ts +0 -1
- package/dist/phantom.js +1 -9
- package/dist/plugin-pipeline.d.ts +32 -0
- package/dist/plugin-pipeline.js +177 -0
- package/dist/plugin.d.ts +8 -7
- package/dist/plugin.js +3 -9
- package/dist/types.d.ts +0 -1
- package/dist/visitor-node.d.ts +12 -0
- package/dist/visitor-node.js +23 -0
- package/dist/visitor-shared.d.ts +26 -30
- package/dist/visitor-shared.js +34 -36
- package/dist/wire-read.d.ts +0 -22
- package/dist/wire-read.js +3 -24
- package/index.d.ts +19 -10
- package/index.js +56 -53
- package/package.json +3 -12
- package/dist/string-pool.d.ts +0 -13
- package/dist/string-pool.js +0 -91
|
@@ -1,62 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { TYPE_NAMES, NAME_TO_TYPE, VISITOR_KEYS, HAST_OPSTREAM_TYPES, } from "./generated/node-types.js";
|
|
4
|
-
import { acquireCommandBuffer, releaseCommandBuffer, CommandBuffer, STRUCTURAL_CMD, } from "../command-buffer.js";
|
|
1
|
+
import { applyCommandsToHandle, textContentHandle, walkHandle } from "#binding";
|
|
2
|
+
import { acquireCommandBuffer, CommandBuffer, releaseCommandBuffer, STRUCTURAL_CMD, } from "../command-buffer.js";
|
|
5
3
|
import { CMD_SET_CHILDREN } from "../generated/wire-constants.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
/** Attach `parseExpression()` to an MDX expression or ESM node. */
|
|
16
|
-
function attachParseExpression(node, parseFn) {
|
|
17
|
-
Object.defineProperty(node, "parseExpression", {
|
|
18
|
-
value() {
|
|
19
|
-
const value = this.value;
|
|
20
|
-
if (typeof value !== "string")
|
|
21
|
-
return null;
|
|
22
|
-
const json = parseFn(value);
|
|
23
|
-
if (json == null)
|
|
24
|
-
return null;
|
|
25
|
-
return JSON.parse(json);
|
|
26
|
-
},
|
|
27
|
-
writable: false,
|
|
28
|
-
enumerable: false,
|
|
29
|
-
configurable: true,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Arena identity of a node, rejecting impostors — the one place the
|
|
34
|
-
* spread/identity invariant is enforced. A spread copy of a matched node or
|
|
35
|
-
* stub must read as NEW content: trusting a copied id would splice the
|
|
36
|
-
* original in as a ref and drop the copy's edits. Walk elements carry their
|
|
37
|
-
* id in a private field behind `instanceof` (spread copies fail the check);
|
|
38
|
-
* other walk-built nodes are keyed in the WeakMap (invisible to spread);
|
|
39
|
-
* `HastChildStub`s (enumerable `_id`, but that key is ignored on plain
|
|
40
|
-
* objects) are recognized by `instanceof`. Plain objects are trusted only via
|
|
41
|
-
* the WeakMap or a NON-enumerable `_nodeId` (the materializers' convention,
|
|
42
|
-
* which spread cannot copy).
|
|
43
|
-
*/
|
|
44
|
-
function nid(node, refs) {
|
|
45
|
-
if (node instanceof WalkElement)
|
|
46
|
-
return node._refs === refs ? node._nid : FOREIGN_REF;
|
|
47
|
-
if (node instanceof HastChildStub)
|
|
48
|
-
return node._refs === refs ? node._id : FOREIGN_REF;
|
|
49
|
-
const id = refs.get(node);
|
|
50
|
-
if (id !== undefined)
|
|
51
|
-
return id;
|
|
52
|
-
const d = Object.getOwnPropertyDescriptor(node, "_nodeId");
|
|
53
|
-
if (d !== undefined && !d.enumerable)
|
|
54
|
-
return FOREIGN_REF;
|
|
55
|
-
return crossPipelineForeign(node);
|
|
56
|
-
}
|
|
57
|
-
const requireNid = makeRequireNid(nid);
|
|
58
|
-
/** `wrapNode` allowlist: an unlisted type fails loud instead of silently
|
|
59
|
-
* mis-wrapping. */
|
|
4
|
+
import { emitMdxAttr, OF_EXPLICIT, OF_NAME, OF_TAGNAME, OF_VALUE, OpWriter, PROP_BOOL_FALSE, PROP_BOOL_TRUE, PROP_INT, PROP_TOKEN_LIST, PROP_STRING, } from "../op-stream.js";
|
|
5
|
+
import { HAST_OPSTREAM_TYPES, NAME_TO_TYPE, VISITOR_KEYS } from "./generated/node-types.js";
|
|
6
|
+
import {} from "./hast-materializer.js";
|
|
7
|
+
import { encodeTokenList } from "./element-props.js";
|
|
8
|
+
import { HAST_ELEMENT, HAST_MDX_JSX_ELEMENT, HAST_MDX_JSX_TEXT_ELEMENT, HAST_ROOT, HastReader, } from "./hast-reader.js";
|
|
9
|
+
import { getNodeId, HastLazyChildResolver, readMatchedNode } from "./hast-walk.js";
|
|
10
|
+
import { LazyChildResolver, markHandleMutated } from "../lazy-child-resolver.js";
|
|
11
|
+
import { asArray, collectCommands, FOREIGN_REF, foreignContentError, makeRequireNid, requireRootReplacement, ROOT_NODE_ID, rootReplacementError, unencodableContentError, withMdxJsxAttribute, } from "../visitor-shared.js";
|
|
12
|
+
const requireNid = makeRequireNid(getNodeId);
|
|
60
13
|
const HAST_PARENT_TYPES = ["element", "mdxJsxFlowElement", "mdxJsxTextElement"];
|
|
61
14
|
const HAST_PARENT_TYPE_SET = new Set(HAST_PARENT_TYPES);
|
|
62
15
|
/** A leaf wrapper would make the patch engine drop or displace the wrapped node. */
|
|
@@ -71,11 +24,9 @@ function assertHastWrapParent(parentNode) {
|
|
|
71
24
|
function hastReusedId(node, refs) {
|
|
72
25
|
if (node === null || typeof node !== "object")
|
|
73
26
|
return undefined;
|
|
74
|
-
const id =
|
|
27
|
+
const id = getNodeId(node, refs);
|
|
75
28
|
return id !== undefined && id !== FOREIGN_REF ? id : undefined;
|
|
76
29
|
}
|
|
77
|
-
/** Emit a set-children command in place: a root-wrapped child list, the shape
|
|
78
|
-
* `Patch::SetChildren` splices in. Reused children become refs. */
|
|
79
30
|
function emitHastChildrenCommand(buffer, id, children, refs) {
|
|
80
31
|
if (!Array.isArray(children))
|
|
81
32
|
return false;
|
|
@@ -89,21 +40,61 @@ function emitHastChildrenCommand(buffer, id, children, refs) {
|
|
|
89
40
|
return true;
|
|
90
41
|
});
|
|
91
42
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* content is always a declarative node (no raw escape hatch), so it
|
|
95
|
-
* compiles or it's a hard error. */
|
|
96
|
-
function emitHastTree(buffer, op, id, node, refs) {
|
|
97
|
-
const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD[op], id, () => emitHastOp(buffer, node, true, refs));
|
|
43
|
+
function emitHastTree(buffer, op, id, node, refs, allowRootRef = false) {
|
|
44
|
+
const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD[op], id, () => emitHastOp(buffer, node, true, refs, false, allowRootRef));
|
|
98
45
|
if (!ok)
|
|
99
46
|
throw unencodableContentError(node);
|
|
100
47
|
}
|
|
101
|
-
/**
|
|
48
|
+
/** Replace `id` with several nodes in one command, root-wrapped so the engine
|
|
49
|
+
* splices the children in place of the node. */
|
|
50
|
+
function emitHastMultiReplace(buffer, id, nodes, refs) {
|
|
51
|
+
const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD.replace, id, () => {
|
|
52
|
+
buffer.open(HAST_ROOT);
|
|
53
|
+
for (const n of nodes)
|
|
54
|
+
if (!emitHastOp(buffer, n, false, refs))
|
|
55
|
+
return false;
|
|
56
|
+
buffer.close();
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
if (!ok)
|
|
60
|
+
throw unencodableContentError(nodes);
|
|
61
|
+
}
|
|
62
|
+
// Root replacement needs a separate encoder because per-node encoding rejects root payloads.
|
|
102
63
|
function emitHastRootReplace(buffer, root, refs) {
|
|
103
64
|
const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD.replace, ROOT_NODE_ID, () => emitHastRootOp(buffer, root, refs));
|
|
104
65
|
if (!ok)
|
|
105
66
|
throw unencodableContentError(root);
|
|
106
67
|
}
|
|
68
|
+
/** A document is encoded standalone, so no node can be a ref into a tree. */
|
|
69
|
+
const NO_REFS = new WeakMap();
|
|
70
|
+
const HAST_DOCTYPE = NAME_TO_TYPE.doctype;
|
|
71
|
+
/**
|
|
72
|
+
* Encode a standalone HAST tree as an op-stream document: one `root` wrapping
|
|
73
|
+
* `nodes`, which Rust replays into an arena of its own and serializes. The
|
|
74
|
+
* bytes are handed to `use` because they are a view into a pooled buffer,
|
|
75
|
+
* valid only until it is released.
|
|
76
|
+
*/
|
|
77
|
+
export function encodeHastDocument(nodes, use) {
|
|
78
|
+
const w = acquireCommandBuffer();
|
|
79
|
+
try {
|
|
80
|
+
w.open(HAST_ROOT);
|
|
81
|
+
for (const node of nodes) {
|
|
82
|
+
// Carries no fields, and patch content has nothing to attach it to.
|
|
83
|
+
if (node.type === "doctype" && HAST_DOCTYPE !== undefined) {
|
|
84
|
+
w.open(HAST_DOCTYPE);
|
|
85
|
+
w.close();
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!emitHastOp(w, node, false, NO_REFS, true))
|
|
89
|
+
throw unencodableContentError(node);
|
|
90
|
+
}
|
|
91
|
+
w.close();
|
|
92
|
+
return use(w.getBuffer());
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
releaseCommandBuffer(w);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
107
98
|
function emitHastRootOp(w, n, refs) {
|
|
108
99
|
w.open(HAST_ROOT);
|
|
109
100
|
if (n.data != null)
|
|
@@ -117,20 +108,23 @@ function emitHastRootOp(w, n, refs) {
|
|
|
117
108
|
w.close();
|
|
118
109
|
return true;
|
|
119
110
|
}
|
|
120
|
-
function emitHastOp(w, node, isRoot, refs) {
|
|
111
|
+
function emitHastOp(w, node, isRoot, refs, document = false, allowRootRef = false) {
|
|
121
112
|
if (node === null || typeof node !== "object")
|
|
122
113
|
return false;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
114
|
+
const id = getNodeId(node, refs);
|
|
115
|
+
if (id === FOREIGN_REF && !document)
|
|
116
|
+
throw foreignContentError();
|
|
117
|
+
if ((!isRoot || allowRootRef) && id !== undefined && id !== FOREIGN_REF) {
|
|
118
|
+
w.ref(id);
|
|
119
|
+
return true;
|
|
129
120
|
}
|
|
130
121
|
const n = node;
|
|
131
122
|
const type = HAST_OPSTREAM_TYPES[n.type];
|
|
132
123
|
if (type === undefined)
|
|
133
124
|
return false;
|
|
125
|
+
// Standalone HTML serialization does not need MDX support or JSON-serializable metadata.
|
|
126
|
+
if (document && n.type.startsWith("mdx"))
|
|
127
|
+
return true;
|
|
134
128
|
w.open(type);
|
|
135
129
|
if (type === HAST_ELEMENT) {
|
|
136
130
|
w.str(OF_TAGNAME, typeof n.tagName === "string" ? n.tagName : "div");
|
|
@@ -142,7 +136,6 @@ function emitHastOp(w, node, isRoot, refs) {
|
|
|
142
136
|
}
|
|
143
137
|
}
|
|
144
138
|
else if (type === HAST_MDX_JSX_ELEMENT || type === HAST_MDX_JSX_TEXT_ELEMENT) {
|
|
145
|
-
// Name falls back to tagName, matching `encode_hast_js_node_data`.
|
|
146
139
|
const name = typeof n.name === "string" ? n.name : typeof n.tagName === "string" ? n.tagName : "";
|
|
147
140
|
if (name !== "")
|
|
148
141
|
w.str(OF_NAME, name);
|
|
@@ -157,19 +150,17 @@ function emitHastOp(w, node, isRoot, refs) {
|
|
|
157
150
|
else {
|
|
158
151
|
w.str(OF_VALUE, typeof n.value === "string" ? n.value : "");
|
|
159
152
|
}
|
|
160
|
-
if (n.data != null)
|
|
153
|
+
if (!document && n.data != null)
|
|
161
154
|
w.data(n.data);
|
|
162
155
|
const children = n.children;
|
|
163
156
|
if (Array.isArray(children)) {
|
|
164
157
|
for (const c of children)
|
|
165
|
-
if (!emitHastOp(w, c, false, refs))
|
|
158
|
+
if (!emitHastOp(w, c, false, refs, document))
|
|
166
159
|
return false;
|
|
167
160
|
}
|
|
168
161
|
w.close();
|
|
169
162
|
return true;
|
|
170
163
|
}
|
|
171
|
-
/** Emit one element property, mirroring `encode_hast_js_node_data` exactly:
|
|
172
|
-
* bool/string/number/array → kind; null/object → skip. */
|
|
173
164
|
function emitHastProp(w, name, value) {
|
|
174
165
|
if (value === true)
|
|
175
166
|
w.prop(name, PROP_BOOL_TRUE, "");
|
|
@@ -177,23 +168,22 @@ function emitHastProp(w, name, value) {
|
|
|
177
168
|
w.prop(name, PROP_BOOL_FALSE, "");
|
|
178
169
|
else if (typeof value === "string")
|
|
179
170
|
w.prop(name, PROP_STRING, value);
|
|
180
|
-
|
|
181
|
-
|
|
171
|
+
// A NaN property is dropped, not rendered as `"NaN"`, matching hast.
|
|
172
|
+
else if (typeof value === "number") {
|
|
173
|
+
if (!Number.isNaN(value))
|
|
174
|
+
w.prop(name, PROP_INT, String(value));
|
|
175
|
+
}
|
|
182
176
|
else if (Array.isArray(value))
|
|
183
|
-
w.prop(name,
|
|
177
|
+
w.prop(name, PROP_TOKEN_LIST, encodeTokenList(value));
|
|
184
178
|
}
|
|
185
179
|
class HastVisitorContextImpl {
|
|
186
180
|
#commandBuffer = acquireCommandBuffer();
|
|
187
181
|
#diagnostics;
|
|
188
|
-
/** Track accumulated node state for multiple setProperty calls on the same node. */
|
|
189
182
|
#pendingNodes = new Map();
|
|
190
183
|
#handle;
|
|
191
184
|
#getSource;
|
|
192
185
|
#resolver;
|
|
193
186
|
#refs;
|
|
194
|
-
/** One canonical object per parent id, so visitors can dedupe by identity.
|
|
195
|
-
* Null until the first `parent()` call; most passes never make one. */
|
|
196
|
-
#parentsById = null;
|
|
197
187
|
fileURL;
|
|
198
188
|
data;
|
|
199
189
|
sourceFormat;
|
|
@@ -213,14 +203,32 @@ class HastVisitorContextImpl {
|
|
|
213
203
|
return value;
|
|
214
204
|
}
|
|
215
205
|
removeNode(node) {
|
|
216
|
-
|
|
206
|
+
const id = requireNid(node, "removeNode", this.#refs);
|
|
207
|
+
this.#commandBuffer.removeNode(id);
|
|
208
|
+
this.#pendingNodes.delete(id);
|
|
217
209
|
}
|
|
218
210
|
replaceNode(node, newNode) {
|
|
219
211
|
const id = requireNid(node, "replaceNode", this.#refs);
|
|
212
|
+
const onlyReplacement = Array.isArray(newNode)
|
|
213
|
+
? newNode.length === 1
|
|
214
|
+
? newNode[0]
|
|
215
|
+
: undefined
|
|
216
|
+
: newNode;
|
|
217
|
+
if (id === ROOT_NODE_ID &&
|
|
218
|
+
onlyReplacement !== undefined &&
|
|
219
|
+
hastReusedId(onlyReplacement, this.#refs) === id) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
220
222
|
if (Array.isArray(newNode)) {
|
|
221
223
|
if (id === ROOT_NODE_ID && newNode.length > 1)
|
|
222
224
|
throw rootReplacementError(newNode);
|
|
223
|
-
//
|
|
225
|
+
// One command, so the node's replacement is its whole slot and a ref back
|
|
226
|
+
// to it resolves to all of it rather than to the last element.
|
|
227
|
+
if (id !== ROOT_NODE_ID && newNode.length > 1) {
|
|
228
|
+
emitHastMultiReplace(this.#commandBuffer, id, newNode, this.#refs);
|
|
229
|
+
this.#pendingNodes.delete(id);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
224
232
|
let previous;
|
|
225
233
|
for (const n of newNode) {
|
|
226
234
|
if (previous !== undefined)
|
|
@@ -228,36 +236,37 @@ class HastVisitorContextImpl {
|
|
|
228
236
|
previous = n;
|
|
229
237
|
}
|
|
230
238
|
if (previous === undefined) {
|
|
231
|
-
// Replacing with nothing drops the node, like removeNode.
|
|
232
239
|
this.removeNode(node);
|
|
233
240
|
}
|
|
234
241
|
else if (id === ROOT_NODE_ID) {
|
|
235
242
|
emitHastRootReplace(this.#commandBuffer, requireRootReplacement(previous), this.#refs);
|
|
236
243
|
}
|
|
237
244
|
else {
|
|
238
|
-
emitHastTree(this.#commandBuffer, "replace", id, previous, this.#refs);
|
|
245
|
+
emitHastTree(this.#commandBuffer, "replace", id, previous, this.#refs, true);
|
|
239
246
|
}
|
|
240
|
-
|
|
241
|
-
|
|
247
|
+
if (previous === undefined)
|
|
248
|
+
this.#pendingNodes.delete(id);
|
|
249
|
+
else
|
|
250
|
+
this.#pendingNodes.set(id, previous);
|
|
242
251
|
return;
|
|
243
252
|
}
|
|
244
253
|
if (id === ROOT_NODE_ID) {
|
|
245
254
|
emitHastRootReplace(this.#commandBuffer, requireRootReplacement(newNode), this.#refs);
|
|
246
255
|
return;
|
|
247
256
|
}
|
|
248
|
-
emitHastTree(this.#commandBuffer, "replace", id, newNode, this.#refs);
|
|
249
|
-
// Track the replacement so a later mdxJsx setProperty can fold into it.
|
|
257
|
+
emitHastTree(this.#commandBuffer, "replace", id, newNode, this.#refs, true);
|
|
250
258
|
this.#pendingNodes.set(id, newNode);
|
|
251
259
|
}
|
|
260
|
+
#splice(anchorId, content, op) {
|
|
261
|
+
for (const n of asArray(content)) {
|
|
262
|
+
emitHastTree(this.#commandBuffer, op, anchorId, n, this.#refs, true);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
252
265
|
insertBefore(node, newNode) {
|
|
253
|
-
|
|
254
|
-
for (const n of asArray(newNode))
|
|
255
|
-
emitHastTree(this.#commandBuffer, "insertBefore", id, n, this.#refs);
|
|
266
|
+
this.#splice(requireNid(node, "insertBefore", this.#refs), newNode, "insertBefore");
|
|
256
267
|
}
|
|
257
268
|
insertAfter(node, newNode) {
|
|
258
|
-
|
|
259
|
-
for (const n of asArray(newNode))
|
|
260
|
-
emitHastTree(this.#commandBuffer, "insertAfter", id, n, this.#refs);
|
|
269
|
+
this.#splice(requireNid(node, "insertAfter", this.#refs), newNode, "insertAfter");
|
|
261
270
|
}
|
|
262
271
|
wrapNode(node, parentNode) {
|
|
263
272
|
const id = requireNid(node, "wrapNode", this.#refs);
|
|
@@ -270,26 +279,19 @@ class HastVisitorContextImpl {
|
|
|
270
279
|
emitHastTree(this.#commandBuffer, "wrapNode", id, parentNode, this.#refs);
|
|
271
280
|
}
|
|
272
281
|
prependChild(node, childNode) {
|
|
273
|
-
|
|
274
|
-
for (const n of asArray(childNode))
|
|
275
|
-
emitHastTree(this.#commandBuffer, "prependChild", id, n, this.#refs);
|
|
282
|
+
this.#splice(requireNid(node, "prependChild", this.#refs), childNode, "prependChild");
|
|
276
283
|
}
|
|
277
284
|
appendChild(node, childNode) {
|
|
278
|
-
|
|
279
|
-
for (const n of asArray(childNode))
|
|
280
|
-
emitHastTree(this.#commandBuffer, "appendChild", id, n, this.#refs);
|
|
285
|
+
this.#splice(requireNid(node, "appendChild", this.#refs), childNode, "appendChild");
|
|
281
286
|
}
|
|
282
287
|
insertChildAt(node, index, childNode) {
|
|
283
288
|
const children = "children" in node ? node.children : [];
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
else {
|
|
291
|
-
this.insertBefore(children[index], childNode);
|
|
292
|
-
}
|
|
289
|
+
const [anchor, op] = index <= 0 || children.length === 0
|
|
290
|
+
? [node, "prependChild"]
|
|
291
|
+
: index >= children.length
|
|
292
|
+
? [node, "appendChild"]
|
|
293
|
+
: [children[index], "insertBefore"];
|
|
294
|
+
this.#splice(requireNid(anchor, "insertChildAt", this.#refs), childNode, op);
|
|
293
295
|
}
|
|
294
296
|
removeChildAt(node, index) {
|
|
295
297
|
const child = "children" in node ? node.children[index] : undefined;
|
|
@@ -299,8 +301,6 @@ class HastVisitorContextImpl {
|
|
|
299
301
|
setProperty(node, key, value) {
|
|
300
302
|
const id = requireNid(node, "setProperty", this.#refs);
|
|
301
303
|
if (key === "children") {
|
|
302
|
-
// children is structural: set-children keeps the node and swaps only its
|
|
303
|
-
// child list (reused children keep their id).
|
|
304
304
|
if (!emitHastChildrenCommand(this.#commandBuffer, id, value, this.#refs)) {
|
|
305
305
|
throw unencodableContentError(value);
|
|
306
306
|
}
|
|
@@ -311,45 +311,67 @@ class HastVisitorContextImpl {
|
|
|
311
311
|
return;
|
|
312
312
|
}
|
|
313
313
|
if (node.type === "element") {
|
|
314
|
-
|
|
314
|
+
if (Array.isArray(value)) {
|
|
315
|
+
this.#commandBuffer.setTokenListProperty(id, key, encodeTokenList(value));
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
// NaN drops the attribute, as it does on a built element.
|
|
319
|
+
const dropped = typeof value === "number" && Number.isNaN(value);
|
|
320
|
+
this.#commandBuffer.setProperty(id, key, dropped ? null : value);
|
|
321
|
+
}
|
|
315
322
|
return;
|
|
316
323
|
}
|
|
317
324
|
if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
|
|
318
|
-
|
|
319
|
-
// already queued for this node, fold the attribute into it so the change
|
|
320
|
-
// survives the rebuild. This spreads the queued replacement object, not
|
|
321
|
-
// the matched node, so it never forces the matched node's children to
|
|
322
|
-
// materialize.
|
|
323
|
-
const pending = this.#pendingNodes.get(id);
|
|
324
|
-
if (pending !== undefined) {
|
|
325
|
-
const updated = { ...pending };
|
|
326
|
-
const attrs = [...(updated.attributes ?? [])];
|
|
327
|
-
const idx = attrs.findIndex((a) => a.type === "mdxJsxAttribute" && a.name === key);
|
|
328
|
-
if (idx !== -1)
|
|
329
|
-
attrs.splice(idx, 1);
|
|
330
|
-
// Arrays space-join, matching the binary path's PROP_SPACE_SEP encoding
|
|
331
|
-
// (hast convention for list-valued properties like className).
|
|
332
|
-
const attrValue = value === true || value === null || value === undefined
|
|
333
|
-
? null
|
|
334
|
-
: typeof value === "string"
|
|
335
|
-
? value
|
|
336
|
-
: Array.isArray(value)
|
|
337
|
-
? value.join(" ")
|
|
338
|
-
: String(value);
|
|
339
|
-
attrs.push({ type: "mdxJsxAttribute", name: key, value: attrValue });
|
|
340
|
-
updated.attributes = attrs;
|
|
341
|
-
this.replaceNode(node, updated);
|
|
325
|
+
if (this.#foldPendingJsxAttribute(node, id, key, value))
|
|
342
326
|
return;
|
|
327
|
+
}
|
|
328
|
+
this.#commandBuffer.setProperty(id, key, value);
|
|
329
|
+
}
|
|
330
|
+
setField(node, key, value) {
|
|
331
|
+
const id = requireNid(node, "setField", this.#refs);
|
|
332
|
+
if (this.#foldPendingField(node, id, key, value))
|
|
333
|
+
return;
|
|
334
|
+
if (key === "children") {
|
|
335
|
+
if (!emitHastChildrenCommand(this.#commandBuffer, id, value, this.#refs)) {
|
|
336
|
+
throw unencodableContentError(value);
|
|
343
337
|
}
|
|
344
|
-
// Binary attribute upsert in the arena's type_data — no child
|
|
345
|
-
// materialization. Rust maps the value-type to a boolean (true/null) or
|
|
346
|
-
// literal (string/number/false) attribute, mirroring the fold path above.
|
|
347
|
-
this.#commandBuffer.setProperty(id, key, value);
|
|
348
338
|
return;
|
|
349
339
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
340
|
+
if ((key === "attributes" &&
|
|
341
|
+
(node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement")) ||
|
|
342
|
+
(key === "properties" && node.type === "element")) {
|
|
343
|
+
// These are structured fields, so re-encode the node rather than a scalar command.
|
|
344
|
+
this.replaceNode(node, { ...node, [key]: value });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (key === "data") {
|
|
348
|
+
this.#commandBuffer.setField(id, key, value != null ? JSON.stringify(value) : null);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.#commandBuffer.setField(id, key, value);
|
|
352
|
+
}
|
|
353
|
+
setAttribute(node, name, value) {
|
|
354
|
+
const id = requireNid(node, "setAttribute", this.#refs);
|
|
355
|
+
if (this.#foldPendingJsxAttribute(node, id, name, value))
|
|
356
|
+
return;
|
|
357
|
+
this.#commandBuffer.setAttribute(id, name, value);
|
|
358
|
+
}
|
|
359
|
+
/** A queued replacement would discard the field, so fold it into a fresh replacement. */
|
|
360
|
+
#foldPendingField(node, id, key, value) {
|
|
361
|
+
const pending = this.#pendingNodes.get(id);
|
|
362
|
+
if (pending === undefined)
|
|
363
|
+
return false;
|
|
364
|
+
this.replaceNode(node, { ...pending, [key]: value });
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
/** A queued replacement would discard the attribute, so fold it in instead. */
|
|
368
|
+
#foldPendingJsxAttribute(node, id, name, value) {
|
|
369
|
+
const pending = this.#pendingNodes.get(id);
|
|
370
|
+
if (pending?.type !== "mdxJsxFlowElement" && pending?.type !== "mdxJsxTextElement") {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
this.replaceNode(node, withMdxJsxAttribute(pending, name, value));
|
|
374
|
+
return true;
|
|
353
375
|
}
|
|
354
376
|
textContent(node) {
|
|
355
377
|
return textContentHandle(this.#handle, requireNid(node, "textContent", this.#refs));
|
|
@@ -358,22 +380,17 @@ class HastVisitorContextImpl {
|
|
|
358
380
|
const parentId = this.#resolver.parentIdOf(requireNid(node, "parent", this.#refs));
|
|
359
381
|
if (parentId === undefined)
|
|
360
382
|
return undefined;
|
|
361
|
-
|
|
362
|
-
let parent = byId.get(parentId);
|
|
363
|
-
if (parent === undefined) {
|
|
364
|
-
parent = this.#resolver.materializeOne(parentId);
|
|
365
|
-
byId.set(parentId, parent);
|
|
366
|
-
}
|
|
367
|
-
return parent;
|
|
383
|
+
return this.#resolver.materializeOne(parentId);
|
|
368
384
|
}
|
|
369
385
|
indexOf(node) {
|
|
370
386
|
return this.#resolver.indexInParent(requireNid(node, "indexOf", this.#refs));
|
|
371
387
|
}
|
|
372
388
|
report({ message, node, severity = "error", }) {
|
|
373
|
-
const id = node ?
|
|
389
|
+
const id = node ? getNodeId(node, this.#refs) : undefined;
|
|
374
390
|
this.#diagnostics.push({
|
|
375
391
|
message,
|
|
376
392
|
nodeId: id === FOREIGN_REF ? undefined : id,
|
|
393
|
+
position: node?.position,
|
|
377
394
|
severity,
|
|
378
395
|
});
|
|
379
396
|
}
|
|
@@ -384,7 +401,6 @@ class HastVisitorContextImpl {
|
|
|
384
401
|
return this.#diagnostics;
|
|
385
402
|
}
|
|
386
403
|
}
|
|
387
|
-
/** Node types that use filtered visitors (have tag/component names). */
|
|
388
404
|
const FILTERED_METHODS = new Set(["element", "mdxJsxFlowElement", "mdxJsxTextElement"]);
|
|
389
405
|
const subscriptionCache = new WeakMap();
|
|
390
406
|
export function resolveSubscriptions(plugin) {
|
|
@@ -395,8 +411,6 @@ export function resolveSubscriptions(plugin) {
|
|
|
395
411
|
subscriptionCache.set(plugin, built);
|
|
396
412
|
return built.subs;
|
|
397
413
|
}
|
|
398
|
-
/** Get the (cached) Rust-side projection of `subs` that strips visitFn so it
|
|
399
|
-
* can cross NAPI. Computed once per plugin object alongside `subs`. */
|
|
400
414
|
function getRustSubs(plugin) {
|
|
401
415
|
const cached = subscriptionCache.get(plugin);
|
|
402
416
|
if (cached !== undefined)
|
|
@@ -411,7 +425,7 @@ function isFilteredVisitor(value) {
|
|
|
411
425
|
Array.isArray(value.filter) &&
|
|
412
426
|
typeof value.visit === "function");
|
|
413
427
|
}
|
|
414
|
-
|
|
428
|
+
// Validate before NAPI so errors name the public visitor contract.
|
|
415
429
|
function malformedFilteredVisitorError(plugin, methodName) {
|
|
416
430
|
const name = plugin.name;
|
|
417
431
|
const pluginName = typeof name === "string" && name !== "" ? name : "(unnamed)";
|
|
@@ -438,319 +452,14 @@ function buildSubscriptions(plugin) {
|
|
|
438
452
|
}
|
|
439
453
|
}
|
|
440
454
|
else {
|
|
441
|
-
// Bare function, empty filter matches all nodes of this type
|
|
442
455
|
subs.push({ nodeType, tagFilter: [], visitFn: value });
|
|
443
456
|
}
|
|
444
457
|
}
|
|
445
458
|
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: s.tagFilter }));
|
|
446
459
|
return { subs, rustSubs };
|
|
447
460
|
}
|
|
448
|
-
/** Visitor method name → node-type tag (method names are the subscribable AST names). */
|
|
449
461
|
const METHOD_TO_TYPE = Object.fromEntries([...VISITOR_KEYS].map((name) => [name, NAME_TO_TYPE[name]]));
|
|
450
|
-
|
|
451
|
-
const propCount = view.getUint16(pos, true);
|
|
452
|
-
pos += 2;
|
|
453
|
-
const properties = {};
|
|
454
|
-
for (let i = 0; i < propCount; i++) {
|
|
455
|
-
const nameLen = view.getUint16(pos, true);
|
|
456
|
-
pos += 2;
|
|
457
|
-
const name = rstr(buf, pos, nameLen);
|
|
458
|
-
pos += nameLen;
|
|
459
|
-
const kind = buf[pos];
|
|
460
|
-
pos += 1;
|
|
461
|
-
const valLen = view.getUint16(pos, true);
|
|
462
|
-
pos += 2;
|
|
463
|
-
const valStr = rstr(buf, pos, valLen);
|
|
464
|
-
pos += valLen;
|
|
465
|
-
properties[name] = decodeElementProp(kind, valStr);
|
|
466
|
-
}
|
|
467
|
-
return properties;
|
|
468
|
-
}
|
|
469
|
-
/** Build the child-stub list for a matched node from the wire's `[child_ids]
|
|
470
|
-
* [child_types]` blocks, no arena snapshot. Stale ids are caught at
|
|
471
|
-
* materialization: the resolver's epoch check refuses a snapshot once the
|
|
472
|
-
* arena has mutated or been dropped. */
|
|
473
|
-
function readChildStubs(view, buf, idsPos, typesPos, count, resolver) {
|
|
474
|
-
// With a hot snapshot a stub's deferral buys nothing; real nodes skip its per-field getters.
|
|
475
|
-
if (resolver.hasHotSnapshot()) {
|
|
476
|
-
const nodes = new Array(count);
|
|
477
|
-
for (let i = 0; i < count; i++) {
|
|
478
|
-
nodes[i] = resolver.materializeOne(view.getUint32(idsPos + i * 4, true));
|
|
479
|
-
}
|
|
480
|
-
return nodes;
|
|
481
|
-
}
|
|
482
|
-
const stubs = new Array(count);
|
|
483
|
-
for (let i = 0; i < count; i++) {
|
|
484
|
-
stubs[i] = new HastChildStub(resolver, view.getUint32(idsPos + i * 4, true), buf[typesPos + i]);
|
|
485
|
-
}
|
|
486
|
-
return stubs;
|
|
487
|
-
}
|
|
488
|
-
// Shared own-getter descriptors for WalkElement's lazy fields, populated in
|
|
489
|
-
// its static block so the getters can read the private wire fields.
|
|
490
|
-
let WALK_PROPS_DESC;
|
|
491
|
-
let WALK_CHILDREN_DESC;
|
|
492
|
-
/**
|
|
493
|
-
* Walk-path element. Spread-correctness requires `properties`/`children` as
|
|
494
|
-
* own enumerable keys (`{ ...node }` copies nothing else), but construction
|
|
495
|
-
* runs per matched element, so everything stays off the expensive paths:
|
|
496
|
-
* wire state in private fields (plain stores, invisible to spread — a WeakMap
|
|
497
|
-
* entry per element caused major-GC ephemeron stalls at this volume), shared
|
|
498
|
-
* getter functions instead of per-node closures, at most one define per lazy
|
|
499
|
-
* field, and `instanceof` gating identity so copies read as new content.
|
|
500
|
-
*/
|
|
501
|
-
class WalkElement {
|
|
502
|
-
type = "element";
|
|
503
|
-
tagName;
|
|
504
|
-
#nodeId;
|
|
505
|
-
#wire;
|
|
506
|
-
#propsPos;
|
|
507
|
-
#childIdsPos;
|
|
508
|
-
#childTypesPos;
|
|
509
|
-
#childCount;
|
|
510
|
-
constructor(tagName, nodeId, wire, propsPos, propCount, childIdsPos, childTypesPos, childCount) {
|
|
511
|
-
this.tagName = tagName;
|
|
512
|
-
this.#nodeId = nodeId;
|
|
513
|
-
this.#wire = wire;
|
|
514
|
-
this.#propsPos = propsPos;
|
|
515
|
-
this.#childIdsPos = childIdsPos;
|
|
516
|
-
this.#childTypesPos = childTypesPos;
|
|
517
|
-
this.#childCount = childCount;
|
|
518
|
-
if (propCount === 0) {
|
|
519
|
-
this.properties = {};
|
|
520
|
-
}
|
|
521
|
-
else {
|
|
522
|
-
Object.defineProperty(this, "properties", WALK_PROPS_DESC);
|
|
523
|
-
}
|
|
524
|
-
if (childCount === 0) {
|
|
525
|
-
this.children = [];
|
|
526
|
-
}
|
|
527
|
-
else {
|
|
528
|
-
Object.defineProperty(this, "children", WALK_CHILDREN_DESC);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
/** @internal */
|
|
532
|
-
get _nid() {
|
|
533
|
-
return this.#nodeId;
|
|
534
|
-
}
|
|
535
|
-
/** @internal */
|
|
536
|
-
get _refs() {
|
|
537
|
-
return this.#wire.resolver.refs;
|
|
538
|
-
}
|
|
539
|
-
static {
|
|
540
|
-
WALK_PROPS_DESC = {
|
|
541
|
-
enumerable: true,
|
|
542
|
-
configurable: true,
|
|
543
|
-
get() {
|
|
544
|
-
const w = this.#wire;
|
|
545
|
-
const val = decodeProperties(w.view, w.buf, this.#propsPos);
|
|
546
|
-
Object.defineProperty(this, "properties", {
|
|
547
|
-
value: val,
|
|
548
|
-
writable: true,
|
|
549
|
-
enumerable: true,
|
|
550
|
-
configurable: true,
|
|
551
|
-
});
|
|
552
|
-
return val;
|
|
553
|
-
},
|
|
554
|
-
};
|
|
555
|
-
WALK_CHILDREN_DESC = {
|
|
556
|
-
enumerable: true,
|
|
557
|
-
configurable: true,
|
|
558
|
-
get() {
|
|
559
|
-
const w = this.#wire;
|
|
560
|
-
const val = readChildStubs(w.view, w.buf, this.#childIdsPos, this.#childTypesPos, this.#childCount, w.resolver);
|
|
561
|
-
Object.defineProperty(this, "children", {
|
|
562
|
-
value: val,
|
|
563
|
-
writable: true,
|
|
564
|
-
enumerable: true,
|
|
565
|
-
configurable: true,
|
|
566
|
-
});
|
|
567
|
-
return val;
|
|
568
|
-
},
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
/** Read the tail of a matched element node (tag + properties).
|
|
573
|
-
* Common prelude (data/position/children) is already consumed by `readMatchedNode`. */
|
|
574
|
-
function readElementFromBinary(wire, offset, nodeId, position, childIdsPos, childTypesPos, childCount, data) {
|
|
575
|
-
let pos = offset;
|
|
576
|
-
// Eager: tagName (almost always accessed by visitors)
|
|
577
|
-
const tagLen = wire.view.getUint16(pos, true);
|
|
578
|
-
pos += 2;
|
|
579
|
-
const tagName = rstr(wire.buf, pos, tagLen);
|
|
580
|
-
pos += tagLen;
|
|
581
|
-
const propCount = wire.view.getUint16(pos, true);
|
|
582
|
-
const node = new WalkElement(tagName, nodeId, wire, pos, propCount, childIdsPos, childTypesPos, childCount);
|
|
583
|
-
if (position !== undefined)
|
|
584
|
-
node.position = position;
|
|
585
|
-
if (data !== null)
|
|
586
|
-
node.data = data;
|
|
587
|
-
return node;
|
|
588
|
-
}
|
|
589
|
-
/** Value-carrying types read by `readTextFromBinary` (tag → AST name). */
|
|
590
|
-
const TEXT_NODE_TYPES = Object.fromEntries(["text", "comment", "raw", "mdxFlowExpression", "mdxTextExpression", "mdxjsEsm"].map((name) => [NAME_TO_TYPE[name], name]));
|
|
591
|
-
function readTextFromBinary(view, buf, offset, nodeId, nodeType, position, data, refs) {
|
|
592
|
-
const valLen = view.getUint32(offset, true);
|
|
593
|
-
const rawValue = rstr(buf, offset + 4, valLen);
|
|
594
|
-
// MDX flow/text expressions store phantom-space sentinels; restore them so
|
|
595
|
-
// the value matches the reader path. ESM and plain text keep their value.
|
|
596
|
-
const value = nodeType === HAST_MDX_FLOW_EXPRESSION || nodeType === HAST_MDX_TEXT_EXPRESSION
|
|
597
|
-
? restorePhantomSpaces(rawValue)
|
|
598
|
-
: rawValue;
|
|
599
|
-
const base = { type: TEXT_NODE_TYPES[nodeType], value };
|
|
600
|
-
if (position !== undefined)
|
|
601
|
-
base.position = position;
|
|
602
|
-
if (data !== null)
|
|
603
|
-
base.data = data;
|
|
604
|
-
const node = base;
|
|
605
|
-
refs.set(node, nodeId);
|
|
606
|
-
if (nodeType === HAST_MDX_FLOW_EXPRESSION || nodeType === HAST_MDX_TEXT_EXPRESSION) {
|
|
607
|
-
attachParseExpression(node, napiParseExpression);
|
|
608
|
-
}
|
|
609
|
-
else if (nodeType === HAST_MDX_ESM) {
|
|
610
|
-
attachParseExpression(node, napiParseEsm);
|
|
611
|
-
}
|
|
612
|
-
return node;
|
|
613
|
-
}
|
|
614
|
-
function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver, position, childIdsPos, childTypesPos, childCount, data) {
|
|
615
|
-
let pos = offset;
|
|
616
|
-
const nameLen = view.getUint16(pos, true);
|
|
617
|
-
pos += 2;
|
|
618
|
-
const name = nameLen > 0 ? rstr(buf, pos, nameLen) : null;
|
|
619
|
-
pos += nameLen;
|
|
620
|
-
// Attributes: [kind: u8][nameLen: u16][name][valLen: u32][val]
|
|
621
|
-
const attrCount = view.getUint16(pos, true);
|
|
622
|
-
pos += 2;
|
|
623
|
-
const attributes = [];
|
|
624
|
-
for (let i = 0; i < attrCount; i++) {
|
|
625
|
-
const kind = buf[pos];
|
|
626
|
-
pos += 1;
|
|
627
|
-
const attrNameLen = view.getUint16(pos, true);
|
|
628
|
-
pos += 2;
|
|
629
|
-
const attrName = rstr(buf, pos, attrNameLen);
|
|
630
|
-
pos += attrNameLen;
|
|
631
|
-
const attrValLen = view.getUint32(pos, true);
|
|
632
|
-
pos += 4;
|
|
633
|
-
const attrVal = rstr(buf, pos, attrValLen);
|
|
634
|
-
pos += attrValLen;
|
|
635
|
-
attributes.push(decodeMdxJsxAttr(kind, attrName, attrVal));
|
|
636
|
-
}
|
|
637
|
-
const typeName = nodeType === HAST_MDX_JSX_ELEMENT ? "mdxJsxFlowElement" : "mdxJsxTextElement";
|
|
638
|
-
const base = { type: typeName, name, attributes };
|
|
639
|
-
if (position !== undefined)
|
|
640
|
-
base.position = position;
|
|
641
|
-
if (data !== null)
|
|
642
|
-
base.data = data;
|
|
643
|
-
resolver.refs.set(base, nodeId);
|
|
644
|
-
makeLazyChildren(base, view, buf, childIdsPos, childTypesPos, childCount, resolver);
|
|
645
|
-
return base;
|
|
646
|
-
}
|
|
647
|
-
function readMatchedNode(wire, offset, nodeId, nodeType) {
|
|
648
|
-
const { view, buf, resolver } = wire;
|
|
649
|
-
let pos = offset;
|
|
650
|
-
// Shared prelude (matches serialize_hast_node_inline / serialize_mdast_node_inline):
|
|
651
|
-
// [data_len: u32][data_bytes][position: 24B][child_count: u32][child_ids: N×u32][child_types: N×u8]
|
|
652
|
-
const dataLen = view.getUint32(pos, true);
|
|
653
|
-
pos += 4;
|
|
654
|
-
let data = null;
|
|
655
|
-
if (dataLen > 0) {
|
|
656
|
-
const jsonStr = rstr(buf, pos, dataLen);
|
|
657
|
-
try {
|
|
658
|
-
data = JSON.parse(jsonStr);
|
|
659
|
-
}
|
|
660
|
-
catch (err) {
|
|
661
|
-
if (process.env.NODE_ENV !== "production") {
|
|
662
|
-
console.warn(`readMatchedNode: malformed node_data for nodeId=${nodeId}`, err);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
pos += dataLen;
|
|
666
|
-
}
|
|
667
|
-
const position = readPosition(view, pos);
|
|
668
|
-
pos += 24;
|
|
669
|
-
const childCount = view.getUint32(pos, true);
|
|
670
|
-
pos += 4;
|
|
671
|
-
// Ids/types decode lazily with `.children` — most matched nodes never read them.
|
|
672
|
-
const childIdsPos = pos;
|
|
673
|
-
pos += childCount * 4;
|
|
674
|
-
const childTypesPos = pos;
|
|
675
|
-
pos += childCount;
|
|
676
|
-
// Dispatch to type-specific tail (pos now sits at the type-specific section)
|
|
677
|
-
if (nodeType === HAST_ELEMENT) {
|
|
678
|
-
return readElementFromBinary(wire, pos, nodeId, position, childIdsPos, childTypesPos, childCount, data);
|
|
679
|
-
}
|
|
680
|
-
else if (nodeType === HAST_TEXT ||
|
|
681
|
-
nodeType === HAST_COMMENT ||
|
|
682
|
-
nodeType === HAST_RAW ||
|
|
683
|
-
nodeType === HAST_MDX_FLOW_EXPRESSION ||
|
|
684
|
-
nodeType === HAST_MDX_TEXT_EXPRESSION ||
|
|
685
|
-
nodeType === HAST_MDX_ESM) {
|
|
686
|
-
return readTextFromBinary(view, buf, pos, nodeId, nodeType, position, data, resolver.refs);
|
|
687
|
-
}
|
|
688
|
-
else if (nodeType === HAST_MDX_JSX_ELEMENT || nodeType === HAST_MDX_JSX_TEXT_ELEMENT) {
|
|
689
|
-
return readMdxJsxFromBinary(view, buf, pos, nodeId, nodeType, resolver, position, childIdsPos, childTypesPos, childCount, data);
|
|
690
|
-
}
|
|
691
|
-
// Fallback: root and doctype.
|
|
692
|
-
const base = { type: TYPE_NAMES[nodeType] ?? `unknown(${nodeType})` };
|
|
693
|
-
if (position !== undefined)
|
|
694
|
-
base.position = position;
|
|
695
|
-
if (data !== null)
|
|
696
|
-
base.data = data;
|
|
697
|
-
if (nodeType === HAST_ROOT) {
|
|
698
|
-
// `...root.children` has to work in a hook.
|
|
699
|
-
if (childCount > 0) {
|
|
700
|
-
makeLazyChildren(base, view, buf, childIdsPos, childTypesPos, childCount, resolver);
|
|
701
|
-
}
|
|
702
|
-
else {
|
|
703
|
-
base.children = [];
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
const node = base;
|
|
707
|
-
resolver.refs.set(node, nodeId);
|
|
708
|
-
return node;
|
|
709
|
-
}
|
|
710
|
-
const HAST_EPOCH_CACHE = registerEpochCacheSlot(new WeakMap());
|
|
711
|
-
class HastLazyChildResolver extends LazyChildResolver {
|
|
712
|
-
cacheSlot() {
|
|
713
|
-
return HAST_EPOCH_CACHE;
|
|
714
|
-
}
|
|
715
|
-
createReader(wire) {
|
|
716
|
-
return new HastReader(wire);
|
|
717
|
-
}
|
|
718
|
-
materializeNode(reader, nodeId, refs) {
|
|
719
|
-
return materializeHastNode(reader, nodeId, true, refs);
|
|
720
|
-
}
|
|
721
|
-
readParentId(reader, nodeId) {
|
|
722
|
-
return reader.getParentId(nodeId);
|
|
723
|
-
}
|
|
724
|
-
readChildIds(reader, nodeId) {
|
|
725
|
-
return reader.getChildIds(nodeId);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
/** Install `children` as an own enumerable getter (spread must carry it),
|
|
729
|
-
* self-replacing with the one stable stub array on first read. One closure
|
|
730
|
-
* and one define per node — installing the wire locals as hidden slots
|
|
731
|
-
* instead measurably regressed every matching pipeline. */
|
|
732
|
-
function makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver) {
|
|
733
|
-
Object.defineProperty(node, "children", {
|
|
734
|
-
get() {
|
|
735
|
-
const val = readChildStubs(view, buf, childIdsPos, childTypesPos, childCount, resolver);
|
|
736
|
-
Object.defineProperty(this, "children", {
|
|
737
|
-
value: val,
|
|
738
|
-
writable: true,
|
|
739
|
-
enumerable: true,
|
|
740
|
-
configurable: true,
|
|
741
|
-
});
|
|
742
|
-
return val;
|
|
743
|
-
},
|
|
744
|
-
enumerable: true,
|
|
745
|
-
configurable: true,
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
/** A result that is the same object as the input node is a no-op, so context
|
|
749
|
-
* mutations (e.g. setProperty) are not clobbered.
|
|
750
|
-
*
|
|
751
|
-
* A same-type text-like node carrying only a new `value` becomes a
|
|
752
|
-
* setProperty("value") rather than a structural replace, which would force
|
|
753
|
-
* the arena into a full rebuild for a shape that didn't change. */
|
|
462
|
+
// Returning the input node must not overwrite mutations queued through the context.
|
|
754
463
|
function applyHastVisitResult(result, nodeId, returnBuffer, originalNode, refs) {
|
|
755
464
|
if (result == null)
|
|
756
465
|
return;
|
|
@@ -760,20 +469,9 @@ function applyHastVisitResult(result, nodeId, returnBuffer, originalNode, refs)
|
|
|
760
469
|
returnBuffer.setProperty(nodeId, "value", result.value);
|
|
761
470
|
return;
|
|
762
471
|
}
|
|
763
|
-
emitHastTree(returnBuffer, "replace", nodeId, result, refs);
|
|
472
|
+
emitHastTree(returnBuffer, "replace", nodeId, result, refs, true);
|
|
764
473
|
}
|
|
765
|
-
|
|
766
|
-
if (result instanceof Promise) {
|
|
767
|
-
const list = deferred ?? [];
|
|
768
|
-
list.push({ nodeId, promise: result, originalNode });
|
|
769
|
-
return list;
|
|
770
|
-
}
|
|
771
|
-
applyHastVisitResult(result, nodeId, returnBuffer, originalNode, refs);
|
|
772
|
-
return deferred;
|
|
773
|
-
}
|
|
774
|
-
/** True when `result` is a same-type text-like node carrying only `type` +
|
|
775
|
-
* `value`. The explicit `=== undefined` checks avoid the array alloc of
|
|
776
|
-
* `Object.keys().length` on this per-text-node hot path. */
|
|
474
|
+
// Explicit field checks avoid allocating Object.keys on the per-text-node hot path.
|
|
777
475
|
function isTextValueSwap(result, original) {
|
|
778
476
|
if (result.type !== original.type)
|
|
779
477
|
return false;
|
|
@@ -803,7 +501,13 @@ function dispatchMatches(matchBuf, subs, ctx, returnBuffer, resolver) {
|
|
|
803
501
|
const sub = subs[subIndex];
|
|
804
502
|
const node = readMatchedNode(wire, dataOffset, nodeId, sub.nodeType);
|
|
805
503
|
const result = sub.visitFn(node, ctx);
|
|
806
|
-
|
|
504
|
+
if (result instanceof Promise) {
|
|
505
|
+
deferred ??= [];
|
|
506
|
+
deferred.push({ nodeId, promise: result, originalNode: node });
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
applyHastVisitResult(result, nodeId, returnBuffer, node, resolver.refs);
|
|
510
|
+
}
|
|
807
511
|
}
|
|
808
512
|
return deferred;
|
|
809
513
|
}
|
|
@@ -822,20 +526,13 @@ export function visitHastHandle(handle, plugin, subs, source, fileURL, data = {}
|
|
|
822
526
|
}
|
|
823
527
|
return applyCollectedCommands(handle, result);
|
|
824
528
|
}
|
|
825
|
-
/** Apply commands collected by `visitHastHandleCollect`; returns the number of
|
|
826
|
-
* patches dropped as stranded (0 when none). */
|
|
827
529
|
function applyCollectedCommands(handle, commands) {
|
|
828
530
|
if (commands.length === 0)
|
|
829
531
|
return 0;
|
|
830
532
|
markHandleMutated(handle);
|
|
831
533
|
return applyCommandsToHandle(handle, commands);
|
|
832
534
|
}
|
|
833
|
-
|
|
834
|
-
* the merged commands so the caller can choose how to dispatch: either via
|
|
835
|
-
* `applyCommandsToHandle` (intermediate plugins in a chain) or via a fused
|
|
836
|
-
* NAPI call like `applyCommandsAndRenderHandle` (final plugin, saves one
|
|
837
|
-
* apply + one render + one drop crossing). Empty result means no mutations.
|
|
838
|
-
*/
|
|
535
|
+
// Collect the last plugin’s commands so the caller can fuse their apply with rendering or compilation.
|
|
839
536
|
export function visitHastHandleCollect(handle, plugin, subs, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
|
|
840
537
|
const getSource = typeof source === "function" ? source : () => source;
|
|
841
538
|
const resolver = new HastLazyChildResolver(handle);
|
|
@@ -844,20 +541,20 @@ export function visitHastHandleCollect(handle, plugin, subs, source, fileURL, da
|
|
|
844
541
|
const rustSubs = getRustSubs(plugin);
|
|
845
542
|
const deferred = dispatchMatches(walkHandle(handle, rustSubs), subs, ctx, returnBuffer, resolver);
|
|
846
543
|
if (deferred) {
|
|
847
|
-
return Promise.all(deferred.map((
|
|
848
|
-
for (
|
|
849
|
-
|
|
544
|
+
return Promise.all(deferred.map((visit) => visit.promise)).then((results) => {
|
|
545
|
+
for (let i = 0; i < deferred.length; i++) {
|
|
546
|
+
const { nodeId, originalNode } = deferred[i];
|
|
547
|
+
applyHastVisitResult(results[i], nodeId, returnBuffer, originalNode, resolver.refs);
|
|
850
548
|
}
|
|
851
|
-
return collectCommands(returnBuffer, ctx);
|
|
549
|
+
return collectCommands(returnBuffer, ctx.getCommandBuffer());
|
|
852
550
|
});
|
|
853
551
|
}
|
|
854
|
-
return collectCommands(returnBuffer, ctx);
|
|
552
|
+
return collectCommands(returnBuffer, ctx.getCommandBuffer());
|
|
855
553
|
}
|
|
856
554
|
const HAST_ROOT_SUBS = [
|
|
857
555
|
{ nodeType: HAST_ROOT, tagFilter: [] },
|
|
858
556
|
];
|
|
859
|
-
/**
|
|
860
|
-
* visitors walk, and the visitors' mutations before `after` reads the tree. */
|
|
557
|
+
/** Hooks run in separate passes so each sees mutations from the preceding pass. */
|
|
861
558
|
export function visitHastHookCollect(handle, plugin, hook, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
|
|
862
559
|
const getSource = typeof source === "function" ? source : () => source;
|
|
863
560
|
const resolver = new HastLazyChildResolver(handle);
|
|
@@ -866,13 +563,13 @@ export function visitHastHookCollect(handle, plugin, hook, source, fileURL, data
|
|
|
866
563
|
const matchBuf = walkHandle(handle, HAST_ROOT_SUBS);
|
|
867
564
|
const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
|
|
868
565
|
if (matchView.getUint32(0, true) === 0)
|
|
869
|
-
return collectCommands(returnBuffer, ctx);
|
|
566
|
+
return collectCommands(returnBuffer, ctx.getCommandBuffer());
|
|
870
567
|
const wire = { view: matchView, buf: matchBuf, resolver };
|
|
871
568
|
const root = readMatchedNode(wire, matchView.getUint32(10, true), matchView.getUint32(4, true), HAST_ROOT);
|
|
872
569
|
const result = hook.call(plugin, root, ctx);
|
|
873
570
|
if (result instanceof Promise)
|
|
874
|
-
return result.then(() => collectCommands(returnBuffer, ctx));
|
|
875
|
-
return collectCommands(returnBuffer, ctx);
|
|
571
|
+
return result.then(() => collectCommands(returnBuffer, ctx.getCommandBuffer()));
|
|
572
|
+
return collectCommands(returnBuffer, ctx.getCommandBuffer());
|
|
876
573
|
}
|
|
877
574
|
export function visitHastHook(handle, plugin, hook, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
|
|
878
575
|
const result = visitHastHookCollect(handle, plugin, hook, source, fileURL, data, sourceFormat, diagnostics);
|
|
@@ -881,11 +578,3 @@ export function visitHastHook(handle, plugin, hook, source, fileURL, data = {},
|
|
|
881
578
|
}
|
|
882
579
|
return applyCollectedCommands(handle, result);
|
|
883
580
|
}
|
|
884
|
-
function collectCommands(returnBuffer, ctx) {
|
|
885
|
-
const { merged } = mergeAndReset(returnBuffer, ctx);
|
|
886
|
-
// Return the buffers to the pool. The merged Uint8Array above already
|
|
887
|
-
// copied the bytes out, so the underlying ArrayBuffers can be reused.
|
|
888
|
-
releaseCommandBuffer(returnBuffer);
|
|
889
|
-
releaseCommandBuffer(ctx.getCommandBuffer());
|
|
890
|
-
return merged;
|
|
891
|
-
}
|