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.
@@ -21,24 +21,37 @@ function attachParseExpression(node, parseFn) {
21
21
  configurable: true,
22
22
  });
23
23
  }
24
- /** Inject `_hast: true` marker on a HastNode and all its children for JSON serialization. */
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
- const n = node;
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 = n.tagName;
42
+ obj.tagName = node.tagName;
30
43
  if ("properties" in node)
31
- obj.properties = n.properties;
44
+ obj.properties = node.properties;
32
45
  if ("value" in node)
33
- obj.value = n.value;
46
+ obj.value = node.value;
34
47
  if ("name" in node)
35
- obj.name = n.name;
48
+ obj.name = node.name;
36
49
  if ("attributes" in node)
37
- obj.attributes = n.attributes;
38
- if ("data" in node && n.data != null)
39
- obj.data = n.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 = n.children.map(markHast);
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
- filename;
56
- constructor(handle, getSource, filename) {
68
+ fileURL;
69
+ constructor(handle, getSource, fileURL) {
57
70
  this.#handle = handle;
58
71
  this.#getSource = getSource;
59
- this.filename = filename;
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 current = this.#pendingNodes.get(id) ?? node;
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
- : `${value}`;
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 from the binary data section into a HastNode.
267
- * Only tagName is decoded eagerly; properties, children, and data are lazy. */
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 = ids;
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 node = { type: TEXT_NODE_TYPES[nodeType], value };
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: u16][val]
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
- // Child IDs
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
- const typeName = nodeType === HAST_MDX_JSX_ELEMENT ? "mdxJsxFlowElement" : "mdxJsxTextElement";
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, offset, nodeId, resolver);
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, offset, nodeId, nodeType);
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, offset, nodeId, nodeType, resolver);
402
- }
403
- // Fallback: minimal node
404
- const node = { type: `unknown(${nodeType})` };
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 void if all visitors are sync, or a Promise if any visitor is async.
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, filename) {
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, filename);
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
- readonly filename: string;
17
- constructor(handle: MdastHandle, getSource: () => string, filename: string);
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), filename: string): MdastVisitResult | Promise<MdastVisitResult>;
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
- filename;
64
- constructor(handle, getSource, filename) {
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.filename = filename;
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, filename) {
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, filename);
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
- /** Apply a command buffer to a HAST handle's arena in-place. */
10
- export declare function applyCommandsToHandle(handle: HastHandle, commandBuf: Uint8Array): void
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
- /** Apply a command buffer to an MDAST handle in-place. */
13
- export declare function applyCommandsToMdastHandle(handle: MdastHandle, commandBuf: Uint8Array): void
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: true. */
99
+ /** Math blocks and inline math (`$$ ... $$`, `$ ... $`). Default: false. */
70
100
  math?: boolean
71
- /** Heading attributes (`# text { #id .class }`). Default: true. */
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