satteri 0.9.5 → 0.10.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 +11 -1
- 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 +4 -0
- package/dist/byte-writer.js +13 -4
- package/dist/child-stub.d.ts +7 -3
- package/dist/child-stub.js +18 -7
- package/dist/command-buffer.d.ts +36 -50
- package/dist/command-buffer.js +102 -49
- package/dist/compile.d.ts +80 -32
- package/dist/compile.js +293 -69
- package/dist/freeze.d.ts +2 -0
- package/dist/freeze.js +10 -0
- package/dist/generated/wire-constants.d.ts +3 -2
- package/dist/generated/wire-constants.js +4 -2
- package/dist/hast/child-stub.d.ts +2 -3
- package/dist/hast/child-stub.js +18 -11
- package/dist/hast/element-props.d.ts +1 -1
- package/dist/hast/element-props.js +14 -6
- package/dist/hast/hast-materializer.d.ts +6 -2
- package/dist/hast/hast-materializer.js +48 -134
- package/dist/hast/hast-reader.d.ts +7 -13
- package/dist/hast/hast-reader.js +76 -21
- package/dist/hast/hast-visitor.d.ts +48 -4
- package/dist/hast/hast-visitor.js +318 -154
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1 -1
- package/dist/lazy-child-resolver.d.ts +20 -13
- package/dist/lazy-child-resolver.js +66 -38
- package/dist/materializer-cache.d.ts +42 -0
- package/dist/materializer-cache.js +192 -0
- package/dist/mdast/child-stub.d.ts +6 -3
- package/dist/mdast/child-stub.js +71 -8
- package/dist/mdast/generated/layout.d.ts +2 -3
- package/dist/mdast/generated/layout.js +24 -28
- package/dist/mdast/generated/node-types.d.ts +4 -0
- package/dist/mdast/generated/node-types.js +25 -0
- package/dist/mdast/mdast-materializer.d.ts +12 -3
- package/dist/mdast/mdast-materializer.js +83 -78
- package/dist/mdast/mdast-reader.d.ts +15 -0
- package/dist/mdast/mdast-reader.js +55 -10
- package/dist/mdast/mdast-visitor.d.ts +73 -32
- package/dist/mdast/mdast-visitor.js +298 -122
- package/dist/op-stream.d.ts +5 -12
- package/dist/op-stream.js +6 -21
- package/dist/plugin.d.ts +36 -10
- package/dist/plugin.js +50 -2
- package/dist/string-pool.d.ts +13 -0
- package/dist/string-pool.js +91 -0
- package/dist/types.d.ts +38 -1
- package/dist/visitor-shared.d.ts +27 -4
- package/dist/visitor-shared.js +36 -10
- package/dist/wire-read.js +3 -1
- package/index.d.ts +130 -7
- package/index.js +89 -68
- package/package.json +23 -15
- package/dist/lazy-props.d.ts +0 -10
- package/dist/lazy-props.js +0 -50
|
@@ -1,37 +1,39 @@
|
|
|
1
|
-
import { materializeNode } from "./mdast-materializer.js";
|
|
1
|
+
import { LEAF_TYPES, isCustomLeaf, materializeNode } from "./mdast-materializer.js";
|
|
2
2
|
import { MdastReader } from "./mdast-reader.js";
|
|
3
|
-
import {
|
|
3
|
+
import { acquireCommandBuffer, releaseCommandBuffer, classifyReturn, CommandBuffer, STRUCTURAL_CMD, } from "../command-buffer.js";
|
|
4
|
+
import { CMD_SET_CHILDREN } from "../generated/wire-constants.js";
|
|
4
5
|
import { ru32, rstr, readPosition } from "../wire-read.js";
|
|
5
6
|
import { decodeMdastTypeData } from "./generated/layout.js";
|
|
6
7
|
import { TYPE_NAMES, NAME_TO_TYPE, VISITOR_KEYS, MDAST_OPSTREAM_TYPES, } from "./generated/node-types.js";
|
|
7
8
|
import { OpWriter, OF_VALUE, OF_URL, OF_TITLE, OF_ALT, OF_LANG, OF_META, OF_IDENTIFIER, OF_LABEL, OF_NAME, OF_REFERENCE_TYPE, OF_DEPTH, OF_CHECKED, OF_START, OF_ORDERED, OF_SPREAD, OF_EXPLICIT, PROP_STRING, emitMdxAttr, } from "../op-stream.js";
|
|
8
9
|
import { walkMdastHandle, mdastTextContentHandle } from "#binding";
|
|
9
|
-
import { asArray, makeRequireNid, mergeAndReset, unencodableContentError, } from "../visitor-shared.js";
|
|
10
|
-
import { LazyChildResolver } from "../lazy-child-resolver.js";
|
|
10
|
+
import { asArray, makeRequireNid, mergeAndReset, crossPipelineForeign, FOREIGN_REF, ROOT_NODE_ID, requireRootReplacement, rootReplacementError, unencodableContentError, } from "../visitor-shared.js";
|
|
11
|
+
import { LazyChildResolver, registerEpochCacheSlot, } from "../lazy-child-resolver.js";
|
|
11
12
|
import { MdastChildStub } from "./child-stub.js";
|
|
12
|
-
|
|
13
|
-
const mdastNodeIdMap = new WeakMap();
|
|
14
|
-
function nid(node) {
|
|
13
|
+
function nid(node, refs) {
|
|
15
14
|
// Genuine stubs carry their id as a plain field; a spread copy is not
|
|
16
15
|
// `instanceof` and has no `_nodeId`, so it correctly reads as new content.
|
|
17
16
|
if (node instanceof MdastChildStub)
|
|
18
|
-
return node._id;
|
|
19
|
-
const id =
|
|
17
|
+
return node._refs === refs ? node._id : FOREIGN_REF;
|
|
18
|
+
const id = refs.get(node);
|
|
20
19
|
if (id !== undefined)
|
|
21
20
|
return id;
|
|
22
|
-
// Plain objects are trusted only via
|
|
21
|
+
// Plain objects are trusted only via this tree's refs or a NON-enumerable
|
|
23
22
|
// `_nodeId` (the materializer's convention, which spread cannot copy) — an
|
|
24
23
|
// enumerable one rode in on a copy and must read as new content.
|
|
25
24
|
const d = Object.getOwnPropertyDescriptor(node, "_nodeId");
|
|
26
|
-
|
|
25
|
+
if (d !== undefined && !d.enumerable)
|
|
26
|
+
return FOREIGN_REF;
|
|
27
|
+
return crossPipelineForeign(node);
|
|
27
28
|
}
|
|
28
29
|
const requireNid = makeRequireNid(nid);
|
|
29
30
|
export class MdastVisitorContext {
|
|
30
|
-
#commandBuffer =
|
|
31
|
-
#diagnostics
|
|
31
|
+
#commandBuffer = acquireCommandBuffer();
|
|
32
|
+
#diagnostics;
|
|
32
33
|
#handle;
|
|
33
34
|
#getSource;
|
|
34
35
|
#resolver;
|
|
36
|
+
#refs;
|
|
35
37
|
/** One canonical object per parent id, so visitors can dedupe by identity.
|
|
36
38
|
* Null until the first `parent()` call; most passes never make one. */
|
|
37
39
|
#parentsById = null;
|
|
@@ -55,13 +57,15 @@ export class MdastVisitorContext {
|
|
|
55
57
|
* pipelines branch on which it is handling.
|
|
56
58
|
*/
|
|
57
59
|
sourceFormat;
|
|
58
|
-
constructor(handle, getSource, fileURL, resolver, data, sourceFormat) {
|
|
60
|
+
constructor(handle, getSource, fileURL, resolver, data, sourceFormat, diagnostics) {
|
|
59
61
|
this.#handle = handle;
|
|
60
62
|
this.#getSource = getSource;
|
|
61
63
|
this.fileURL = fileURL;
|
|
62
64
|
this.#resolver = resolver;
|
|
65
|
+
this.#refs = resolver.refs;
|
|
63
66
|
this.data = data;
|
|
64
67
|
this.sourceFormat = sourceFormat;
|
|
68
|
+
this.#diagnostics = diagnostics;
|
|
65
69
|
}
|
|
66
70
|
get source() {
|
|
67
71
|
const value = this.#getSource();
|
|
@@ -69,39 +73,44 @@ export class MdastVisitorContext {
|
|
|
69
73
|
return value;
|
|
70
74
|
}
|
|
71
75
|
removeNode(node) {
|
|
72
|
-
this.#commandBuffer.removeNode(requireNid(node, "removeNode"));
|
|
76
|
+
this.#commandBuffer.removeNode(requireNid(node, "removeNode", this.#refs));
|
|
73
77
|
}
|
|
74
78
|
insertBefore(node, newNode) {
|
|
75
|
-
const id = requireNid(node, "insertBefore");
|
|
79
|
+
const id = requireNid(node, "insertBefore", this.#refs);
|
|
76
80
|
for (const n of asArray(newNode))
|
|
77
|
-
emitMdastTree(this.#commandBuffer, "insertBefore", id, n);
|
|
81
|
+
emitMdastTree(this.#commandBuffer, "insertBefore", id, n, false, this.#refs);
|
|
78
82
|
}
|
|
79
83
|
insertAfter(node, newNode) {
|
|
80
|
-
const id = requireNid(node, "insertAfter");
|
|
84
|
+
const id = requireNid(node, "insertAfter", this.#refs);
|
|
81
85
|
for (const n of asArray(newNode))
|
|
82
|
-
emitMdastTree(this.#commandBuffer, "insertAfter", id, n);
|
|
86
|
+
emitMdastTree(this.#commandBuffer, "insertAfter", id, n, false, this.#refs);
|
|
83
87
|
}
|
|
84
88
|
/**
|
|
85
89
|
* Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any
|
|
86
|
-
* children `parentNode` declares are kept after it.
|
|
90
|
+
* children `parentNode` declares are kept after it. `parentNode` must be a
|
|
91
|
+
* node type that can hold children, or a raw string parsing to exactly one
|
|
92
|
+
* such block (`{ raw: "> " }` wraps in a blockquote); to surround a node
|
|
93
|
+
* with raw HTML tags, use `replaceNode(node, [openTag, node, closeTag])`
|
|
94
|
+
* instead.
|
|
87
95
|
*/
|
|
88
96
|
wrapNode(node, parentNode) {
|
|
89
|
-
const id = requireNid(node, "wrapNode");
|
|
90
|
-
|
|
97
|
+
const id = requireNid(node, "wrapNode", this.#refs);
|
|
98
|
+
assertMdastWrapParent(parentNode);
|
|
99
|
+
emitMdastTree(this.#commandBuffer, "wrapNode", id, parentNode, false, this.#refs);
|
|
91
100
|
}
|
|
92
101
|
prependChild(node, childNode) {
|
|
93
|
-
const id = requireNid(node, "prependChild");
|
|
102
|
+
const id = requireNid(node, "prependChild", this.#refs);
|
|
94
103
|
for (const n of asArray(childNode))
|
|
95
|
-
emitMdastTree(this.#commandBuffer, "prependChild", id, n);
|
|
104
|
+
emitMdastTree(this.#commandBuffer, "prependChild", id, n, false, this.#refs);
|
|
96
105
|
}
|
|
97
106
|
appendChild(node, childNode) {
|
|
98
|
-
const id = requireNid(node, "appendChild");
|
|
107
|
+
const id = requireNid(node, "appendChild", this.#refs);
|
|
99
108
|
for (const n of asArray(childNode))
|
|
100
|
-
emitMdastTree(this.#commandBuffer, "appendChild", id, n);
|
|
109
|
+
emitMdastTree(this.#commandBuffer, "appendChild", id, n, false, this.#refs);
|
|
101
110
|
}
|
|
102
111
|
/** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */
|
|
103
112
|
insertChildAt(node, index, childNode) {
|
|
104
|
-
const children = "children" in node ? node.children : [];
|
|
113
|
+
const children = ("children" in node ? node.children : undefined) ?? [];
|
|
105
114
|
if (index <= 0 || children.length === 0) {
|
|
106
115
|
this.prependChild(node, childNode);
|
|
107
116
|
}
|
|
@@ -114,38 +123,70 @@ export class MdastVisitorContext {
|
|
|
114
123
|
}
|
|
115
124
|
/** Remove the `index`-th child of `node`; a no-op when there is no such child. */
|
|
116
125
|
removeChildAt(node, index) {
|
|
117
|
-
const child = "children" in node ? node.children[index] : undefined;
|
|
126
|
+
const child = "children" in node ? node.children?.[index] : undefined;
|
|
118
127
|
if (child)
|
|
119
128
|
this.removeNode(child);
|
|
120
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Swap `node` for one node, or for an array of nodes placed in order at its
|
|
132
|
+
* position. An empty array drops the node, the same as `removeNode`.
|
|
133
|
+
* The document root takes a `root`, the one place a `root` is accepted as
|
|
134
|
+
* content, or a raw string, which parses to a root of its own.
|
|
135
|
+
*/
|
|
121
136
|
replaceNode(node, newNode) {
|
|
122
|
-
const id = requireNid(node, "replaceNode");
|
|
123
|
-
|
|
137
|
+
const id = requireNid(node, "replaceNode", this.#refs);
|
|
138
|
+
if (Array.isArray(newNode)) {
|
|
139
|
+
if (id === ROOT_NODE_ID && newNode.length > 1)
|
|
140
|
+
throw rootReplacementError(newNode);
|
|
141
|
+
// The last node carries the `replace` so refs back to the target still splice.
|
|
142
|
+
let previous;
|
|
143
|
+
for (const n of newNode) {
|
|
144
|
+
if (previous !== undefined) {
|
|
145
|
+
emitMdastTree(this.#commandBuffer, "insertBefore", id, previous, false, this.#refs);
|
|
146
|
+
}
|
|
147
|
+
previous = n;
|
|
148
|
+
}
|
|
149
|
+
if (previous === undefined) {
|
|
150
|
+
// Replacing with nothing drops the node, like removeNode.
|
|
151
|
+
this.removeNode(node);
|
|
152
|
+
}
|
|
153
|
+
else if (id === ROOT_NODE_ID && !isRawMdastContent(previous)) {
|
|
154
|
+
emitMdastRootReplace(this.#commandBuffer, requireRootReplacement(previous), this.#refs);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
emitMdastTree(this.#commandBuffer, "replace", id, previous, true, this.#refs);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (id === ROOT_NODE_ID && !isRawMdastContent(newNode)) {
|
|
162
|
+
emitMdastRootReplace(this.#commandBuffer, requireRootReplacement(newNode), this.#refs);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
emitMdastTree(this.#commandBuffer, "replace", id, newNode, true, this.#refs);
|
|
124
166
|
}
|
|
125
167
|
setProperty(node, key, value) {
|
|
126
168
|
if (key === "children") {
|
|
127
169
|
// children is structural: set-children keeps the node and swaps only its
|
|
128
170
|
// child list (reused children keep their id).
|
|
129
|
-
const id = requireNid(node, "setProperty");
|
|
130
|
-
|
|
131
|
-
if (!ops)
|
|
171
|
+
const id = requireNid(node, "setProperty", this.#refs);
|
|
172
|
+
if (!emitMdastChildrenCommand(this.#commandBuffer, id, value, this.#refs)) {
|
|
132
173
|
throw unencodableContentError(value);
|
|
133
|
-
|
|
174
|
+
}
|
|
134
175
|
return;
|
|
135
176
|
}
|
|
136
177
|
if (key === "data") {
|
|
137
178
|
// data is stored as JSON in the arena, serialize it for the command buffer
|
|
138
|
-
this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value != null ? JSON.stringify(value) : null);
|
|
179
|
+
this.#commandBuffer.setProperty(requireNid(node, "setProperty", this.#refs), key, value != null ? JSON.stringify(value) : null);
|
|
139
180
|
return;
|
|
140
181
|
}
|
|
141
|
-
this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value);
|
|
182
|
+
this.#commandBuffer.setProperty(requireNid(node, "setProperty", this.#refs), key, value);
|
|
142
183
|
}
|
|
143
184
|
/** Collect the concatenated text of all descendant text nodes (like mdast-util-to-string). */
|
|
144
185
|
textContent(node, options) {
|
|
145
|
-
return mdastTextContentHandle(this.#handle, requireNid(node, "textContent"), options);
|
|
186
|
+
return mdastTextContentHandle(this.#handle, requireNid(node, "textContent", this.#refs), options);
|
|
146
187
|
}
|
|
147
188
|
parent(node) {
|
|
148
|
-
const parentId = this.#resolver.parentIdOf(requireNid(node, "parent"));
|
|
189
|
+
const parentId = this.#resolver.parentIdOf(requireNid(node, "parent", this.#refs));
|
|
149
190
|
if (parentId === undefined)
|
|
150
191
|
return undefined;
|
|
151
192
|
const byId = (this.#parentsById ??= new Map());
|
|
@@ -161,12 +202,13 @@ export class MdastVisitorContext {
|
|
|
161
202
|
* Use this rather than `parent.children.indexOf(node)`, which won't find it.
|
|
162
203
|
*/
|
|
163
204
|
indexOf(node) {
|
|
164
|
-
return this.#resolver.indexInParent(requireNid(node, "indexOf"));
|
|
205
|
+
return this.#resolver.indexInParent(requireNid(node, "indexOf", this.#refs));
|
|
165
206
|
}
|
|
166
207
|
report({ message, node, severity = "error", }) {
|
|
208
|
+
const id = node ? nid(node, this.#refs) : undefined;
|
|
167
209
|
this.#diagnostics.push({
|
|
168
210
|
message,
|
|
169
|
-
nodeId:
|
|
211
|
+
nodeId: id === FOREIGN_REF ? undefined : id,
|
|
170
212
|
position: node?.position,
|
|
171
213
|
severity,
|
|
172
214
|
});
|
|
@@ -179,7 +221,24 @@ export class MdastVisitorContext {
|
|
|
179
221
|
return this.#diagnostics;
|
|
180
222
|
}
|
|
181
223
|
}
|
|
224
|
+
const mdastSubscriptionCache = new WeakMap();
|
|
182
225
|
export function resolveMdastSubscriptions(plugin) {
|
|
226
|
+
const cached = mdastSubscriptionCache.get(plugin);
|
|
227
|
+
if (cached !== undefined)
|
|
228
|
+
return cached.subs;
|
|
229
|
+
const built = buildMdastSubscriptions(plugin);
|
|
230
|
+
mdastSubscriptionCache.set(plugin, built);
|
|
231
|
+
return built.subs;
|
|
232
|
+
}
|
|
233
|
+
function getMdastRustSubs(plugin) {
|
|
234
|
+
const cached = mdastSubscriptionCache.get(plugin);
|
|
235
|
+
if (cached !== undefined)
|
|
236
|
+
return cached.rustSubs;
|
|
237
|
+
const built = buildMdastSubscriptions(plugin);
|
|
238
|
+
mdastSubscriptionCache.set(plugin, built);
|
|
239
|
+
return built.rustSubs;
|
|
240
|
+
}
|
|
241
|
+
function buildMdastSubscriptions(plugin) {
|
|
183
242
|
const subs = [];
|
|
184
243
|
for (const [name, fn] of Object.entries(plugin)) {
|
|
185
244
|
if (VISITOR_KEYS.has(name) && typeof fn === "function") {
|
|
@@ -192,14 +251,19 @@ export function resolveMdastSubscriptions(plugin) {
|
|
|
192
251
|
}
|
|
193
252
|
}
|
|
194
253
|
}
|
|
195
|
-
|
|
254
|
+
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: [] }));
|
|
255
|
+
return { subs, rustSubs };
|
|
196
256
|
}
|
|
257
|
+
const MDAST_EPOCH_CACHE = registerEpochCacheSlot(new WeakMap());
|
|
197
258
|
class MdastLazyChildResolver extends LazyChildResolver {
|
|
259
|
+
cacheSlot() {
|
|
260
|
+
return MDAST_EPOCH_CACHE;
|
|
261
|
+
}
|
|
198
262
|
createReader(wire) {
|
|
199
263
|
return new MdastReader(wire);
|
|
200
264
|
}
|
|
201
|
-
materializeNode(reader, nodeId) {
|
|
202
|
-
return materializeNode(reader, nodeId);
|
|
265
|
+
materializeNode(reader, nodeId, refs) {
|
|
266
|
+
return materializeNode(reader, nodeId, true, refs);
|
|
203
267
|
}
|
|
204
268
|
readParentId(reader, nodeId) {
|
|
205
269
|
return reader.getParentId(nodeId);
|
|
@@ -209,11 +273,18 @@ class MdastLazyChildResolver extends LazyChildResolver {
|
|
|
209
273
|
}
|
|
210
274
|
}
|
|
211
275
|
/** Build the child-stub list for a matched node from the wire's `[child_ids]
|
|
212
|
-
* [child_types]` blocks
|
|
213
|
-
*
|
|
214
|
-
*
|
|
276
|
+
* [child_types]` blocks, no arena snapshot. Stale ids are caught at
|
|
277
|
+
* materialization: the resolver's epoch check refuses a snapshot once the
|
|
278
|
+
* arena has mutated or been dropped. */
|
|
215
279
|
function readMdastChildStubs(view, buf, idsPos, typesPos, count, resolver) {
|
|
216
|
-
|
|
280
|
+
// With a hot snapshot a stub's deferral buys nothing; real nodes skip its per-field getters.
|
|
281
|
+
if (resolver.hasHotSnapshot()) {
|
|
282
|
+
const nodes = new Array(count);
|
|
283
|
+
for (let i = 0; i < count; i++) {
|
|
284
|
+
nodes[i] = resolver.materializeOne(ru32(view, idsPos + i * 4));
|
|
285
|
+
}
|
|
286
|
+
return nodes;
|
|
287
|
+
}
|
|
217
288
|
const stubs = new Array(count);
|
|
218
289
|
for (let i = 0; i < count; i++) {
|
|
219
290
|
stubs[i] = new MdastChildStub(resolver, ru32(view, idsPos + i * 4), buf[typesPos + i]);
|
|
@@ -300,82 +371,124 @@ function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver)
|
|
|
300
371
|
node.spread = buf[pos + 1] !== 0;
|
|
301
372
|
break;
|
|
302
373
|
}
|
|
374
|
+
case 37: {
|
|
375
|
+
// descriptionDetails
|
|
376
|
+
node.spread = buf[pos] !== 0;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
303
379
|
// table (21), directives (30/31/32) and mdxJsx elements (100/101) are
|
|
304
380
|
// decoded by the generated `decodeMdastTypeData` from their tails.
|
|
305
381
|
// root(0), paragraph(1), thematicBreak(3), blockquote(4), emphasis(11),
|
|
306
382
|
// strong(12), break(14), tableRow(22), tableCell(23), delete(24): no extra data
|
|
307
383
|
}
|
|
308
384
|
}
|
|
309
|
-
|
|
385
|
+
// User-defined node: the stored `name` field holds the author's public type
|
|
386
|
+
// string. Surface it as `node.type` (open type string) instead of the
|
|
387
|
+
// internal `"custom"`, drop the redundant `name`, and drop an empty `value`
|
|
388
|
+
// so a parent node isn't given a spurious leaf field.
|
|
389
|
+
if (nodeType === MDAST_CUSTOM) {
|
|
390
|
+
node.type = node.name;
|
|
391
|
+
delete node.name;
|
|
392
|
+
if (node.value === "")
|
|
393
|
+
delete node.value;
|
|
394
|
+
}
|
|
395
|
+
const leafCustom = nodeType === MDAST_CUSTOM && isCustomLeaf({ value: node.value, data: initialData }, childCount);
|
|
396
|
+
if (childCount === 0 && !LEAF_TYPES.has(nodeType) && !leafCustom) {
|
|
397
|
+
node.children = [];
|
|
398
|
+
}
|
|
399
|
+
resolver.refs.set(node, nodeId);
|
|
310
400
|
if (initialData) {
|
|
311
401
|
node.data = initialData;
|
|
312
402
|
}
|
|
313
403
|
return node;
|
|
314
404
|
}
|
|
405
|
+
const MDAST_ROOT = NAME_TO_TYPE.root;
|
|
406
|
+
/** Internal tag for user-defined nodes. The stored `name` field holds the
|
|
407
|
+
* author's public `type` string; the read paths surface it as `node.type`
|
|
408
|
+
* and the emit path routes any unrecognized `type` here. */
|
|
409
|
+
const MDAST_CUSTOM = NAME_TO_TYPE.custom;
|
|
315
410
|
/** The arena id of a node if it is an existing (materialized) node, else
|
|
316
411
|
* undefined for a freshly-built one. */
|
|
317
|
-
function reusedId(node) {
|
|
412
|
+
function reusedId(node, refs) {
|
|
318
413
|
if (node === null || typeof node !== "object")
|
|
319
414
|
return undefined;
|
|
320
|
-
const id = nid(node);
|
|
321
|
-
return
|
|
322
|
-
}
|
|
323
|
-
// Reused across every replacement in a pass: compile is synchronous and its
|
|
324
|
-
// result is copied into the command buffer before the next call, so a single
|
|
325
|
-
// writer is safe and avoids a 512-byte allocation per built node.
|
|
326
|
-
const mdastWriter = new OpWriter();
|
|
327
|
-
/**
|
|
328
|
-
* Compile a declarative MDAST replacement tree to the op-stream — the only
|
|
329
|
-
* structural encoding. Reused nodes (those still carrying an arena id) become
|
|
330
|
-
* `ref`s so the rebuild splices the original back in place. Returns null when
|
|
331
|
-
* the replay can't reproduce the tree identically (unsupported node type or
|
|
332
|
-
* out-of-range numeric field); the caller turns that into a hard error.
|
|
333
|
-
*/
|
|
334
|
-
function compileMdastToOpstream(root, forReplace = false) {
|
|
335
|
-
mdastWriter.begin();
|
|
336
|
-
try {
|
|
337
|
-
if (!emitMdastOp(mdastWriter, root, true, forReplace))
|
|
338
|
-
return null;
|
|
339
|
-
return mdastWriter.take();
|
|
340
|
-
}
|
|
341
|
-
finally {
|
|
342
|
-
mdastWriter.end();
|
|
343
|
-
}
|
|
415
|
+
const id = nid(node, refs);
|
|
416
|
+
return id !== undefined && id !== FOREIGN_REF ? id : undefined;
|
|
344
417
|
}
|
|
345
|
-
/**
|
|
418
|
+
/** Emit a set-children command in place: a root-wrapped child list, the shape
|
|
346
419
|
* `Patch::SetChildren` splices in. Reused children become refs. */
|
|
347
|
-
function
|
|
420
|
+
function emitMdastChildrenCommand(buffer, id, children, refs) {
|
|
348
421
|
if (!Array.isArray(children))
|
|
349
|
-
return
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
mdastWriter.open(NAME_TO_TYPE.root);
|
|
422
|
+
return false;
|
|
423
|
+
return buffer.emitOpstreamCommand(CMD_SET_CHILDREN, id, () => {
|
|
424
|
+
buffer.open(MDAST_ROOT);
|
|
353
425
|
for (const c of children) {
|
|
354
|
-
if (!emitMdastOp(
|
|
355
|
-
return
|
|
426
|
+
if (!emitMdastOp(buffer, c, false, false, refs))
|
|
427
|
+
return false;
|
|
356
428
|
}
|
|
357
|
-
|
|
358
|
-
return
|
|
429
|
+
buffer.close();
|
|
430
|
+
return true;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
/** Separate from the per-node encoder, which rejects a `root` payload. */
|
|
434
|
+
function emitMdastRootReplace(buffer, root, refs) {
|
|
435
|
+
const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD.replace, ROOT_NODE_ID, () => emitMdastRootOp(buffer, root, refs));
|
|
436
|
+
if (!ok)
|
|
437
|
+
throw unencodableContentError(root);
|
|
438
|
+
}
|
|
439
|
+
function emitMdastRootOp(w, n, refs) {
|
|
440
|
+
w.open(MDAST_ROOT);
|
|
441
|
+
if (n.data != null)
|
|
442
|
+
w.data(n.data);
|
|
443
|
+
if (n._keepChildren === true) {
|
|
444
|
+
w.keepChildren();
|
|
359
445
|
}
|
|
360
|
-
|
|
361
|
-
|
|
446
|
+
else {
|
|
447
|
+
const children = n.children;
|
|
448
|
+
if (Array.isArray(children)) {
|
|
449
|
+
for (const c of children)
|
|
450
|
+
if (!emitMdastOp(w, c, false, true, refs))
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
362
453
|
}
|
|
454
|
+
w.close();
|
|
455
|
+
return true;
|
|
363
456
|
}
|
|
364
|
-
function emitMdastOp(w, node, isRoot, forReplace) {
|
|
457
|
+
function emitMdastOp(w, node, isRoot, forReplace, refs) {
|
|
365
458
|
if (node === null || typeof node !== "object")
|
|
366
459
|
return false;
|
|
367
460
|
if (!isRoot) {
|
|
368
|
-
const id = reusedId(node);
|
|
461
|
+
const id = reusedId(node, refs);
|
|
369
462
|
if (id !== undefined) {
|
|
370
463
|
w.ref(id);
|
|
371
464
|
return true;
|
|
372
465
|
}
|
|
373
466
|
}
|
|
374
467
|
const n = node;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
468
|
+
// Any `type` outside the built-in set is a user-defined node: route it to the
|
|
469
|
+
// internal `custom` tag and carry the author's `type` string as the name.
|
|
470
|
+
let type = MDAST_OPSTREAM_TYPES[n.type];
|
|
471
|
+
let isCustom = false;
|
|
472
|
+
if (type === undefined) {
|
|
473
|
+
if (typeof n.type !== "string" || n.type.length === 0)
|
|
474
|
+
return false;
|
|
475
|
+
// A known built-in that just isn't op-stream-encodable (e.g. `root`) is a
|
|
476
|
+
// real type used wrong, so fail loudly rather than reinterpreting it as a
|
|
477
|
+
// user-defined node. Only genuinely-unknown type strings become custom.
|
|
478
|
+
if (NAME_TO_TYPE[n.type] !== undefined)
|
|
479
|
+
return false;
|
|
480
|
+
type = MDAST_CUSTOM;
|
|
481
|
+
isCustom = true;
|
|
482
|
+
}
|
|
483
|
+
else if (type === MDAST_CUSTOM) {
|
|
484
|
+
// `"custom"` is the internal tag's own public name, so it resolves here
|
|
485
|
+
// instead of falling through as unknown. Still a user-defined node, so carry
|
|
486
|
+
// the `type` string as the name so it round-trips rather than vanishing.
|
|
487
|
+
isCustom = true;
|
|
488
|
+
}
|
|
378
489
|
w.open(type);
|
|
490
|
+
if (isCustom)
|
|
491
|
+
w.str(OF_NAME, n.type);
|
|
379
492
|
if (typeof n.value === "string")
|
|
380
493
|
w.str(OF_VALUE, n.value);
|
|
381
494
|
if (typeof n.url === "string")
|
|
@@ -412,7 +525,7 @@ function emitMdastOp(w, node, isRoot, forReplace) {
|
|
|
412
525
|
w.bool(OF_ORDERED, n.ordered);
|
|
413
526
|
if (typeof n.spread === "boolean")
|
|
414
527
|
w.bool(OF_SPREAD, n.spread);
|
|
415
|
-
if (typeof n.name === "string")
|
|
528
|
+
if (!isCustom && typeof n.name === "string")
|
|
416
529
|
w.str(OF_NAME, n.name);
|
|
417
530
|
const attrs = n.attributes;
|
|
418
531
|
if (Array.isArray(attrs)) {
|
|
@@ -446,7 +559,7 @@ function emitMdastOp(w, node, isRoot, forReplace) {
|
|
|
446
559
|
const children = n.children;
|
|
447
560
|
if (Array.isArray(children)) {
|
|
448
561
|
for (const c of children)
|
|
449
|
-
if (!emitMdastOp(w, c, false, forReplace))
|
|
562
|
+
if (!emitMdastOp(w, c, false, forReplace, refs))
|
|
450
563
|
return false;
|
|
451
564
|
}
|
|
452
565
|
}
|
|
@@ -470,7 +583,7 @@ function isRawMdastContent(content) {
|
|
|
470
583
|
* op-stream is the only declarative encoding. The switches stay inline so the
|
|
471
584
|
* buffer calls are monomorphic (computed method names defeat inline caches on
|
|
472
585
|
* this warm path). */
|
|
473
|
-
function emitMdastTree(buffer, op, id, content, forReplace
|
|
586
|
+
function emitMdastTree(buffer, op, id, content, forReplace, refs) {
|
|
474
587
|
if (isRawMdastContent(content)) {
|
|
475
588
|
switch (op) {
|
|
476
589
|
case "replace":
|
|
@@ -487,27 +600,65 @@ function emitMdastTree(buffer, op, id, content, forReplace = false) {
|
|
|
487
600
|
return buffer.wrapNode(id, content);
|
|
488
601
|
}
|
|
489
602
|
}
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
603
|
+
const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD[op], id, () => emitMdastOp(buffer, content, true, forReplace, refs));
|
|
604
|
+
if (!ok)
|
|
492
605
|
throw unencodableContentError(content);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
606
|
+
}
|
|
607
|
+
/** A leaf wrapper would make the patch engine drop or displace the wrapped node. */
|
|
608
|
+
function assertMdastWrapParent(parentNode) {
|
|
609
|
+
const sandwich = 'replaceNode(node, [{ type: "html", value: "<div>" }, node, { type: "html", value: "</div>" }])';
|
|
610
|
+
// A raw wrapper is only a tree once Rust has parsed it, so it is checked there.
|
|
611
|
+
if (isRawMdastContent(parentNode))
|
|
612
|
+
return;
|
|
613
|
+
const type = parentNode.type;
|
|
614
|
+
const tag = typeof type === "string" ? NAME_TO_TYPE[type] : undefined;
|
|
615
|
+
if (tag === undefined) {
|
|
616
|
+
// User-defined type: only the declared shape distinguishes a parent from
|
|
617
|
+
// a text leaf.
|
|
618
|
+
if (Array.isArray(parentNode.children))
|
|
619
|
+
return;
|
|
620
|
+
throw new Error(`wrapNode: a user-defined "${String(type)}" wrapper must declare a children array. ` +
|
|
621
|
+
"A leaf-shaped custom node renders as text and cannot hold the wrapped node.");
|
|
506
622
|
}
|
|
623
|
+
if (!LEAF_TYPES.has(tag))
|
|
624
|
+
return;
|
|
625
|
+
throw new Error(`wrapNode: "${String(type)}" nodes cannot hold children, so they cannot wrap a node. ` +
|
|
626
|
+
'Pass a parent node such as { type: "blockquote", children: [] }, a raw wrapper ' +
|
|
627
|
+
`such as { raw: "> " }, or surround the node with tags: ${sandwich}.`);
|
|
628
|
+
}
|
|
629
|
+
/** MDAST node types whose `value` Rust can set in place via setProperty. A
|
|
630
|
+
* visitor returning one of these as `{type, value}` with no other fields
|
|
631
|
+
* skips a full arena rebuild. */
|
|
632
|
+
const MDAST_VALUE_ONLY_TYPES = new Set([
|
|
633
|
+
"text",
|
|
634
|
+
"html",
|
|
635
|
+
"inlineCode",
|
|
636
|
+
"yaml",
|
|
637
|
+
"toml",
|
|
638
|
+
"inlineMath",
|
|
639
|
+
]);
|
|
640
|
+
/** True when the visitor returned a same-type MDAST node carrying only `type`
|
|
641
|
+
* + `value`. Any other field present (children, position, data, lang, meta)
|
|
642
|
+
* falls back to the full replace path so nothing is silently dropped. */
|
|
643
|
+
function isMdastTextValueSwap(result, original) {
|
|
644
|
+
if (original === undefined)
|
|
645
|
+
return false;
|
|
646
|
+
if (result.type !== original.type)
|
|
647
|
+
return false;
|
|
648
|
+
if (!MDAST_VALUE_ONLY_TYPES.has(result.type))
|
|
649
|
+
return false;
|
|
650
|
+
const r = result;
|
|
651
|
+
if (typeof r.value !== "string")
|
|
652
|
+
return false;
|
|
653
|
+
return (r.children === undefined &&
|
|
654
|
+
r.position === undefined &&
|
|
655
|
+
r.data === undefined &&
|
|
656
|
+
r.lang === undefined &&
|
|
657
|
+
r.meta === undefined);
|
|
507
658
|
}
|
|
508
659
|
/** A result that is the same object as the input node is a no-op, so context
|
|
509
660
|
* mutations (e.g. setProperty) are not clobbered. */
|
|
510
|
-
function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
|
|
661
|
+
function applyMdastVisitResult(result, nodeId, returnBuffer, refs, originalNode) {
|
|
511
662
|
if (result === undefined || result === null)
|
|
512
663
|
return;
|
|
513
664
|
if (result === originalNode)
|
|
@@ -520,9 +671,15 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
|
|
|
520
671
|
case "raw_html":
|
|
521
672
|
returnBuffer.replace(nodeId, result);
|
|
522
673
|
break;
|
|
523
|
-
case "structured_node":
|
|
524
|
-
|
|
674
|
+
case "structured_node": {
|
|
675
|
+
const node = result;
|
|
676
|
+
if (isMdastTextValueSwap(node, originalNode)) {
|
|
677
|
+
returnBuffer.setProperty(nodeId, "value", node.value);
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
emitMdastTree(returnBuffer, "replace", nodeId, node, true, refs);
|
|
525
681
|
break;
|
|
682
|
+
}
|
|
526
683
|
}
|
|
527
684
|
}
|
|
528
685
|
/**
|
|
@@ -532,12 +689,12 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
|
|
|
532
689
|
* Returns MdastVisitResult synchronously if all visitors are sync,
|
|
533
690
|
* or Promise<MdastVisitResult> if any visitor is async.
|
|
534
691
|
*/
|
|
535
|
-
export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {}, sourceFormat = "markdown") {
|
|
692
|
+
export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
|
|
536
693
|
const getSource = typeof source === "function" ? source : () => source;
|
|
537
694
|
const resolver = new MdastLazyChildResolver(handle);
|
|
538
|
-
const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data, sourceFormat);
|
|
539
|
-
const returnBuffer =
|
|
540
|
-
const rustSubs =
|
|
695
|
+
const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data, sourceFormat, diagnostics);
|
|
696
|
+
const returnBuffer = acquireCommandBuffer();
|
|
697
|
+
const rustSubs = getMdastRustSubs(plugin);
|
|
541
698
|
const matchBuf = walkMdastHandle(handle, rustSubs);
|
|
542
699
|
const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
|
|
543
700
|
const matchCount = ru32(matchView, 0);
|
|
@@ -555,25 +712,44 @@ export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {
|
|
|
555
712
|
deferred.push({ nodeId, promise: result, originalNode: node });
|
|
556
713
|
}
|
|
557
714
|
else {
|
|
558
|
-
applyMdastVisitResult(result, nodeId, returnBuffer, node);
|
|
715
|
+
applyMdastVisitResult(result, nodeId, returnBuffer, resolver.refs, node);
|
|
559
716
|
}
|
|
560
717
|
}
|
|
561
718
|
if (deferred) {
|
|
562
719
|
return Promise.all(deferred.map((d) => d.promise.then((r) => ({ nodeId: d.nodeId, result: r, originalNode: d.originalNode })))).then((results) => {
|
|
563
720
|
for (const { nodeId, result, originalNode } of results) {
|
|
564
|
-
applyMdastVisitResult(result, nodeId, returnBuffer, originalNode);
|
|
721
|
+
applyMdastVisitResult(result, nodeId, returnBuffer, resolver.refs, originalNode);
|
|
565
722
|
}
|
|
566
|
-
// End of the pass — the caller applies the returned command buffer next,
|
|
567
|
-
// renumbering the arena, so later snapshots would resolve match-time
|
|
568
|
-
// child ids against wrong nodes. This is the last point we control.
|
|
569
|
-
resolver.seal();
|
|
570
723
|
return finalizeMdastVisit(handle, context, returnBuffer);
|
|
571
724
|
});
|
|
572
725
|
}
|
|
573
|
-
|
|
726
|
+
return finalizeMdastVisit(handle, context, returnBuffer);
|
|
727
|
+
}
|
|
728
|
+
const MDAST_ROOT_SUBS = [
|
|
729
|
+
{ nodeType: MDAST_ROOT, tagFilter: [] },
|
|
730
|
+
];
|
|
731
|
+
/** Its own pass so the caller can apply what `before` queued before the
|
|
732
|
+
* visitors walk, and the visitors' mutations before `after` reads the tree. */
|
|
733
|
+
export function visitMdastHook(handle, plugin, hook, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
|
|
734
|
+
const getSource = typeof source === "function" ? source : () => source;
|
|
735
|
+
const resolver = new MdastLazyChildResolver(handle);
|
|
736
|
+
const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data, sourceFormat, diagnostics);
|
|
737
|
+
const returnBuffer = acquireCommandBuffer();
|
|
738
|
+
const matchBuf = walkMdastHandle(handle, MDAST_ROOT_SUBS);
|
|
739
|
+
const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
|
|
740
|
+
if (ru32(matchView, 0) === 0)
|
|
741
|
+
return finalizeMdastVisit(handle, context, returnBuffer);
|
|
742
|
+
const root = readMdastMatchedNode(matchView, matchBuf, ru32(matchView, 10), ru32(matchView, 4), MDAST_ROOT, resolver);
|
|
743
|
+
const result = hook.call(plugin, root, context);
|
|
744
|
+
if (result instanceof Promise) {
|
|
745
|
+
return result.then(() => finalizeMdastVisit(handle, context, returnBuffer));
|
|
746
|
+
}
|
|
574
747
|
return finalizeMdastVisit(handle, context, returnBuffer);
|
|
575
748
|
}
|
|
576
749
|
function finalizeMdastVisit(handle, context, returnBuffer) {
|
|
577
750
|
const { merged, hasMutations } = mergeAndReset(returnBuffer, context);
|
|
751
|
+
// Return both buffers to the pool. Bytes were copied into `merged` above.
|
|
752
|
+
releaseCommandBuffer(returnBuffer);
|
|
753
|
+
releaseCommandBuffer(context.getCommandBuffer());
|
|
578
754
|
return { commandBuffer: merged, diagnostics: context.getDiagnostics(), hasMutations };
|
|
579
755
|
}
|