satteri 0.8.0 → 0.9.0
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 +35 -0
- package/dist/byte-writer.d.ts +31 -0
- package/dist/byte-writer.js +106 -0
- package/dist/child-stub.d.ts +17 -0
- package/dist/child-stub.js +57 -0
- package/dist/command-buffer.d.ts +23 -24
- package/dist/command-buffer.js +65 -128
- package/dist/compile.d.ts +5 -1
- package/dist/compile.js +40 -26
- package/dist/directive-types.d.ts +4 -2
- package/dist/generated/arena-layout.d.ts +37 -0
- package/dist/generated/arena-layout.js +41 -0
- package/dist/generated/wire-constants.d.ts +51 -0
- package/dist/generated/wire-constants.js +63 -0
- package/dist/handles.d.ts +21 -0
- package/dist/handles.js +1 -0
- package/dist/hast/child-stub.d.ts +18 -0
- package/dist/hast/child-stub.js +43 -0
- package/dist/hast/element-props.d.ts +2 -0
- package/dist/hast/element-props.js +23 -0
- package/dist/hast/generated/node-types.d.ts +14 -0
- package/dist/hast/generated/node-types.js +64 -0
- package/dist/hast/hast-materializer.d.ts +2 -1
- package/dist/hast/hast-materializer.js +9 -45
- package/dist/hast/hast-reader.d.ts +18 -23
- package/dist/hast/hast-reader.js +48 -115
- package/dist/hast/hast-visitor.d.ts +38 -12
- package/dist/hast/hast-visitor.js +417 -281
- package/dist/index.d.ts +10 -4
- package/dist/index.js +23 -1
- package/dist/lazy-child-resolver.d.ts +36 -0
- package/dist/lazy-child-resolver.js +95 -0
- package/dist/lazy-props.d.ts +1 -1
- package/dist/mdast/child-stub.d.ts +18 -0
- package/dist/mdast/child-stub.js +46 -0
- package/dist/mdast/column-align.d.ts +1 -0
- package/dist/mdast/column-align.js +6 -0
- package/dist/mdast/generated/layout.d.ts +19 -0
- package/dist/mdast/generated/layout.js +272 -0
- package/dist/mdast/generated/node-types.d.ts +58 -0
- package/dist/mdast/generated/node-types.js +266 -0
- package/dist/mdast/mdast-materializer.d.ts +3 -2
- package/dist/mdast/mdast-materializer.js +9 -93
- package/dist/mdast/mdast-reader.d.ts +4 -120
- package/dist/mdast/mdast-reader.js +45 -272
- package/dist/mdast/mdast-visitor.d.ts +55 -13
- package/dist/mdast/mdast-visitor.js +375 -410
- package/dist/mdx-attr.d.ts +2 -0
- package/dist/mdx-attr.js +20 -0
- package/dist/op-stream.d.ts +38 -0
- package/dist/op-stream.js +125 -0
- package/dist/phantom.d.ts +2 -0
- package/dist/phantom.js +13 -0
- package/dist/types.d.ts +34 -4
- package/dist/visitor-shared.d.ts +28 -0
- package/dist/visitor-shared.js +52 -0
- package/dist/wire-read.d.ts +27 -0
- package/dist/wire-read.js +59 -0
- package/index.js +52 -52
- package/package.json +6 -6
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { materializeHastNode } from "./hast-materializer.js";
|
|
2
2
|
import { HastReader, HAST_ELEMENT, HAST_TEXT, HAST_COMMENT, HAST_RAW, HAST_MDX_JSX_ELEMENT, HAST_MDX_JSX_TEXT_ELEMENT, HAST_MDX_FLOW_EXPRESSION, HAST_MDX_TEXT_EXPRESSION, HAST_MDX_ESM, } from "./hast-reader.js";
|
|
3
|
+
import { TYPE_NAMES, NAME_TO_TYPE, VISITOR_KEYS, HAST_OPSTREAM_TYPES, } from "./generated/node-types.js";
|
|
3
4
|
import { CommandBuffer } from "../command-buffer.js";
|
|
4
|
-
import {
|
|
5
|
+
import { OpWriter, OF_VALUE, OF_TAGNAME, OF_NAME, OF_EXPLICIT, PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_INT, emitMdxAttr, } from "../op-stream.js";
|
|
6
|
+
import { restorePhantomSpaces } from "../phantom.js";
|
|
7
|
+
import { decodeMdxJsxAttr } from "../mdx-attr.js";
|
|
8
|
+
import { decodeElementProp } from "./element-props.js";
|
|
9
|
+
import { readPosition, rstr } from "../wire-read.js";
|
|
10
|
+
import { walkHandle, applyCommandsToHandle, textContentHandle, parseExpression as napiParseExpression, parseEsm as napiParseEsm, } from "#binding";
|
|
11
|
+
import { asArray, makeRequireNid, mergeAndReset, unencodableContentError, } from "../visitor-shared.js";
|
|
12
|
+
import { LazyChildResolver, markHandleMutated } from "../lazy-child-resolver.js";
|
|
13
|
+
import { HastChildStub } from "./child-stub.js";
|
|
5
14
|
/** Maps HastNode objects to their arena node IDs without Object.defineProperty overhead. */
|
|
6
15
|
const nodeIdMap = new WeakMap();
|
|
7
16
|
/** Attach `parseExpression()` to an MDX expression or ESM node. */
|
|
@@ -22,41 +31,161 @@ function attachParseExpression(node, parseFn) {
|
|
|
22
31
|
});
|
|
23
32
|
}
|
|
24
33
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
34
|
+
* Arena identity of a node, rejecting impostors — the one place the
|
|
35
|
+
* spread/identity invariant is enforced. A spread copy of a matched node or
|
|
36
|
+
* stub must read as NEW content: trusting a copied id would splice the
|
|
37
|
+
* original in as a ref and drop the copy's edits. Walk elements carry their
|
|
38
|
+
* id in a private field behind `instanceof` (spread copies fail the check);
|
|
39
|
+
* other walk-built nodes are keyed in the WeakMap (invisible to spread);
|
|
40
|
+
* `HastChildStub`s (enumerable `_id`, but that key is ignored on plain
|
|
41
|
+
* objects) are recognized by `instanceof`. Plain objects are trusted only via
|
|
42
|
+
* the WeakMap or a NON-enumerable `_nodeId` (the materializers' convention,
|
|
43
|
+
* which spread cannot copy).
|
|
30
44
|
*/
|
|
31
|
-
function
|
|
32
|
-
|
|
45
|
+
function nid(node) {
|
|
46
|
+
if (node instanceof WalkElement)
|
|
47
|
+
return node._nid;
|
|
48
|
+
if (node instanceof HastChildStub)
|
|
49
|
+
return node._id;
|
|
50
|
+
const id = nodeIdMap.get(node);
|
|
51
|
+
if (id !== undefined)
|
|
52
|
+
return id;
|
|
53
|
+
const d = Object.getOwnPropertyDescriptor(node, "_nodeId");
|
|
54
|
+
return d !== undefined && !d.enumerable ? d.value : undefined;
|
|
55
|
+
}
|
|
56
|
+
const requireNid = makeRequireNid(nid);
|
|
57
|
+
function hastReusedId(node) {
|
|
58
|
+
if (node === null || typeof node !== "object")
|
|
59
|
+
return undefined;
|
|
60
|
+
const id = nid(node);
|
|
61
|
+
return typeof id === "number" ? id : undefined;
|
|
62
|
+
}
|
|
63
|
+
// Reused across replacements in a pass — see the note on `mdastWriter`.
|
|
64
|
+
const hastWriter = new OpWriter();
|
|
65
|
+
/** Compile a set-children payload: a root-wrapped child list, the shape
|
|
66
|
+
* `Patch::SetChildren` splices in. Reused children become refs. */
|
|
67
|
+
function compileHastChildrenToOpstream(children) {
|
|
68
|
+
if (!Array.isArray(children))
|
|
69
|
+
return null;
|
|
70
|
+
hastWriter.begin();
|
|
71
|
+
try {
|
|
72
|
+
hastWriter.open(NAME_TO_TYPE.root);
|
|
73
|
+
for (const c of children) {
|
|
74
|
+
if (!emitHastOp(hastWriter, c, false))
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
hastWriter.close();
|
|
78
|
+
return hastWriter.take();
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
hastWriter.end();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Encode `node` as the `op` structural command. HAST content is always a
|
|
85
|
+
* declarative node (no raw escape hatch), so it compiles to the op-stream or
|
|
86
|
+
* it's a hard error — the op-stream is the only structural encoding. The
|
|
87
|
+
* switch stays inline so the buffer calls are monomorphic (computed method
|
|
88
|
+
* names defeat inline caches on this warm path). */
|
|
89
|
+
function emitHastTree(buffer, op, id, node) {
|
|
90
|
+
const ops = compileHastToOpstream(node);
|
|
91
|
+
if (ops === null)
|
|
92
|
+
throw unencodableContentError(node);
|
|
93
|
+
switch (op) {
|
|
94
|
+
case "replace":
|
|
95
|
+
return buffer.replaceOpstream(id, ops);
|
|
96
|
+
case "insertBefore":
|
|
97
|
+
return buffer.insertBeforeOpstream(id, ops);
|
|
98
|
+
case "insertAfter":
|
|
99
|
+
return buffer.insertAfterOpstream(id, ops);
|
|
100
|
+
case "prependChild":
|
|
101
|
+
return buffer.prependChildOpstream(id, ops);
|
|
102
|
+
case "appendChild":
|
|
103
|
+
return buffer.appendChildOpstream(id, ops);
|
|
104
|
+
case "wrapNode":
|
|
105
|
+
return buffer.wrapNodeOpstream(id, ops);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Compile a declarative HAST replacement tree to the op-stream — the only
|
|
110
|
+
* structural encoding. Reused nodes become `ref`s (transparent passthrough).
|
|
111
|
+
* Returns null when the tree holds a type the replay can't reproduce
|
|
112
|
+
* identically; the caller throws.
|
|
113
|
+
*/
|
|
114
|
+
function compileHastToOpstream(root) {
|
|
115
|
+
hastWriter.begin();
|
|
116
|
+
try {
|
|
117
|
+
if (!emitHastOp(hastWriter, root, true))
|
|
118
|
+
return null;
|
|
119
|
+
return hastWriter.take();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
hastWriter.end();
|
|
123
|
+
}
|
|
33
124
|
}
|
|
34
|
-
function
|
|
125
|
+
function emitHastOp(w, node, isRoot) {
|
|
126
|
+
if (node === null || typeof node !== "object")
|
|
127
|
+
return false;
|
|
35
128
|
if (!isRoot) {
|
|
36
|
-
const id =
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
129
|
+
const id = hastReusedId(node);
|
|
130
|
+
if (id !== undefined) {
|
|
131
|
+
w.ref(id);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const n = node;
|
|
136
|
+
const type = HAST_OPSTREAM_TYPES[n.type];
|
|
137
|
+
if (type === undefined)
|
|
138
|
+
return false;
|
|
139
|
+
w.open(type);
|
|
140
|
+
if (type === HAST_ELEMENT) {
|
|
141
|
+
w.str(OF_TAGNAME, typeof n.tagName === "string" ? n.tagName : "div");
|
|
142
|
+
const props = n.properties;
|
|
143
|
+
if (props !== null && typeof props === "object") {
|
|
144
|
+
for (const key in props) {
|
|
145
|
+
emitHastProp(w, key, props[key]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (type === HAST_MDX_JSX_ELEMENT || type === HAST_MDX_JSX_TEXT_ELEMENT) {
|
|
150
|
+
// Name falls back to tagName, matching `encode_hast_js_node_data`.
|
|
151
|
+
const name = typeof n.name === "string" ? n.name : typeof n.tagName === "string" ? n.tagName : "";
|
|
152
|
+
if (name !== "")
|
|
153
|
+
w.str(OF_NAME, name);
|
|
154
|
+
if (Array.isArray(n.attributes)) {
|
|
155
|
+
for (const a of n.attributes)
|
|
156
|
+
emitMdxAttr(w, a);
|
|
157
|
+
}
|
|
158
|
+
if (n.data?._mdxExplicitJsx === true) {
|
|
159
|
+
w.bool(OF_EXPLICIT, true);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
w.str(OF_VALUE, typeof n.value === "string" ? n.value : "");
|
|
164
|
+
}
|
|
165
|
+
if (n.data != null)
|
|
166
|
+
w.data(n.data);
|
|
167
|
+
const children = n.children;
|
|
168
|
+
if (Array.isArray(children)) {
|
|
169
|
+
for (const c of children)
|
|
170
|
+
if (!emitHastOp(w, c, false))
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
w.close();
|
|
174
|
+
return true;
|
|
57
175
|
}
|
|
58
|
-
|
|
59
|
-
|
|
176
|
+
/** Emit one element property, mirroring `encode_hast_js_node_data` exactly:
|
|
177
|
+
* bool/string/number/array → kind; null/object → skip. */
|
|
178
|
+
function emitHastProp(w, name, value) {
|
|
179
|
+
if (value === true)
|
|
180
|
+
w.prop(name, PROP_BOOL_TRUE, "");
|
|
181
|
+
else if (value === false)
|
|
182
|
+
w.prop(name, PROP_BOOL_FALSE, "");
|
|
183
|
+
else if (typeof value === "string")
|
|
184
|
+
w.prop(name, PROP_STRING, value);
|
|
185
|
+
else if (typeof value === "number")
|
|
186
|
+
w.prop(name, PROP_INT, String(value));
|
|
187
|
+
else if (Array.isArray(value))
|
|
188
|
+
w.prop(name, PROP_SPACE_SEP, value.filter((v) => typeof v === "string").join(" "));
|
|
60
189
|
}
|
|
61
190
|
class HastVisitorContextImpl {
|
|
62
191
|
#commandBuffer = new CommandBuffer();
|
|
@@ -65,11 +194,18 @@ class HastVisitorContextImpl {
|
|
|
65
194
|
#pendingNodes = new Map();
|
|
66
195
|
#handle;
|
|
67
196
|
#getSource;
|
|
197
|
+
#resolver;
|
|
198
|
+
/** One canonical object per parent id, so visitors can dedupe by identity.
|
|
199
|
+
* Null until the first `parent()` call; most passes never make one. */
|
|
200
|
+
#parentsById = null;
|
|
68
201
|
fileURL;
|
|
69
|
-
|
|
202
|
+
data;
|
|
203
|
+
constructor(handle, getSource, fileURL, resolver, data) {
|
|
70
204
|
this.#handle = handle;
|
|
71
205
|
this.#getSource = getSource;
|
|
72
206
|
this.fileURL = fileURL;
|
|
207
|
+
this.#resolver = resolver;
|
|
208
|
+
this.data = data;
|
|
73
209
|
}
|
|
74
210
|
get source() {
|
|
75
211
|
const value = this.#getSource();
|
|
@@ -77,64 +213,128 @@ class HastVisitorContextImpl {
|
|
|
77
213
|
return value;
|
|
78
214
|
}
|
|
79
215
|
removeNode(node) {
|
|
80
|
-
this.#commandBuffer.removeNode(
|
|
216
|
+
this.#commandBuffer.removeNode(requireNid(node, "removeNode"));
|
|
81
217
|
}
|
|
82
218
|
replaceNode(node, newNode) {
|
|
83
|
-
const id =
|
|
84
|
-
this.#commandBuffer
|
|
219
|
+
const id = requireNid(node, "replaceNode");
|
|
220
|
+
emitHastTree(this.#commandBuffer, "replace", id, newNode);
|
|
221
|
+
// Track the replacement so a later mdxJsx setProperty can fold into it.
|
|
85
222
|
this.#pendingNodes.set(id, newNode);
|
|
86
223
|
}
|
|
87
224
|
insertBefore(node, newNode) {
|
|
88
|
-
|
|
225
|
+
const id = requireNid(node, "insertBefore");
|
|
226
|
+
for (const n of asArray(newNode))
|
|
227
|
+
emitHastTree(this.#commandBuffer, "insertBefore", id, n);
|
|
89
228
|
}
|
|
90
229
|
insertAfter(node, newNode) {
|
|
91
|
-
|
|
230
|
+
const id = requireNid(node, "insertAfter");
|
|
231
|
+
for (const n of asArray(newNode))
|
|
232
|
+
emitHastTree(this.#commandBuffer, "insertAfter", id, n);
|
|
92
233
|
}
|
|
93
234
|
wrapNode(node, parentNode) {
|
|
94
|
-
|
|
235
|
+
const id = requireNid(node, "wrapNode");
|
|
236
|
+
emitHastTree(this.#commandBuffer, "wrapNode", id, parentNode);
|
|
95
237
|
}
|
|
96
238
|
prependChild(node, childNode) {
|
|
97
|
-
|
|
239
|
+
const id = requireNid(node, "prependChild");
|
|
240
|
+
for (const n of asArray(childNode))
|
|
241
|
+
emitHastTree(this.#commandBuffer, "prependChild", id, n);
|
|
98
242
|
}
|
|
99
243
|
appendChild(node, childNode) {
|
|
100
|
-
|
|
244
|
+
const id = requireNid(node, "appendChild");
|
|
245
|
+
for (const n of asArray(childNode))
|
|
246
|
+
emitHastTree(this.#commandBuffer, "appendChild", id, n);
|
|
247
|
+
}
|
|
248
|
+
insertChildAt(node, index, childNode) {
|
|
249
|
+
const children = "children" in node ? node.children : [];
|
|
250
|
+
if (index <= 0 || children.length === 0) {
|
|
251
|
+
this.prependChild(node, childNode);
|
|
252
|
+
}
|
|
253
|
+
else if (index >= children.length) {
|
|
254
|
+
this.appendChild(node, childNode);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.insertBefore(children[index], childNode);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
removeChildAt(node, index) {
|
|
261
|
+
const child = "children" in node ? node.children[index] : undefined;
|
|
262
|
+
if (child)
|
|
263
|
+
this.removeNode(child);
|
|
101
264
|
}
|
|
102
265
|
setProperty(node, key, value) {
|
|
103
|
-
const id =
|
|
266
|
+
const id = requireNid(node, "setProperty");
|
|
267
|
+
if (key === "children") {
|
|
268
|
+
// children is structural: set-children keeps the node and swaps only its
|
|
269
|
+
// child list (reused children keep their id).
|
|
270
|
+
const ops = compileHastChildrenToOpstream(value);
|
|
271
|
+
if (!ops)
|
|
272
|
+
throw unencodableContentError(value);
|
|
273
|
+
this.#commandBuffer.setChildrenOpstream(id, ops);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
104
276
|
if (key === "data") {
|
|
105
277
|
this.#commandBuffer.setProperty(id, key, value != null ? JSON.stringify(value) : null);
|
|
106
278
|
return;
|
|
107
279
|
}
|
|
108
280
|
if (node.type === "element") {
|
|
109
|
-
// Fast binary path, no materialization, no JSON serialization
|
|
110
281
|
this.#commandBuffer.setProperty(id, key, value);
|
|
111
282
|
return;
|
|
112
283
|
}
|
|
113
284
|
if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
|
|
114
|
-
// MDX JSX nodes
|
|
285
|
+
// MDX JSX nodes carry `attributes`, not `properties`. If a replacement is
|
|
286
|
+
// already queued for this node, fold the attribute into it so the change
|
|
287
|
+
// survives the rebuild. This spreads the queued replacement object, not
|
|
288
|
+
// the matched node, so it never forces the matched node's children to
|
|
289
|
+
// materialize.
|
|
115
290
|
const pending = this.#pendingNodes.get(id);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
?
|
|
126
|
-
:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
291
|
+
if (pending !== undefined) {
|
|
292
|
+
const updated = { ...pending };
|
|
293
|
+
const attrs = [...(updated.attributes ?? [])];
|
|
294
|
+
const idx = attrs.findIndex((a) => a.type === "mdxJsxAttribute" && a.name === key);
|
|
295
|
+
if (idx !== -1)
|
|
296
|
+
attrs.splice(idx, 1);
|
|
297
|
+
// Arrays space-join, matching the binary path's PROP_SPACE_SEP encoding
|
|
298
|
+
// (hast convention for list-valued properties like className).
|
|
299
|
+
const attrValue = value === true || value === null || value === undefined
|
|
300
|
+
? null
|
|
301
|
+
: typeof value === "string"
|
|
302
|
+
? value
|
|
303
|
+
: Array.isArray(value)
|
|
304
|
+
? value.join(" ")
|
|
305
|
+
: String(value);
|
|
306
|
+
attrs.push({ type: "mdxJsxAttribute", name: key, value: attrValue });
|
|
307
|
+
updated.attributes = attrs;
|
|
308
|
+
this.replaceNode(node, updated);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// Binary attribute upsert in the arena's type_data — no child
|
|
312
|
+
// materialization. Rust maps the value-type to a boolean (true/null) or
|
|
313
|
+
// literal (string/number/false) attribute, mirroring the fold path above.
|
|
314
|
+
this.#commandBuffer.setProperty(id, key, value);
|
|
130
315
|
return;
|
|
131
316
|
}
|
|
132
|
-
// Text-like nodes (text, comment, raw, expressions, esm)
|
|
133
|
-
//
|
|
317
|
+
// Text-like nodes (text, comment, raw, expressions, esm): Rust handles
|
|
318
|
+
// `value` directly on these types.
|
|
134
319
|
this.#commandBuffer.setProperty(id, key, value);
|
|
135
320
|
}
|
|
136
321
|
textContent(node) {
|
|
137
|
-
return textContentHandle(this.#handle,
|
|
322
|
+
return textContentHandle(this.#handle, requireNid(node, "textContent"));
|
|
323
|
+
}
|
|
324
|
+
parent(node) {
|
|
325
|
+
const parentId = this.#resolver.parentIdOf(requireNid(node, "parent"));
|
|
326
|
+
if (parentId === undefined)
|
|
327
|
+
return undefined;
|
|
328
|
+
const byId = (this.#parentsById ??= new Map());
|
|
329
|
+
let parent = byId.get(parentId);
|
|
330
|
+
if (parent === undefined) {
|
|
331
|
+
parent = this.#resolver.materializeOne(parentId);
|
|
332
|
+
byId.set(parentId, parent);
|
|
333
|
+
}
|
|
334
|
+
return parent;
|
|
335
|
+
}
|
|
336
|
+
indexOf(node) {
|
|
337
|
+
return this.#resolver.indexInParent(requireNid(node, "indexOf"));
|
|
138
338
|
}
|
|
139
339
|
report({ message, node, severity = "error", }) {
|
|
140
340
|
this.#diagnostics.push({ message, nodeId: node ? nid(node) : undefined, severity });
|
|
@@ -146,12 +346,8 @@ class HastVisitorContextImpl {
|
|
|
146
346
|
return this.#diagnostics;
|
|
147
347
|
}
|
|
148
348
|
}
|
|
149
|
-
function isFilteredVisitor(v) {
|
|
150
|
-
return typeof v === "object" && v !== null && "filter" in v && "visit" in v;
|
|
151
|
-
}
|
|
152
349
|
/** Node types that use filtered visitors (have tag/component names). */
|
|
153
350
|
const FILTERED_METHODS = new Set(["element", "mdxJsxFlowElement", "mdxJsxTextElement"]);
|
|
154
|
-
/** Resolve subscriptions from a plugin instance. */
|
|
155
351
|
export function resolveSubscriptions(plugin) {
|
|
156
352
|
const subs = [];
|
|
157
353
|
for (const [methodName, nodeType] of Object.entries(METHOD_TO_TYPE)) {
|
|
@@ -175,24 +371,8 @@ export function resolveSubscriptions(plugin) {
|
|
|
175
371
|
}
|
|
176
372
|
return subs;
|
|
177
373
|
}
|
|
178
|
-
/**
|
|
179
|
-
const METHOD_TO_TYPE =
|
|
180
|
-
element: HAST_ELEMENT,
|
|
181
|
-
text: HAST_TEXT,
|
|
182
|
-
comment: HAST_COMMENT,
|
|
183
|
-
raw: HAST_RAW,
|
|
184
|
-
doctype: 4, // HAST_DOCTYPE
|
|
185
|
-
mdxJsxFlowElement: HAST_MDX_JSX_ELEMENT,
|
|
186
|
-
mdxJsxTextElement: HAST_MDX_JSX_TEXT_ELEMENT,
|
|
187
|
-
mdxFlowExpression: HAST_MDX_FLOW_EXPRESSION,
|
|
188
|
-
mdxTextExpression: HAST_MDX_TEXT_EXPRESSION,
|
|
189
|
-
mdxjsEsm: HAST_MDX_ESM,
|
|
190
|
-
};
|
|
191
|
-
/**
|
|
192
|
-
* Selective walk path: Rust walks the tree, only sends matched nodes to JS.
|
|
193
|
-
*/
|
|
194
|
-
const textDecoder = new TextDecoder("utf-8");
|
|
195
|
-
/** Decode properties from the walk buffer at the given position. */
|
|
374
|
+
/** Visitor method name → node-type tag (method names are the subscribable AST names). */
|
|
375
|
+
const METHOD_TO_TYPE = Object.fromEntries([...VISITOR_KEYS].map((name) => [name, NAME_TO_TYPE[name]]));
|
|
196
376
|
function decodeProperties(view, buf, pos) {
|
|
197
377
|
const propCount = view.getUint16(pos, true);
|
|
198
378
|
pos += 2;
|
|
@@ -200,121 +380,139 @@ function decodeProperties(view, buf, pos) {
|
|
|
200
380
|
for (let i = 0; i < propCount; i++) {
|
|
201
381
|
const nameLen = view.getUint16(pos, true);
|
|
202
382
|
pos += 2;
|
|
203
|
-
const name =
|
|
383
|
+
const name = rstr(buf, pos, nameLen);
|
|
204
384
|
pos += nameLen;
|
|
205
385
|
const kind = buf[pos];
|
|
206
386
|
pos += 1;
|
|
207
387
|
const valLen = view.getUint16(pos, true);
|
|
208
388
|
pos += 2;
|
|
209
|
-
const valStr =
|
|
389
|
+
const valStr = rstr(buf, pos, valLen);
|
|
210
390
|
pos += valLen;
|
|
211
|
-
|
|
212
|
-
case 0: // PROP_STRING
|
|
213
|
-
properties[name] = valStr;
|
|
214
|
-
break;
|
|
215
|
-
case 1: // PROP_BOOL_TRUE
|
|
216
|
-
properties[name] = true;
|
|
217
|
-
break;
|
|
218
|
-
case 2: // PROP_BOOL_FALSE
|
|
219
|
-
break;
|
|
220
|
-
case 3: // PROP_SPACE_SEP
|
|
221
|
-
properties[name] = valStr.split(" ").filter((s) => s.length > 0);
|
|
222
|
-
break;
|
|
223
|
-
case 5: // PROP_INT
|
|
224
|
-
properties[name] = Number(valStr);
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
391
|
+
properties[name] = decodeElementProp(kind, valStr);
|
|
227
392
|
}
|
|
228
393
|
return properties;
|
|
229
394
|
}
|
|
230
|
-
/**
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return
|
|
241
|
-
start: { offset: startOffset, line: startLine, column: startColumn },
|
|
242
|
-
end: { offset: endOffset, line: endLine, column: endColumn },
|
|
243
|
-
};
|
|
395
|
+
/** Build the child-stub list for a matched node from the wire's `[child_ids]
|
|
396
|
+
* [child_types]` blocks — no arena snapshot. The seal check still applies:
|
|
397
|
+
* post-pass ids are stale, and a stub built from them could later splice the
|
|
398
|
+
* wrong node as a ref. */
|
|
399
|
+
function readChildStubs(view, buf, idsPos, typesPos, count, resolver) {
|
|
400
|
+
resolver.assertUnsealed();
|
|
401
|
+
const stubs = new Array(count);
|
|
402
|
+
for (let i = 0; i < count; i++) {
|
|
403
|
+
stubs[i] = new HastChildStub(resolver, view.getUint32(idsPos + i * 4, true), buf[typesPos + i]);
|
|
404
|
+
}
|
|
405
|
+
return stubs;
|
|
244
406
|
}
|
|
407
|
+
// Shared own-getter descriptors for WalkElement's lazy fields, populated in
|
|
408
|
+
// its static block so the getters can read the private wire fields.
|
|
409
|
+
let WALK_PROPS_DESC;
|
|
410
|
+
let WALK_CHILDREN_DESC;
|
|
245
411
|
/**
|
|
246
|
-
* Walk-path element
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
412
|
+
* Walk-path element. Spread-correctness requires `properties`/`children` as
|
|
413
|
+
* own enumerable keys (`{ ...node }` copies nothing else), but construction
|
|
414
|
+
* runs per matched element, so everything stays off the expensive paths:
|
|
415
|
+
* wire state in private fields (plain stores, invisible to spread — a WeakMap
|
|
416
|
+
* entry per element caused major-GC ephemeron stalls at this volume), shared
|
|
417
|
+
* getter functions instead of per-node closures, at most one define per lazy
|
|
418
|
+
* field, and `instanceof` gating identity so copies read as new content.
|
|
252
419
|
*/
|
|
253
420
|
class WalkElement {
|
|
254
421
|
type = "element";
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
422
|
+
tagName;
|
|
423
|
+
#nodeId;
|
|
424
|
+
#view;
|
|
425
|
+
#buf;
|
|
426
|
+
#propsPos;
|
|
427
|
+
#childIdsPos;
|
|
428
|
+
#childTypesPos;
|
|
429
|
+
#childCount;
|
|
430
|
+
#resolver;
|
|
431
|
+
constructor(tagName, nodeId, view, buf, propsPos, propCount, childIdsPos, childTypesPos, childCount, resolver) {
|
|
432
|
+
this.tagName = tagName;
|
|
433
|
+
this.#nodeId = nodeId;
|
|
434
|
+
this.#view = view;
|
|
435
|
+
this.#buf = buf;
|
|
436
|
+
this.#propsPos = propsPos;
|
|
437
|
+
this.#childIdsPos = childIdsPos;
|
|
438
|
+
this.#childTypesPos = childTypesPos;
|
|
439
|
+
this.#childCount = childCount;
|
|
440
|
+
this.#resolver = resolver;
|
|
441
|
+
if (propCount === 0) {
|
|
442
|
+
this.properties = {};
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
Object.defineProperty(this, "properties", WALK_PROPS_DESC);
|
|
446
|
+
}
|
|
447
|
+
if (childCount === 0) {
|
|
448
|
+
this.children = [];
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
Object.defineProperty(this, "children", WALK_CHILDREN_DESC);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/** @internal */
|
|
455
|
+
get _nid() {
|
|
456
|
+
return this.#nodeId;
|
|
457
|
+
}
|
|
458
|
+
static {
|
|
459
|
+
WALK_PROPS_DESC = {
|
|
265
460
|
enumerable: true,
|
|
266
461
|
configurable: true,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
462
|
+
get() {
|
|
463
|
+
const val = decodeProperties(this.#view, this.#buf, this.#propsPos);
|
|
464
|
+
Object.defineProperty(this, "properties", {
|
|
465
|
+
value: val,
|
|
466
|
+
writable: true,
|
|
467
|
+
enumerable: true,
|
|
468
|
+
configurable: true,
|
|
469
|
+
});
|
|
470
|
+
return val;
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
WALK_CHILDREN_DESC = {
|
|
275
474
|
enumerable: true,
|
|
276
475
|
configurable: true,
|
|
277
|
-
|
|
278
|
-
|
|
476
|
+
get() {
|
|
477
|
+
const val = readChildStubs(this.#view, this.#buf, this.#childIdsPos, this.#childTypesPos, this.#childCount, this.#resolver);
|
|
478
|
+
Object.defineProperty(this, "children", {
|
|
479
|
+
value: val,
|
|
480
|
+
writable: true,
|
|
481
|
+
enumerable: true,
|
|
482
|
+
configurable: true,
|
|
483
|
+
});
|
|
484
|
+
return val;
|
|
485
|
+
},
|
|
486
|
+
};
|
|
279
487
|
}
|
|
280
488
|
}
|
|
281
489
|
/** Read the tail of a matched element node (tag + properties).
|
|
282
490
|
* Common prelude (data/position/children) is already consumed by `readMatchedNode`. */
|
|
283
|
-
function readElementFromBinary(view, buf, offset, nodeId, resolver, position,
|
|
491
|
+
function readElementFromBinary(view, buf, offset, nodeId, resolver, position, childIdsPos, childTypesPos, childCount, data) {
|
|
284
492
|
let pos = offset;
|
|
285
493
|
// Eager: tagName (almost always accessed by visitors)
|
|
286
494
|
const tagLen = view.getUint16(pos, true);
|
|
287
495
|
pos += 2;
|
|
288
|
-
const tagName =
|
|
496
|
+
const tagName = rstr(buf, pos, tagLen);
|
|
289
497
|
pos += tagLen;
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
const node = new WalkElement();
|
|
293
|
-
node.tagName = tagName;
|
|
498
|
+
const propCount = view.getUint16(pos, true);
|
|
499
|
+
const node = new WalkElement(tagName, nodeId, view, buf, pos, propCount, childIdsPos, childTypesPos, childCount, resolver);
|
|
294
500
|
if (position !== undefined)
|
|
295
501
|
node.position = position;
|
|
296
502
|
if (data !== null)
|
|
297
503
|
node.data = data;
|
|
298
|
-
node._view = view;
|
|
299
|
-
node._buf = buf;
|
|
300
|
-
node._propsPos = propsPos;
|
|
301
|
-
node._childIds = childIds;
|
|
302
|
-
node._resolver = resolver;
|
|
303
|
-
nodeIdMap.set(node, nodeId);
|
|
304
504
|
return node;
|
|
305
505
|
}
|
|
306
|
-
/**
|
|
307
|
-
const TEXT_NODE_TYPES =
|
|
308
|
-
2: "text",
|
|
309
|
-
3: "comment",
|
|
310
|
-
5: "raw",
|
|
311
|
-
[HAST_MDX_FLOW_EXPRESSION]: "mdxFlowExpression",
|
|
312
|
-
[HAST_MDX_TEXT_EXPRESSION]: "mdxTextExpression",
|
|
313
|
-
[HAST_MDX_ESM]: "mdxjsEsm",
|
|
314
|
-
};
|
|
506
|
+
/** Value-carrying types read by `readTextFromBinary` (tag → AST name). */
|
|
507
|
+
const TEXT_NODE_TYPES = Object.fromEntries(["text", "comment", "raw", "mdxFlowExpression", "mdxTextExpression", "mdxjsEsm"].map((name) => [NAME_TO_TYPE[name], name]));
|
|
315
508
|
function readTextFromBinary(view, buf, offset, nodeId, nodeType, position, data) {
|
|
316
509
|
const valLen = view.getUint32(offset, true);
|
|
317
|
-
const
|
|
510
|
+
const rawValue = rstr(buf, offset + 4, valLen);
|
|
511
|
+
// MDX flow/text expressions store phantom-space sentinels; restore them so
|
|
512
|
+
// the value matches the reader path. ESM and plain text keep their value.
|
|
513
|
+
const value = nodeType === HAST_MDX_FLOW_EXPRESSION || nodeType === HAST_MDX_TEXT_EXPRESSION
|
|
514
|
+
? restorePhantomSpaces(rawValue)
|
|
515
|
+
: rawValue;
|
|
318
516
|
const base = { type: TEXT_NODE_TYPES[nodeType], value };
|
|
319
517
|
if (position !== undefined)
|
|
320
518
|
base.position = position;
|
|
@@ -330,13 +528,11 @@ function readTextFromBinary(view, buf, offset, nodeId, nodeType, position, data)
|
|
|
330
528
|
}
|
|
331
529
|
return node;
|
|
332
530
|
}
|
|
333
|
-
|
|
334
|
-
function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver, position, childIds, data) {
|
|
531
|
+
function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver, position, childIdsPos, childTypesPos, childCount, data) {
|
|
335
532
|
let pos = offset;
|
|
336
|
-
// Name
|
|
337
533
|
const nameLen = view.getUint16(pos, true);
|
|
338
534
|
pos += 2;
|
|
339
|
-
const name = nameLen > 0 ?
|
|
535
|
+
const name = nameLen > 0 ? rstr(buf, pos, nameLen) : null;
|
|
340
536
|
pos += nameLen;
|
|
341
537
|
// Attributes: [kind: u8][nameLen: u16][name][valLen: u32][val]
|
|
342
538
|
const attrCount = view.getUint16(pos, true);
|
|
@@ -347,30 +543,13 @@ function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver, pos
|
|
|
347
543
|
pos += 1;
|
|
348
544
|
const attrNameLen = view.getUint16(pos, true);
|
|
349
545
|
pos += 2;
|
|
350
|
-
const attrName =
|
|
546
|
+
const attrName = rstr(buf, pos, attrNameLen);
|
|
351
547
|
pos += attrNameLen;
|
|
352
548
|
const attrValLen = view.getUint32(pos, true);
|
|
353
549
|
pos += 4;
|
|
354
|
-
const attrVal =
|
|
550
|
+
const attrVal = rstr(buf, pos, attrValLen);
|
|
355
551
|
pos += attrValLen;
|
|
356
|
-
|
|
357
|
-
case 0: // BooleanProp
|
|
358
|
-
attributes.push({ type: "mdxJsxAttribute", name: attrName, value: null });
|
|
359
|
-
break;
|
|
360
|
-
case 1: // LiteralProp
|
|
361
|
-
attributes.push({ type: "mdxJsxAttribute", name: attrName, value: attrVal });
|
|
362
|
-
break;
|
|
363
|
-
case 2: // ExpressionProp
|
|
364
|
-
attributes.push({
|
|
365
|
-
type: "mdxJsxAttribute",
|
|
366
|
-
name: attrName,
|
|
367
|
-
value: { type: "mdxJsxAttributeValueExpression", value: attrVal },
|
|
368
|
-
});
|
|
369
|
-
break;
|
|
370
|
-
case 3: // Spread
|
|
371
|
-
attributes.push({ type: "mdxJsxExpressionAttribute", value: attrVal });
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
552
|
+
attributes.push(decodeMdxJsxAttr(kind, attrName, attrVal));
|
|
374
553
|
}
|
|
375
554
|
const typeName = nodeType === HAST_MDX_JSX_ELEMENT ? "mdxJsxFlowElement" : "mdxJsxTextElement";
|
|
376
555
|
const base = { type: typeName, name, attributes };
|
|
@@ -379,39 +558,40 @@ function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver, pos
|
|
|
379
558
|
if (data !== null)
|
|
380
559
|
base.data = data;
|
|
381
560
|
nodeIdMap.set(base, nodeId);
|
|
382
|
-
makeLazyChildren(base,
|
|
561
|
+
makeLazyChildren(base, view, buf, childIdsPos, childTypesPos, childCount, resolver);
|
|
383
562
|
return base;
|
|
384
563
|
}
|
|
385
564
|
function readMatchedNode(view, buf, offset, nodeId, nodeType, resolver) {
|
|
386
565
|
let pos = offset;
|
|
387
566
|
// Shared prelude (matches serialize_hast_node_inline / serialize_mdast_node_inline):
|
|
388
|
-
// [data_len: u32][data_bytes][position: 24B][child_count:
|
|
389
|
-
// Data (JSON), eagerly parsed
|
|
567
|
+
// [data_len: u32][data_bytes][position: 24B][child_count: u32][child_ids: N×u32][child_types: N×u8]
|
|
390
568
|
const dataLen = view.getUint32(pos, true);
|
|
391
569
|
pos += 4;
|
|
392
570
|
let data = null;
|
|
393
571
|
if (dataLen > 0) {
|
|
394
|
-
const jsonStr =
|
|
572
|
+
const jsonStr = rstr(buf, pos, dataLen);
|
|
395
573
|
try {
|
|
396
574
|
data = JSON.parse(jsonStr);
|
|
397
575
|
}
|
|
398
|
-
catch {
|
|
399
|
-
|
|
576
|
+
catch (err) {
|
|
577
|
+
if (process.env.NODE_ENV !== "production") {
|
|
578
|
+
console.warn(`readMatchedNode: malformed node_data for nodeId=${nodeId}`, err);
|
|
579
|
+
}
|
|
400
580
|
}
|
|
401
581
|
pos += dataLen;
|
|
402
582
|
}
|
|
403
|
-
const position =
|
|
583
|
+
const position = readPosition(view, pos);
|
|
404
584
|
pos += 24;
|
|
405
|
-
const childCount = view.
|
|
406
|
-
pos +=
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
585
|
+
const childCount = view.getUint32(pos, true);
|
|
586
|
+
pos += 4;
|
|
587
|
+
// Ids/types decode lazily with `.children` — most matched nodes never read them.
|
|
588
|
+
const childIdsPos = pos;
|
|
589
|
+
pos += childCount * 4;
|
|
590
|
+
const childTypesPos = pos;
|
|
591
|
+
pos += childCount;
|
|
412
592
|
// Dispatch to type-specific tail (pos now sits at the type-specific section)
|
|
413
593
|
if (nodeType === HAST_ELEMENT) {
|
|
414
|
-
return readElementFromBinary(view, buf, pos, nodeId, resolver, position,
|
|
594
|
+
return readElementFromBinary(view, buf, pos, nodeId, resolver, position, childIdsPos, childTypesPos, childCount, data);
|
|
415
595
|
}
|
|
416
596
|
else if (nodeType === HAST_TEXT ||
|
|
417
597
|
nodeType === HAST_COMMENT ||
|
|
@@ -422,10 +602,10 @@ function readMatchedNode(view, buf, offset, nodeId, nodeType, resolver) {
|
|
|
422
602
|
return readTextFromBinary(view, buf, pos, nodeId, nodeType, position, data);
|
|
423
603
|
}
|
|
424
604
|
else if (nodeType === HAST_MDX_JSX_ELEMENT || nodeType === HAST_MDX_JSX_TEXT_ELEMENT) {
|
|
425
|
-
return readMdxJsxFromBinary(view, buf, pos, nodeId, nodeType, resolver, position,
|
|
605
|
+
return readMdxJsxFromBinary(view, buf, pos, nodeId, nodeType, resolver, position, childIdsPos, childTypesPos, childCount, data);
|
|
426
606
|
}
|
|
427
|
-
// Fallback: minimal node carrying whatever prelude data we found
|
|
428
|
-
const base = { type: `unknown(${nodeType})` };
|
|
607
|
+
// Fallback (e.g. doctype): minimal node carrying whatever prelude data we found
|
|
608
|
+
const base = { type: TYPE_NAMES[nodeType] ?? `unknown(${nodeType})` };
|
|
429
609
|
if (position !== undefined)
|
|
430
610
|
base.position = position;
|
|
431
611
|
if (data !== null)
|
|
@@ -434,71 +614,42 @@ function readMatchedNode(view, buf, offset, nodeId, nodeType, resolver) {
|
|
|
434
614
|
nodeIdMap.set(node, nodeId);
|
|
435
615
|
return node;
|
|
436
616
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
* child is accessed, then materializes children from it via HastReader.
|
|
441
|
-
*/
|
|
442
|
-
class LazyChildResolver {
|
|
443
|
-
#handle;
|
|
444
|
-
#reader = null;
|
|
445
|
-
constructor(handle) {
|
|
446
|
-
this.#handle = handle;
|
|
617
|
+
class HastLazyChildResolver extends LazyChildResolver {
|
|
618
|
+
createReader(wire) {
|
|
619
|
+
return new HastReader(wire);
|
|
447
620
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
this.#reader = new HastReader(serializeHandle(this.#handle));
|
|
451
|
-
}
|
|
452
|
-
return this.#reader;
|
|
453
|
-
}
|
|
454
|
-
materializeChildren(childIds) {
|
|
455
|
-
const reader = this.#ensure();
|
|
456
|
-
return childIds.map((id) => {
|
|
457
|
-
const node = materializeHastNode(reader, id);
|
|
458
|
-
this.attachLazyData(node, id);
|
|
459
|
-
return node;
|
|
460
|
-
});
|
|
621
|
+
materializeNode(reader, nodeId) {
|
|
622
|
+
return materializeHastNode(reader, nodeId);
|
|
461
623
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const json = napiGetNodeData(handle, nodeId);
|
|
468
|
-
const val = json ? JSON.parse(json) : null;
|
|
469
|
-
Object.defineProperty(this, "data", {
|
|
470
|
-
value: val,
|
|
471
|
-
writable: true,
|
|
472
|
-
enumerable: true,
|
|
473
|
-
configurable: true,
|
|
474
|
-
});
|
|
475
|
-
return val;
|
|
476
|
-
},
|
|
477
|
-
configurable: true,
|
|
478
|
-
enumerable: true,
|
|
479
|
-
});
|
|
624
|
+
readParentId(reader, nodeId) {
|
|
625
|
+
return reader.getParentId(nodeId);
|
|
626
|
+
}
|
|
627
|
+
readChildIds(reader, nodeId) {
|
|
628
|
+
return reader.getChildIds(nodeId);
|
|
480
629
|
}
|
|
481
630
|
}
|
|
482
|
-
/**
|
|
483
|
-
|
|
631
|
+
/** Install `children` as an own enumerable getter (spread must carry it),
|
|
632
|
+
* self-replacing with the one stable stub array on first read. One closure
|
|
633
|
+
* and one define per node — installing the wire locals as hidden slots
|
|
634
|
+
* instead measurably regressed every matching pipeline. */
|
|
635
|
+
function makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver) {
|
|
484
636
|
Object.defineProperty(node, "children", {
|
|
485
637
|
get() {
|
|
486
|
-
const
|
|
638
|
+
const val = readChildStubs(view, buf, childIdsPos, childTypesPos, childCount, resolver);
|
|
487
639
|
Object.defineProperty(this, "children", {
|
|
488
|
-
value:
|
|
640
|
+
value: val,
|
|
489
641
|
writable: true,
|
|
490
642
|
enumerable: true,
|
|
491
643
|
configurable: true,
|
|
492
644
|
});
|
|
493
|
-
return
|
|
645
|
+
return val;
|
|
494
646
|
},
|
|
495
|
-
configurable: true,
|
|
496
647
|
enumerable: true,
|
|
648
|
+
configurable: true,
|
|
497
649
|
});
|
|
498
650
|
}
|
|
499
|
-
/**
|
|
500
|
-
*
|
|
501
|
-
* so that context mutations (e.g. setProperty) are not clobbered. */
|
|
651
|
+
/** A result that is the same object as the input node is a no-op, so context
|
|
652
|
+
* mutations (e.g. setProperty) are not clobbered. */
|
|
502
653
|
function handleVisitResult(result, nodeId, returnBuffer, deferred, originalNode) {
|
|
503
654
|
if (result == null)
|
|
504
655
|
return deferred;
|
|
@@ -509,7 +660,7 @@ function handleVisitResult(result, nodeId, returnBuffer, deferred, originalNode)
|
|
|
509
660
|
list.push({ nodeId, promise: result, originalNode });
|
|
510
661
|
return list;
|
|
511
662
|
}
|
|
512
|
-
returnBuffer
|
|
663
|
+
emitHastTree(returnBuffer, "replace", nodeId, result);
|
|
513
664
|
return deferred;
|
|
514
665
|
}
|
|
515
666
|
/**
|
|
@@ -521,7 +672,7 @@ function dispatchMatches(matchBuf, subs, ctx, returnBuffer, resolver) {
|
|
|
521
672
|
const matchCount = matchView.getUint32(0, true);
|
|
522
673
|
let deferred = null;
|
|
523
674
|
for (let i = 0; i < matchCount; i++) {
|
|
524
|
-
const indexBase = 4 + i *
|
|
675
|
+
const indexBase = 4 + i * 10;
|
|
525
676
|
const nodeId = matchView.getUint32(indexBase, true);
|
|
526
677
|
const subIndex = matchBuf[indexBase + 4];
|
|
527
678
|
const dataOffset = matchView.getUint32(indexBase + 6, true);
|
|
@@ -532,26 +683,6 @@ function dispatchMatches(matchBuf, subs, ctx, returnBuffer, resolver) {
|
|
|
532
683
|
}
|
|
533
684
|
return deferred;
|
|
534
685
|
}
|
|
535
|
-
/** Merge return-value + context command buffers and release internals. */
|
|
536
|
-
function mergeAndReset(returnBuffer, ctx) {
|
|
537
|
-
const ctxCmdBuf = ctx.getCommandBuffer();
|
|
538
|
-
const ctxBuf = ctxCmdBuf.getBuffer();
|
|
539
|
-
const retBuf = returnBuffer.getBuffer();
|
|
540
|
-
const totalLen = retBuf.length + ctxBuf.length;
|
|
541
|
-
let merged;
|
|
542
|
-
if (totalLen === 0) {
|
|
543
|
-
merged = new Uint8Array(0);
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
merged = new Uint8Array(totalLen);
|
|
547
|
-
merged.set(retBuf, 0);
|
|
548
|
-
merged.set(ctxBuf, retBuf.length);
|
|
549
|
-
}
|
|
550
|
-
returnBuffer.reset();
|
|
551
|
-
ctxCmdBuf.reset();
|
|
552
|
-
return { merged, hasMutations: totalLen > 0 };
|
|
553
|
-
}
|
|
554
|
-
// Handle-based visitor
|
|
555
686
|
/**
|
|
556
687
|
* Walk a handle's arena in Rust, dispatch matched nodes to JS visitor functions,
|
|
557
688
|
* and apply mutations back to the handle. No arena buffers cross NAPI.
|
|
@@ -560,29 +691,34 @@ function mergeAndReset(returnBuffer, ctx) {
|
|
|
560
691
|
* replaced earlier in the same pass (the caller warns when non-zero), or a
|
|
561
692
|
* Promise of that count if any visitor is async.
|
|
562
693
|
*/
|
|
563
|
-
export function visitHastHandle(handle, plugin, subs, source, fileURL) {
|
|
694
|
+
export function visitHastHandle(handle, plugin, subs, source, fileURL, data = {}) {
|
|
564
695
|
const getSource = typeof source === "function" ? source : () => source;
|
|
565
|
-
const
|
|
696
|
+
const resolver = new HastLazyChildResolver(handle);
|
|
697
|
+
const ctx = new HastVisitorContextImpl(handle, getSource, fileURL, resolver, data);
|
|
566
698
|
const returnBuffer = new CommandBuffer();
|
|
567
|
-
const resolver = new LazyChildResolver(handle);
|
|
568
699
|
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: s.tagFilter }));
|
|
569
700
|
const deferred = dispatchMatches(walkHandle(handle, rustSubs), subs, ctx, returnBuffer, resolver);
|
|
570
701
|
if (deferred) {
|
|
571
702
|
return Promise.all(deferred.map((d) => d.promise.then((result) => ({ nodeId: d.nodeId, result, originalNode: d.originalNode })))).then((results) => {
|
|
572
703
|
for (const { nodeId, result, originalNode } of results) {
|
|
573
704
|
if (result != null && result !== originalNode) {
|
|
574
|
-
returnBuffer
|
|
705
|
+
emitHastTree(returnBuffer, "replace", nodeId, result);
|
|
575
706
|
}
|
|
576
707
|
}
|
|
708
|
+
// Mutations land next, renumbering the arena: snapshots taken after
|
|
709
|
+
// this point would resolve match-time child ids against wrong nodes.
|
|
710
|
+
resolver.seal();
|
|
577
711
|
return applyMutations(handle, returnBuffer, ctx);
|
|
578
712
|
});
|
|
579
713
|
}
|
|
714
|
+
resolver.seal();
|
|
580
715
|
return applyMutations(handle, returnBuffer, ctx);
|
|
581
716
|
}
|
|
582
717
|
/** Returns the number of patches dropped as stranded (0 when none). */
|
|
583
718
|
function applyMutations(handle, returnBuffer, ctx) {
|
|
584
719
|
const { merged, hasMutations } = mergeAndReset(returnBuffer, ctx);
|
|
585
720
|
if (hasMutations) {
|
|
721
|
+
markHandleMutated(handle);
|
|
586
722
|
return applyCommandsToHandle(handle, merged);
|
|
587
723
|
}
|
|
588
724
|
return 0;
|