satteri 0.10.5 → 0.11.3
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 +101 -0
- package/dist/compile-types.d.ts +342 -0
- package/dist/compile-types.js +1 -0
- package/dist/compile.d.ts +22 -328
- package/dist/compile.js +133 -383
- 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.d.ts +1 -0
- package/dist/generated/parse-options.js +1 -5
- package/dist/generated/wire-constants.d.ts +4 -0
- package/dist/generated/wire-constants.js +34 -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 +200 -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 +189 -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 +4 -15
- package/dist/op-stream.js +31 -20
- 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 +14 -8
- package/dist/plugin.js +5 -10
- 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 +6 -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,21 +108,27 @@ 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);
|
|
129
|
+
// A standalone document is only serialized, and has no source for a position to point into.
|
|
130
|
+
if (!document)
|
|
131
|
+
w.position(n.position);
|
|
135
132
|
if (type === HAST_ELEMENT) {
|
|
136
133
|
w.str(OF_TAGNAME, typeof n.tagName === "string" ? n.tagName : "div");
|
|
137
134
|
const props = n.properties;
|
|
@@ -142,7 +139,6 @@ function emitHastOp(w, node, isRoot, refs) {
|
|
|
142
139
|
}
|
|
143
140
|
}
|
|
144
141
|
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
142
|
const name = typeof n.name === "string" ? n.name : typeof n.tagName === "string" ? n.tagName : "";
|
|
147
143
|
if (name !== "")
|
|
148
144
|
w.str(OF_NAME, name);
|
|
@@ -157,19 +153,17 @@ function emitHastOp(w, node, isRoot, refs) {
|
|
|
157
153
|
else {
|
|
158
154
|
w.str(OF_VALUE, typeof n.value === "string" ? n.value : "");
|
|
159
155
|
}
|
|
160
|
-
if (n.data != null)
|
|
156
|
+
if (!document && n.data != null)
|
|
161
157
|
w.data(n.data);
|
|
162
158
|
const children = n.children;
|
|
163
159
|
if (Array.isArray(children)) {
|
|
164
160
|
for (const c of children)
|
|
165
|
-
if (!emitHastOp(w, c, false, refs))
|
|
161
|
+
if (!emitHastOp(w, c, false, refs, document))
|
|
166
162
|
return false;
|
|
167
163
|
}
|
|
168
164
|
w.close();
|
|
169
165
|
return true;
|
|
170
166
|
}
|
|
171
|
-
/** Emit one element property, mirroring `encode_hast_js_node_data` exactly:
|
|
172
|
-
* bool/string/number/array → kind; null/object → skip. */
|
|
173
167
|
function emitHastProp(w, name, value) {
|
|
174
168
|
if (value === true)
|
|
175
169
|
w.prop(name, PROP_BOOL_TRUE, "");
|
|
@@ -177,23 +171,22 @@ function emitHastProp(w, name, value) {
|
|
|
177
171
|
w.prop(name, PROP_BOOL_FALSE, "");
|
|
178
172
|
else if (typeof value === "string")
|
|
179
173
|
w.prop(name, PROP_STRING, value);
|
|
180
|
-
|
|
181
|
-
|
|
174
|
+
// A NaN property is dropped, not rendered as `"NaN"`, matching hast.
|
|
175
|
+
else if (typeof value === "number") {
|
|
176
|
+
if (!Number.isNaN(value))
|
|
177
|
+
w.prop(name, PROP_INT, String(value));
|
|
178
|
+
}
|
|
182
179
|
else if (Array.isArray(value))
|
|
183
|
-
w.prop(name,
|
|
180
|
+
w.prop(name, PROP_TOKEN_LIST, encodeTokenList(value));
|
|
184
181
|
}
|
|
185
182
|
class HastVisitorContextImpl {
|
|
186
183
|
#commandBuffer = acquireCommandBuffer();
|
|
187
184
|
#diagnostics;
|
|
188
|
-
/** Track accumulated node state for multiple setProperty calls on the same node. */
|
|
189
185
|
#pendingNodes = new Map();
|
|
190
186
|
#handle;
|
|
191
187
|
#getSource;
|
|
192
188
|
#resolver;
|
|
193
189
|
#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
190
|
fileURL;
|
|
198
191
|
data;
|
|
199
192
|
sourceFormat;
|
|
@@ -213,14 +206,32 @@ class HastVisitorContextImpl {
|
|
|
213
206
|
return value;
|
|
214
207
|
}
|
|
215
208
|
removeNode(node) {
|
|
216
|
-
|
|
209
|
+
const id = requireNid(node, "removeNode", this.#refs);
|
|
210
|
+
this.#commandBuffer.removeNode(id);
|
|
211
|
+
this.#pendingNodes.delete(id);
|
|
217
212
|
}
|
|
218
213
|
replaceNode(node, newNode) {
|
|
219
214
|
const id = requireNid(node, "replaceNode", this.#refs);
|
|
215
|
+
const onlyReplacement = Array.isArray(newNode)
|
|
216
|
+
? newNode.length === 1
|
|
217
|
+
? newNode[0]
|
|
218
|
+
: undefined
|
|
219
|
+
: newNode;
|
|
220
|
+
if (id === ROOT_NODE_ID &&
|
|
221
|
+
onlyReplacement !== undefined &&
|
|
222
|
+
hastReusedId(onlyReplacement, this.#refs) === id) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
220
225
|
if (Array.isArray(newNode)) {
|
|
221
226
|
if (id === ROOT_NODE_ID && newNode.length > 1)
|
|
222
227
|
throw rootReplacementError(newNode);
|
|
223
|
-
//
|
|
228
|
+
// One command, so the node's replacement is its whole slot and a ref back
|
|
229
|
+
// to it resolves to all of it rather than to the last element.
|
|
230
|
+
if (id !== ROOT_NODE_ID && newNode.length > 1) {
|
|
231
|
+
emitHastMultiReplace(this.#commandBuffer, id, newNode, this.#refs);
|
|
232
|
+
this.#pendingNodes.delete(id);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
224
235
|
let previous;
|
|
225
236
|
for (const n of newNode) {
|
|
226
237
|
if (previous !== undefined)
|
|
@@ -228,36 +239,37 @@ class HastVisitorContextImpl {
|
|
|
228
239
|
previous = n;
|
|
229
240
|
}
|
|
230
241
|
if (previous === undefined) {
|
|
231
|
-
// Replacing with nothing drops the node, like removeNode.
|
|
232
242
|
this.removeNode(node);
|
|
233
243
|
}
|
|
234
244
|
else if (id === ROOT_NODE_ID) {
|
|
235
245
|
emitHastRootReplace(this.#commandBuffer, requireRootReplacement(previous), this.#refs);
|
|
236
246
|
}
|
|
237
247
|
else {
|
|
238
|
-
emitHastTree(this.#commandBuffer, "replace", id, previous, this.#refs);
|
|
248
|
+
emitHastTree(this.#commandBuffer, "replace", id, previous, this.#refs, true);
|
|
239
249
|
}
|
|
240
|
-
|
|
241
|
-
|
|
250
|
+
if (previous === undefined)
|
|
251
|
+
this.#pendingNodes.delete(id);
|
|
252
|
+
else
|
|
253
|
+
this.#pendingNodes.set(id, previous);
|
|
242
254
|
return;
|
|
243
255
|
}
|
|
244
256
|
if (id === ROOT_NODE_ID) {
|
|
245
257
|
emitHastRootReplace(this.#commandBuffer, requireRootReplacement(newNode), this.#refs);
|
|
246
258
|
return;
|
|
247
259
|
}
|
|
248
|
-
emitHastTree(this.#commandBuffer, "replace", id, newNode, this.#refs);
|
|
249
|
-
// Track the replacement so a later mdxJsx setProperty can fold into it.
|
|
260
|
+
emitHastTree(this.#commandBuffer, "replace", id, newNode, this.#refs, true);
|
|
250
261
|
this.#pendingNodes.set(id, newNode);
|
|
251
262
|
}
|
|
263
|
+
#splice(anchorId, content, op) {
|
|
264
|
+
for (const n of asArray(content)) {
|
|
265
|
+
emitHastTree(this.#commandBuffer, op, anchorId, n, this.#refs, true);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
252
268
|
insertBefore(node, newNode) {
|
|
253
|
-
|
|
254
|
-
for (const n of asArray(newNode))
|
|
255
|
-
emitHastTree(this.#commandBuffer, "insertBefore", id, n, this.#refs);
|
|
269
|
+
this.#splice(requireNid(node, "insertBefore", this.#refs), newNode, "insertBefore");
|
|
256
270
|
}
|
|
257
271
|
insertAfter(node, newNode) {
|
|
258
|
-
|
|
259
|
-
for (const n of asArray(newNode))
|
|
260
|
-
emitHastTree(this.#commandBuffer, "insertAfter", id, n, this.#refs);
|
|
272
|
+
this.#splice(requireNid(node, "insertAfter", this.#refs), newNode, "insertAfter");
|
|
261
273
|
}
|
|
262
274
|
wrapNode(node, parentNode) {
|
|
263
275
|
const id = requireNid(node, "wrapNode", this.#refs);
|
|
@@ -270,26 +282,19 @@ class HastVisitorContextImpl {
|
|
|
270
282
|
emitHastTree(this.#commandBuffer, "wrapNode", id, parentNode, this.#refs);
|
|
271
283
|
}
|
|
272
284
|
prependChild(node, childNode) {
|
|
273
|
-
|
|
274
|
-
for (const n of asArray(childNode))
|
|
275
|
-
emitHastTree(this.#commandBuffer, "prependChild", id, n, this.#refs);
|
|
285
|
+
this.#splice(requireNid(node, "prependChild", this.#refs), childNode, "prependChild");
|
|
276
286
|
}
|
|
277
287
|
appendChild(node, childNode) {
|
|
278
|
-
|
|
279
|
-
for (const n of asArray(childNode))
|
|
280
|
-
emitHastTree(this.#commandBuffer, "appendChild", id, n, this.#refs);
|
|
288
|
+
this.#splice(requireNid(node, "appendChild", this.#refs), childNode, "appendChild");
|
|
281
289
|
}
|
|
282
290
|
insertChildAt(node, index, childNode) {
|
|
283
291
|
const children = "children" in node ? node.children : [];
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
else {
|
|
291
|
-
this.insertBefore(children[index], childNode);
|
|
292
|
-
}
|
|
292
|
+
const [anchor, op] = index <= 0 || children.length === 0
|
|
293
|
+
? [node, "prependChild"]
|
|
294
|
+
: index >= children.length
|
|
295
|
+
? [node, "appendChild"]
|
|
296
|
+
: [children[index], "insertBefore"];
|
|
297
|
+
this.#splice(requireNid(anchor, "insertChildAt", this.#refs), childNode, op);
|
|
293
298
|
}
|
|
294
299
|
removeChildAt(node, index) {
|
|
295
300
|
const child = "children" in node ? node.children[index] : undefined;
|
|
@@ -299,8 +304,6 @@ class HastVisitorContextImpl {
|
|
|
299
304
|
setProperty(node, key, value) {
|
|
300
305
|
const id = requireNid(node, "setProperty", this.#refs);
|
|
301
306
|
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
307
|
if (!emitHastChildrenCommand(this.#commandBuffer, id, value, this.#refs)) {
|
|
305
308
|
throw unencodableContentError(value);
|
|
306
309
|
}
|
|
@@ -311,45 +314,67 @@ class HastVisitorContextImpl {
|
|
|
311
314
|
return;
|
|
312
315
|
}
|
|
313
316
|
if (node.type === "element") {
|
|
314
|
-
|
|
317
|
+
if (Array.isArray(value)) {
|
|
318
|
+
this.#commandBuffer.setTokenListProperty(id, key, encodeTokenList(value));
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
// NaN drops the attribute, as it does on a built element.
|
|
322
|
+
const dropped = typeof value === "number" && Number.isNaN(value);
|
|
323
|
+
this.#commandBuffer.setProperty(id, key, dropped ? null : value);
|
|
324
|
+
}
|
|
315
325
|
return;
|
|
316
326
|
}
|
|
317
327
|
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);
|
|
328
|
+
if (this.#foldPendingJsxAttribute(node, id, key, value))
|
|
342
329
|
return;
|
|
330
|
+
}
|
|
331
|
+
this.#commandBuffer.setProperty(id, key, value);
|
|
332
|
+
}
|
|
333
|
+
setField(node, key, value) {
|
|
334
|
+
const id = requireNid(node, "setField", this.#refs);
|
|
335
|
+
if (this.#foldPendingField(node, id, key, value))
|
|
336
|
+
return;
|
|
337
|
+
if (key === "children") {
|
|
338
|
+
if (!emitHastChildrenCommand(this.#commandBuffer, id, value, this.#refs)) {
|
|
339
|
+
throw unencodableContentError(value);
|
|
343
340
|
}
|
|
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
341
|
return;
|
|
349
342
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
343
|
+
if ((key === "attributes" &&
|
|
344
|
+
(node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement")) ||
|
|
345
|
+
(key === "properties" && node.type === "element")) {
|
|
346
|
+
// These are structured fields, so re-encode the node rather than a scalar command.
|
|
347
|
+
this.replaceNode(node, { ...node, [key]: value });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (key === "data") {
|
|
351
|
+
this.#commandBuffer.setField(id, key, value != null ? JSON.stringify(value) : null);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
this.#commandBuffer.setField(id, key, value);
|
|
355
|
+
}
|
|
356
|
+
setAttribute(node, name, value) {
|
|
357
|
+
const id = requireNid(node, "setAttribute", this.#refs);
|
|
358
|
+
if (this.#foldPendingJsxAttribute(node, id, name, value))
|
|
359
|
+
return;
|
|
360
|
+
this.#commandBuffer.setAttribute(id, name, value);
|
|
361
|
+
}
|
|
362
|
+
/** A queued replacement would discard the field, so fold it into a fresh replacement. */
|
|
363
|
+
#foldPendingField(node, id, key, value) {
|
|
364
|
+
const pending = this.#pendingNodes.get(id);
|
|
365
|
+
if (pending === undefined)
|
|
366
|
+
return false;
|
|
367
|
+
this.replaceNode(node, { ...pending, [key]: value });
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
/** A queued replacement would discard the attribute, so fold it in instead. */
|
|
371
|
+
#foldPendingJsxAttribute(node, id, name, value) {
|
|
372
|
+
const pending = this.#pendingNodes.get(id);
|
|
373
|
+
if (pending?.type !== "mdxJsxFlowElement" && pending?.type !== "mdxJsxTextElement") {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
this.replaceNode(node, withMdxJsxAttribute(pending, name, value));
|
|
377
|
+
return true;
|
|
353
378
|
}
|
|
354
379
|
textContent(node) {
|
|
355
380
|
return textContentHandle(this.#handle, requireNid(node, "textContent", this.#refs));
|
|
@@ -358,22 +383,17 @@ class HastVisitorContextImpl {
|
|
|
358
383
|
const parentId = this.#resolver.parentIdOf(requireNid(node, "parent", this.#refs));
|
|
359
384
|
if (parentId === undefined)
|
|
360
385
|
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;
|
|
386
|
+
return this.#resolver.materializeOne(parentId);
|
|
368
387
|
}
|
|
369
388
|
indexOf(node) {
|
|
370
389
|
return this.#resolver.indexInParent(requireNid(node, "indexOf", this.#refs));
|
|
371
390
|
}
|
|
372
391
|
report({ message, node, severity = "error", }) {
|
|
373
|
-
const id = node ?
|
|
392
|
+
const id = node ? getNodeId(node, this.#refs) : undefined;
|
|
374
393
|
this.#diagnostics.push({
|
|
375
394
|
message,
|
|
376
395
|
nodeId: id === FOREIGN_REF ? undefined : id,
|
|
396
|
+
position: node?.position,
|
|
377
397
|
severity,
|
|
378
398
|
});
|
|
379
399
|
}
|
|
@@ -384,7 +404,6 @@ class HastVisitorContextImpl {
|
|
|
384
404
|
return this.#diagnostics;
|
|
385
405
|
}
|
|
386
406
|
}
|
|
387
|
-
/** Node types that use filtered visitors (have tag/component names). */
|
|
388
407
|
const FILTERED_METHODS = new Set(["element", "mdxJsxFlowElement", "mdxJsxTextElement"]);
|
|
389
408
|
const subscriptionCache = new WeakMap();
|
|
390
409
|
export function resolveSubscriptions(plugin) {
|
|
@@ -395,8 +414,6 @@ export function resolveSubscriptions(plugin) {
|
|
|
395
414
|
subscriptionCache.set(plugin, built);
|
|
396
415
|
return built.subs;
|
|
397
416
|
}
|
|
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
417
|
function getRustSubs(plugin) {
|
|
401
418
|
const cached = subscriptionCache.get(plugin);
|
|
402
419
|
if (cached !== undefined)
|
|
@@ -411,7 +428,7 @@ function isFilteredVisitor(value) {
|
|
|
411
428
|
Array.isArray(value.filter) &&
|
|
412
429
|
typeof value.visit === "function");
|
|
413
430
|
}
|
|
414
|
-
|
|
431
|
+
// Validate before NAPI so errors name the public visitor contract.
|
|
415
432
|
function malformedFilteredVisitorError(plugin, methodName) {
|
|
416
433
|
const name = plugin.name;
|
|
417
434
|
const pluginName = typeof name === "string" && name !== "" ? name : "(unnamed)";
|
|
@@ -438,319 +455,14 @@ function buildSubscriptions(plugin) {
|
|
|
438
455
|
}
|
|
439
456
|
}
|
|
440
457
|
else {
|
|
441
|
-
// Bare function, empty filter matches all nodes of this type
|
|
442
458
|
subs.push({ nodeType, tagFilter: [], visitFn: value });
|
|
443
459
|
}
|
|
444
460
|
}
|
|
445
461
|
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: s.tagFilter }));
|
|
446
462
|
return { subs, rustSubs };
|
|
447
463
|
}
|
|
448
|
-
/** Visitor method name → node-type tag (method names are the subscribable AST names). */
|
|
449
464
|
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. */
|
|
465
|
+
// Returning the input node must not overwrite mutations queued through the context.
|
|
754
466
|
function applyHastVisitResult(result, nodeId, returnBuffer, originalNode, refs) {
|
|
755
467
|
if (result == null)
|
|
756
468
|
return;
|
|
@@ -760,20 +472,9 @@ function applyHastVisitResult(result, nodeId, returnBuffer, originalNode, refs)
|
|
|
760
472
|
returnBuffer.setProperty(nodeId, "value", result.value);
|
|
761
473
|
return;
|
|
762
474
|
}
|
|
763
|
-
emitHastTree(returnBuffer, "replace", nodeId, result, refs);
|
|
475
|
+
emitHastTree(returnBuffer, "replace", nodeId, result, refs, true);
|
|
764
476
|
}
|
|
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. */
|
|
477
|
+
// Explicit field checks avoid allocating Object.keys on the per-text-node hot path.
|
|
777
478
|
function isTextValueSwap(result, original) {
|
|
778
479
|
if (result.type !== original.type)
|
|
779
480
|
return false;
|
|
@@ -803,7 +504,13 @@ function dispatchMatches(matchBuf, subs, ctx, returnBuffer, resolver) {
|
|
|
803
504
|
const sub = subs[subIndex];
|
|
804
505
|
const node = readMatchedNode(wire, dataOffset, nodeId, sub.nodeType);
|
|
805
506
|
const result = sub.visitFn(node, ctx);
|
|
806
|
-
|
|
507
|
+
if (result instanceof Promise) {
|
|
508
|
+
deferred ??= [];
|
|
509
|
+
deferred.push({ nodeId, promise: result, originalNode: node });
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
applyHastVisitResult(result, nodeId, returnBuffer, node, resolver.refs);
|
|
513
|
+
}
|
|
807
514
|
}
|
|
808
515
|
return deferred;
|
|
809
516
|
}
|
|
@@ -822,20 +529,13 @@ export function visitHastHandle(handle, plugin, subs, source, fileURL, data = {}
|
|
|
822
529
|
}
|
|
823
530
|
return applyCollectedCommands(handle, result);
|
|
824
531
|
}
|
|
825
|
-
/** Apply commands collected by `visitHastHandleCollect`; returns the number of
|
|
826
|
-
* patches dropped as stranded (0 when none). */
|
|
827
532
|
function applyCollectedCommands(handle, commands) {
|
|
828
533
|
if (commands.length === 0)
|
|
829
534
|
return 0;
|
|
830
535
|
markHandleMutated(handle);
|
|
831
536
|
return applyCommandsToHandle(handle, commands);
|
|
832
537
|
}
|
|
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
|
-
*/
|
|
538
|
+
// Collect the last plugin’s commands so the caller can fuse their apply with rendering or compilation.
|
|
839
539
|
export function visitHastHandleCollect(handle, plugin, subs, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
|
|
840
540
|
const getSource = typeof source === "function" ? source : () => source;
|
|
841
541
|
const resolver = new HastLazyChildResolver(handle);
|
|
@@ -844,20 +544,20 @@ export function visitHastHandleCollect(handle, plugin, subs, source, fileURL, da
|
|
|
844
544
|
const rustSubs = getRustSubs(plugin);
|
|
845
545
|
const deferred = dispatchMatches(walkHandle(handle, rustSubs), subs, ctx, returnBuffer, resolver);
|
|
846
546
|
if (deferred) {
|
|
847
|
-
return Promise.all(deferred.map((
|
|
848
|
-
for (
|
|
849
|
-
|
|
547
|
+
return Promise.all(deferred.map((visit) => visit.promise)).then((results) => {
|
|
548
|
+
for (let i = 0; i < deferred.length; i++) {
|
|
549
|
+
const { nodeId, originalNode } = deferred[i];
|
|
550
|
+
applyHastVisitResult(results[i], nodeId, returnBuffer, originalNode, resolver.refs);
|
|
850
551
|
}
|
|
851
|
-
return collectCommands(returnBuffer, ctx);
|
|
552
|
+
return collectCommands(returnBuffer, ctx.getCommandBuffer());
|
|
852
553
|
});
|
|
853
554
|
}
|
|
854
|
-
return collectCommands(returnBuffer, ctx);
|
|
555
|
+
return collectCommands(returnBuffer, ctx.getCommandBuffer());
|
|
855
556
|
}
|
|
856
557
|
const HAST_ROOT_SUBS = [
|
|
857
558
|
{ nodeType: HAST_ROOT, tagFilter: [] },
|
|
858
559
|
];
|
|
859
|
-
/**
|
|
860
|
-
* visitors walk, and the visitors' mutations before `after` reads the tree. */
|
|
560
|
+
/** Hooks run in separate passes so each sees mutations from the preceding pass. */
|
|
861
561
|
export function visitHastHookCollect(handle, plugin, hook, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
|
|
862
562
|
const getSource = typeof source === "function" ? source : () => source;
|
|
863
563
|
const resolver = new HastLazyChildResolver(handle);
|
|
@@ -866,13 +566,13 @@ export function visitHastHookCollect(handle, plugin, hook, source, fileURL, data
|
|
|
866
566
|
const matchBuf = walkHandle(handle, HAST_ROOT_SUBS);
|
|
867
567
|
const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
|
|
868
568
|
if (matchView.getUint32(0, true) === 0)
|
|
869
|
-
return collectCommands(returnBuffer, ctx);
|
|
569
|
+
return collectCommands(returnBuffer, ctx.getCommandBuffer());
|
|
870
570
|
const wire = { view: matchView, buf: matchBuf, resolver };
|
|
871
571
|
const root = readMatchedNode(wire, matchView.getUint32(10, true), matchView.getUint32(4, true), HAST_ROOT);
|
|
872
572
|
const result = hook.call(plugin, root, ctx);
|
|
873
573
|
if (result instanceof Promise)
|
|
874
|
-
return result.then(() => collectCommands(returnBuffer, ctx));
|
|
875
|
-
return collectCommands(returnBuffer, ctx);
|
|
574
|
+
return result.then(() => collectCommands(returnBuffer, ctx.getCommandBuffer()));
|
|
575
|
+
return collectCommands(returnBuffer, ctx.getCommandBuffer());
|
|
876
576
|
}
|
|
877
577
|
export function visitHastHook(handle, plugin, hook, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
|
|
878
578
|
const result = visitHastHookCollect(handle, plugin, hook, source, fileURL, data, sourceFormat, diagnostics);
|
|
@@ -881,11 +581,3 @@ export function visitHastHook(handle, plugin, hook, source, fileURL, data = {},
|
|
|
881
581
|
}
|
|
882
582
|
return applyCollectedCommands(handle, result);
|
|
883
583
|
}
|
|
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
|
-
}
|