satteri 0.6.3 → 0.8.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 +14 -1
- package/dist/compile.d.ts +87 -6
- package/dist/compile.js +106 -53
- package/dist/hast/hast-visitor.d.ts +15 -3
- package/dist/hast/hast-visitor.js +113 -80
- package/dist/mdast/mdast-visitor.d.ts +12 -3
- package/dist/mdast/mdast-visitor.js +45 -6
- package/index.d.ts +68 -12
- package/index.js +52 -52
- package/package.json +8 -6
|
@@ -21,24 +21,37 @@ function attachParseExpression(node, parseFn) {
|
|
|
21
21
|
configurable: true,
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Serialize a HastNode for the command buffer. Marks each node `_hast: true`,
|
|
26
|
+
* and — except at the root, which is the new replacement content — emits a
|
|
27
|
+
* reused (materialized) node as a `{ _ref: id }` placeholder so the rebuild
|
|
28
|
+
* splices the original in place (preserving its id, applying any pending patch
|
|
29
|
+
* on it) instead of rebuilding it fresh.
|
|
30
|
+
*/
|
|
25
31
|
function markHast(node) {
|
|
26
|
-
|
|
32
|
+
return markHastNode(node, true);
|
|
33
|
+
}
|
|
34
|
+
function markHastNode(node, isRoot) {
|
|
35
|
+
if (!isRoot) {
|
|
36
|
+
const id = nodeIdMap.get(node) ?? node._nodeId;
|
|
37
|
+
if (typeof id === "number")
|
|
38
|
+
return { _ref: id };
|
|
39
|
+
}
|
|
27
40
|
const obj = { _hast: true, type: node.type };
|
|
28
41
|
if ("tagName" in node)
|
|
29
|
-
obj.tagName =
|
|
42
|
+
obj.tagName = node.tagName;
|
|
30
43
|
if ("properties" in node)
|
|
31
|
-
obj.properties =
|
|
44
|
+
obj.properties = node.properties;
|
|
32
45
|
if ("value" in node)
|
|
33
|
-
obj.value =
|
|
46
|
+
obj.value = node.value;
|
|
34
47
|
if ("name" in node)
|
|
35
|
-
obj.name =
|
|
48
|
+
obj.name = node.name;
|
|
36
49
|
if ("attributes" in node)
|
|
37
|
-
obj.attributes =
|
|
38
|
-
if ("data" in node &&
|
|
39
|
-
obj.data =
|
|
50
|
+
obj.attributes = node.attributes;
|
|
51
|
+
if ("data" in node && node.data != null)
|
|
52
|
+
obj.data = node.data;
|
|
40
53
|
if ("children" in node) {
|
|
41
|
-
obj.children =
|
|
54
|
+
obj.children = node.children.map((c) => markHastNode(c, false));
|
|
42
55
|
}
|
|
43
56
|
return obj;
|
|
44
57
|
}
|
|
@@ -52,11 +65,11 @@ class HastVisitorContextImpl {
|
|
|
52
65
|
#pendingNodes = new Map();
|
|
53
66
|
#handle;
|
|
54
67
|
#getSource;
|
|
55
|
-
|
|
56
|
-
constructor(handle, getSource,
|
|
68
|
+
fileURL;
|
|
69
|
+
constructor(handle, getSource, fileURL) {
|
|
57
70
|
this.#handle = handle;
|
|
58
71
|
this.#getSource = getSource;
|
|
59
|
-
this.
|
|
72
|
+
this.fileURL = fileURL;
|
|
60
73
|
}
|
|
61
74
|
get source() {
|
|
62
75
|
const value = this.#getSource();
|
|
@@ -99,7 +112,8 @@ class HastVisitorContextImpl {
|
|
|
99
112
|
}
|
|
100
113
|
if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
|
|
101
114
|
// MDX JSX nodes use `attributes`, not `properties`, keep replaceNode path
|
|
102
|
-
const
|
|
115
|
+
const pending = this.#pendingNodes.get(id);
|
|
116
|
+
const current = (pending ?? node);
|
|
103
117
|
const updated = { ...current };
|
|
104
118
|
const attrs = [...(updated.attributes ?? [])];
|
|
105
119
|
const idx = attrs.findIndex((a) => a.type === "mdxJsxAttribute" && a.name === key);
|
|
@@ -109,7 +123,7 @@ class HastVisitorContextImpl {
|
|
|
109
123
|
? null
|
|
110
124
|
: typeof value === "string"
|
|
111
125
|
? value
|
|
112
|
-
:
|
|
126
|
+
: String(value);
|
|
113
127
|
attrs.push({ type: "mdxJsxAttribute", name: key, value: attrValue });
|
|
114
128
|
updated.attributes = attrs;
|
|
115
129
|
this.replaceNode(node, updated);
|
|
@@ -213,6 +227,21 @@ function decodeProperties(view, buf, pos) {
|
|
|
213
227
|
}
|
|
214
228
|
return properties;
|
|
215
229
|
}
|
|
230
|
+
/** Decode the 24-byte position block written by `serialize_hast_node_inline`. */
|
|
231
|
+
function readPositionPrefix(view, offset) {
|
|
232
|
+
const startOffset = view.getUint32(offset, true);
|
|
233
|
+
const endOffset = view.getUint32(offset + 4, true);
|
|
234
|
+
const startLine = view.getUint32(offset + 8, true);
|
|
235
|
+
const startColumn = view.getUint32(offset + 12, true);
|
|
236
|
+
const endLine = view.getUint32(offset + 16, true);
|
|
237
|
+
const endColumn = view.getUint32(offset + 20, true);
|
|
238
|
+
if (startLine === 0 && startOffset === 0)
|
|
239
|
+
return undefined;
|
|
240
|
+
return {
|
|
241
|
+
start: { offset: startOffset, line: startLine, column: startColumn },
|
|
242
|
+
end: { offset: endOffset, line: endLine, column: endColumn },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
216
245
|
/**
|
|
217
246
|
* Walk-path element node: uses prototype getters instead of per-instance
|
|
218
247
|
* Object.defineProperty. V8 optimises shared hidden classes far better,
|
|
@@ -228,8 +257,6 @@ class WalkElement {
|
|
|
228
257
|
/** @internal */ _propsPos;
|
|
229
258
|
/** @internal */ _childIds;
|
|
230
259
|
/** @internal */ _resolver;
|
|
231
|
-
/** @internal */ _dataPos;
|
|
232
|
-
/** @internal */ _dataLen;
|
|
233
260
|
get properties() {
|
|
234
261
|
const val = decodeProperties(this._view, this._buf, this._propsPos);
|
|
235
262
|
Object.defineProperty(this, "properties", {
|
|
@@ -250,59 +277,29 @@ class WalkElement {
|
|
|
250
277
|
});
|
|
251
278
|
return val;
|
|
252
279
|
}
|
|
253
|
-
get data() {
|
|
254
|
-
if (this._dataPos < 0)
|
|
255
|
-
return undefined;
|
|
256
|
-
const val = JSON.parse(textDecoder.decode(this._buf.subarray(this._dataPos, this._dataPos + this._dataLen)));
|
|
257
|
-
Object.defineProperty(this, "data", {
|
|
258
|
-
value: val,
|
|
259
|
-
writable: true,
|
|
260
|
-
enumerable: true,
|
|
261
|
-
configurable: true,
|
|
262
|
-
});
|
|
263
|
-
return val;
|
|
264
|
-
}
|
|
265
280
|
}
|
|
266
|
-
/** Read a matched element node
|
|
267
|
-
*
|
|
268
|
-
function readElementFromBinary(view, buf, offset, nodeId, resolver) {
|
|
281
|
+
/** Read the tail of a matched element node (tag + properties).
|
|
282
|
+
* Common prelude (data/position/children) is already consumed by `readMatchedNode`. */
|
|
283
|
+
function readElementFromBinary(view, buf, offset, nodeId, resolver, position, childIds, data) {
|
|
269
284
|
let pos = offset;
|
|
270
285
|
// Eager: tagName (almost always accessed by visitors)
|
|
271
286
|
const tagLen = view.getUint16(pos, true);
|
|
272
287
|
pos += 2;
|
|
273
288
|
const tagName = textDecoder.decode(buf.subarray(pos, pos + tagLen));
|
|
274
289
|
pos += tagLen;
|
|
275
|
-
// Pre-scan: find section byte offsets without decoding strings
|
|
276
290
|
const propsPos = pos;
|
|
277
|
-
const propCount = view.getUint16(pos, true);
|
|
278
|
-
pos += 2;
|
|
279
|
-
for (let i = 0; i < propCount; i++) {
|
|
280
|
-
const nLen = view.getUint16(pos, true);
|
|
281
|
-
pos += 2 + nLen + 1; // name + kind byte
|
|
282
|
-
const vLen = view.getUint16(pos, true);
|
|
283
|
-
pos += 2 + vLen; // value
|
|
284
|
-
}
|
|
285
|
-
const childCount = view.getUint16(pos, true);
|
|
286
|
-
pos += 2;
|
|
287
|
-
const childIdsPos = pos;
|
|
288
|
-
pos += childCount * 4;
|
|
289
|
-
const nodeDataLen = view.getUint32(pos, true);
|
|
290
|
-
pos += 4;
|
|
291
|
-
const nodeDataPos = nodeDataLen > 0 ? pos : -1;
|
|
292
|
-
// Collect child IDs for lazy materialization
|
|
293
|
-
const ids = [];
|
|
294
|
-
for (let i = 0; i < childCount; i++)
|
|
295
|
-
ids.push(view.getUint32(childIdsPos + i * 4, true));
|
|
296
291
|
// Build node using class (prototype getters, no per-instance defineProperty)
|
|
297
292
|
const node = new WalkElement();
|
|
298
293
|
node.tagName = tagName;
|
|
294
|
+
if (position !== undefined)
|
|
295
|
+
node.position = position;
|
|
296
|
+
if (data !== null)
|
|
297
|
+
node.data = data;
|
|
299
298
|
node._view = view;
|
|
300
299
|
node._buf = buf;
|
|
301
300
|
node._propsPos = propsPos;
|
|
302
|
-
node._childIds =
|
|
301
|
+
node._childIds = childIds;
|
|
303
302
|
node._resolver = resolver;
|
|
304
|
-
node._dataPos = nodeDataPos;
|
|
305
|
-
node._dataLen = nodeDataLen;
|
|
306
303
|
nodeIdMap.set(node, nodeId);
|
|
307
304
|
return node;
|
|
308
305
|
}
|
|
@@ -315,10 +312,15 @@ const TEXT_NODE_TYPES = {
|
|
|
315
312
|
[HAST_MDX_TEXT_EXPRESSION]: "mdxTextExpression",
|
|
316
313
|
[HAST_MDX_ESM]: "mdxjsEsm",
|
|
317
314
|
};
|
|
318
|
-
function readTextFromBinary(view, buf, offset, nodeId, nodeType) {
|
|
315
|
+
function readTextFromBinary(view, buf, offset, nodeId, nodeType, position, data) {
|
|
319
316
|
const valLen = view.getUint32(offset, true);
|
|
320
317
|
const value = textDecoder.decode(buf.subarray(offset + 4, offset + 4 + valLen));
|
|
321
|
-
const
|
|
318
|
+
const base = { type: TEXT_NODE_TYPES[nodeType], value };
|
|
319
|
+
if (position !== undefined)
|
|
320
|
+
base.position = position;
|
|
321
|
+
if (data !== null)
|
|
322
|
+
base.data = data;
|
|
323
|
+
const node = base;
|
|
322
324
|
nodeIdMap.set(node, nodeId);
|
|
323
325
|
if (nodeType === HAST_MDX_FLOW_EXPRESSION || nodeType === HAST_MDX_TEXT_EXPRESSION) {
|
|
324
326
|
attachParseExpression(node, napiParseExpression);
|
|
@@ -329,14 +331,14 @@ function readTextFromBinary(view, buf, offset, nodeId, nodeType) {
|
|
|
329
331
|
return node;
|
|
330
332
|
}
|
|
331
333
|
/** Read an MDX JSX element from the binary data section. */
|
|
332
|
-
function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver) {
|
|
334
|
+
function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver, position, childIds, data) {
|
|
333
335
|
let pos = offset;
|
|
334
336
|
// Name
|
|
335
337
|
const nameLen = view.getUint16(pos, true);
|
|
336
338
|
pos += 2;
|
|
337
339
|
const name = nameLen > 0 ? textDecoder.decode(buf.subarray(pos, pos + nameLen)) : null;
|
|
338
340
|
pos += nameLen;
|
|
339
|
-
// Attributes: [kind: u8][nameLen: u16][name][valLen:
|
|
341
|
+
// Attributes: [kind: u8][nameLen: u16][name][valLen: u32][val]
|
|
340
342
|
const attrCount = view.getUint16(pos, true);
|
|
341
343
|
pos += 2;
|
|
342
344
|
const attributes = [];
|
|
@@ -370,7 +372,36 @@ function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver) {
|
|
|
370
372
|
break;
|
|
371
373
|
}
|
|
372
374
|
}
|
|
373
|
-
|
|
375
|
+
const typeName = nodeType === HAST_MDX_JSX_ELEMENT ? "mdxJsxFlowElement" : "mdxJsxTextElement";
|
|
376
|
+
const base = { type: typeName, name, attributes };
|
|
377
|
+
if (position !== undefined)
|
|
378
|
+
base.position = position;
|
|
379
|
+
if (data !== null)
|
|
380
|
+
base.data = data;
|
|
381
|
+
nodeIdMap.set(base, nodeId);
|
|
382
|
+
makeLazyChildren(base, childIds, resolver);
|
|
383
|
+
return base;
|
|
384
|
+
}
|
|
385
|
+
function readMatchedNode(view, buf, offset, nodeId, nodeType, resolver) {
|
|
386
|
+
let pos = offset;
|
|
387
|
+
// Shared prelude (matches serialize_hast_node_inline / serialize_mdast_node_inline):
|
|
388
|
+
// [data_len: u32][data_bytes][position: 24B][child_count: u16][child_ids: N×u32]
|
|
389
|
+
// Data (JSON), eagerly parsed
|
|
390
|
+
const dataLen = view.getUint32(pos, true);
|
|
391
|
+
pos += 4;
|
|
392
|
+
let data = null;
|
|
393
|
+
if (dataLen > 0) {
|
|
394
|
+
const jsonStr = textDecoder.decode(buf.subarray(pos, pos + dataLen));
|
|
395
|
+
try {
|
|
396
|
+
data = JSON.parse(jsonStr);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
/* ignore malformed JSON */
|
|
400
|
+
}
|
|
401
|
+
pos += dataLen;
|
|
402
|
+
}
|
|
403
|
+
const position = readPositionPrefix(view, pos);
|
|
404
|
+
pos += 24;
|
|
374
405
|
const childCount = view.getUint16(pos, true);
|
|
375
406
|
pos += 2;
|
|
376
407
|
const childIds = [];
|
|
@@ -378,16 +409,9 @@ function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver) {
|
|
|
378
409
|
childIds.push(view.getUint32(pos, true));
|
|
379
410
|
pos += 4;
|
|
380
411
|
}
|
|
381
|
-
|
|
382
|
-
const node = { type: typeName, name, attributes };
|
|
383
|
-
nodeIdMap.set(node, nodeId);
|
|
384
|
-
makeLazyChildren(node, childIds, resolver);
|
|
385
|
-
resolver.attachLazyData(node, nodeId);
|
|
386
|
-
return node;
|
|
387
|
-
}
|
|
388
|
-
function readMatchedNode(view, buf, offset, nodeId, nodeType, resolver) {
|
|
412
|
+
// Dispatch to type-specific tail (pos now sits at the type-specific section)
|
|
389
413
|
if (nodeType === HAST_ELEMENT) {
|
|
390
|
-
return readElementFromBinary(view, buf,
|
|
414
|
+
return readElementFromBinary(view, buf, pos, nodeId, resolver, position, childIds, data);
|
|
391
415
|
}
|
|
392
416
|
else if (nodeType === HAST_TEXT ||
|
|
393
417
|
nodeType === HAST_COMMENT ||
|
|
@@ -395,13 +419,18 @@ function readMatchedNode(view, buf, offset, nodeId, nodeType, resolver) {
|
|
|
395
419
|
nodeType === HAST_MDX_FLOW_EXPRESSION ||
|
|
396
420
|
nodeType === HAST_MDX_TEXT_EXPRESSION ||
|
|
397
421
|
nodeType === HAST_MDX_ESM) {
|
|
398
|
-
return readTextFromBinary(view, buf,
|
|
422
|
+
return readTextFromBinary(view, buf, pos, nodeId, nodeType, position, data);
|
|
399
423
|
}
|
|
400
424
|
else if (nodeType === HAST_MDX_JSX_ELEMENT || nodeType === HAST_MDX_JSX_TEXT_ELEMENT) {
|
|
401
|
-
return readMdxJsxFromBinary(view, buf,
|
|
402
|
-
}
|
|
403
|
-
// Fallback: minimal node
|
|
404
|
-
const
|
|
425
|
+
return readMdxJsxFromBinary(view, buf, pos, nodeId, nodeType, resolver, position, childIds, data);
|
|
426
|
+
}
|
|
427
|
+
// Fallback: minimal node carrying whatever prelude data we found
|
|
428
|
+
const base = { type: `unknown(${nodeType})` };
|
|
429
|
+
if (position !== undefined)
|
|
430
|
+
base.position = position;
|
|
431
|
+
if (data !== null)
|
|
432
|
+
base.data = data;
|
|
433
|
+
const node = base;
|
|
405
434
|
nodeIdMap.set(node, nodeId);
|
|
406
435
|
return node;
|
|
407
436
|
}
|
|
@@ -527,11 +556,13 @@ function mergeAndReset(returnBuffer, ctx) {
|
|
|
527
556
|
* Walk a handle's arena in Rust, dispatch matched nodes to JS visitor functions,
|
|
528
557
|
* and apply mutations back to the handle. No arena buffers cross NAPI.
|
|
529
558
|
*
|
|
530
|
-
* Returns
|
|
559
|
+
* Returns the number of patches dropped because their target was removed or
|
|
560
|
+
* replaced earlier in the same pass (the caller warns when non-zero), or a
|
|
561
|
+
* Promise of that count if any visitor is async.
|
|
531
562
|
*/
|
|
532
|
-
export function visitHastHandle(handle, plugin, subs, source,
|
|
563
|
+
export function visitHastHandle(handle, plugin, subs, source, fileURL) {
|
|
533
564
|
const getSource = typeof source === "function" ? source : () => source;
|
|
534
|
-
const ctx = new HastVisitorContextImpl(handle, getSource,
|
|
565
|
+
const ctx = new HastVisitorContextImpl(handle, getSource, fileURL);
|
|
535
566
|
const returnBuffer = new CommandBuffer();
|
|
536
567
|
const resolver = new LazyChildResolver(handle);
|
|
537
568
|
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: s.tagFilter }));
|
|
@@ -543,14 +574,16 @@ export function visitHastHandle(handle, plugin, subs, source, filename) {
|
|
|
543
574
|
returnBuffer.replaceRawJson(nodeId, JSON.stringify(markHast(result)));
|
|
544
575
|
}
|
|
545
576
|
}
|
|
546
|
-
applyMutations(handle, returnBuffer, ctx);
|
|
577
|
+
return applyMutations(handle, returnBuffer, ctx);
|
|
547
578
|
});
|
|
548
579
|
}
|
|
549
|
-
applyMutations(handle, returnBuffer, ctx);
|
|
580
|
+
return applyMutations(handle, returnBuffer, ctx);
|
|
550
581
|
}
|
|
582
|
+
/** Returns the number of patches dropped as stranded (0 when none). */
|
|
551
583
|
function applyMutations(handle, returnBuffer, ctx) {
|
|
552
584
|
const { merged, hasMutations } = mergeAndReset(returnBuffer, ctx);
|
|
553
585
|
if (hasMutations) {
|
|
554
|
-
applyCommandsToHandle(handle, merged);
|
|
586
|
+
return applyCommandsToHandle(handle, merged);
|
|
555
587
|
}
|
|
588
|
+
return 0;
|
|
556
589
|
}
|
|
@@ -13,12 +13,21 @@ export interface MdastDiagnostic {
|
|
|
13
13
|
}
|
|
14
14
|
export declare class MdastVisitorContext {
|
|
15
15
|
#private;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The URL of the document being processed (the compile `fileURL` option),
|
|
18
|
+
* or `undefined` when none was given. Use `fileURLToPath(ctx.fileURL)` for a
|
|
19
|
+
* decoded filesystem path.
|
|
20
|
+
*/
|
|
21
|
+
readonly fileURL: URL | undefined;
|
|
22
|
+
constructor(handle: MdastHandle, getSource: () => string, fileURL: URL | undefined);
|
|
18
23
|
get source(): string;
|
|
19
24
|
removeNode(node: Readonly<MdastNode>): void;
|
|
20
25
|
insertBefore(node: Readonly<MdastNode>, newNode: MdastNode): void;
|
|
21
26
|
insertAfter(node: Readonly<MdastNode>, newNode: MdastNode): void;
|
|
27
|
+
/**
|
|
28
|
+
* Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any
|
|
29
|
+
* children `parentNode` declares are kept after it.
|
|
30
|
+
*/
|
|
22
31
|
wrapNode(node: Readonly<MdastNode>, parentNode: MdastNode): void;
|
|
23
32
|
prependChild(node: Readonly<MdastNode>, childNode: MdastNode): void;
|
|
24
33
|
appendChild(node: Readonly<MdastNode>, childNode: MdastNode): void;
|
|
@@ -102,5 +111,5 @@ export declare function resolveMdastSubscriptions(plugin: MdastPluginInstance):
|
|
|
102
111
|
* Returns MdastVisitResult synchronously if all visitors are sync,
|
|
103
112
|
* or Promise<MdastVisitResult> if any visitor is async.
|
|
104
113
|
*/
|
|
105
|
-
export declare function visitMdastHandle(handle: MdastHandle, plugin: MdastPluginInstance, subs: MdastSubscription[], source: string | (() => string),
|
|
114
|
+
export declare function visitMdastHandle(handle: MdastHandle, plugin: MdastPluginInstance, subs: MdastSubscription[], source: string | (() => string), fileURL: URL | undefined): MdastVisitResult | Promise<MdastVisitResult>;
|
|
106
115
|
export {};
|
|
@@ -60,11 +60,16 @@ export class MdastVisitorContext {
|
|
|
60
60
|
#diagnostics = [];
|
|
61
61
|
#handle;
|
|
62
62
|
#getSource;
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The URL of the document being processed (the compile `fileURL` option),
|
|
65
|
+
* or `undefined` when none was given. Use `fileURLToPath(ctx.fileURL)` for a
|
|
66
|
+
* decoded filesystem path.
|
|
67
|
+
*/
|
|
68
|
+
fileURL;
|
|
69
|
+
constructor(handle, getSource, fileURL) {
|
|
65
70
|
this.#handle = handle;
|
|
66
71
|
this.#getSource = getSource;
|
|
67
|
-
this.
|
|
72
|
+
this.fileURL = fileURL;
|
|
68
73
|
}
|
|
69
74
|
get source() {
|
|
70
75
|
const value = this.#getSource();
|
|
@@ -80,6 +85,10 @@ export class MdastVisitorContext {
|
|
|
80
85
|
insertAfter(node, newNode) {
|
|
81
86
|
this.#commandBuffer.insertAfter(nid(node), newNode);
|
|
82
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any
|
|
90
|
+
* children `parentNode` declares are kept after it.
|
|
91
|
+
*/
|
|
83
92
|
wrapNode(node, parentNode) {
|
|
84
93
|
this.#commandBuffer.wrapNode(nid(node), parentNode);
|
|
85
94
|
}
|
|
@@ -501,6 +510,36 @@ function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver)
|
|
|
501
510
|
/** Apply a sync visitor result to the return buffer.
|
|
502
511
|
* If the result is the same object as the input node, treat it as a no-op
|
|
503
512
|
* so that context mutations (e.g. setProperty) are not clobbered. */
|
|
513
|
+
/** The arena id of a node if it is an existing (materialized) node, else
|
|
514
|
+
* undefined for a freshly-built one. */
|
|
515
|
+
function reusedId(node) {
|
|
516
|
+
if (node === null || typeof node !== "object")
|
|
517
|
+
return undefined;
|
|
518
|
+
const id = nid(node);
|
|
519
|
+
return typeof id === "number" ? id : undefined;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Rewrite a returned replacement tree so every *reused* node (one that came
|
|
523
|
+
* from the arena, at any depth) becomes a `{ _ref: id }` placeholder. The
|
|
524
|
+
* rebuild splices those originals back in place, preserving their ids — so a
|
|
525
|
+
* patch a nested visitor queued on a passed-through child still lands, in the
|
|
526
|
+
* same pass. Freshly-built nodes serialize as before. The root is never reffed:
|
|
527
|
+
* it is the new shape replacing the visited node.
|
|
528
|
+
*/
|
|
529
|
+
function refifyReusedNodes(node, isRoot) {
|
|
530
|
+
if (node === null || typeof node !== "object")
|
|
531
|
+
return node;
|
|
532
|
+
if (!isRoot) {
|
|
533
|
+
const id = reusedId(node);
|
|
534
|
+
if (id !== undefined)
|
|
535
|
+
return { _ref: id };
|
|
536
|
+
}
|
|
537
|
+
const children = node.children;
|
|
538
|
+
if (Array.isArray(children)) {
|
|
539
|
+
return { ...node, children: children.map((c) => refifyReusedNodes(c, false)) };
|
|
540
|
+
}
|
|
541
|
+
return node;
|
|
542
|
+
}
|
|
504
543
|
function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
|
|
505
544
|
if (result === undefined || result === null)
|
|
506
545
|
return;
|
|
@@ -515,7 +554,7 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
|
|
|
515
554
|
returnBuffer.replace(nodeId, result);
|
|
516
555
|
break;
|
|
517
556
|
case "structured_node":
|
|
518
|
-
returnBuffer.replace(nodeId, result);
|
|
557
|
+
returnBuffer.replace(nodeId, refifyReusedNodes(result, true));
|
|
519
558
|
break;
|
|
520
559
|
}
|
|
521
560
|
}
|
|
@@ -526,9 +565,9 @@ function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {
|
|
|
526
565
|
* Returns MdastVisitResult synchronously if all visitors are sync,
|
|
527
566
|
* or Promise<MdastVisitResult> if any visitor is async.
|
|
528
567
|
*/
|
|
529
|
-
export function visitMdastHandle(handle, plugin, subs, source,
|
|
568
|
+
export function visitMdastHandle(handle, plugin, subs, source, fileURL) {
|
|
530
569
|
const getSource = typeof source === "function" ? source : () => source;
|
|
531
|
-
const context = new MdastVisitorContext(handle, getSource,
|
|
570
|
+
const context = new MdastVisitorContext(handle, getSource, fileURL);
|
|
532
571
|
const returnBuffer = new CommandBuffer();
|
|
533
572
|
const resolver = new MdastLazyChildResolver(handle);
|
|
534
573
|
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: [] }));
|
package/index.d.ts
CHANGED
|
@@ -4,34 +4,43 @@
|
|
|
4
4
|
* Apply MDAST commands and convert to HAST handle in one step.
|
|
5
5
|
* The MDAST handle is consumed (emptied).
|
|
6
6
|
*/
|
|
7
|
-
export declare function applyCommandsAndConvertToHastHandle(handle: MdastHandle, commandBuf: Uint8Array): HastHandle
|
|
7
|
+
export declare function applyCommandsAndConvertToHastHandle(handle: MdastHandle, commandBuf: Uint8Array, convertOptions?: JsConvertOptions | undefined | null): HastHandle
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Apply a command buffer to a HAST handle's arena in-place. Returns how many
|
|
11
|
+
* patches were dropped because their target lived inside a subtree this pass
|
|
12
|
+
* removed or replaced (see the lenient note below); the JS pipeline warns when
|
|
13
|
+
* non-zero. Mirrors `apply_commands_to_mdast_handle`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function applyCommandsToHandle(handle: HastHandle, commandBuf: Uint8Array): number
|
|
11
16
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Apply a command buffer to an MDAST handle in-place. Returns how many patches
|
|
19
|
+
* were dropped because their target lived inside a subtree this pass removed or
|
|
20
|
+
* replaced (see the lenient note below); the JS pipeline warns when non-zero.
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyCommandsToMdastHandle(handle: MdastHandle, commandBuf: Uint8Array): number
|
|
14
23
|
|
|
15
24
|
/** Compile a HAST handle's arena to MDX JavaScript. Does not consume the handle. */
|
|
16
25
|
export declare function compileHandle(handle: HastHandle, options?: JsMdxOptions | undefined | null): string
|
|
17
26
|
|
|
18
27
|
/** Compile MDX source directly to JavaScript. */
|
|
19
|
-
export declare function compileMdx(source: string, options?: JsMdxOptions | undefined | null, features?: JsFeatures | undefined | null): string
|
|
28
|
+
export declare function compileMdx(source: string, options?: JsMdxOptions | undefined | null, features?: JsFeatures | undefined | null, convertOptions?: JsConvertOptions | undefined | null): string
|
|
20
29
|
|
|
21
30
|
/** Convert an MDAST handle to a HAST handle. The MDAST handle is consumed (emptied). */
|
|
22
|
-
export declare function convertMdastToHastHandle(handle: MdastHandle): HastHandle
|
|
31
|
+
export declare function convertMdastToHastHandle(handle: MdastHandle, convertOptions?: JsConvertOptions | undefined | null): HastHandle
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
34
|
* Parse markdown source and convert to HAST. Returns an opaque handle.
|
|
26
35
|
* The arena stays in Rust memory, no buffer is copied to JS.
|
|
27
36
|
*/
|
|
28
|
-
export declare function createHastHandle(source: string, features?: JsFeatures | undefined | null): HastHandle
|
|
37
|
+
export declare function createHastHandle(source: string, features?: JsFeatures | undefined | null, convertOptions?: JsConvertOptions | undefined | null): HastHandle
|
|
29
38
|
|
|
30
39
|
/** Parse markdown source into an MDAST arena handle. */
|
|
31
40
|
export declare function createMdastHandle(source: string, features?: JsFeatures | undefined | null): MdastHandle
|
|
32
41
|
|
|
33
42
|
/** Parse MDX source and convert to HAST. Returns an opaque handle. */
|
|
34
|
-
export declare function createMdxHastHandle(source: string, features?: JsFeatures | undefined | null): HastHandle
|
|
43
|
+
export declare function createMdxHastHandle(source: string, features?: JsFeatures | undefined | null, convertOptions?: JsConvertOptions | undefined | null): HastHandle
|
|
35
44
|
|
|
36
45
|
/** Parse MDX source into an MDAST arena handle. */
|
|
37
46
|
export declare function createMdxMdastHandle(source: string, features?: JsFeatures | undefined | null): MdastHandle
|
|
@@ -60,15 +69,38 @@ export declare function getMdastFrontmatter(handle: MdastHandle): JsFrontmatter
|
|
|
60
69
|
*/
|
|
61
70
|
export declare function getNodeData(handle: AnyHandle, nodeId: number): string | null
|
|
62
71
|
|
|
72
|
+
/**
|
|
73
|
+
* MDAST→HAST conversion options passed from JavaScript.
|
|
74
|
+
*
|
|
75
|
+
* Input-only: `object_to_js = false` because `FunctionRef` only crosses
|
|
76
|
+
* JS → Rust. A `JsConvertOptions` never gets serialized back to JS.
|
|
77
|
+
*/
|
|
78
|
+
export interface JsConvertOptions {
|
|
79
|
+
/** `<h2>` label opening the footnotes section. Default: `"Footnotes"`. */
|
|
80
|
+
footnoteLabel?: string
|
|
81
|
+
/** Backref `<a>` content. Default: `"\u{21a9}"` (↩). */
|
|
82
|
+
footnoteBackContent?: string | ((arg0: number, arg1: number) => string)
|
|
83
|
+
/**
|
|
84
|
+
* Backref `aria-label`. The token `{reference}` is replaced with the
|
|
85
|
+
* footnote number (`1`) or `number-K` (`1-2`) for repeated references.
|
|
86
|
+
* Default: `"Back to reference {reference}"`.
|
|
87
|
+
*/
|
|
88
|
+
footnoteBackLabel?: string | ((arg0: number, arg1: number) => string)
|
|
89
|
+
}
|
|
90
|
+
|
|
63
91
|
/** Feature toggles for the Markdown/MDX parser, passed from JavaScript. */
|
|
64
92
|
export interface JsFeatures {
|
|
65
93
|
/** GFM: tables, footnotes, strikethrough, task lists. Default: true. */
|
|
66
94
|
gfm?: boolean
|
|
95
|
+
/** Granular GFM control (overrides `gfm`). */
|
|
96
|
+
gfmOptions?: JsGfmOptions
|
|
67
97
|
/** Frontmatter: YAML (`--- ... ---`) and TOML (`+++ ... +++`). Default: true. */
|
|
68
98
|
frontmatter?: boolean
|
|
69
|
-
/** Math blocks and inline math (`$$ ... $$`, `$ ... $`). Default:
|
|
99
|
+
/** Math blocks and inline math (`$$ ... $$`, `$ ... $`). Default: false. */
|
|
70
100
|
math?: boolean
|
|
71
|
-
/**
|
|
101
|
+
/** Granular math control (overrides `math`). */
|
|
102
|
+
mathOptions?: JsMathOptions
|
|
103
|
+
/** Heading attributes (`# text { #id .class }`). Default: false. */
|
|
72
104
|
headingAttributes?: boolean
|
|
73
105
|
/** Colon-delimited container directive blocks (`:::`). Default: false. */
|
|
74
106
|
directive?: boolean
|
|
@@ -92,6 +124,30 @@ export interface JsFrontmatter {
|
|
|
92
124
|
value: string
|
|
93
125
|
}
|
|
94
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Granular GFM toggles, nested under `features.gfm`. The footnote i18n
|
|
129
|
+
* strings (label, back-content, back-label) travel separately via the
|
|
130
|
+
* `JsConvertOptions` argument on conversion entry points; the JS package
|
|
131
|
+
* extracts them from `features.gfm.footnotes` before calling in.
|
|
132
|
+
*/
|
|
133
|
+
export interface JsGfmOptions {
|
|
134
|
+
/**
|
|
135
|
+
* Enable GFM footnotes (`[^id]`). Default: true. Set `false` to drop
|
|
136
|
+
* footnote parsing while keeping the rest of the GFM bundle.
|
|
137
|
+
*/
|
|
138
|
+
footnotes?: boolean
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Granular math toggles, nested under `features.math`. */
|
|
142
|
+
export interface JsMathOptions {
|
|
143
|
+
/**
|
|
144
|
+
* Treat single-dollar runs (`$ ... $`) as inline math. Default: true.
|
|
145
|
+
* Set `false` to keep single `$` as literal text (prose with currency)
|
|
146
|
+
* while still parsing double-dollar (`$$ ... $$`) display math.
|
|
147
|
+
*/
|
|
148
|
+
singleDollarTextMath?: boolean
|
|
149
|
+
}
|
|
150
|
+
|
|
95
151
|
/** MDX compile options passed from JavaScript. */
|
|
96
152
|
export interface JsMdxOptions {
|
|
97
153
|
/**
|
|
@@ -189,7 +245,7 @@ export declare function parseEsm(source: string): string | null
|
|
|
189
245
|
export declare function parseExpression(source: string): string | null
|
|
190
246
|
|
|
191
247
|
/** Parse Markdown source and return HTML string directly. */
|
|
192
|
-
export declare function parseToHtml(source: string, features?: JsFeatures | undefined | null): string
|
|
248
|
+
export declare function parseToHtml(source: string, features?: JsFeatures | undefined | null, convertOptions?: JsConvertOptions | undefined | null): string
|
|
193
249
|
|
|
194
250
|
/** Render a HAST handle's arena to HTML. Does not consume the handle. */
|
|
195
251
|
export declare function renderHandle(handle: HastHandle): string
|