satteri 0.8.1 → 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 -26
- package/dist/command-buffer.js +64 -132
- 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 +35 -13
- package/dist/hast/hast-visitor.js +395 -302
- 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 +52 -14
- package/dist/mdast/mdast-visitor.js +348 -427
- 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,44 +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;
|
|
33
55
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const obj = { _hast: true, type: node.type };
|
|
41
|
-
if ("tagName" in node)
|
|
42
|
-
obj.tagName = node.tagName;
|
|
43
|
-
if ("properties" in node)
|
|
44
|
-
obj.properties = node.properties;
|
|
45
|
-
if ("value" in node)
|
|
46
|
-
obj.value = node.value;
|
|
47
|
-
if ("name" in node)
|
|
48
|
-
obj.name = node.name;
|
|
49
|
-
if ("attributes" in node)
|
|
50
|
-
obj.attributes = node.attributes;
|
|
51
|
-
if ("data" in node && node.data != null)
|
|
52
|
-
obj.data = node.data;
|
|
53
|
-
if ("children" in node) {
|
|
54
|
-
obj.children = node.children.map((c) => markHastNode(c, false));
|
|
55
|
-
}
|
|
56
|
-
return obj;
|
|
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;
|
|
57
62
|
}
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function emitHastOp(w, node, isRoot) {
|
|
126
|
+
if (node === null || typeof node !== "object")
|
|
127
|
+
return false;
|
|
128
|
+
if (!isRoot) {
|
|
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;
|
|
60
175
|
}
|
|
61
|
-
|
|
62
|
-
|
|
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(" "));
|
|
63
189
|
}
|
|
64
190
|
class HastVisitorContextImpl {
|
|
65
191
|
#commandBuffer = new CommandBuffer();
|
|
@@ -68,11 +194,18 @@ class HastVisitorContextImpl {
|
|
|
68
194
|
#pendingNodes = new Map();
|
|
69
195
|
#handle;
|
|
70
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;
|
|
71
201
|
fileURL;
|
|
72
|
-
|
|
202
|
+
data;
|
|
203
|
+
constructor(handle, getSource, fileURL, resolver, data) {
|
|
73
204
|
this.#handle = handle;
|
|
74
205
|
this.#getSource = getSource;
|
|
75
206
|
this.fileURL = fileURL;
|
|
207
|
+
this.#resolver = resolver;
|
|
208
|
+
this.data = data;
|
|
76
209
|
}
|
|
77
210
|
get source() {
|
|
78
211
|
const value = this.#getSource();
|
|
@@ -80,39 +213,37 @@ class HastVisitorContextImpl {
|
|
|
80
213
|
return value;
|
|
81
214
|
}
|
|
82
215
|
removeNode(node) {
|
|
83
|
-
this.#commandBuffer.removeNode(
|
|
216
|
+
this.#commandBuffer.removeNode(requireNid(node, "removeNode"));
|
|
84
217
|
}
|
|
85
218
|
replaceNode(node, newNode) {
|
|
86
|
-
const id =
|
|
87
|
-
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.
|
|
88
222
|
this.#pendingNodes.set(id, newNode);
|
|
89
223
|
}
|
|
90
224
|
insertBefore(node, newNode) {
|
|
91
|
-
const id =
|
|
92
|
-
for (const n of asArray(newNode))
|
|
93
|
-
this.#commandBuffer
|
|
94
|
-
}
|
|
225
|
+
const id = requireNid(node, "insertBefore");
|
|
226
|
+
for (const n of asArray(newNode))
|
|
227
|
+
emitHastTree(this.#commandBuffer, "insertBefore", id, n);
|
|
95
228
|
}
|
|
96
229
|
insertAfter(node, newNode) {
|
|
97
|
-
const id =
|
|
98
|
-
for (const n of asArray(newNode))
|
|
99
|
-
this.#commandBuffer
|
|
100
|
-
}
|
|
230
|
+
const id = requireNid(node, "insertAfter");
|
|
231
|
+
for (const n of asArray(newNode))
|
|
232
|
+
emitHastTree(this.#commandBuffer, "insertAfter", id, n);
|
|
101
233
|
}
|
|
102
234
|
wrapNode(node, parentNode) {
|
|
103
|
-
|
|
235
|
+
const id = requireNid(node, "wrapNode");
|
|
236
|
+
emitHastTree(this.#commandBuffer, "wrapNode", id, parentNode);
|
|
104
237
|
}
|
|
105
238
|
prependChild(node, childNode) {
|
|
106
|
-
const id =
|
|
107
|
-
for (const n of asArray(childNode))
|
|
108
|
-
this.#commandBuffer
|
|
109
|
-
}
|
|
239
|
+
const id = requireNid(node, "prependChild");
|
|
240
|
+
for (const n of asArray(childNode))
|
|
241
|
+
emitHastTree(this.#commandBuffer, "prependChild", id, n);
|
|
110
242
|
}
|
|
111
243
|
appendChild(node, childNode) {
|
|
112
|
-
const id =
|
|
113
|
-
for (const n of asArray(childNode))
|
|
114
|
-
this.#commandBuffer
|
|
115
|
-
}
|
|
244
|
+
const id = requireNid(node, "appendChild");
|
|
245
|
+
for (const n of asArray(childNode))
|
|
246
|
+
emitHastTree(this.#commandBuffer, "appendChild", id, n);
|
|
116
247
|
}
|
|
117
248
|
insertChildAt(node, index, childNode) {
|
|
118
249
|
const children = "children" in node ? node.children : [];
|
|
@@ -132,16 +263,14 @@ class HastVisitorContextImpl {
|
|
|
132
263
|
this.removeNode(child);
|
|
133
264
|
}
|
|
134
265
|
setProperty(node, key, value) {
|
|
135
|
-
const id =
|
|
266
|
+
const id = requireNid(node, "setProperty");
|
|
136
267
|
if (key === "children") {
|
|
137
268
|
// children is structural: set-children keeps the node and swaps only its
|
|
138
269
|
// child list (reused children keep their id).
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
};
|
|
144
|
-
this.#commandBuffer.setChildren(id, JSON.stringify(wrapper));
|
|
270
|
+
const ops = compileHastChildrenToOpstream(value);
|
|
271
|
+
if (!ops)
|
|
272
|
+
throw unencodableContentError(value);
|
|
273
|
+
this.#commandBuffer.setChildrenOpstream(id, ops);
|
|
145
274
|
return;
|
|
146
275
|
}
|
|
147
276
|
if (key === "data") {
|
|
@@ -149,35 +278,63 @@ class HastVisitorContextImpl {
|
|
|
149
278
|
return;
|
|
150
279
|
}
|
|
151
280
|
if (node.type === "element") {
|
|
152
|
-
// Fast binary path, no materialization, no JSON serialization
|
|
153
281
|
this.#commandBuffer.setProperty(id, key, value);
|
|
154
282
|
return;
|
|
155
283
|
}
|
|
156
284
|
if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
|
|
157
|
-
// 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.
|
|
158
290
|
const pending = this.#pendingNodes.get(id);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
?
|
|
169
|
-
:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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);
|
|
173
315
|
return;
|
|
174
316
|
}
|
|
175
|
-
// Text-like nodes (text, comment, raw, expressions, esm)
|
|
176
|
-
//
|
|
317
|
+
// Text-like nodes (text, comment, raw, expressions, esm): Rust handles
|
|
318
|
+
// `value` directly on these types.
|
|
177
319
|
this.#commandBuffer.setProperty(id, key, value);
|
|
178
320
|
}
|
|
179
321
|
textContent(node) {
|
|
180
|
-
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"));
|
|
181
338
|
}
|
|
182
339
|
report({ message, node, severity = "error", }) {
|
|
183
340
|
this.#diagnostics.push({ message, nodeId: node ? nid(node) : undefined, severity });
|
|
@@ -189,12 +346,8 @@ class HastVisitorContextImpl {
|
|
|
189
346
|
return this.#diagnostics;
|
|
190
347
|
}
|
|
191
348
|
}
|
|
192
|
-
function isFilteredVisitor(v) {
|
|
193
|
-
return typeof v === "object" && v !== null && "filter" in v && "visit" in v;
|
|
194
|
-
}
|
|
195
349
|
/** Node types that use filtered visitors (have tag/component names). */
|
|
196
350
|
const FILTERED_METHODS = new Set(["element", "mdxJsxFlowElement", "mdxJsxTextElement"]);
|
|
197
|
-
/** Resolve subscriptions from a plugin instance. */
|
|
198
351
|
export function resolveSubscriptions(plugin) {
|
|
199
352
|
const subs = [];
|
|
200
353
|
for (const [methodName, nodeType] of Object.entries(METHOD_TO_TYPE)) {
|
|
@@ -218,24 +371,8 @@ export function resolveSubscriptions(plugin) {
|
|
|
218
371
|
}
|
|
219
372
|
return subs;
|
|
220
373
|
}
|
|
221
|
-
/**
|
|
222
|
-
const METHOD_TO_TYPE =
|
|
223
|
-
element: HAST_ELEMENT,
|
|
224
|
-
text: HAST_TEXT,
|
|
225
|
-
comment: HAST_COMMENT,
|
|
226
|
-
raw: HAST_RAW,
|
|
227
|
-
doctype: 4, // HAST_DOCTYPE
|
|
228
|
-
mdxJsxFlowElement: HAST_MDX_JSX_ELEMENT,
|
|
229
|
-
mdxJsxTextElement: HAST_MDX_JSX_TEXT_ELEMENT,
|
|
230
|
-
mdxFlowExpression: HAST_MDX_FLOW_EXPRESSION,
|
|
231
|
-
mdxTextExpression: HAST_MDX_TEXT_EXPRESSION,
|
|
232
|
-
mdxjsEsm: HAST_MDX_ESM,
|
|
233
|
-
};
|
|
234
|
-
/**
|
|
235
|
-
* Selective walk path: Rust walks the tree, only sends matched nodes to JS.
|
|
236
|
-
*/
|
|
237
|
-
const textDecoder = new TextDecoder("utf-8");
|
|
238
|
-
/** 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]]));
|
|
239
376
|
function decodeProperties(view, buf, pos) {
|
|
240
377
|
const propCount = view.getUint16(pos, true);
|
|
241
378
|
pos += 2;
|
|
@@ -243,121 +380,139 @@ function decodeProperties(view, buf, pos) {
|
|
|
243
380
|
for (let i = 0; i < propCount; i++) {
|
|
244
381
|
const nameLen = view.getUint16(pos, true);
|
|
245
382
|
pos += 2;
|
|
246
|
-
const name =
|
|
383
|
+
const name = rstr(buf, pos, nameLen);
|
|
247
384
|
pos += nameLen;
|
|
248
385
|
const kind = buf[pos];
|
|
249
386
|
pos += 1;
|
|
250
387
|
const valLen = view.getUint16(pos, true);
|
|
251
388
|
pos += 2;
|
|
252
|
-
const valStr =
|
|
389
|
+
const valStr = rstr(buf, pos, valLen);
|
|
253
390
|
pos += valLen;
|
|
254
|
-
|
|
255
|
-
case 0: // PROP_STRING
|
|
256
|
-
properties[name] = valStr;
|
|
257
|
-
break;
|
|
258
|
-
case 1: // PROP_BOOL_TRUE
|
|
259
|
-
properties[name] = true;
|
|
260
|
-
break;
|
|
261
|
-
case 2: // PROP_BOOL_FALSE
|
|
262
|
-
break;
|
|
263
|
-
case 3: // PROP_SPACE_SEP
|
|
264
|
-
properties[name] = valStr.split(" ").filter((s) => s.length > 0);
|
|
265
|
-
break;
|
|
266
|
-
case 5: // PROP_INT
|
|
267
|
-
properties[name] = Number(valStr);
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
391
|
+
properties[name] = decodeElementProp(kind, valStr);
|
|
270
392
|
}
|
|
271
393
|
return properties;
|
|
272
394
|
}
|
|
273
|
-
/**
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return
|
|
284
|
-
start: { offset: startOffset, line: startLine, column: startColumn },
|
|
285
|
-
end: { offset: endOffset, line: endLine, column: endColumn },
|
|
286
|
-
};
|
|
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;
|
|
287
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;
|
|
288
411
|
/**
|
|
289
|
-
* Walk-path element
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
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.
|
|
295
419
|
*/
|
|
296
420
|
class WalkElement {
|
|
297
421
|
type = "element";
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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 = {
|
|
308
460
|
enumerable: true,
|
|
309
461
|
configurable: true,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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 = {
|
|
318
474
|
enumerable: true,
|
|
319
475
|
configurable: true,
|
|
320
|
-
|
|
321
|
-
|
|
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
|
+
};
|
|
322
487
|
}
|
|
323
488
|
}
|
|
324
489
|
/** Read the tail of a matched element node (tag + properties).
|
|
325
490
|
* Common prelude (data/position/children) is already consumed by `readMatchedNode`. */
|
|
326
|
-
function readElementFromBinary(view, buf, offset, nodeId, resolver, position,
|
|
491
|
+
function readElementFromBinary(view, buf, offset, nodeId, resolver, position, childIdsPos, childTypesPos, childCount, data) {
|
|
327
492
|
let pos = offset;
|
|
328
493
|
// Eager: tagName (almost always accessed by visitors)
|
|
329
494
|
const tagLen = view.getUint16(pos, true);
|
|
330
495
|
pos += 2;
|
|
331
|
-
const tagName =
|
|
496
|
+
const tagName = rstr(buf, pos, tagLen);
|
|
332
497
|
pos += tagLen;
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
const node = new WalkElement();
|
|
336
|
-
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);
|
|
337
500
|
if (position !== undefined)
|
|
338
501
|
node.position = position;
|
|
339
502
|
if (data !== null)
|
|
340
503
|
node.data = data;
|
|
341
|
-
node._view = view;
|
|
342
|
-
node._buf = buf;
|
|
343
|
-
node._propsPos = propsPos;
|
|
344
|
-
node._childIds = childIds;
|
|
345
|
-
node._resolver = resolver;
|
|
346
|
-
nodeIdMap.set(node, nodeId);
|
|
347
504
|
return node;
|
|
348
505
|
}
|
|
349
|
-
/**
|
|
350
|
-
const TEXT_NODE_TYPES =
|
|
351
|
-
2: "text",
|
|
352
|
-
3: "comment",
|
|
353
|
-
5: "raw",
|
|
354
|
-
[HAST_MDX_FLOW_EXPRESSION]: "mdxFlowExpression",
|
|
355
|
-
[HAST_MDX_TEXT_EXPRESSION]: "mdxTextExpression",
|
|
356
|
-
[HAST_MDX_ESM]: "mdxjsEsm",
|
|
357
|
-
};
|
|
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]));
|
|
358
508
|
function readTextFromBinary(view, buf, offset, nodeId, nodeType, position, data) {
|
|
359
509
|
const valLen = view.getUint32(offset, true);
|
|
360
|
-
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;
|
|
361
516
|
const base = { type: TEXT_NODE_TYPES[nodeType], value };
|
|
362
517
|
if (position !== undefined)
|
|
363
518
|
base.position = position;
|
|
@@ -373,13 +528,11 @@ function readTextFromBinary(view, buf, offset, nodeId, nodeType, position, data)
|
|
|
373
528
|
}
|
|
374
529
|
return node;
|
|
375
530
|
}
|
|
376
|
-
|
|
377
|
-
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) {
|
|
378
532
|
let pos = offset;
|
|
379
|
-
// Name
|
|
380
533
|
const nameLen = view.getUint16(pos, true);
|
|
381
534
|
pos += 2;
|
|
382
|
-
const name = nameLen > 0 ?
|
|
535
|
+
const name = nameLen > 0 ? rstr(buf, pos, nameLen) : null;
|
|
383
536
|
pos += nameLen;
|
|
384
537
|
// Attributes: [kind: u8][nameLen: u16][name][valLen: u32][val]
|
|
385
538
|
const attrCount = view.getUint16(pos, true);
|
|
@@ -390,30 +543,13 @@ function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver, pos
|
|
|
390
543
|
pos += 1;
|
|
391
544
|
const attrNameLen = view.getUint16(pos, true);
|
|
392
545
|
pos += 2;
|
|
393
|
-
const attrName =
|
|
546
|
+
const attrName = rstr(buf, pos, attrNameLen);
|
|
394
547
|
pos += attrNameLen;
|
|
395
548
|
const attrValLen = view.getUint32(pos, true);
|
|
396
549
|
pos += 4;
|
|
397
|
-
const attrVal =
|
|
550
|
+
const attrVal = rstr(buf, pos, attrValLen);
|
|
398
551
|
pos += attrValLen;
|
|
399
|
-
|
|
400
|
-
case 0: // BooleanProp
|
|
401
|
-
attributes.push({ type: "mdxJsxAttribute", name: attrName, value: null });
|
|
402
|
-
break;
|
|
403
|
-
case 1: // LiteralProp
|
|
404
|
-
attributes.push({ type: "mdxJsxAttribute", name: attrName, value: attrVal });
|
|
405
|
-
break;
|
|
406
|
-
case 2: // ExpressionProp
|
|
407
|
-
attributes.push({
|
|
408
|
-
type: "mdxJsxAttribute",
|
|
409
|
-
name: attrName,
|
|
410
|
-
value: { type: "mdxJsxAttributeValueExpression", value: attrVal },
|
|
411
|
-
});
|
|
412
|
-
break;
|
|
413
|
-
case 3: // Spread
|
|
414
|
-
attributes.push({ type: "mdxJsxExpressionAttribute", value: attrVal });
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
552
|
+
attributes.push(decodeMdxJsxAttr(kind, attrName, attrVal));
|
|
417
553
|
}
|
|
418
554
|
const typeName = nodeType === HAST_MDX_JSX_ELEMENT ? "mdxJsxFlowElement" : "mdxJsxTextElement";
|
|
419
555
|
const base = { type: typeName, name, attributes };
|
|
@@ -422,39 +558,40 @@ function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver, pos
|
|
|
422
558
|
if (data !== null)
|
|
423
559
|
base.data = data;
|
|
424
560
|
nodeIdMap.set(base, nodeId);
|
|
425
|
-
makeLazyChildren(base,
|
|
561
|
+
makeLazyChildren(base, view, buf, childIdsPos, childTypesPos, childCount, resolver);
|
|
426
562
|
return base;
|
|
427
563
|
}
|
|
428
564
|
function readMatchedNode(view, buf, offset, nodeId, nodeType, resolver) {
|
|
429
565
|
let pos = offset;
|
|
430
566
|
// Shared prelude (matches serialize_hast_node_inline / serialize_mdast_node_inline):
|
|
431
|
-
// [data_len: u32][data_bytes][position: 24B][child_count:
|
|
432
|
-
// Data (JSON), eagerly parsed
|
|
567
|
+
// [data_len: u32][data_bytes][position: 24B][child_count: u32][child_ids: N×u32][child_types: N×u8]
|
|
433
568
|
const dataLen = view.getUint32(pos, true);
|
|
434
569
|
pos += 4;
|
|
435
570
|
let data = null;
|
|
436
571
|
if (dataLen > 0) {
|
|
437
|
-
const jsonStr =
|
|
572
|
+
const jsonStr = rstr(buf, pos, dataLen);
|
|
438
573
|
try {
|
|
439
574
|
data = JSON.parse(jsonStr);
|
|
440
575
|
}
|
|
441
|
-
catch {
|
|
442
|
-
|
|
576
|
+
catch (err) {
|
|
577
|
+
if (process.env.NODE_ENV !== "production") {
|
|
578
|
+
console.warn(`readMatchedNode: malformed node_data for nodeId=${nodeId}`, err);
|
|
579
|
+
}
|
|
443
580
|
}
|
|
444
581
|
pos += dataLen;
|
|
445
582
|
}
|
|
446
|
-
const position =
|
|
583
|
+
const position = readPosition(view, pos);
|
|
447
584
|
pos += 24;
|
|
448
|
-
const childCount = view.
|
|
449
|
-
pos +=
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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;
|
|
455
592
|
// Dispatch to type-specific tail (pos now sits at the type-specific section)
|
|
456
593
|
if (nodeType === HAST_ELEMENT) {
|
|
457
|
-
return readElementFromBinary(view, buf, pos, nodeId, resolver, position,
|
|
594
|
+
return readElementFromBinary(view, buf, pos, nodeId, resolver, position, childIdsPos, childTypesPos, childCount, data);
|
|
458
595
|
}
|
|
459
596
|
else if (nodeType === HAST_TEXT ||
|
|
460
597
|
nodeType === HAST_COMMENT ||
|
|
@@ -465,10 +602,10 @@ function readMatchedNode(view, buf, offset, nodeId, nodeType, resolver) {
|
|
|
465
602
|
return readTextFromBinary(view, buf, pos, nodeId, nodeType, position, data);
|
|
466
603
|
}
|
|
467
604
|
else if (nodeType === HAST_MDX_JSX_ELEMENT || nodeType === HAST_MDX_JSX_TEXT_ELEMENT) {
|
|
468
|
-
return readMdxJsxFromBinary(view, buf, pos, nodeId, nodeType, resolver, position,
|
|
605
|
+
return readMdxJsxFromBinary(view, buf, pos, nodeId, nodeType, resolver, position, childIdsPos, childTypesPos, childCount, data);
|
|
469
606
|
}
|
|
470
|
-
// Fallback: minimal node carrying whatever prelude data we found
|
|
471
|
-
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})` };
|
|
472
609
|
if (position !== undefined)
|
|
473
610
|
base.position = position;
|
|
474
611
|
if (data !== null)
|
|
@@ -477,71 +614,42 @@ function readMatchedNode(view, buf, offset, nodeId, nodeType, resolver) {
|
|
|
477
614
|
nodeIdMap.set(node, nodeId);
|
|
478
615
|
return node;
|
|
479
616
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
* child is accessed, then materializes children from it via HastReader.
|
|
484
|
-
*/
|
|
485
|
-
class LazyChildResolver {
|
|
486
|
-
#handle;
|
|
487
|
-
#reader = null;
|
|
488
|
-
constructor(handle) {
|
|
489
|
-
this.#handle = handle;
|
|
617
|
+
class HastLazyChildResolver extends LazyChildResolver {
|
|
618
|
+
createReader(wire) {
|
|
619
|
+
return new HastReader(wire);
|
|
490
620
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
this.#reader = new HastReader(serializeHandle(this.#handle));
|
|
494
|
-
}
|
|
495
|
-
return this.#reader;
|
|
496
|
-
}
|
|
497
|
-
materializeChildren(childIds) {
|
|
498
|
-
const reader = this.#ensure();
|
|
499
|
-
return childIds.map((id) => {
|
|
500
|
-
const node = materializeHastNode(reader, id);
|
|
501
|
-
this.attachLazyData(node, id);
|
|
502
|
-
return node;
|
|
503
|
-
});
|
|
621
|
+
materializeNode(reader, nodeId) {
|
|
622
|
+
return materializeHastNode(reader, nodeId);
|
|
504
623
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const json = napiGetNodeData(handle, nodeId);
|
|
511
|
-
const val = json ? JSON.parse(json) : null;
|
|
512
|
-
Object.defineProperty(this, "data", {
|
|
513
|
-
value: val,
|
|
514
|
-
writable: true,
|
|
515
|
-
enumerable: true,
|
|
516
|
-
configurable: true,
|
|
517
|
-
});
|
|
518
|
-
return val;
|
|
519
|
-
},
|
|
520
|
-
configurable: true,
|
|
521
|
-
enumerable: true,
|
|
522
|
-
});
|
|
624
|
+
readParentId(reader, nodeId) {
|
|
625
|
+
return reader.getParentId(nodeId);
|
|
626
|
+
}
|
|
627
|
+
readChildIds(reader, nodeId) {
|
|
628
|
+
return reader.getChildIds(nodeId);
|
|
523
629
|
}
|
|
524
630
|
}
|
|
525
|
-
/**
|
|
526
|
-
|
|
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) {
|
|
527
636
|
Object.defineProperty(node, "children", {
|
|
528
637
|
get() {
|
|
529
|
-
const
|
|
638
|
+
const val = readChildStubs(view, buf, childIdsPos, childTypesPos, childCount, resolver);
|
|
530
639
|
Object.defineProperty(this, "children", {
|
|
531
|
-
value:
|
|
640
|
+
value: val,
|
|
532
641
|
writable: true,
|
|
533
642
|
enumerable: true,
|
|
534
643
|
configurable: true,
|
|
535
644
|
});
|
|
536
|
-
return
|
|
645
|
+
return val;
|
|
537
646
|
},
|
|
538
|
-
configurable: true,
|
|
539
647
|
enumerable: true,
|
|
648
|
+
configurable: true,
|
|
540
649
|
});
|
|
541
650
|
}
|
|
542
|
-
/**
|
|
543
|
-
*
|
|
544
|
-
* 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. */
|
|
545
653
|
function handleVisitResult(result, nodeId, returnBuffer, deferred, originalNode) {
|
|
546
654
|
if (result == null)
|
|
547
655
|
return deferred;
|
|
@@ -552,7 +660,7 @@ function handleVisitResult(result, nodeId, returnBuffer, deferred, originalNode)
|
|
|
552
660
|
list.push({ nodeId, promise: result, originalNode });
|
|
553
661
|
return list;
|
|
554
662
|
}
|
|
555
|
-
returnBuffer
|
|
663
|
+
emitHastTree(returnBuffer, "replace", nodeId, result);
|
|
556
664
|
return deferred;
|
|
557
665
|
}
|
|
558
666
|
/**
|
|
@@ -564,7 +672,7 @@ function dispatchMatches(matchBuf, subs, ctx, returnBuffer, resolver) {
|
|
|
564
672
|
const matchCount = matchView.getUint32(0, true);
|
|
565
673
|
let deferred = null;
|
|
566
674
|
for (let i = 0; i < matchCount; i++) {
|
|
567
|
-
const indexBase = 4 + i *
|
|
675
|
+
const indexBase = 4 + i * 10;
|
|
568
676
|
const nodeId = matchView.getUint32(indexBase, true);
|
|
569
677
|
const subIndex = matchBuf[indexBase + 4];
|
|
570
678
|
const dataOffset = matchView.getUint32(indexBase + 6, true);
|
|
@@ -575,26 +683,6 @@ function dispatchMatches(matchBuf, subs, ctx, returnBuffer, resolver) {
|
|
|
575
683
|
}
|
|
576
684
|
return deferred;
|
|
577
685
|
}
|
|
578
|
-
/** Merge return-value + context command buffers and release internals. */
|
|
579
|
-
function mergeAndReset(returnBuffer, ctx) {
|
|
580
|
-
const ctxCmdBuf = ctx.getCommandBuffer();
|
|
581
|
-
const ctxBuf = ctxCmdBuf.getBuffer();
|
|
582
|
-
const retBuf = returnBuffer.getBuffer();
|
|
583
|
-
const totalLen = retBuf.length + ctxBuf.length;
|
|
584
|
-
let merged;
|
|
585
|
-
if (totalLen === 0) {
|
|
586
|
-
merged = new Uint8Array(0);
|
|
587
|
-
}
|
|
588
|
-
else {
|
|
589
|
-
merged = new Uint8Array(totalLen);
|
|
590
|
-
merged.set(retBuf, 0);
|
|
591
|
-
merged.set(ctxBuf, retBuf.length);
|
|
592
|
-
}
|
|
593
|
-
returnBuffer.reset();
|
|
594
|
-
ctxCmdBuf.reset();
|
|
595
|
-
return { merged, hasMutations: totalLen > 0 };
|
|
596
|
-
}
|
|
597
|
-
// Handle-based visitor
|
|
598
686
|
/**
|
|
599
687
|
* Walk a handle's arena in Rust, dispatch matched nodes to JS visitor functions,
|
|
600
688
|
* and apply mutations back to the handle. No arena buffers cross NAPI.
|
|
@@ -603,29 +691,34 @@ function mergeAndReset(returnBuffer, ctx) {
|
|
|
603
691
|
* replaced earlier in the same pass (the caller warns when non-zero), or a
|
|
604
692
|
* Promise of that count if any visitor is async.
|
|
605
693
|
*/
|
|
606
|
-
export function visitHastHandle(handle, plugin, subs, source, fileURL) {
|
|
694
|
+
export function visitHastHandle(handle, plugin, subs, source, fileURL, data = {}) {
|
|
607
695
|
const getSource = typeof source === "function" ? source : () => source;
|
|
608
|
-
const
|
|
696
|
+
const resolver = new HastLazyChildResolver(handle);
|
|
697
|
+
const ctx = new HastVisitorContextImpl(handle, getSource, fileURL, resolver, data);
|
|
609
698
|
const returnBuffer = new CommandBuffer();
|
|
610
|
-
const resolver = new LazyChildResolver(handle);
|
|
611
699
|
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: s.tagFilter }));
|
|
612
700
|
const deferred = dispatchMatches(walkHandle(handle, rustSubs), subs, ctx, returnBuffer, resolver);
|
|
613
701
|
if (deferred) {
|
|
614
702
|
return Promise.all(deferred.map((d) => d.promise.then((result) => ({ nodeId: d.nodeId, result, originalNode: d.originalNode })))).then((results) => {
|
|
615
703
|
for (const { nodeId, result, originalNode } of results) {
|
|
616
704
|
if (result != null && result !== originalNode) {
|
|
617
|
-
returnBuffer
|
|
705
|
+
emitHastTree(returnBuffer, "replace", nodeId, result);
|
|
618
706
|
}
|
|
619
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();
|
|
620
711
|
return applyMutations(handle, returnBuffer, ctx);
|
|
621
712
|
});
|
|
622
713
|
}
|
|
714
|
+
resolver.seal();
|
|
623
715
|
return applyMutations(handle, returnBuffer, ctx);
|
|
624
716
|
}
|
|
625
717
|
/** Returns the number of patches dropped as stranded (0 when none). */
|
|
626
718
|
function applyMutations(handle, returnBuffer, ctx) {
|
|
627
719
|
const { merged, hasMutations } = mergeAndReset(returnBuffer, ctx);
|
|
628
720
|
if (hasMutations) {
|
|
721
|
+
markHandleMutated(handle);
|
|
629
722
|
return applyCommandsToHandle(handle, merged);
|
|
630
723
|
}
|
|
631
724
|
return 0;
|