satteri 0.9.4 → 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 +295 -70
- 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 +55 -5
- package/dist/hast/hast-visitor.js +320 -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 +79 -32
- package/dist/mdast/mdast-visitor.js +305 -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 +44 -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 +24 -19
- package/dist/lazy-props.d.ts +0 -10
- package/dist/lazy-props.js +0 -50
- package/satteri_napi.wasi-browser.js +0 -83
- package/satteri_napi.wasi.cjs +0 -134
- package/wasi-worker-browser.mjs +0 -36
- package/wasi-worker.mjs +0 -63
|
@@ -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;
|
|
@@ -49,12 +51,21 @@ export class MdastVisitorContext {
|
|
|
49
51
|
* instances. Returned to the caller as `result.data`.
|
|
50
52
|
*/
|
|
51
53
|
data;
|
|
52
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The source format this compile is processing: `"markdown"` for a plain
|
|
56
|
+
* Markdown compile, `"mdx"` for an MDX one. Lets a plugin shared between both
|
|
57
|
+
* pipelines branch on which it is handling.
|
|
58
|
+
*/
|
|
59
|
+
sourceFormat;
|
|
60
|
+
constructor(handle, getSource, fileURL, resolver, data, sourceFormat, diagnostics) {
|
|
53
61
|
this.#handle = handle;
|
|
54
62
|
this.#getSource = getSource;
|
|
55
63
|
this.fileURL = fileURL;
|
|
56
64
|
this.#resolver = resolver;
|
|
65
|
+
this.#refs = resolver.refs;
|
|
57
66
|
this.data = data;
|
|
67
|
+
this.sourceFormat = sourceFormat;
|
|
68
|
+
this.#diagnostics = diagnostics;
|
|
58
69
|
}
|
|
59
70
|
get source() {
|
|
60
71
|
const value = this.#getSource();
|
|
@@ -62,39 +73,44 @@ export class MdastVisitorContext {
|
|
|
62
73
|
return value;
|
|
63
74
|
}
|
|
64
75
|
removeNode(node) {
|
|
65
|
-
this.#commandBuffer.removeNode(requireNid(node, "removeNode"));
|
|
76
|
+
this.#commandBuffer.removeNode(requireNid(node, "removeNode", this.#refs));
|
|
66
77
|
}
|
|
67
78
|
insertBefore(node, newNode) {
|
|
68
|
-
const id = requireNid(node, "insertBefore");
|
|
79
|
+
const id = requireNid(node, "insertBefore", this.#refs);
|
|
69
80
|
for (const n of asArray(newNode))
|
|
70
|
-
emitMdastTree(this.#commandBuffer, "insertBefore", id, n);
|
|
81
|
+
emitMdastTree(this.#commandBuffer, "insertBefore", id, n, false, this.#refs);
|
|
71
82
|
}
|
|
72
83
|
insertAfter(node, newNode) {
|
|
73
|
-
const id = requireNid(node, "insertAfter");
|
|
84
|
+
const id = requireNid(node, "insertAfter", this.#refs);
|
|
74
85
|
for (const n of asArray(newNode))
|
|
75
|
-
emitMdastTree(this.#commandBuffer, "insertAfter", id, n);
|
|
86
|
+
emitMdastTree(this.#commandBuffer, "insertAfter", id, n, false, this.#refs);
|
|
76
87
|
}
|
|
77
88
|
/**
|
|
78
89
|
* Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any
|
|
79
|
-
* 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.
|
|
80
95
|
*/
|
|
81
96
|
wrapNode(node, parentNode) {
|
|
82
|
-
const id = requireNid(node, "wrapNode");
|
|
83
|
-
|
|
97
|
+
const id = requireNid(node, "wrapNode", this.#refs);
|
|
98
|
+
assertMdastWrapParent(parentNode);
|
|
99
|
+
emitMdastTree(this.#commandBuffer, "wrapNode", id, parentNode, false, this.#refs);
|
|
84
100
|
}
|
|
85
101
|
prependChild(node, childNode) {
|
|
86
|
-
const id = requireNid(node, "prependChild");
|
|
102
|
+
const id = requireNid(node, "prependChild", this.#refs);
|
|
87
103
|
for (const n of asArray(childNode))
|
|
88
|
-
emitMdastTree(this.#commandBuffer, "prependChild", id, n);
|
|
104
|
+
emitMdastTree(this.#commandBuffer, "prependChild", id, n, false, this.#refs);
|
|
89
105
|
}
|
|
90
106
|
appendChild(node, childNode) {
|
|
91
|
-
const id = requireNid(node, "appendChild");
|
|
107
|
+
const id = requireNid(node, "appendChild", this.#refs);
|
|
92
108
|
for (const n of asArray(childNode))
|
|
93
|
-
emitMdastTree(this.#commandBuffer, "appendChild", id, n);
|
|
109
|
+
emitMdastTree(this.#commandBuffer, "appendChild", id, n, false, this.#refs);
|
|
94
110
|
}
|
|
95
111
|
/** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */
|
|
96
112
|
insertChildAt(node, index, childNode) {
|
|
97
|
-
const children = "children" in node ? node.children : [];
|
|
113
|
+
const children = ("children" in node ? node.children : undefined) ?? [];
|
|
98
114
|
if (index <= 0 || children.length === 0) {
|
|
99
115
|
this.prependChild(node, childNode);
|
|
100
116
|
}
|
|
@@ -107,38 +123,70 @@ export class MdastVisitorContext {
|
|
|
107
123
|
}
|
|
108
124
|
/** Remove the `index`-th child of `node`; a no-op when there is no such child. */
|
|
109
125
|
removeChildAt(node, index) {
|
|
110
|
-
const child = "children" in node ? node.children[index] : undefined;
|
|
126
|
+
const child = "children" in node ? node.children?.[index] : undefined;
|
|
111
127
|
if (child)
|
|
112
128
|
this.removeNode(child);
|
|
113
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
|
+
*/
|
|
114
136
|
replaceNode(node, newNode) {
|
|
115
|
-
const id = requireNid(node, "replaceNode");
|
|
116
|
-
|
|
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);
|
|
117
166
|
}
|
|
118
167
|
setProperty(node, key, value) {
|
|
119
168
|
if (key === "children") {
|
|
120
169
|
// children is structural: set-children keeps the node and swaps only its
|
|
121
170
|
// child list (reused children keep their id).
|
|
122
|
-
const id = requireNid(node, "setProperty");
|
|
123
|
-
|
|
124
|
-
if (!ops)
|
|
171
|
+
const id = requireNid(node, "setProperty", this.#refs);
|
|
172
|
+
if (!emitMdastChildrenCommand(this.#commandBuffer, id, value, this.#refs)) {
|
|
125
173
|
throw unencodableContentError(value);
|
|
126
|
-
|
|
174
|
+
}
|
|
127
175
|
return;
|
|
128
176
|
}
|
|
129
177
|
if (key === "data") {
|
|
130
178
|
// data is stored as JSON in the arena, serialize it for the command buffer
|
|
131
|
-
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);
|
|
132
180
|
return;
|
|
133
181
|
}
|
|
134
|
-
this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value);
|
|
182
|
+
this.#commandBuffer.setProperty(requireNid(node, "setProperty", this.#refs), key, value);
|
|
135
183
|
}
|
|
136
184
|
/** Collect the concatenated text of all descendant text nodes (like mdast-util-to-string). */
|
|
137
185
|
textContent(node, options) {
|
|
138
|
-
return mdastTextContentHandle(this.#handle, requireNid(node, "textContent"), options);
|
|
186
|
+
return mdastTextContentHandle(this.#handle, requireNid(node, "textContent", this.#refs), options);
|
|
139
187
|
}
|
|
140
188
|
parent(node) {
|
|
141
|
-
const parentId = this.#resolver.parentIdOf(requireNid(node, "parent"));
|
|
189
|
+
const parentId = this.#resolver.parentIdOf(requireNid(node, "parent", this.#refs));
|
|
142
190
|
if (parentId === undefined)
|
|
143
191
|
return undefined;
|
|
144
192
|
const byId = (this.#parentsById ??= new Map());
|
|
@@ -154,12 +202,13 @@ export class MdastVisitorContext {
|
|
|
154
202
|
* Use this rather than `parent.children.indexOf(node)`, which won't find it.
|
|
155
203
|
*/
|
|
156
204
|
indexOf(node) {
|
|
157
|
-
return this.#resolver.indexInParent(requireNid(node, "indexOf"));
|
|
205
|
+
return this.#resolver.indexInParent(requireNid(node, "indexOf", this.#refs));
|
|
158
206
|
}
|
|
159
207
|
report({ message, node, severity = "error", }) {
|
|
208
|
+
const id = node ? nid(node, this.#refs) : undefined;
|
|
160
209
|
this.#diagnostics.push({
|
|
161
210
|
message,
|
|
162
|
-
nodeId:
|
|
211
|
+
nodeId: id === FOREIGN_REF ? undefined : id,
|
|
163
212
|
position: node?.position,
|
|
164
213
|
severity,
|
|
165
214
|
});
|
|
@@ -172,7 +221,24 @@ export class MdastVisitorContext {
|
|
|
172
221
|
return this.#diagnostics;
|
|
173
222
|
}
|
|
174
223
|
}
|
|
224
|
+
const mdastSubscriptionCache = new WeakMap();
|
|
175
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) {
|
|
176
242
|
const subs = [];
|
|
177
243
|
for (const [name, fn] of Object.entries(plugin)) {
|
|
178
244
|
if (VISITOR_KEYS.has(name) && typeof fn === "function") {
|
|
@@ -185,14 +251,19 @@ export function resolveMdastSubscriptions(plugin) {
|
|
|
185
251
|
}
|
|
186
252
|
}
|
|
187
253
|
}
|
|
188
|
-
|
|
254
|
+
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: [] }));
|
|
255
|
+
return { subs, rustSubs };
|
|
189
256
|
}
|
|
257
|
+
const MDAST_EPOCH_CACHE = registerEpochCacheSlot(new WeakMap());
|
|
190
258
|
class MdastLazyChildResolver extends LazyChildResolver {
|
|
259
|
+
cacheSlot() {
|
|
260
|
+
return MDAST_EPOCH_CACHE;
|
|
261
|
+
}
|
|
191
262
|
createReader(wire) {
|
|
192
263
|
return new MdastReader(wire);
|
|
193
264
|
}
|
|
194
|
-
materializeNode(reader, nodeId) {
|
|
195
|
-
return materializeNode(reader, nodeId);
|
|
265
|
+
materializeNode(reader, nodeId, refs) {
|
|
266
|
+
return materializeNode(reader, nodeId, true, refs);
|
|
196
267
|
}
|
|
197
268
|
readParentId(reader, nodeId) {
|
|
198
269
|
return reader.getParentId(nodeId);
|
|
@@ -202,11 +273,18 @@ class MdastLazyChildResolver extends LazyChildResolver {
|
|
|
202
273
|
}
|
|
203
274
|
}
|
|
204
275
|
/** Build the child-stub list for a matched node from the wire's `[child_ids]
|
|
205
|
-
* [child_types]` blocks
|
|
206
|
-
*
|
|
207
|
-
*
|
|
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. */
|
|
208
279
|
function readMdastChildStubs(view, buf, idsPos, typesPos, count, resolver) {
|
|
209
|
-
|
|
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
|
+
}
|
|
210
288
|
const stubs = new Array(count);
|
|
211
289
|
for (let i = 0; i < count; i++) {
|
|
212
290
|
stubs[i] = new MdastChildStub(resolver, ru32(view, idsPos + i * 4), buf[typesPos + i]);
|
|
@@ -293,82 +371,124 @@ function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver)
|
|
|
293
371
|
node.spread = buf[pos + 1] !== 0;
|
|
294
372
|
break;
|
|
295
373
|
}
|
|
374
|
+
case 37: {
|
|
375
|
+
// descriptionDetails
|
|
376
|
+
node.spread = buf[pos] !== 0;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
296
379
|
// table (21), directives (30/31/32) and mdxJsx elements (100/101) are
|
|
297
380
|
// decoded by the generated `decodeMdastTypeData` from their tails.
|
|
298
381
|
// root(0), paragraph(1), thematicBreak(3), blockquote(4), emphasis(11),
|
|
299
382
|
// strong(12), break(14), tableRow(22), tableCell(23), delete(24): no extra data
|
|
300
383
|
}
|
|
301
384
|
}
|
|
302
|
-
|
|
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);
|
|
303
400
|
if (initialData) {
|
|
304
401
|
node.data = initialData;
|
|
305
402
|
}
|
|
306
403
|
return node;
|
|
307
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;
|
|
308
410
|
/** The arena id of a node if it is an existing (materialized) node, else
|
|
309
411
|
* undefined for a freshly-built one. */
|
|
310
|
-
function reusedId(node) {
|
|
412
|
+
function reusedId(node, refs) {
|
|
311
413
|
if (node === null || typeof node !== "object")
|
|
312
414
|
return undefined;
|
|
313
|
-
const id = nid(node);
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
// Reused across every replacement in a pass: compile is synchronous and its
|
|
317
|
-
// result is copied into the command buffer before the next call, so a single
|
|
318
|
-
// writer is safe and avoids a 512-byte allocation per built node.
|
|
319
|
-
const mdastWriter = new OpWriter();
|
|
320
|
-
/**
|
|
321
|
-
* Compile a declarative MDAST replacement tree to the op-stream — the only
|
|
322
|
-
* structural encoding. Reused nodes (those still carrying an arena id) become
|
|
323
|
-
* `ref`s so the rebuild splices the original back in place. Returns null when
|
|
324
|
-
* the replay can't reproduce the tree identically (unsupported node type or
|
|
325
|
-
* out-of-range numeric field); the caller turns that into a hard error.
|
|
326
|
-
*/
|
|
327
|
-
function compileMdastToOpstream(root, forReplace = false) {
|
|
328
|
-
mdastWriter.begin();
|
|
329
|
-
try {
|
|
330
|
-
if (!emitMdastOp(mdastWriter, root, true, forReplace))
|
|
331
|
-
return null;
|
|
332
|
-
return mdastWriter.take();
|
|
333
|
-
}
|
|
334
|
-
finally {
|
|
335
|
-
mdastWriter.end();
|
|
336
|
-
}
|
|
415
|
+
const id = nid(node, refs);
|
|
416
|
+
return id !== undefined && id !== FOREIGN_REF ? id : undefined;
|
|
337
417
|
}
|
|
338
|
-
/**
|
|
418
|
+
/** Emit a set-children command in place: a root-wrapped child list, the shape
|
|
339
419
|
* `Patch::SetChildren` splices in. Reused children become refs. */
|
|
340
|
-
function
|
|
420
|
+
function emitMdastChildrenCommand(buffer, id, children, refs) {
|
|
341
421
|
if (!Array.isArray(children))
|
|
342
|
-
return
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
mdastWriter.open(NAME_TO_TYPE.root);
|
|
422
|
+
return false;
|
|
423
|
+
return buffer.emitOpstreamCommand(CMD_SET_CHILDREN, id, () => {
|
|
424
|
+
buffer.open(MDAST_ROOT);
|
|
346
425
|
for (const c of children) {
|
|
347
|
-
if (!emitMdastOp(
|
|
348
|
-
return
|
|
426
|
+
if (!emitMdastOp(buffer, c, false, false, refs))
|
|
427
|
+
return false;
|
|
349
428
|
}
|
|
350
|
-
|
|
351
|
-
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();
|
|
352
445
|
}
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
}
|
|
355
453
|
}
|
|
454
|
+
w.close();
|
|
455
|
+
return true;
|
|
356
456
|
}
|
|
357
|
-
function emitMdastOp(w, node, isRoot, forReplace) {
|
|
457
|
+
function emitMdastOp(w, node, isRoot, forReplace, refs) {
|
|
358
458
|
if (node === null || typeof node !== "object")
|
|
359
459
|
return false;
|
|
360
460
|
if (!isRoot) {
|
|
361
|
-
const id = reusedId(node);
|
|
461
|
+
const id = reusedId(node, refs);
|
|
362
462
|
if (id !== undefined) {
|
|
363
463
|
w.ref(id);
|
|
364
464
|
return true;
|
|
365
465
|
}
|
|
366
466
|
}
|
|
367
467
|
const n = node;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
+
}
|
|
371
489
|
w.open(type);
|
|
490
|
+
if (isCustom)
|
|
491
|
+
w.str(OF_NAME, n.type);
|
|
372
492
|
if (typeof n.value === "string")
|
|
373
493
|
w.str(OF_VALUE, n.value);
|
|
374
494
|
if (typeof n.url === "string")
|
|
@@ -405,7 +525,7 @@ function emitMdastOp(w, node, isRoot, forReplace) {
|
|
|
405
525
|
w.bool(OF_ORDERED, n.ordered);
|
|
406
526
|
if (typeof n.spread === "boolean")
|
|
407
527
|
w.bool(OF_SPREAD, n.spread);
|
|
408
|
-
if (typeof n.name === "string")
|
|
528
|
+
if (!isCustom && typeof n.name === "string")
|
|
409
529
|
w.str(OF_NAME, n.name);
|
|
410
530
|
const attrs = n.attributes;
|
|
411
531
|
if (Array.isArray(attrs)) {
|
|
@@ -439,7 +559,7 @@ function emitMdastOp(w, node, isRoot, forReplace) {
|
|
|
439
559
|
const children = n.children;
|
|
440
560
|
if (Array.isArray(children)) {
|
|
441
561
|
for (const c of children)
|
|
442
|
-
if (!emitMdastOp(w, c, false, forReplace))
|
|
562
|
+
if (!emitMdastOp(w, c, false, forReplace, refs))
|
|
443
563
|
return false;
|
|
444
564
|
}
|
|
445
565
|
}
|
|
@@ -463,7 +583,7 @@ function isRawMdastContent(content) {
|
|
|
463
583
|
* op-stream is the only declarative encoding. The switches stay inline so the
|
|
464
584
|
* buffer calls are monomorphic (computed method names defeat inline caches on
|
|
465
585
|
* this warm path). */
|
|
466
|
-
function emitMdastTree(buffer, op, id, content, forReplace
|
|
586
|
+
function emitMdastTree(buffer, op, id, content, forReplace, refs) {
|
|
467
587
|
if (isRawMdastContent(content)) {
|
|
468
588
|
switch (op) {
|
|
469
589
|
case "replace":
|
|
@@ -480,27 +600,65 @@ function emitMdastTree(buffer, op, id, content, forReplace = false) {
|
|
|
480
600
|
return buffer.wrapNode(id, content);
|
|
481
601
|
}
|
|
482
602
|
}
|
|
483
|
-
const
|
|
484
|
-
if (
|
|
603
|
+
const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD[op], id, () => emitMdastOp(buffer, content, true, forReplace, refs));
|
|
604
|
+
if (!ok)
|
|
485
605
|
throw unencodableContentError(content);
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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.");
|
|
499
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);
|
|
500
658
|
}
|
|
501
659
|
/** A result that is the same object as the input node is a no-op, so context
|
|
502
660
|
* mutations (e.g. setProperty) are not clobbered. */
|
|
503
|
-
function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
|
|
661
|
+
function applyMdastVisitResult(result, nodeId, returnBuffer, refs, originalNode) {
|
|
504
662
|
if (result === undefined || result === null)
|
|
505
663
|
return;
|
|
506
664
|
if (result === originalNode)
|
|
@@ -513,9 +671,15 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
|
|
|
513
671
|
case "raw_html":
|
|
514
672
|
returnBuffer.replace(nodeId, result);
|
|
515
673
|
break;
|
|
516
|
-
case "structured_node":
|
|
517
|
-
|
|
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);
|
|
518
681
|
break;
|
|
682
|
+
}
|
|
519
683
|
}
|
|
520
684
|
}
|
|
521
685
|
/**
|
|
@@ -525,12 +689,12 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
|
|
|
525
689
|
* Returns MdastVisitResult synchronously if all visitors are sync,
|
|
526
690
|
* or Promise<MdastVisitResult> if any visitor is async.
|
|
527
691
|
*/
|
|
528
|
-
export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {}) {
|
|
692
|
+
export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
|
|
529
693
|
const getSource = typeof source === "function" ? source : () => source;
|
|
530
694
|
const resolver = new MdastLazyChildResolver(handle);
|
|
531
|
-
const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data);
|
|
532
|
-
const returnBuffer =
|
|
533
|
-
const rustSubs =
|
|
695
|
+
const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data, sourceFormat, diagnostics);
|
|
696
|
+
const returnBuffer = acquireCommandBuffer();
|
|
697
|
+
const rustSubs = getMdastRustSubs(plugin);
|
|
534
698
|
const matchBuf = walkMdastHandle(handle, rustSubs);
|
|
535
699
|
const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
|
|
536
700
|
const matchCount = ru32(matchView, 0);
|
|
@@ -548,25 +712,44 @@ export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {
|
|
|
548
712
|
deferred.push({ nodeId, promise: result, originalNode: node });
|
|
549
713
|
}
|
|
550
714
|
else {
|
|
551
|
-
applyMdastVisitResult(result, nodeId, returnBuffer, node);
|
|
715
|
+
applyMdastVisitResult(result, nodeId, returnBuffer, resolver.refs, node);
|
|
552
716
|
}
|
|
553
717
|
}
|
|
554
718
|
if (deferred) {
|
|
555
719
|
return Promise.all(deferred.map((d) => d.promise.then((r) => ({ nodeId: d.nodeId, result: r, originalNode: d.originalNode })))).then((results) => {
|
|
556
720
|
for (const { nodeId, result, originalNode } of results) {
|
|
557
|
-
applyMdastVisitResult(result, nodeId, returnBuffer, originalNode);
|
|
721
|
+
applyMdastVisitResult(result, nodeId, returnBuffer, resolver.refs, originalNode);
|
|
558
722
|
}
|
|
559
|
-
// End of the pass — the caller applies the returned command buffer next,
|
|
560
|
-
// renumbering the arena, so later snapshots would resolve match-time
|
|
561
|
-
// child ids against wrong nodes. This is the last point we control.
|
|
562
|
-
resolver.seal();
|
|
563
723
|
return finalizeMdastVisit(handle, context, returnBuffer);
|
|
564
724
|
});
|
|
565
725
|
}
|
|
566
|
-
|
|
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
|
+
}
|
|
567
747
|
return finalizeMdastVisit(handle, context, returnBuffer);
|
|
568
748
|
}
|
|
569
749
|
function finalizeMdastVisit(handle, context, returnBuffer) {
|
|
570
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());
|
|
571
754
|
return { commandBuffer: merged, diagnostics: context.getDiagnostics(), hasMutations };
|
|
572
755
|
}
|