satteri 0.10.5 → 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.
Files changed (89) hide show
  1. package/README.md +11 -0
  2. package/dist/arena-reader.d.ts +13 -0
  3. package/dist/arena-reader.js +104 -0
  4. package/dist/binding.browser.d.ts +1 -1
  5. package/dist/binding.browser.js +1 -1
  6. package/dist/binding.d.ts +1 -1
  7. package/dist/binding.js +1 -1
  8. package/dist/byte-writer.d.ts +0 -18
  9. package/dist/byte-writer.js +6 -30
  10. package/dist/child-stub.d.ts +0 -17
  11. package/dist/child-stub.js +6 -22
  12. package/dist/command-buffer.d.ts +5 -26
  13. package/dist/command-buffer.js +28 -39
  14. package/dist/compile-options.d.ts +11 -0
  15. package/dist/compile-options.js +99 -0
  16. package/dist/compile-types.d.ts +330 -0
  17. package/dist/compile-types.js +1 -0
  18. package/dist/compile.d.ts +22 -328
  19. package/dist/compile.js +129 -379
  20. package/dist/directive-types.d.ts +1 -0
  21. package/dist/directive-types.js +1 -6
  22. package/dist/generated/arena-layout.d.ts +6 -4
  23. package/dist/generated/arena-layout.js +6 -7
  24. package/dist/generated/fused-wire.d.ts +3 -0
  25. package/dist/generated/fused-wire.js +229 -0
  26. package/dist/generated/parse-options.js +0 -5
  27. package/dist/generated/wire-constants.d.ts +3 -0
  28. package/dist/generated/wire-constants.js +33 -42
  29. package/dist/handles.d.ts +0 -7
  30. package/dist/hast/child-stub.d.ts +1 -8
  31. package/dist/hast/child-stub.js +14 -13
  32. package/dist/hast/element-props.d.ts +13 -0
  33. package/dist/hast/element-props.js +52 -6
  34. package/dist/hast/generated/node-types.d.ts +1 -9
  35. package/dist/hast/generated/node-types.js +1 -9
  36. package/dist/hast/generated/walk-decode.d.ts +10 -0
  37. package/dist/hast/generated/walk-decode.js +61 -0
  38. package/dist/hast/hast-materializer.d.ts +3 -10
  39. package/dist/hast/hast-materializer.js +73 -47
  40. package/dist/hast/hast-reader.d.ts +9 -38
  41. package/dist/hast/hast-reader.js +82 -271
  42. package/dist/hast/hast-visitor.d.ts +29 -16
  43. package/dist/hast/hast-visitor.js +197 -508
  44. package/dist/hast/hast-walk.d.ts +18 -0
  45. package/dist/hast/hast-walk.js +264 -0
  46. package/dist/index.d.ts +5 -5
  47. package/dist/index.js +4 -12
  48. package/dist/lazy-child-resolver.d.ts +1 -21
  49. package/dist/lazy-child-resolver.js +11 -32
  50. package/dist/materializer-cache.d.ts +5 -29
  51. package/dist/materializer-cache.js +29 -101
  52. package/dist/mdast/child-stub.d.ts +1 -12
  53. package/dist/mdast/child-stub.js +18 -28
  54. package/dist/mdast/column-align.d.ts +1 -1
  55. package/dist/mdast/column-align.js +0 -2
  56. package/dist/mdast/generated/layout.d.ts +0 -16
  57. package/dist/mdast/generated/layout.js +6 -47
  58. package/dist/mdast/generated/node-types.d.ts +1 -9
  59. package/dist/mdast/generated/node-types.js +3 -9
  60. package/dist/mdast/mdast-materializer.d.ts +3 -11
  61. package/dist/mdast/mdast-materializer.js +91 -63
  62. package/dist/mdast/mdast-reader.d.ts +15 -63
  63. package/dist/mdast/mdast-reader.js +132 -269
  64. package/dist/mdast/mdast-visitor.d.ts +23 -15
  65. package/dist/mdast/mdast-visitor.js +188 -294
  66. package/dist/mdast/mdast-walk.d.ts +13 -0
  67. package/dist/mdast/mdast-walk.js +144 -0
  68. package/dist/mdx-attr.js +0 -3
  69. package/dist/mdx-types.js +1 -6
  70. package/dist/op-stream.d.ts +1 -15
  71. package/dist/op-stream.js +3 -19
  72. package/dist/phantom.d.ts +0 -1
  73. package/dist/phantom.js +1 -9
  74. package/dist/plugin-pipeline.d.ts +32 -0
  75. package/dist/plugin-pipeline.js +177 -0
  76. package/dist/plugin.d.ts +8 -7
  77. package/dist/plugin.js +3 -9
  78. package/dist/types.d.ts +0 -1
  79. package/dist/visitor-node.d.ts +12 -0
  80. package/dist/visitor-node.js +23 -0
  81. package/dist/visitor-shared.d.ts +26 -30
  82. package/dist/visitor-shared.js +34 -36
  83. package/dist/wire-read.d.ts +0 -22
  84. package/dist/wire-read.js +3 -24
  85. package/index.d.ts +19 -10
  86. package/index.js +56 -53
  87. package/package.json +3 -12
  88. package/dist/string-pool.d.ts +0 -13
  89. package/dist/string-pool.js +0 -91
package/dist/compile.js CHANGED
@@ -1,310 +1,36 @@
1
- import { visitHastHandle, visitHastHandleCollect, visitHastHook, visitHastHookCollect, resolveSubscriptions, } from "./hast/hast-visitor.js";
2
- import { visitMdastHandle, visitMdastHook, resolveMdastSubscriptions, } from "./mdast/mdast-visitor.js";
3
- import { normalizePlugins } from "./plugin.js";
4
- import { applyCommandsAndCompileHandle, applyCommandsAndRenderHandle, applyCommandsToMdastHandle, applyMdastCommandsAndConvertAndCompile, applyMdastCommandsAndConvertAndRender, compileHandle, convertMdastToHastHandle, createHastHandle, createHastHandleFromHtml, createMdastHandle, createMdxHastHandle, createMdxMdastHandle, dropHandle, getHandleSource, createHastHandleWithFrontmatter, createMdxHastHandleWithFrontmatter, getMdastFrontmatter, markdownToHtmlFast, markdownToJsFast, mdxToJsFast, renderHandle, serializeHandle, } from "#binding";
5
- import { ENABLE_DEFINITION_LIST, ENABLE_DIRECTIVE, ENABLE_FOOTNOTES, ENABLE_GFM, ENABLE_HEADING_ATTRIBUTES, ENABLE_MATH, ENABLE_MATH_MULTI_DOLLAR, ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS, ENABLE_SMART_DASHES, ENABLE_SMART_ELLIPSES, ENABLE_SMART_PUNCTUATION, ENABLE_SMART_QUOTES, ENABLE_STRIKETHROUGH, ENABLE_SUBSCRIPT, ENABLE_SUPERSCRIPT, ENABLE_TABLES, ENABLE_TASKLISTS, ENABLE_WIKILINKS, ENABLE_YAML_STYLE_METADATA_BLOCKS, } from "./generated/parse-options.js";
6
- import { MdastReader } from "./mdast/mdast-reader.js";
7
- import { materializeMdastTree } from "./mdast/mdast-materializer.js";
8
- import { markHandleMutated } from "./lazy-child-resolver.js";
9
- import { HastReader } from "./hast/hast-reader.js";
1
+ import { encodeHastDocument } from "./hast/hast-visitor.js";
2
+ import { featuresToNative, mdxOptionsToNative } from "./compile-options.js";
3
+ import { createHastHandleFromMdast, EMPTY_COMMAND_BUFFER, releaseHandle, runHastPluginsCollectLast, runMdastPluginsOnHandle, warnIfDroppedTransforms, } from "./plugin-pipeline.js";
4
+ export { DEFAULT_PARSE_OPTIONS, featuresToNative } from "./compile-options.js";
5
+ import { applyCommandsAndCompileHandle, applyCommandsAndRenderHandle, applyCommandsToHandle, applyMdastCommandsAndConvertAndCompile, applyMdastCommandsAndConvertAndRender, compileHandle, createHastHandleFromHtml, createMdastHandle, createMdxMdastHandle, markdownToHtmlFast, markdownToJsFast, mdxToJsFast, parseHastWire, parseMdastWire, renderHandle, renderHastOpstream, serializeHandle, } from "#binding";
10
6
  import { materializeHastTree } from "./hast/hast-materializer.js";
11
- const GFM_PARSE_OPTIONS = ENABLE_TABLES | ENABLE_STRIKETHROUGH | ENABLE_TASKLISTS | ENABLE_GFM;
12
- const FRONTMATTER_PARSE_OPTIONS = ENABLE_YAML_STYLE_METADATA_BLOCKS | ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS;
13
- export const DEFAULT_PARSE_OPTIONS = GFM_PARSE_OPTIONS | ENABLE_FOOTNOTES | FRONTMATTER_PARSE_OPTIONS;
14
- /** Packs into the bit set napi reads straight back as `satteri_pulldown_cmark::Options`. */
15
- function featuresToParseOptions(features) {
16
- let options = 0;
17
- const { gfm } = features;
18
- if (typeof gfm === "object") {
19
- options |= GFM_PARSE_OPTIONS;
20
- if (gfm.footnotes !== false)
21
- options |= ENABLE_FOOTNOTES;
22
- }
23
- else if (gfm ?? true) {
24
- options |= GFM_PARSE_OPTIONS | ENABLE_FOOTNOTES;
25
- }
26
- if (features.frontmatter ?? true)
27
- options |= FRONTMATTER_PARSE_OPTIONS;
28
- // Opting out of single-dollar math sets the multi-dollar sub-flag so the parser skips lone `$`.
29
- const { math } = features;
30
- if (typeof math === "object") {
31
- options |= math.singleDollarTextMath === false ? ENABLE_MATH_MULTI_DOLLAR : ENABLE_MATH;
32
- }
33
- else if (math) {
34
- options |= ENABLE_MATH;
35
- }
36
- if (features.headingAttributes)
37
- options |= ENABLE_HEADING_ATTRIBUTES;
38
- if (features.directive)
39
- options |= ENABLE_DIRECTIVE;
40
- if (features.superscript)
41
- options |= ENABLE_SUPERSCRIPT;
42
- if (features.subscript)
43
- options |= ENABLE_SUBSCRIPT;
44
- if (features.wikilinks)
45
- options |= ENABLE_WIKILINKS;
46
- if (features.definitionList)
47
- options |= ENABLE_DEFINITION_LIST;
48
- const smartPunctuation = features.smartPunctuation;
49
- if (typeof smartPunctuation === "object") {
50
- if (smartPunctuation.quotes ?? true)
51
- options |= ENABLE_SMART_QUOTES;
52
- if (smartPunctuation.dashes ?? true)
53
- options |= ENABLE_SMART_DASHES;
54
- if (smartPunctuation.ellipses ?? true)
55
- options |= ENABLE_SMART_ELLIPSES;
56
- }
57
- else if (smartPunctuation) {
58
- options |= ENABLE_SMART_PUNCTUATION;
59
- }
60
- return options;
61
- }
62
- /**
63
- * Split the user-facing `Features` into the packed parser options plus the
64
- * conversion-side `JsConvertOptions` carrying the footnote i18n strings and the
65
- * raw-HTML reparse. The public API only exposes `features`; both halves are
66
- * routed to napi internally.
67
- */
68
- export function featuresToNative(features) {
69
- if (!features)
70
- return { parseOptions: DEFAULT_PARSE_OPTIONS, convertOptions: undefined };
71
- let convertOptions;
72
- const { gfm } = features;
73
- if (typeof gfm === "object" && typeof gfm.footnotes === "object") {
74
- const { label, backContent, backLabel, clobberPrefix } = gfm.footnotes;
75
- convertOptions = {};
76
- if (label !== undefined)
77
- convertOptions.footnoteLabel = label;
78
- if (backContent !== undefined)
79
- convertOptions.footnoteBackContent = backContent;
80
- if (backLabel !== undefined)
81
- convertOptions.footnoteBackLabel = backLabel;
82
- if (clobberPrefix !== undefined)
83
- convertOptions.clobberPrefix = clobberPrefix;
84
- }
85
- if (features.rawHtml !== undefined) {
86
- // Routed to convert options so every pipeline (fast paths, plugin paths,
87
- // *ToHast) applies the reparse.
88
- convertOptions = convertOptions ?? {};
89
- convertOptions.rawHtml = features.rawHtml;
90
- }
91
- return { parseOptions: featuresToParseOptions(features), convertOptions };
92
- }
93
- /** Free a handle's arena. A plugin's visitor can hand child stubs to user code;
94
- * pass `invalidateStubs` so the epoch bump makes any stub retained past this
95
- * point hit the designed retention error instead of snapshotting the freed arena. */
96
- function releaseHandle(handle, invalidateStubs) {
97
- if (invalidateStubs)
98
- markHandleMutated(handle);
99
- dropHandle(handle);
100
- }
101
- function warnDroppedTransforms(plugin, dropped, kind) {
102
- const name = plugin.name ?? "<anonymous>";
103
- const noun = dropped === 1 ? "transform" : "transforms";
104
- console.warn(`satteri: plugin "${name}" queued ${dropped} ${kind} ${noun} on node(s) that a plugin had ` +
105
- `already removed or replaced; ${dropped === 1 ? "it was" : "they were"} dropped.`);
106
- }
107
- function settle(value, fn) {
108
- if (value instanceof Promise)
109
- return value.then(fn);
110
- fn(value);
111
- return undefined;
112
- }
113
- /** Stays synchronous until a step actually returns a promise. */
114
- function sequence(steps) {
115
- let pending;
116
- for (const step of steps) {
117
- if (step === undefined)
118
- continue;
119
- pending = pending === undefined ? step() : pending.then(step);
120
- }
121
- return pending;
122
- }
123
- function runMdastPluginsOnHandle(handle, plugins, fileURL, data, sourceFormat, collectLast = false) {
124
- const out = { handle };
125
- // A plugin's visitors run once over the tree. A transform that passes a child
126
- // through (returning it inside the replacement) keeps that child's identity,
127
- // so a patch the same pass queued on it still applies: nesting composes in
128
- // one pass. Visitor-built nodes are not re-walked; transform them up front,
129
- // or hand off to a later plugin that sees the materialized tree.
130
- const runPlugin = (plugin, isLastPlugin) => {
131
- const subs = resolveMdastSubscriptions(plugin);
132
- const apply = (r, isFinalPass) => {
133
- if (!r.hasMutations)
134
- return;
135
- if (collectLast && isLastPlugin && isFinalPass) {
136
- out.pendingCommands = r.commandBuffer;
137
- out.lastPlugin = plugin;
138
- return;
139
- }
140
- markHandleMutated(handle);
141
- const dropped = applyCommandsToMdastHandle(handle, r.commandBuffer);
142
- if (dropped)
143
- warnDroppedTransforms(plugin, dropped, "mdast");
144
- };
145
- const before = typeof plugin.before === "function" ? plugin.before : undefined;
146
- const after = typeof plugin.after === "function" ? plugin.after : undefined;
147
- const hasVisitors = subs.length > 0;
148
- // Threaded like `data`: one context per pass, since a resolver is epoch-bound.
149
- const diagnostics = [];
150
- const runHook = (hook, isFinalPass) => () => settle(visitMdastHook(handle, plugin, hook, () => getHandleSource(handle), fileURL, data, sourceFormat, diagnostics), (r) => apply(r, isFinalPass));
151
- const runVisitors = (isFinalPass) => () => settle(visitMdastHandle(handle, plugin, subs, () => getHandleSource(handle), fileURL, data, sourceFormat, diagnostics), (r) => apply(r, isFinalPass));
152
- return sequence([
153
- before ? runHook(before, !hasVisitors && after === undefined) : undefined,
154
- hasVisitors ? runVisitors(after === undefined) : undefined,
155
- after ? runHook(after, true) : undefined,
156
- ]);
157
- };
158
- let i = 0;
159
- const runNext = () => {
160
- for (;;) {
161
- const plugin = plugins[i];
162
- if (plugin === undefined)
163
- break;
164
- i++;
165
- const r = runPlugin(plugin, i === plugins.length);
166
- if (r instanceof Promise)
167
- return r.then(runNext);
168
- }
169
- return out;
170
- };
171
- return runNext();
172
- }
173
- const EMPTY_COMMAND_BUFFER = new Uint8Array(0);
174
- const NO_HAST_COMMANDS = {
175
- commands: EMPTY_COMMAND_BUFFER,
176
- lastPlugin: null,
177
- };
178
- /** Run every HAST plugin except the last one normally (each applies its
179
- * commands inline), then return the last plugin's commands without applying.
180
- * Lets the caller fuse the final apply with the downstream NAPI step
181
- * (`render` or `compile`), saving one NAPI roundtrip per compile. */
182
- function runHastPluginsCollectLast(handle, plugins, source, fileURL, data, sourceFormat) {
183
- let i = 0;
184
- const runNext = () => {
185
- for (;;) {
186
- const plugin = plugins[i];
187
- if (plugin === undefined)
188
- break;
189
- const isLastPlugin = i === plugins.length - 1;
190
- i++;
191
- const subs = resolveSubscriptions(plugin);
192
- const { before, after } = plugin;
193
- const passes = [];
194
- if (typeof before === "function")
195
- passes.push(before);
196
- if (subs.length > 0)
197
- passes.push("visitors");
198
- if (typeof after === "function")
199
- passes.push(after);
200
- const finalPass = passes.pop();
201
- if (finalPass === undefined)
202
- continue;
203
- const warnIfDropped = (dropped) => {
204
- if (dropped)
205
- warnDroppedTransforms(plugin, dropped, "hast");
206
- };
207
- // Threaded like `data`: one context per pass, since a resolver is epoch-bound.
208
- const diagnostics = [];
209
- const runApplied = (pass) => () => settle(pass === "visitors"
210
- ? visitHastHandle(handle, plugin, subs, source, fileURL, data, sourceFormat, diagnostics)
211
- : visitHastHook(handle, plugin, pass, source, fileURL, data, sourceFormat, diagnostics), warnIfDropped);
212
- const collectFinal = () => {
213
- const collected = finalPass === "visitors"
214
- ? visitHastHandleCollect(handle, plugin, subs, source, fileURL, data, sourceFormat, diagnostics)
215
- : visitHastHookCollect(handle, plugin, finalPass, source, fileURL, data, sourceFormat, diagnostics);
216
- return collected instanceof Promise
217
- ? collected.then((commands) => ({ commands, lastPlugin: plugin }))
218
- : { commands: collected, lastPlugin: plugin };
219
- };
220
- const head = sequence(passes.map(runApplied));
221
- if (isLastPlugin) {
222
- return head instanceof Promise ? head.then(collectFinal) : collectFinal();
223
- }
224
- const tail = head instanceof Promise ? head.then(runApplied(finalPass)) : runApplied(finalPass)();
225
- if (tail instanceof Promise)
226
- return tail.then(runNext);
227
- }
228
- return NO_HAST_COMMANDS;
229
- };
230
- return runNext();
231
- }
232
- // Public API
233
- function mdxOptionsToNative(opts) {
234
- const hasAny = opts.optimizeStatic ||
235
- opts.jsxImportSource !== undefined ||
236
- opts.jsx !== undefined ||
237
- opts.jsxRuntime !== undefined ||
238
- opts.development !== undefined ||
239
- opts.providerImportSource !== undefined ||
240
- opts.pragma !== undefined ||
241
- opts.pragmaFrag !== undefined ||
242
- opts.pragmaImportSource !== undefined ||
243
- opts.outputFormat !== undefined ||
244
- opts.elementAttributeNameCase !== undefined ||
245
- opts.stylePropertyNameCase !== undefined;
246
- if (!hasAny)
247
- return undefined;
248
- const result = {};
249
- if (opts.optimizeStatic)
250
- result.optimizeStatic = opts.optimizeStatic;
251
- if (opts.jsxImportSource !== undefined)
252
- result.jsxImportSource = opts.jsxImportSource;
253
- if (opts.jsx !== undefined)
254
- result.jsx = opts.jsx;
255
- if (opts.jsxRuntime !== undefined)
256
- result.jsxRuntime = opts.jsxRuntime;
257
- if (opts.development !== undefined)
258
- result.development = opts.development;
259
- if (opts.providerImportSource !== undefined)
260
- result.providerImportSource = opts.providerImportSource;
261
- if (opts.pragma !== undefined)
262
- result.pragma = opts.pragma;
263
- if (opts.pragmaFrag !== undefined)
264
- result.pragmaFrag = opts.pragmaFrag;
265
- if (opts.pragmaImportSource !== undefined)
266
- result.pragmaImportSource = opts.pragmaImportSource;
267
- if (opts.outputFormat !== undefined)
268
- result.outputFormat = opts.outputFormat;
269
- if (opts.elementAttributeNameCase !== undefined)
270
- result.elementAttributeNameCase = opts.elementAttributeNameCase;
271
- if (opts.stylePropertyNameCase !== undefined)
272
- result.stylePropertyNameCase = opts.stylePropertyNameCase;
273
- return result;
274
- }
7
+ import { HastReader } from "./hast/hast-reader.js";
8
+ import { materializeMdastTree } from "./mdast/mdast-materializer.js";
9
+ import { MdastReader } from "./mdast/mdast-reader.js";
10
+ import { normalizePlugins } from "./plugin.js";
275
11
  export function markdownToHtml(source, options = {}) {
276
12
  const { features, fileURL, data = {} } = options;
277
13
  const mdastPlugins = normalizePlugins(options.mdastPlugins ?? [], "mdastPlugins", source, fileURL, "markdown", data);
278
14
  const hastPlugins = normalizePlugins(options.hastPlugins ?? [], "hastPlugins", source, fileURL, "markdown", data);
279
15
  const hastMayHaveStubs = hastPlugins.length > 0;
280
16
  const { parseOptions, convertOptions: nativeConvertOptions } = featuresToNative(features);
281
- // Fast path: no plugins → parse, convert, render, and frontmatter all happen
282
- // inside a single NAPI call. Skips 5 handle-NAPI roundtrips that the
283
- // plugin-capable path needs to keep the arena live across passes.
17
+ // Without plugins, one NAPI call avoids the handle lifecycle’s extra boundary crossings.
284
18
  if (mdastPlugins.length === 0 && hastPlugins.length === 0) {
285
19
  const { html, frontmatter } = markdownToHtmlFast(source, parseOptions, nativeConvertOptions);
286
20
  return { html, frontmatter: frontmatter ?? null, data };
287
21
  }
288
- // Track source positions only when some plugin opts in with `position: true`;
289
- // otherwise the parse skips the LineIndex build and per-node line/column lookups.
22
+ // Skip position tracking unless requested to avoid line-index construction and per-node lookups.
290
23
  const trackPositions = mdastPlugins.some((p) => p.options?.position) || hastPlugins.some((p) => p.options?.position);
291
- // Fused tail for MDAST-plugins-only (no HAST plugins): after the MDAST
292
- // plugin pass returns its pending commands, apply + convert + render all
293
- // happen inside a single NAPI roundtrip. Saves the convert-to-hast handle
294
- // create + render + drop crossings the generic path makes separately.
24
+ // Without HAST plugins, fuse apply, conversion, and rendering to avoid extra NAPI crossings.
295
25
  if (hastPlugins.length === 0) {
296
26
  const mdastHandle = createMdastHandle(source, parseOptions, trackPositions);
297
27
  try {
298
28
  const mdastResult = runMdastPluginsOnHandle(mdastHandle, mdastPlugins, fileURL, data, "markdown", true);
299
29
  const finishMdast = (r) => {
300
30
  try {
301
- // Fused tail: apply pending commands (empty buffer is a Rust no-op),
302
- // extract frontmatter post-mutation, convert, render, all in one
303
- // NAPI roundtrip.
304
31
  const commands = r.pendingCommands ?? EMPTY_COMMAND_BUFFER;
305
32
  const { html, frontmatter, droppedTransforms } = applyMdastCommandsAndConvertAndRender(r.handle, commands, nativeConvertOptions);
306
- if (droppedTransforms && r.lastPlugin)
307
- warnDroppedTransforms(r.lastPlugin, droppedTransforms, "mdast");
33
+ warnIfDroppedTransforms(droppedTransforms, r.lastPlugin, "mdast");
308
34
  return {
309
35
  html,
310
36
  frontmatter: frontmatter ?? null,
@@ -330,9 +56,6 @@ export function markdownToHtml(source, options = {}) {
330
56
  }
331
57
  const result = createHastHandleFromMdast(source, mdastPlugins, false, fileURL, parseOptions, nativeConvertOptions, data, trackPositions);
332
58
  const runHastThenRender = (r) => {
333
- // Run all but the last HAST plugin normally (each one applies its
334
- // commands inline); collect the last plugin's commands and fuse apply +
335
- // render + handle-drop into one NAPI call.
336
59
  let collected;
337
60
  try {
338
61
  collected = runHastPluginsCollectLast(r.hastHandle, hastPlugins, source, fileURL, data, "markdown");
@@ -353,10 +76,9 @@ export function markdownToHtml(source, options = {}) {
353
76
  try {
354
77
  let html;
355
78
  if (collected.commands.length > 0) {
356
- const result = applyCommandsAndRenderHandle(h, collected.commands);
357
- if (result.droppedTransforms && collected.lastPlugin)
358
- warnDroppedTransforms(collected.lastPlugin, result.droppedTransforms, "hast");
359
- html = result.html;
79
+ const transformed = applyCommandsAndRenderHandle(h, collected.commands);
80
+ warnIfDroppedTransforms(transformed.droppedTransforms, collected.lastPlugin, "hast");
81
+ html = transformed.html;
360
82
  }
361
83
  else {
362
84
  html = renderHandle(h);
@@ -377,7 +99,6 @@ export function mdxToJs(source, options = {}) {
377
99
  export function markdownToJs(source, options = {}) {
378
100
  return toJsImpl(source, options, false);
379
101
  }
380
- /** `mdx` picks the parser; the pipeline is identical from MDAST on. */
381
102
  function toJsImpl(source, options, mdx) {
382
103
  const { mdastPlugins: mdastInput = [], hastPlugins: hastInput = [], features, fileURL, data = {}, ...mdxFields } = options;
383
104
  const sourceFormat = mdx ? "mdx" : "markdown";
@@ -386,17 +107,13 @@ function toJsImpl(source, options, mdx) {
386
107
  const hastMayHaveStubs = hastPlugins.length > 0;
387
108
  const mdxOptions = mdxOptionsToNative(mdxFields);
388
109
  const { parseOptions, convertOptions: nativeConvertOptions } = featuresToNative(features);
389
- // Fast path: same trick as `markdownToHtml`. Parse → MDAST → HAST → JS plus
390
- // frontmatter extraction all happen inside a single NAPI call. Skips 5 of
391
- // the 6 handle-based crossings the plugin-capable path needs.
110
+ // Without plugins, one NAPI call avoids the handle lifecycle’s extra boundary crossings.
392
111
  if (mdastPlugins.length === 0 && hastPlugins.length === 0) {
393
112
  const { code, frontmatter } = (mdx ? mdxToJsFast : markdownToJsFast)(source, parseOptions, mdxOptions, nativeConvertOptions);
394
113
  return { code, frontmatter: frontmatter ?? null, data };
395
114
  }
396
- // Track positions only when a plugin opts in (see `markdownToHtml`).
397
115
  const trackPositions = mdastPlugins.some((p) => p.options?.position) || hastPlugins.some((p) => p.options?.position);
398
- // MDAST-plugins-only fused tail (no HAST plugins): apply + extract
399
- // frontmatter + convert + simplify + compile happen in one NAPI call.
116
+ // Without HAST plugins, fuse apply, conversion, and compilation to avoid extra NAPI crossings.
400
117
  if (hastPlugins.length === 0) {
401
118
  const mdastHandle = mdx
402
119
  ? createMdxMdastHandle(source, parseOptions, trackPositions)
@@ -407,8 +124,7 @@ function toJsImpl(source, options, mdx) {
407
124
  try {
408
125
  const commands = r.pendingCommands ?? EMPTY_COMMAND_BUFFER;
409
126
  const { code, frontmatter, droppedTransforms } = applyMdastCommandsAndConvertAndCompile(r.handle, commands, mdxOptions, nativeConvertOptions);
410
- if (droppedTransforms && r.lastPlugin)
411
- warnDroppedTransforms(r.lastPlugin, droppedTransforms, "mdast");
127
+ warnIfDroppedTransforms(droppedTransforms, r.lastPlugin, "mdast");
412
128
  return {
413
129
  code,
414
130
  frontmatter: frontmatter ?? null,
@@ -454,10 +170,9 @@ function toJsImpl(source, options, mdx) {
454
170
  try {
455
171
  let code;
456
172
  if (collected.commands.length > 0) {
457
- const result = applyCommandsAndCompileHandle(h, collected.commands, mdxOptions);
458
- if (result.droppedTransforms && collected.lastPlugin)
459
- warnDroppedTransforms(collected.lastPlugin, result.droppedTransforms, "hast");
460
- code = result.code;
173
+ const transformed = applyCommandsAndCompileHandle(h, collected.commands, mdxOptions);
174
+ warnIfDroppedTransforms(transformed.droppedTransforms, collected.lastPlugin, "hast");
175
+ code = transformed.code;
461
176
  }
462
177
  else {
463
178
  code = compileHandle(h, mdxOptions);
@@ -492,99 +207,111 @@ export function evaluate(source, options) {
492
207
  }
493
208
  return new Function(result.code)(runtime);
494
209
  }
495
- function readFrontmatter(handle) {
496
- const raw = getMdastFrontmatter(handle);
497
- return raw ? { kind: raw.kind === "toml" ? "toml" : "yaml", value: raw.value } : null;
210
+ function materializeMdastHandle(handle) {
211
+ try {
212
+ return materializeMdastTree(new MdastReader(serializeHandle(handle)));
213
+ }
214
+ finally {
215
+ releaseHandle(handle, true);
216
+ }
498
217
  }
499
- /** Parse, run mdast plugins, capture frontmatter, then convert to HAST.
500
- * Frontmatter is read from the post-plugin MDAST so visitor mutations to
501
- * the yaml/toml node are reflected in the returned value. */
502
- function createHastHandleFromMdast(source, mdastPlugins, mdx, fileURL, parseOptions, nativeConvertOptions, data, trackPositions) {
503
- if (mdastPlugins.length === 0) {
504
- const [hastHandle, raw] = mdx
505
- ? createMdxHastHandleWithFrontmatter(source, parseOptions, nativeConvertOptions, trackPositions)
506
- : createHastHandleWithFrontmatter(source, parseOptions, nativeConvertOptions, trackPositions);
507
- return {
508
- hastHandle,
509
- frontmatter: raw ? { kind: raw.kind === "toml" ? "toml" : "yaml", value: raw.value } : null,
510
- };
218
+ function materializeHastHandle(handle, invalidateStubs) {
219
+ try {
220
+ return materializeHastTree(new HastReader(serializeHandle(handle)));
511
221
  }
512
- const mdastHandle = mdx
513
- ? createMdxMdastHandle(source, parseOptions, trackPositions)
514
- : createMdastHandle(source, parseOptions, trackPositions);
515
- const sourceFormat = mdx ? "mdx" : "markdown";
516
- const mdastMayHaveStubs = mdastPlugins.length > 0;
517
- // finally{release} is intentional: convertMdastToHastHandle empties the arena
518
- // on success, but if any step here throws the handle would otherwise leak.
519
- const finalize = (r) => {
222
+ finally {
223
+ releaseHandle(handle, invalidateStubs);
224
+ }
225
+ }
226
+ function applyCollectedHastCommands(handle, collected, invalidateStubs) {
227
+ if (collected.commands.length > 0) {
520
228
  try {
521
- const frontmatter = readFrontmatter(r.handle);
522
- // convert empties the mdast arena, so invalidate any stub held past this point.
523
- if (mdastMayHaveStubs)
524
- markHandleMutated(r.handle);
525
- const hastHandle = convertMdastToHastHandle(r.handle, nativeConvertOptions);
526
- return { hastHandle, frontmatter };
229
+ const dropped = applyCommandsToHandle(handle, collected.commands);
230
+ warnIfDroppedTransforms(dropped, collected.lastPlugin, "hast");
527
231
  }
528
- finally {
529
- releaseHandle(r.handle, mdastMayHaveStubs);
232
+ catch (err) {
233
+ releaseHandle(handle, invalidateStubs);
234
+ throw err;
530
235
  }
531
- };
236
+ }
237
+ return materializeHastHandle(handle, invalidateStubs);
238
+ }
239
+ function toMdastImpl(source, options, mdx) {
240
+ const { features, fileURL, data = {} } = options;
241
+ const sourceFormat = mdx ? "mdx" : "markdown";
242
+ const mdastPlugins = normalizePlugins(options.mdastPlugins ?? [], "mdastPlugins", source, fileURL, sourceFormat, data);
243
+ const { parseOptions } = featuresToNative(features);
244
+ if (mdastPlugins.length === 0) {
245
+ const wire = parseMdastWire(source, parseOptions, mdx, options.position);
246
+ return materializeMdastTree(new MdastReader(wire));
247
+ }
248
+ // Tree callers control positions independently of plugin requirements.
249
+ const trackPositions = options.position ?? true;
250
+ const handle = mdx
251
+ ? createMdxMdastHandle(source, parseOptions, trackPositions)
252
+ : createMdastHandle(source, parseOptions, trackPositions);
532
253
  try {
533
- const mdastResult = runMdastPluginsOnHandle(mdastHandle, mdastPlugins, fileURL, data, sourceFormat);
534
- if (mdastResult instanceof Promise) {
535
- return mdastResult.then(finalize, (err) => {
536
- releaseHandle(mdastHandle, mdastMayHaveStubs);
254
+ const result = runMdastPluginsOnHandle(handle, mdastPlugins, fileURL, data, sourceFormat);
255
+ if (result instanceof Promise) {
256
+ return result.then((r) => materializeMdastHandle(r.handle), (err) => {
257
+ releaseHandle(handle, true);
537
258
  throw err;
538
259
  });
539
260
  }
540
- return finalize(mdastResult);
261
+ return materializeMdastHandle(result.handle);
541
262
  }
542
263
  catch (err) {
543
- releaseHandle(mdastHandle, mdastMayHaveStubs);
264
+ releaseHandle(handle, true);
544
265
  throw err;
545
266
  }
546
267
  }
547
- /** Parse Markdown source into a materialized mdast tree. */
548
- export function markdownToMdast(source, options = {}) {
549
- const handle = createMdastHandle(source, featuresToNative(options.features).parseOptions, options.position);
550
- try {
551
- return materializeMdastTree(new MdastReader(serializeHandle(handle)));
552
- }
553
- finally {
554
- releaseHandle(handle, true);
268
+ function toHastImpl(source, options, mdx) {
269
+ const { features, fileURL, data = {} } = options;
270
+ const sourceFormat = mdx ? "mdx" : "markdown";
271
+ const mdastPlugins = normalizePlugins(options.mdastPlugins ?? [], "mdastPlugins", source, fileURL, sourceFormat, data);
272
+ const hastPlugins = normalizePlugins(options.hastPlugins ?? [], "hastPlugins", source, fileURL, sourceFormat, data);
273
+ const { parseOptions, convertOptions } = featuresToNative(features);
274
+ if (mdastPlugins.length === 0 && hastPlugins.length === 0) {
275
+ const wire = parseHastWire(source, parseOptions, convertOptions, mdx, options.position);
276
+ return materializeHastTree(new HastReader(wire));
555
277
  }
278
+ const trackPositions = options.position ?? true;
279
+ const hastMayHaveStubs = hastPlugins.length > 0;
280
+ const runHastPluginsAndMaterialize = (handle) => {
281
+ if (hastPlugins.length === 0)
282
+ return materializeHastHandle(handle, hastMayHaveStubs);
283
+ let collected;
284
+ try {
285
+ collected = runHastPluginsCollectLast(handle, hastPlugins, source, fileURL, data, sourceFormat);
286
+ }
287
+ catch (err) {
288
+ releaseHandle(handle, hastMayHaveStubs);
289
+ throw err;
290
+ }
291
+ if (collected instanceof Promise) {
292
+ return collected.then((c) => applyCollectedHastCommands(handle, c, hastMayHaveStubs), (err) => {
293
+ releaseHandle(handle, hastMayHaveStubs);
294
+ throw err;
295
+ });
296
+ }
297
+ return applyCollectedHastCommands(handle, collected, hastMayHaveStubs);
298
+ };
299
+ const result = createHastHandleFromMdast(source, mdastPlugins, mdx, fileURL, parseOptions, convertOptions, data, trackPositions);
300
+ if (result instanceof Promise)
301
+ return result.then((r) => runHastPluginsAndMaterialize(r.hastHandle));
302
+ return runHastPluginsAndMaterialize(result.hastHandle);
303
+ }
304
+ export function markdownToMdast(source, options = {}) {
305
+ return toMdastImpl(source, options, false);
556
306
  }
557
- /** Parse MDX source into a materialized mdast tree. */
558
307
  export function mdxToMdast(source, options = {}) {
559
- const handle = createMdxMdastHandle(source, featuresToNative(options.features).parseOptions, options.position);
560
- try {
561
- return materializeMdastTree(new MdastReader(serializeHandle(handle)));
562
- }
563
- finally {
564
- releaseHandle(handle, true);
565
- }
308
+ return toMdastImpl(source, options, true);
566
309
  }
567
- /** Convert Markdown source to a materialized hast tree. */
568
310
  export function markdownToHast(source, options = {}) {
569
- const { parseOptions, convertOptions } = featuresToNative(options.features);
570
- const handle = createHastHandle(source, parseOptions, convertOptions, options.position);
571
- try {
572
- return materializeHastTree(new HastReader(serializeHandle(handle)));
573
- }
574
- finally {
575
- releaseHandle(handle, true);
576
- }
311
+ return toHastImpl(source, options, false);
577
312
  }
578
- /** Convert MDX source to a materialized hast tree. */
579
313
  export function mdxToHast(source, options = {}) {
580
- const { parseOptions, convertOptions } = featuresToNative(options.features);
581
- const handle = createMdxHastHandle(source, parseOptions, convertOptions, options.position);
582
- try {
583
- return materializeHastTree(new HastReader(serializeHandle(handle)));
584
- }
585
- finally {
586
- releaseHandle(handle, true);
587
- }
314
+ return toHastImpl(source, options, true);
588
315
  }
589
316
  /**
590
317
  * Parse an HTML string into a materialized hast tree: a `root` whose children
@@ -601,3 +328,26 @@ export function htmlToHast(html, options = {}) {
601
328
  releaseHandle(handle, true);
602
329
  }
603
330
  }
331
+ /**
332
+ * Serialize a HAST tree to an HTML string: a `root` renders its children, any
333
+ * other node renders itself, and a list renders in order. MDX nodes have no
334
+ * HTML representation and are skipped; `raw` nodes are emitted verbatim.
335
+ *
336
+ * The result is the tree's exact serialization, with no trailing newline (the
337
+ * one `markdownToHtml` ends its document with).
338
+ */
339
+ export function hastToHtml(tree) {
340
+ return encodeHastDocument(documentNodes(tree), renderHastOpstream);
341
+ }
342
+ /** A `root` contributes its children, wherever it sits, so a list of trees
343
+ * serializes as the concatenation of what each renders alone. */
344
+ function documentNodes(tree) {
345
+ if (!Array.isArray(tree)) {
346
+ const node = tree;
347
+ return node.type === "root" ? (node.children ?? []) : [node];
348
+ }
349
+ const nodes = tree;
350
+ return nodes.some((node) => node.type === "root")
351
+ ? nodes.flatMap((node) => (node.type === "root" ? (node.children ?? []) : [node]))
352
+ : nodes;
353
+ }
@@ -33,6 +33,7 @@ declare module "mdast" {
33
33
  leafDirective: LeafDirective;
34
34
  }
35
35
  interface ParagraphData {
36
+ /** Marks a container directive’s label paragraph (`:::note[label]`). */
36
37
  directiveLabel?: boolean | null | undefined;
37
38
  }
38
39
  interface PhrasingContentMap {
@@ -1,7 +1,2 @@
1
- // Manually-defined directive AST node types.
2
- //
3
- // These replicate the type definitions from mdast-util-directive so we can
4
- // avoid pulling in that package (and its transitive deps) just for the
5
- // node interfaces. The shapes match what `getDirectiveData` returns from
6
- // the Rust arena: `name: string`, `attributes: Record<string, string>`.
1
+ // Local interfaces avoid a runtime dependency on mdast-util-directive.
7
2
  export {};
@@ -1,9 +1,6 @@
1
- /** `b"MDAR"` magic, read as a little-endian u32. */
2
1
  export declare const ARENA_MAGIC = 1380009037;
3
- /** `Arena<K>` kind tags carried in the header's `kind` field. */
4
2
  export declare const KIND_MDAST = 1;
5
3
  export declare const KIND_HAST = 2;
6
- /** `ArenaNode` `#[repr(C)]` field byte offsets (u32 fields; `node_type` is a u8). */
7
4
  export declare const FIELD: {
8
5
  readonly id: 0;
9
6
  readonly node_type: 4;
@@ -19,7 +16,12 @@ export declare const FIELD: {
19
16
  readonly data_offset: 44;
20
17
  readonly data_len: 48;
21
18
  };
22
- /** Raw-buffer header byte offsets (4-byte fields, u32 LE). */
19
+ export declare const W_PARENT = 2;
20
+ export declare const W_START_OFFSET = 3;
21
+ export declare const W_CHILDREN_START = 9;
22
+ export declare const W_CHILDREN_COUNT = 10;
23
+ export declare const W_DATA_OFFSET = 11;
24
+ export declare const W_DATA_LEN = 12;
23
25
  export declare const HEADER: {
24
26
  readonly magic: 0;
25
27
  readonly kind: 4;