satteri 0.10.4 → 0.11.1
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 +99 -0
- package/dist/compile-types.d.ts +330 -0
- package/dist/compile-types.js +1 -0
- package/dist/compile.d.ts +22 -328
- package/dist/compile.js +129 -379
- 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.js +0 -5
- package/dist/generated/wire-constants.d.ts +3 -0
- package/dist/generated/wire-constants.js +33 -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 +197 -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 +188 -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 +1 -15
- package/dist/op-stream.js +3 -19
- 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 +8 -7
- package/dist/plugin.js +3 -9
- 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 +3 -12
- package/dist/string-pool.d.ts +0 -13
- package/dist/string-pool.js +0 -91
|
@@ -1,42 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { acquireCommandBuffer, releaseCommandBuffer, classifyReturn, CommandBuffer, STRUCTURAL_CMD, } from "../command-buffer.js";
|
|
1
|
+
import { mdastTextContentHandle, walkMdastHandle } from "#binding";
|
|
2
|
+
import { acquireCommandBuffer, classifyReturn, CommandBuffer, STRUCTURAL_CMD, } from "../command-buffer.js";
|
|
4
3
|
import { CMD_SET_CHILDREN } from "../generated/wire-constants.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
// Genuine stubs carry their id as a plain field; a spread copy is not
|
|
15
|
-
// `instanceof` and has no `_nodeId`, so it correctly reads as new content.
|
|
16
|
-
if (node instanceof MdastChildStub)
|
|
17
|
-
return node._refs === refs ? node._id : FOREIGN_REF;
|
|
18
|
-
const id = refs.get(node);
|
|
19
|
-
if (id !== undefined)
|
|
20
|
-
return id;
|
|
21
|
-
// Plain objects are trusted only via this tree's refs or a NON-enumerable
|
|
22
|
-
// `_nodeId` (the materializer's convention, which spread cannot copy) — an
|
|
23
|
-
// enumerable one rode in on a copy and must read as new content.
|
|
24
|
-
const d = Object.getOwnPropertyDescriptor(node, "_nodeId");
|
|
25
|
-
if (d !== undefined && !d.enumerable)
|
|
26
|
-
return FOREIGN_REF;
|
|
27
|
-
return crossPipelineForeign(node);
|
|
28
|
-
}
|
|
29
|
-
const requireNid = makeRequireNid(nid);
|
|
4
|
+
import { LazyChildResolver } from "../lazy-child-resolver.js";
|
|
5
|
+
import { emitMdxAttr, OF_ALT, OF_CHECKED, OF_DEPTH, OF_EXPLICIT, OF_IDENTIFIER, OF_LABEL, OF_LANG, OF_META, OF_NAME, OF_ORDERED, OF_REFERENCE_TYPE, OF_SPREAD, OF_START, OF_TITLE, OF_URL, OF_VALUE, OpWriter, PROP_STRING, } from "../op-stream.js";
|
|
6
|
+
import { asArray, collectCommands, FOREIGN_REF, foreignContentError, makeRequireNid, requireRootReplacement, ROOT_NODE_ID, rootReplacementError, unencodableContentError, withMdxJsxAttribute, } from "../visitor-shared.js";
|
|
7
|
+
import { ru32 } from "../wire-read.js";
|
|
8
|
+
import { MDAST_OPSTREAM_TYPES, NAME_TO_TYPE, VISITOR_KEYS } from "./generated/node-types.js";
|
|
9
|
+
import { LEAF_TYPES } from "./mdast-materializer.js";
|
|
10
|
+
import { MdastReader } from "./mdast-reader.js";
|
|
11
|
+
import { getNodeId, MdastLazyChildResolver, readMdastMatchedNode } from "./mdast-walk.js";
|
|
12
|
+
const requireNid = makeRequireNid(getNodeId);
|
|
30
13
|
export class MdastVisitorContext {
|
|
31
14
|
#commandBuffer = acquireCommandBuffer();
|
|
32
15
|
#diagnostics;
|
|
16
|
+
#pendingNodes = new Map();
|
|
33
17
|
#handle;
|
|
34
18
|
#getSource;
|
|
35
19
|
#resolver;
|
|
36
20
|
#refs;
|
|
37
|
-
/** One canonical object per parent id, so visitors can dedupe by identity.
|
|
38
|
-
* Null until the first `parent()` call; most passes never make one. */
|
|
39
|
-
#parentsById = null;
|
|
40
21
|
/**
|
|
41
22
|
* The URL of the document being processed (the compile `fileURL` option),
|
|
42
23
|
* or `undefined` when none was given. Use `fileURLToPath(ctx.fileURL)` for a
|
|
@@ -73,17 +54,20 @@ export class MdastVisitorContext {
|
|
|
73
54
|
return value;
|
|
74
55
|
}
|
|
75
56
|
removeNode(node) {
|
|
76
|
-
|
|
57
|
+
const id = requireNid(node, "removeNode", this.#refs);
|
|
58
|
+
this.#commandBuffer.removeNode(id);
|
|
59
|
+
this.#pendingNodes.delete(id);
|
|
77
60
|
}
|
|
78
61
|
insertBefore(node, newNode) {
|
|
79
|
-
|
|
80
|
-
for (const n of asArray(newNode))
|
|
81
|
-
emitMdastTree(this.#commandBuffer, "insertBefore", id, n, false, this.#refs);
|
|
62
|
+
this.#splice(requireNid(node, "insertBefore", this.#refs), newNode, "insertBefore");
|
|
82
63
|
}
|
|
83
64
|
insertAfter(node, newNode) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
65
|
+
this.#splice(requireNid(node, "insertAfter", this.#refs), newNode, "insertAfter");
|
|
66
|
+
}
|
|
67
|
+
#splice(anchorId, content, op) {
|
|
68
|
+
for (const n of asArray(content)) {
|
|
69
|
+
emitMdastTree(this.#commandBuffer, op, anchorId, n, false, this.#refs, true);
|
|
70
|
+
}
|
|
87
71
|
}
|
|
88
72
|
/**
|
|
89
73
|
* Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any
|
|
@@ -99,27 +83,20 @@ export class MdastVisitorContext {
|
|
|
99
83
|
emitMdastTree(this.#commandBuffer, "wrapNode", id, parentNode, false, this.#refs);
|
|
100
84
|
}
|
|
101
85
|
prependChild(node, childNode) {
|
|
102
|
-
|
|
103
|
-
for (const n of asArray(childNode))
|
|
104
|
-
emitMdastTree(this.#commandBuffer, "prependChild", id, n, false, this.#refs);
|
|
86
|
+
this.#splice(requireNid(node, "prependChild", this.#refs), childNode, "prependChild");
|
|
105
87
|
}
|
|
106
88
|
appendChild(node, childNode) {
|
|
107
|
-
|
|
108
|
-
for (const n of asArray(childNode))
|
|
109
|
-
emitMdastTree(this.#commandBuffer, "appendChild", id, n, false, this.#refs);
|
|
89
|
+
this.#splice(requireNid(node, "appendChild", this.#refs), childNode, "appendChild");
|
|
110
90
|
}
|
|
111
91
|
/** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */
|
|
112
92
|
insertChildAt(node, index, childNode) {
|
|
113
93
|
const children = ("children" in node ? node.children : undefined) ?? [];
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
else {
|
|
121
|
-
this.insertBefore(children[index], childNode);
|
|
122
|
-
}
|
|
94
|
+
const [anchor, op] = index <= 0 || children.length === 0
|
|
95
|
+
? [node, "prependChild"]
|
|
96
|
+
: index >= children.length
|
|
97
|
+
? [node, "appendChild"]
|
|
98
|
+
: [children[index], "insertBefore"];
|
|
99
|
+
this.#splice(requireNid(anchor, "insertChildAt", this.#refs), childNode, op);
|
|
123
100
|
}
|
|
124
101
|
/** Remove the `index`-th child of `node`; a no-op when there is no such child. */
|
|
125
102
|
removeChildAt(node, index) {
|
|
@@ -135,26 +112,46 @@ export class MdastVisitorContext {
|
|
|
135
112
|
*/
|
|
136
113
|
replaceNode(node, newNode) {
|
|
137
114
|
const id = requireNid(node, "replaceNode", this.#refs);
|
|
115
|
+
const onlyReplacement = Array.isArray(newNode)
|
|
116
|
+
? newNode.length === 1
|
|
117
|
+
? newNode[0]
|
|
118
|
+
: undefined
|
|
119
|
+
: newNode;
|
|
120
|
+
if (id === ROOT_NODE_ID &&
|
|
121
|
+
onlyReplacement !== undefined &&
|
|
122
|
+
reusedId(onlyReplacement, this.#refs) === id) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
138
125
|
if (Array.isArray(newNode)) {
|
|
139
126
|
if (id === ROOT_NODE_ID && newNode.length > 1)
|
|
140
127
|
throw rootReplacementError(newNode);
|
|
141
|
-
//
|
|
128
|
+
// One command, so the node's replacement is its whole slot and a ref back
|
|
129
|
+
// to it resolves to all of it rather than to the last element.
|
|
130
|
+
if (id !== ROOT_NODE_ID && newNode.length > 1 && newNode.every(isPlainReplacement)) {
|
|
131
|
+
emitMdastMultiReplace(this.#commandBuffer, id, newNode, this.#refs);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
142
134
|
let previous;
|
|
143
135
|
for (const n of newNode) {
|
|
144
136
|
if (previous !== undefined) {
|
|
145
|
-
emitMdastTree(this.#commandBuffer, "insertBefore", id, previous, false, this.#refs);
|
|
137
|
+
emitMdastTree(this.#commandBuffer, "insertBefore", id, previous, false, this.#refs, true);
|
|
146
138
|
}
|
|
147
139
|
previous = n;
|
|
148
140
|
}
|
|
149
141
|
if (previous === undefined) {
|
|
150
|
-
// Replacing with nothing drops the node, like removeNode.
|
|
151
142
|
this.removeNode(node);
|
|
152
143
|
}
|
|
153
144
|
else if (id === ROOT_NODE_ID && !isRawMdastContent(previous)) {
|
|
154
145
|
emitMdastRootReplace(this.#commandBuffer, requireRootReplacement(previous), this.#refs);
|
|
155
146
|
}
|
|
156
147
|
else {
|
|
157
|
-
emitMdastTree(this.#commandBuffer, "replace", id, previous, true, this.#refs);
|
|
148
|
+
emitMdastTree(this.#commandBuffer, "replace", id, previous, true, this.#refs, true);
|
|
149
|
+
}
|
|
150
|
+
if (previous !== undefined && !isRawMdastContent(previous)) {
|
|
151
|
+
this.#pendingNodes.set(id, previous);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
this.#pendingNodes.delete(id);
|
|
158
155
|
}
|
|
159
156
|
return;
|
|
160
157
|
}
|
|
@@ -162,24 +159,84 @@ export class MdastVisitorContext {
|
|
|
162
159
|
emitMdastRootReplace(this.#commandBuffer, requireRootReplacement(newNode), this.#refs);
|
|
163
160
|
return;
|
|
164
161
|
}
|
|
165
|
-
|
|
162
|
+
if (isRawMdastContent(newNode))
|
|
163
|
+
this.#pendingNodes.delete(id);
|
|
164
|
+
else
|
|
165
|
+
this.#pendingNodes.set(id, newNode);
|
|
166
|
+
emitMdastTree(this.#commandBuffer, "replace", id, newNode, true, this.#refs, true);
|
|
167
|
+
}
|
|
168
|
+
setField(node, key, value) {
|
|
169
|
+
const id = requireNid(node, "setField", this.#refs);
|
|
170
|
+
if (this.#foldPendingField(node, id, key, value))
|
|
171
|
+
return;
|
|
172
|
+
if (key === "children") {
|
|
173
|
+
if (!emitMdastChildrenCommand(this.#commandBuffer, id, value, this.#refs)) {
|
|
174
|
+
throw unencodableContentError(value);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if ((key === "attributes" && (isMdxJsxElement(node) || isDirective(node))) ||
|
|
179
|
+
(key === "align" && node.type === "table")) {
|
|
180
|
+
// These are structured node fields, so re-encode the node rather than a scalar command.
|
|
181
|
+
this.replaceNode(node, { ...node, [key]: value });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (key === "data")
|
|
185
|
+
value = value != null ? JSON.stringify(value) : null;
|
|
186
|
+
this.#commandBuffer.setField(id, key, value);
|
|
187
|
+
}
|
|
188
|
+
setAttribute(node, name, value) {
|
|
189
|
+
const id = requireNid(node, "setAttribute", this.#refs);
|
|
190
|
+
const target = this.#pendingNodes.get(id) ?? node;
|
|
191
|
+
if (isDirective(target) && typeof value !== "string") {
|
|
192
|
+
throw new TypeError("setAttribute: directive attributes must be strings");
|
|
193
|
+
}
|
|
194
|
+
if (this.#foldPendingAttribute(node, id, name, value))
|
|
195
|
+
return;
|
|
196
|
+
this.#commandBuffer.setAttribute(id, name, value);
|
|
197
|
+
}
|
|
198
|
+
/** A queued replacement would discard the field, so fold it into a fresh replacement. */
|
|
199
|
+
#foldPendingField(node, id, key, value) {
|
|
200
|
+
const pending = this.#pendingNodes.get(id);
|
|
201
|
+
if (pending === undefined)
|
|
202
|
+
return false;
|
|
203
|
+
this.replaceNode(node, { ...pending, [key]: value });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
/** A queued replacement would discard the attribute, so fold it in instead. */
|
|
207
|
+
#foldPendingAttribute(node, id, name, value) {
|
|
208
|
+
const pending = this.#pendingNodes.get(id);
|
|
209
|
+
if (pending === undefined)
|
|
210
|
+
return false;
|
|
211
|
+
if (isDirective(pending)) {
|
|
212
|
+
this.replaceNode(node, {
|
|
213
|
+
...pending,
|
|
214
|
+
attributes: { ...pending.attributes, [name]: value },
|
|
215
|
+
});
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (isMdxJsxElement(pending)) {
|
|
219
|
+
this.replaceNode(node, withMdxJsxAttribute(pending, name, value));
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
166
223
|
}
|
|
167
224
|
setProperty(node, key, value) {
|
|
225
|
+
const id = requireNid(node, "setProperty", this.#refs);
|
|
168
226
|
if (key === "children") {
|
|
169
|
-
// children is structural: set-children keeps the node and swaps only its
|
|
170
|
-
// child list (reused children keep their id).
|
|
171
|
-
const id = requireNid(node, "setProperty", this.#refs);
|
|
172
227
|
if (!emitMdastChildrenCommand(this.#commandBuffer, id, value, this.#refs)) {
|
|
173
228
|
throw unencodableContentError(value);
|
|
174
229
|
}
|
|
175
230
|
return;
|
|
176
231
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
this
|
|
232
|
+
const target = this.#pendingNodes.get(id) ?? node;
|
|
233
|
+
if (key === "attributes" && isMdxJsxElement(target)) {
|
|
234
|
+
this.replaceNode(node, { ...target, attributes: value });
|
|
180
235
|
return;
|
|
181
236
|
}
|
|
182
|
-
|
|
237
|
+
if (key === "data")
|
|
238
|
+
value = value != null ? JSON.stringify(value) : null;
|
|
239
|
+
this.#commandBuffer.setProperty(id, key, value);
|
|
183
240
|
}
|
|
184
241
|
/** Collect the concatenated text of all descendant text nodes (like mdast-util-to-string). */
|
|
185
242
|
textContent(node, options) {
|
|
@@ -189,13 +246,7 @@ export class MdastVisitorContext {
|
|
|
189
246
|
const parentId = this.#resolver.parentIdOf(requireNid(node, "parent", this.#refs));
|
|
190
247
|
if (parentId === undefined)
|
|
191
248
|
return undefined;
|
|
192
|
-
|
|
193
|
-
let parent = byId.get(parentId);
|
|
194
|
-
if (parent === undefined) {
|
|
195
|
-
parent = this.#resolver.materializeOne(parentId);
|
|
196
|
-
byId.set(parentId, parent);
|
|
197
|
-
}
|
|
198
|
-
return parent;
|
|
249
|
+
return this.#resolver.materializeOne(parentId);
|
|
199
250
|
}
|
|
200
251
|
/**
|
|
201
252
|
* Index of `node` within its parent's children, or `undefined` at the root.
|
|
@@ -205,7 +256,7 @@ export class MdastVisitorContext {
|
|
|
205
256
|
return this.#resolver.indexInParent(requireNid(node, "indexOf", this.#refs));
|
|
206
257
|
}
|
|
207
258
|
report({ message, node, severity = "error", }) {
|
|
208
|
-
const id = node ?
|
|
259
|
+
const id = node ? getNodeId(node, this.#refs) : undefined;
|
|
209
260
|
this.#diagnostics.push({
|
|
210
261
|
message,
|
|
211
262
|
nodeId: id === FOREIGN_REF ? undefined : id,
|
|
@@ -213,7 +264,6 @@ export class MdastVisitorContext {
|
|
|
213
264
|
severity,
|
|
214
265
|
});
|
|
215
266
|
}
|
|
216
|
-
/** Get the binary command buffer for all mutations recorded via context methods. */
|
|
217
267
|
getCommandBuffer() {
|
|
218
268
|
return this.#commandBuffer;
|
|
219
269
|
}
|
|
@@ -254,169 +304,14 @@ function buildMdastSubscriptions(plugin) {
|
|
|
254
304
|
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: [] }));
|
|
255
305
|
return { subs, rustSubs };
|
|
256
306
|
}
|
|
257
|
-
const MDAST_EPOCH_CACHE = registerEpochCacheSlot(new WeakMap());
|
|
258
|
-
class MdastLazyChildResolver extends LazyChildResolver {
|
|
259
|
-
cacheSlot() {
|
|
260
|
-
return MDAST_EPOCH_CACHE;
|
|
261
|
-
}
|
|
262
|
-
createReader(wire) {
|
|
263
|
-
return new MdastReader(wire);
|
|
264
|
-
}
|
|
265
|
-
materializeNode(reader, nodeId, refs) {
|
|
266
|
-
return materializeNode(reader, nodeId, true, refs);
|
|
267
|
-
}
|
|
268
|
-
readParentId(reader, nodeId) {
|
|
269
|
-
return reader.getParentId(nodeId);
|
|
270
|
-
}
|
|
271
|
-
readChildIds(reader, nodeId) {
|
|
272
|
-
return reader.getChildIds(nodeId);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
/** Build the child-stub list for a matched node from the wire's `[child_ids]
|
|
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. */
|
|
279
|
-
function readMdastChildStubs(view, buf, idsPos, typesPos, count, resolver) {
|
|
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
|
-
}
|
|
288
|
-
const stubs = new Array(count);
|
|
289
|
-
for (let i = 0; i < count; i++) {
|
|
290
|
-
stubs[i] = new MdastChildStub(resolver, ru32(view, idsPos + i * 4), buf[typesPos + i]);
|
|
291
|
-
}
|
|
292
|
-
return stubs;
|
|
293
|
-
}
|
|
294
|
-
/** Install `children` as an own enumerable getter (spread must carry it),
|
|
295
|
-
* self-replacing with the one stable stub array on first read. One closure
|
|
296
|
-
* and one define per node — installing the wire locals as hidden slots
|
|
297
|
-
* instead measurably regressed every matching pipeline. */
|
|
298
|
-
function makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver) {
|
|
299
|
-
Object.defineProperty(node, "children", {
|
|
300
|
-
get() {
|
|
301
|
-
const val = readMdastChildStubs(view, buf, childIdsPos, childTypesPos, childCount, resolver);
|
|
302
|
-
Object.defineProperty(this, "children", {
|
|
303
|
-
value: val,
|
|
304
|
-
writable: true,
|
|
305
|
-
enumerable: true,
|
|
306
|
-
configurable: true,
|
|
307
|
-
});
|
|
308
|
-
return val;
|
|
309
|
-
},
|
|
310
|
-
enumerable: true,
|
|
311
|
-
configurable: true,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Read an MDAST node from the inline data in a match buffer entry.
|
|
316
|
-
*
|
|
317
|
-
* Inline format (from Rust serialize_mdast_node_inline):
|
|
318
|
-
* [node_data: u32+bytes][position: 6×u32 = 24B][child_count: u32][child_ids: N×u32]
|
|
319
|
-
* [child_types: N×u8][type-specific data]
|
|
320
|
-
*/
|
|
321
|
-
function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver) {
|
|
322
|
-
let pos = dataOffset;
|
|
323
|
-
const dataJsonLen = ru32(view, pos);
|
|
324
|
-
pos += 4;
|
|
325
|
-
let initialData = null;
|
|
326
|
-
if (dataJsonLen > 0) {
|
|
327
|
-
const jsonStr = rstr(buf, pos, dataJsonLen);
|
|
328
|
-
try {
|
|
329
|
-
initialData = JSON.parse(jsonStr);
|
|
330
|
-
}
|
|
331
|
-
catch (err) {
|
|
332
|
-
if (process.env.NODE_ENV !== "production") {
|
|
333
|
-
console.warn(`readMdastMatchedNode: malformed node_data for nodeId=${nodeId}`, err);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
pos += dataJsonLen;
|
|
337
|
-
}
|
|
338
|
-
const position = readPosition(view, pos);
|
|
339
|
-
pos += 24;
|
|
340
|
-
const childCount = ru32(view, pos);
|
|
341
|
-
pos += 4;
|
|
342
|
-
// Ids/types decode lazily with `.children` — most matched nodes never read them.
|
|
343
|
-
const childIdsPos = pos;
|
|
344
|
-
pos += childCount * 4;
|
|
345
|
-
const childTypesPos = pos;
|
|
346
|
-
pos += childCount;
|
|
347
|
-
const typeName = TYPE_NAMES[nodeType] ?? `unknown(${nodeType})`;
|
|
348
|
-
const node = { type: typeName };
|
|
349
|
-
if (position !== undefined)
|
|
350
|
-
node.position = position;
|
|
351
|
-
if (childCount > 0) {
|
|
352
|
-
makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver);
|
|
353
|
-
}
|
|
354
|
-
// Fixed-field types decode from the generated layout table; the rest
|
|
355
|
-
// (variable-length / cross-field) stay in the hand-written switch.
|
|
356
|
-
if (!decodeMdastTypeData(view, buf, pos, nodeType, node)) {
|
|
357
|
-
switch (nodeType) {
|
|
358
|
-
case 5: {
|
|
359
|
-
// list
|
|
360
|
-
node.start = ru32(view, pos);
|
|
361
|
-
node.ordered = buf[pos + 4] !== 0;
|
|
362
|
-
node.spread = buf[pos + 5] !== 0;
|
|
363
|
-
if (!node.ordered)
|
|
364
|
-
node.start = null;
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
case 6: {
|
|
368
|
-
// listItem
|
|
369
|
-
const checked = buf[pos];
|
|
370
|
-
node.checked = checked === 2 ? null : checked === 1;
|
|
371
|
-
node.spread = buf[pos + 1] !== 0;
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
case 37: {
|
|
375
|
-
// descriptionDetails
|
|
376
|
-
node.spread = buf[pos] !== 0;
|
|
377
|
-
break;
|
|
378
|
-
}
|
|
379
|
-
// table (21), directives (30/31/32) and mdxJsx elements (100/101) are
|
|
380
|
-
// decoded by the generated `decodeMdastTypeData` from their tails.
|
|
381
|
-
// root(0), paragraph(1), thematicBreak(3), blockquote(4), emphasis(11),
|
|
382
|
-
// strong(12), break(14), tableRow(22), tableCell(23), delete(24): no extra data
|
|
383
|
-
}
|
|
384
|
-
}
|
|
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);
|
|
400
|
-
if (initialData) {
|
|
401
|
-
node.data = initialData;
|
|
402
|
-
}
|
|
403
|
-
return node;
|
|
404
|
-
}
|
|
405
307
|
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
308
|
const MDAST_CUSTOM = NAME_TO_TYPE.custom;
|
|
410
|
-
/** The arena id of a node if it is an existing (materialized) node, else
|
|
411
|
-
* undefined for a freshly-built one. */
|
|
412
309
|
function reusedId(node, refs) {
|
|
413
310
|
if (node === null || typeof node !== "object")
|
|
414
311
|
return undefined;
|
|
415
|
-
const id =
|
|
312
|
+
const id = getNodeId(node, refs);
|
|
416
313
|
return id !== undefined && id !== FOREIGN_REF ? id : undefined;
|
|
417
314
|
}
|
|
418
|
-
/** Emit a set-children command in place: a root-wrapped child list, the shape
|
|
419
|
-
* `Patch::SetChildren` splices in. Reused children become refs. */
|
|
420
315
|
function emitMdastChildrenCommand(buffer, id, children, refs) {
|
|
421
316
|
if (!Array.isArray(children))
|
|
422
317
|
return false;
|
|
@@ -430,7 +325,25 @@ function emitMdastChildrenCommand(buffer, id, children, refs) {
|
|
|
430
325
|
return true;
|
|
431
326
|
});
|
|
432
327
|
}
|
|
433
|
-
/**
|
|
328
|
+
/** True for content the op-stream can carry inside one root-wrapped payload. */
|
|
329
|
+
function isPlainReplacement(content) {
|
|
330
|
+
return (!isRawMdastContent(content) && content._keepChildren !== true);
|
|
331
|
+
}
|
|
332
|
+
/** Replace `id` with several nodes in one command, root-wrapped so the engine
|
|
333
|
+
* splices the children in place of the node. */
|
|
334
|
+
function emitMdastMultiReplace(buffer, id, nodes, refs) {
|
|
335
|
+
const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD.replace, id, () => {
|
|
336
|
+
buffer.open(MDAST_ROOT);
|
|
337
|
+
for (const n of nodes)
|
|
338
|
+
if (!emitMdastOp(buffer, n, false, true, refs))
|
|
339
|
+
return false;
|
|
340
|
+
buffer.close();
|
|
341
|
+
return true;
|
|
342
|
+
});
|
|
343
|
+
if (!ok)
|
|
344
|
+
throw unencodableContentError(nodes);
|
|
345
|
+
}
|
|
346
|
+
// Root replacement needs a separate encoder because per-node encoding rejects root payloads.
|
|
434
347
|
function emitMdastRootReplace(buffer, root, refs) {
|
|
435
348
|
const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD.replace, ROOT_NODE_ID, () => emitMdastRootOp(buffer, root, refs));
|
|
436
349
|
if (!ok)
|
|
@@ -454,36 +367,29 @@ function emitMdastRootOp(w, n, refs) {
|
|
|
454
367
|
w.close();
|
|
455
368
|
return true;
|
|
456
369
|
}
|
|
457
|
-
function emitMdastOp(w, node, isRoot, forReplace, refs) {
|
|
370
|
+
function emitMdastOp(w, node, isRoot, forReplace, refs, allowRootRef = false) {
|
|
458
371
|
if (node === null || typeof node !== "object")
|
|
459
372
|
return false;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
373
|
+
const id = getNodeId(node, refs);
|
|
374
|
+
if (id === FOREIGN_REF)
|
|
375
|
+
throw foreignContentError();
|
|
376
|
+
if ((!isRoot || allowRootRef) && id !== undefined) {
|
|
377
|
+
w.ref(id);
|
|
378
|
+
return true;
|
|
466
379
|
}
|
|
467
380
|
const n = node;
|
|
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
381
|
let type = MDAST_OPSTREAM_TYPES[n.type];
|
|
471
382
|
let isCustom = false;
|
|
472
383
|
if (type === undefined) {
|
|
473
384
|
if (typeof n.type !== "string" || n.type.length === 0)
|
|
474
385
|
return false;
|
|
475
|
-
//
|
|
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.
|
|
386
|
+
// Unsupported built-in types must fail rather than silently becoming custom nodes.
|
|
478
387
|
if (NAME_TO_TYPE[n.type] !== undefined)
|
|
479
388
|
return false;
|
|
480
389
|
type = MDAST_CUSTOM;
|
|
481
390
|
isCustom = true;
|
|
482
391
|
}
|
|
483
392
|
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
393
|
isCustom = true;
|
|
488
394
|
}
|
|
489
395
|
w.open(type);
|
|
@@ -507,8 +413,7 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
|
|
|
507
413
|
w.str(OF_LABEL, n.label);
|
|
508
414
|
if (typeof n.referenceType === "string")
|
|
509
415
|
w.str(OF_REFERENCE_TYPE, n.referenceType);
|
|
510
|
-
//
|
|
511
|
-
// error instead of silently masking the bits.
|
|
416
|
+
// Reject out-of-range values before integer writes can truncate them.
|
|
512
417
|
if (typeof n.depth === "number") {
|
|
513
418
|
if (!Number.isInteger(n.depth) || n.depth < 0 || n.depth > 255)
|
|
514
419
|
return false;
|
|
@@ -533,8 +438,6 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
|
|
|
533
438
|
emitMdxAttr(w, a);
|
|
534
439
|
}
|
|
535
440
|
else if (attrs !== null && typeof attrs === "object") {
|
|
536
|
-
// Directive attributes: a string→string map; non-string values are
|
|
537
|
-
// dropped, since the stored form holds only strings.
|
|
538
441
|
for (const key in attrs) {
|
|
539
442
|
const v = attrs[key];
|
|
540
443
|
if (typeof v === "string")
|
|
@@ -549,13 +452,10 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
|
|
|
549
452
|
if (n.data != null)
|
|
550
453
|
w.data(n.data);
|
|
551
454
|
if (isRoot && forReplace && n._keepChildren === true) {
|
|
552
|
-
// Replace splices the target's original children, discarding any the
|
|
553
|
-
// replacement declares.
|
|
554
455
|
w.keepChildren();
|
|
555
456
|
}
|
|
556
457
|
else {
|
|
557
|
-
//
|
|
558
|
-
// and emit the declared children.
|
|
458
|
+
// Only replacement can reuse the target’s original children.
|
|
559
459
|
const children = n.children;
|
|
560
460
|
if (Array.isArray(children)) {
|
|
561
461
|
for (const c of children)
|
|
@@ -566,24 +466,23 @@ function emitMdastOp(w, node, isRoot, forReplace, refs) {
|
|
|
566
466
|
w.close();
|
|
567
467
|
return true;
|
|
568
468
|
}
|
|
569
|
-
/** Map a table `align` entry to its arena code (none=0). */
|
|
570
469
|
function alignCode(a) {
|
|
571
470
|
return a === "left" ? 1 : a === "right" ? 2 : a === "center" ? 3 : 0;
|
|
572
471
|
}
|
|
573
|
-
/** True for the `{raw}` / `{rawHtml}` escape hatches — re-parsed by Rust rather
|
|
574
|
-
* than compiled to an op-stream, so they ride the RAW_MARKDOWN / RAW_HTML
|
|
575
|
-
* payloads instead of the declarative encoder. */
|
|
576
472
|
function isRawMdastContent(content) {
|
|
577
473
|
const c = content;
|
|
578
474
|
return typeof c.raw === "string" || typeof c.rawHtml === "string";
|
|
579
475
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
476
|
+
function isDirective(node) {
|
|
477
|
+
return (node.type === "containerDirective" ||
|
|
478
|
+
node.type === "leafDirective" ||
|
|
479
|
+
node.type === "textDirective");
|
|
480
|
+
}
|
|
481
|
+
function isMdxJsxElement(node) {
|
|
482
|
+
return ((node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") && "attributes" in node);
|
|
483
|
+
}
|
|
484
|
+
// Keep direct method calls so structural command dispatch stays monomorphic.
|
|
485
|
+
function emitMdastTree(buffer, op, id, content, forReplace, refs, allowRootRef = false) {
|
|
587
486
|
if (isRawMdastContent(content)) {
|
|
588
487
|
switch (op) {
|
|
589
488
|
case "replace":
|
|
@@ -600,7 +499,7 @@ function emitMdastTree(buffer, op, id, content, forReplace, refs) {
|
|
|
600
499
|
return buffer.wrapNode(id, content);
|
|
601
500
|
}
|
|
602
501
|
}
|
|
603
|
-
const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD[op], id, () => emitMdastOp(buffer, content, true, forReplace, refs));
|
|
502
|
+
const ok = buffer.emitOpstreamCommand(STRUCTURAL_CMD[op], id, () => emitMdastOp(buffer, content, true, forReplace, refs, allowRootRef));
|
|
604
503
|
if (!ok)
|
|
605
504
|
throw unencodableContentError(content);
|
|
606
505
|
}
|
|
@@ -613,8 +512,6 @@ function assertMdastWrapParent(parentNode) {
|
|
|
613
512
|
const type = parentNode.type;
|
|
614
513
|
const tag = typeof type === "string" ? NAME_TO_TYPE[type] : undefined;
|
|
615
514
|
if (tag === undefined) {
|
|
616
|
-
// User-defined type: only the declared shape distinguishes a parent from
|
|
617
|
-
// a text leaf.
|
|
618
515
|
if (Array.isArray(parentNode.children))
|
|
619
516
|
return;
|
|
620
517
|
throw new Error(`wrapNode: a user-defined "${String(type)}" wrapper must declare a children array. ` +
|
|
@@ -626,9 +523,7 @@ function assertMdastWrapParent(parentNode) {
|
|
|
626
523
|
'Pass a parent node such as { type: "blockquote", children: [] }, a raw wrapper ' +
|
|
627
524
|
`such as { raw: "> " }, or surround the node with tags: ${sandwich}.`);
|
|
628
525
|
}
|
|
629
|
-
|
|
630
|
-
* visitor returning one of these as `{type, value}` with no other fields
|
|
631
|
-
* skips a full arena rebuild. */
|
|
526
|
+
// Updating only value avoids an arena rebuild for these node types.
|
|
632
527
|
const MDAST_VALUE_ONLY_TYPES = new Set([
|
|
633
528
|
"text",
|
|
634
529
|
"html",
|
|
@@ -637,9 +532,7 @@ const MDAST_VALUE_ONLY_TYPES = new Set([
|
|
|
637
532
|
"toml",
|
|
638
533
|
"inlineMath",
|
|
639
534
|
]);
|
|
640
|
-
|
|
641
|
-
* + `value`. Any other field present (children, position, data, lang, meta)
|
|
642
|
-
* falls back to the full replace path so nothing is silently dropped. */
|
|
535
|
+
// Extra fields require full replacement so the value-only fast path cannot discard them.
|
|
643
536
|
function isMdastTextValueSwap(result, original) {
|
|
644
537
|
if (original === undefined)
|
|
645
538
|
return false;
|
|
@@ -656,8 +549,7 @@ function isMdastTextValueSwap(result, original) {
|
|
|
656
549
|
r.lang === undefined &&
|
|
657
550
|
r.meta === undefined);
|
|
658
551
|
}
|
|
659
|
-
|
|
660
|
-
* mutations (e.g. setProperty) are not clobbered. */
|
|
552
|
+
// Returning the input node must not overwrite mutations queued through the context.
|
|
661
553
|
function applyMdastVisitResult(result, nodeId, returnBuffer, refs, originalNode) {
|
|
662
554
|
if (result === undefined || result === null)
|
|
663
555
|
return;
|
|
@@ -677,14 +569,14 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, refs, originalNode)
|
|
|
677
569
|
returnBuffer.setProperty(nodeId, "value", node.value);
|
|
678
570
|
break;
|
|
679
571
|
}
|
|
680
|
-
emitMdastTree(returnBuffer, "replace", nodeId, node, true, refs);
|
|
572
|
+
emitMdastTree(returnBuffer, "replace", nodeId, node, true, refs, true);
|
|
681
573
|
break;
|
|
682
574
|
}
|
|
683
575
|
}
|
|
684
576
|
}
|
|
685
577
|
/**
|
|
686
578
|
* Walk an MDAST handle in Rust, dispatch matched nodes to JS visitor functions,
|
|
687
|
-
* and
|
|
579
|
+
* and collect mutations for the caller to apply. No arena buffers cross NAPI.
|
|
688
580
|
*
|
|
689
581
|
* Returns MdastVisitResult synchronously if all visitors are sync,
|
|
690
582
|
* or Promise<MdastVisitResult> if any visitor is async.
|
|
@@ -716,20 +608,21 @@ export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {
|
|
|
716
608
|
}
|
|
717
609
|
}
|
|
718
610
|
if (deferred) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
611
|
+
const visits = deferred;
|
|
612
|
+
return Promise.all(visits.map((visit) => visit.promise)).then((results) => {
|
|
613
|
+
for (let i = 0; i < visits.length; i++) {
|
|
614
|
+
const { nodeId, originalNode } = visits[i];
|
|
615
|
+
applyMdastVisitResult(results[i], nodeId, returnBuffer, resolver.refs, originalNode);
|
|
722
616
|
}
|
|
723
|
-
return finalizeMdastVisit(
|
|
617
|
+
return finalizeMdastVisit(context, returnBuffer);
|
|
724
618
|
});
|
|
725
619
|
}
|
|
726
|
-
return finalizeMdastVisit(
|
|
620
|
+
return finalizeMdastVisit(context, returnBuffer);
|
|
727
621
|
}
|
|
728
622
|
const MDAST_ROOT_SUBS = [
|
|
729
623
|
{ nodeType: MDAST_ROOT, tagFilter: [] },
|
|
730
624
|
];
|
|
731
|
-
/**
|
|
732
|
-
* visitors walk, and the visitors' mutations before `after` reads the tree. */
|
|
625
|
+
/** Hooks run in separate passes so each sees mutations from the preceding pass. */
|
|
733
626
|
export function visitMdastHook(handle, plugin, hook, source, fileURL, data = {}, sourceFormat = "markdown", diagnostics = []) {
|
|
734
627
|
const getSource = typeof source === "function" ? source : () => source;
|
|
735
628
|
const resolver = new MdastLazyChildResolver(handle);
|
|
@@ -738,18 +631,19 @@ export function visitMdastHook(handle, plugin, hook, source, fileURL, data = {},
|
|
|
738
631
|
const matchBuf = walkMdastHandle(handle, MDAST_ROOT_SUBS);
|
|
739
632
|
const matchView = new DataView(matchBuf.buffer, matchBuf.byteOffset, matchBuf.byteLength);
|
|
740
633
|
if (ru32(matchView, 0) === 0)
|
|
741
|
-
return finalizeMdastVisit(
|
|
634
|
+
return finalizeMdastVisit(context, returnBuffer);
|
|
742
635
|
const root = readMdastMatchedNode(matchView, matchBuf, ru32(matchView, 10), ru32(matchView, 4), MDAST_ROOT, resolver);
|
|
743
636
|
const result = hook.call(plugin, root, context);
|
|
744
637
|
if (result instanceof Promise) {
|
|
745
|
-
return result.then(() => finalizeMdastVisit(
|
|
638
|
+
return result.then(() => finalizeMdastVisit(context, returnBuffer));
|
|
746
639
|
}
|
|
747
|
-
return finalizeMdastVisit(
|
|
640
|
+
return finalizeMdastVisit(context, returnBuffer);
|
|
748
641
|
}
|
|
749
|
-
function finalizeMdastVisit(
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
642
|
+
function finalizeMdastVisit(context, returnBuffer) {
|
|
643
|
+
const commandBuffer = collectCommands(returnBuffer, context.getCommandBuffer());
|
|
644
|
+
return {
|
|
645
|
+
commandBuffer,
|
|
646
|
+
diagnostics: context.getDiagnostics(),
|
|
647
|
+
hasMutations: commandBuffer.length > 0,
|
|
648
|
+
};
|
|
755
649
|
}
|