satteri 0.9.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +11 -1
  2. package/dist/binding.browser.d.ts +1 -1
  3. package/dist/binding.browser.js +1 -1
  4. package/dist/binding.d.ts +1 -1
  5. package/dist/binding.js +1 -1
  6. package/dist/byte-writer.d.ts +4 -0
  7. package/dist/byte-writer.js +13 -4
  8. package/dist/child-stub.d.ts +7 -3
  9. package/dist/child-stub.js +18 -7
  10. package/dist/command-buffer.d.ts +36 -50
  11. package/dist/command-buffer.js +102 -49
  12. package/dist/compile.d.ts +80 -32
  13. package/dist/compile.js +295 -70
  14. package/dist/freeze.d.ts +2 -0
  15. package/dist/freeze.js +10 -0
  16. package/dist/generated/wire-constants.d.ts +3 -2
  17. package/dist/generated/wire-constants.js +4 -2
  18. package/dist/hast/child-stub.d.ts +2 -3
  19. package/dist/hast/child-stub.js +18 -11
  20. package/dist/hast/element-props.d.ts +1 -1
  21. package/dist/hast/element-props.js +14 -6
  22. package/dist/hast/hast-materializer.d.ts +6 -2
  23. package/dist/hast/hast-materializer.js +48 -134
  24. package/dist/hast/hast-reader.d.ts +7 -13
  25. package/dist/hast/hast-reader.js +76 -21
  26. package/dist/hast/hast-visitor.d.ts +55 -5
  27. package/dist/hast/hast-visitor.js +320 -154
  28. package/dist/index.d.ts +6 -6
  29. package/dist/index.js +1 -1
  30. package/dist/lazy-child-resolver.d.ts +20 -13
  31. package/dist/lazy-child-resolver.js +66 -38
  32. package/dist/materializer-cache.d.ts +42 -0
  33. package/dist/materializer-cache.js +192 -0
  34. package/dist/mdast/child-stub.d.ts +6 -3
  35. package/dist/mdast/child-stub.js +71 -8
  36. package/dist/mdast/generated/layout.d.ts +2 -3
  37. package/dist/mdast/generated/layout.js +24 -28
  38. package/dist/mdast/generated/node-types.d.ts +4 -0
  39. package/dist/mdast/generated/node-types.js +25 -0
  40. package/dist/mdast/mdast-materializer.d.ts +12 -3
  41. package/dist/mdast/mdast-materializer.js +83 -78
  42. package/dist/mdast/mdast-reader.d.ts +15 -0
  43. package/dist/mdast/mdast-reader.js +55 -10
  44. package/dist/mdast/mdast-visitor.d.ts +79 -32
  45. package/dist/mdast/mdast-visitor.js +305 -122
  46. package/dist/op-stream.d.ts +5 -12
  47. package/dist/op-stream.js +6 -21
  48. package/dist/plugin.d.ts +36 -10
  49. package/dist/plugin.js +50 -2
  50. package/dist/string-pool.d.ts +13 -0
  51. package/dist/string-pool.js +91 -0
  52. package/dist/types.d.ts +44 -1
  53. package/dist/visitor-shared.d.ts +27 -4
  54. package/dist/visitor-shared.js +36 -10
  55. package/dist/wire-read.js +3 -1
  56. package/index.d.ts +130 -7
  57. package/index.js +89 -68
  58. package/package.json +24 -19
  59. package/dist/lazy-props.d.ts +0 -10
  60. package/dist/lazy-props.js +0 -50
  61. package/satteri_napi.wasi-browser.js +0 -83
  62. package/satteri_napi.wasi.cjs +0 -134
  63. package/wasi-worker-browser.mjs +0 -36
  64. package/wasi-worker.mjs +0 -63
package/dist/compile.js CHANGED
@@ -1,6 +1,7 @@
1
- import { visitHastHandle, resolveSubscriptions } from "./hast/hast-visitor.js";
2
- import { visitMdastHandle, resolveMdastSubscriptions, } from "./mdast/mdast-visitor.js";
3
- import { createHastHandle, createMdxHastHandle, renderHandle, compileHandle, dropHandle, createMdastHandle, createMdxMdastHandle, applyCommandsToMdastHandle, convertMdastToHastHandle, getHandleSource, getMdastFrontmatter, serializeHandle, } from "#binding";
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";
4
5
  import { MdastReader } from "./mdast/mdast-reader.js";
5
6
  import { materializeMdastTree } from "./mdast/mdast-materializer.js";
6
7
  import { markHandleMutated } from "./lazy-child-resolver.js";
@@ -65,6 +66,8 @@ function featuresToNative(features) {
65
66
  result.subscript = features.subscript;
66
67
  if (features.wikilinks !== undefined)
67
68
  result.wikilinks = features.wikilinks;
69
+ if (features.definitionList !== undefined)
70
+ result.definitionList = features.definitionList;
68
71
  if (features.smartPunctuation !== undefined) {
69
72
  if (typeof features.smartPunctuation === "object") {
70
73
  result.smartPunctuationOptions = features.smartPunctuation;
@@ -73,6 +76,12 @@ function featuresToNative(features) {
73
76
  result.smartPunctuation = features.smartPunctuation;
74
77
  }
75
78
  }
79
+ if (features.rawHtml !== undefined) {
80
+ // Routed to convert options so every pipeline (fast paths, plugin paths,
81
+ // *ToHast) applies the reparse.
82
+ convertOptions = convertOptions ?? {};
83
+ convertOptions.rawHtml = features.rawHtml;
84
+ }
76
85
  return { features: result, convertOptions };
77
86
  }
78
87
  /** Free a handle's arena. A plugin's visitor can hand child stubs to user code;
@@ -86,64 +95,131 @@ function releaseHandle(handle, invalidateStubs) {
86
95
  function warnDroppedTransforms(plugin, dropped, kind) {
87
96
  const name = plugin.name ?? "<anonymous>";
88
97
  const noun = dropped === 1 ? "transform" : "transforms";
89
- console.warn(`satteri: plugin "${name}" queued ${dropped} ${kind} ${noun} on node(s) that were removed or ` +
90
- `replaced earlier in the same pass; ${dropped === 1 ? "it was" : "they were"} dropped.`);
98
+ console.warn(`satteri: plugin "${name}" queued ${dropped} ${kind} ${noun} on node(s) that a plugin had ` +
99
+ `already removed or replaced; ${dropped === 1 ? "it was" : "they were"} dropped.`);
100
+ }
101
+ function settle(value, fn) {
102
+ if (value instanceof Promise)
103
+ return value.then(fn);
104
+ fn(value);
105
+ return undefined;
91
106
  }
92
- function runMdastPluginsOnHandle(handle, plugins, fileURL, data) {
93
- // Each plugin runs once over the tree. A transform that passes a child
107
+ /** Stays synchronous until a step actually returns a promise. */
108
+ function sequence(steps) {
109
+ let pending;
110
+ for (const step of steps) {
111
+ if (step === undefined)
112
+ continue;
113
+ pending = pending === undefined ? step() : pending.then(step);
114
+ }
115
+ return pending;
116
+ }
117
+ function runMdastPluginsOnHandle(handle, plugins, fileURL, data, sourceFormat, collectLast = false) {
118
+ const out = { handle };
119
+ // A plugin's visitors run once over the tree. A transform that passes a child
94
120
  // through (returning it inside the replacement) keeps that child's identity,
95
- // so a patch the same pass queued on it still applies — nesting composes in
96
- // one pass. A plugin's own freshly-built nodes are not re-walked; transform
97
- // them up front, or hand off to a later plugin that sees the materialized tree.
98
- const runPlugin = (plugin) => {
121
+ // so a patch the same pass queued on it still applies: nesting composes in
122
+ // one pass. Visitor-built nodes are not re-walked; transform them up front,
123
+ // or hand off to a later plugin that sees the materialized tree.
124
+ const runPlugin = (plugin, isLastPlugin) => {
99
125
  const subs = resolveMdastSubscriptions(plugin);
100
- const result = visitMdastHandle(handle, plugin, subs, () => getHandleSource(handle), fileURL, data);
101
- const apply = (r) => {
126
+ const apply = (r, isFinalPass) => {
102
127
  if (!r.hasMutations)
103
128
  return;
129
+ if (collectLast && isLastPlugin && isFinalPass) {
130
+ out.pendingCommands = r.commandBuffer;
131
+ out.lastPlugin = plugin;
132
+ return;
133
+ }
104
134
  markHandleMutated(handle);
105
135
  const dropped = applyCommandsToMdastHandle(handle, r.commandBuffer);
106
136
  if (dropped)
107
137
  warnDroppedTransforms(plugin, dropped, "mdast");
108
138
  };
109
- return result instanceof Promise ? result.then(apply) : apply(result);
139
+ const before = typeof plugin.before === "function" ? plugin.before : undefined;
140
+ const after = typeof plugin.after === "function" ? plugin.after : undefined;
141
+ const hasVisitors = subs.length > 0;
142
+ // Threaded like `data`: one context per pass, since a resolver is epoch-bound.
143
+ const diagnostics = [];
144
+ const runHook = (hook, isFinalPass) => () => settle(visitMdastHook(handle, plugin, hook, () => getHandleSource(handle), fileURL, data, sourceFormat, diagnostics), (r) => apply(r, isFinalPass));
145
+ const runVisitors = (isFinalPass) => () => settle(visitMdastHandle(handle, plugin, subs, () => getHandleSource(handle), fileURL, data, sourceFormat, diagnostics), (r) => apply(r, isFinalPass));
146
+ return sequence([
147
+ before ? runHook(before, !hasVisitors && after === undefined) : undefined,
148
+ hasVisitors ? runVisitors(after === undefined) : undefined,
149
+ after ? runHook(after, true) : undefined,
150
+ ]);
110
151
  };
111
152
  let i = 0;
112
153
  const runNext = () => {
113
- while (i < plugins.length) {
114
- const raw = plugins[i++];
115
- const plugin = typeof raw === "function" ? raw() : raw;
116
- const r = runPlugin(plugin);
154
+ for (;;) {
155
+ const plugin = plugins[i];
156
+ if (plugin === undefined)
157
+ break;
158
+ i++;
159
+ const r = runPlugin(plugin, i === plugins.length);
117
160
  if (r instanceof Promise)
118
161
  return r.then(runNext);
119
162
  }
120
- return { handle };
163
+ return out;
121
164
  };
122
165
  return runNext();
123
166
  }
124
- function runHastPluginsOnHandle(handle, plugins, source, fileURL, data) {
125
- if (plugins.length === 0)
126
- return;
167
+ const EMPTY_COMMAND_BUFFER = new Uint8Array(0);
168
+ const NO_HAST_COMMANDS = {
169
+ commands: EMPTY_COMMAND_BUFFER,
170
+ lastPlugin: null,
171
+ };
172
+ /** Run every HAST plugin except the last one normally (each applies its
173
+ * commands inline), then return the last plugin's commands without applying.
174
+ * Lets the caller fuse the final apply with the downstream NAPI step
175
+ * (`render` or `compile`), saving one NAPI roundtrip per compile. */
176
+ function runHastPluginsCollectLast(handle, plugins, source, fileURL, data, sourceFormat) {
127
177
  let i = 0;
128
178
  const runNext = () => {
129
- while (i < plugins.length) {
130
- const raw = plugins[i];
179
+ for (;;) {
180
+ const plugin = plugins[i];
181
+ if (plugin === undefined)
182
+ break;
183
+ const isLastPlugin = i === plugins.length - 1;
131
184
  i++;
132
- const plugin = typeof raw === "function" ? raw() : raw;
133
185
  const subs = resolveSubscriptions(plugin);
134
- const result = visitHastHandle(handle, plugin, subs, source, fileURL, data);
186
+ const { before, after } = plugin;
187
+ const passes = [];
188
+ if (typeof before === "function")
189
+ passes.push(before);
190
+ if (subs.length > 0)
191
+ passes.push("visitors");
192
+ if (typeof after === "function")
193
+ passes.push(after);
194
+ const finalPass = passes.pop();
195
+ if (finalPass === undefined)
196
+ continue;
135
197
  const warnIfDropped = (dropped) => {
136
198
  if (dropped)
137
199
  warnDroppedTransforms(plugin, dropped, "hast");
138
200
  };
139
- if (result instanceof Promise) {
140
- return result.then((dropped) => {
141
- warnIfDropped(dropped);
142
- return runNext();
143
- });
201
+ // Threaded like `data`: one context per pass, since a resolver is epoch-bound.
202
+ const diagnostics = [];
203
+ const runApplied = (pass) => () => settle(pass === "visitors"
204
+ ? visitHastHandle(handle, plugin, subs, source, fileURL, data, sourceFormat, diagnostics)
205
+ : visitHastHook(handle, plugin, pass, source, fileURL, data, sourceFormat, diagnostics), warnIfDropped);
206
+ const collectFinal = () => {
207
+ const collected = finalPass === "visitors"
208
+ ? visitHastHandleCollect(handle, plugin, subs, source, fileURL, data, sourceFormat, diagnostics)
209
+ : visitHastHookCollect(handle, plugin, finalPass, source, fileURL, data, sourceFormat, diagnostics);
210
+ return collected instanceof Promise
211
+ ? collected.then((commands) => ({ commands, lastPlugin: plugin }))
212
+ : { commands: collected, lastPlugin: plugin };
213
+ };
214
+ const head = sequence(passes.map(runApplied));
215
+ if (isLastPlugin) {
216
+ return head instanceof Promise ? head.then(collectFinal) : collectFinal();
144
217
  }
145
- warnIfDropped(result);
218
+ const tail = head instanceof Promise ? head.then(runApplied(finalPass)) : runApplied(finalPass)();
219
+ if (tail instanceof Promise)
220
+ return tail.then(runNext);
146
221
  }
222
+ return NO_HAST_COMMANDS;
147
223
  };
148
224
  return runNext();
149
225
  }
@@ -191,71 +267,200 @@ function mdxOptionsToNative(opts) {
191
267
  return result;
192
268
  }
193
269
  export function markdownToHtml(source, options = {}) {
194
- const { mdastPlugins = [], hastPlugins = [], features, fileURL, data = {} } = options;
270
+ const { features, fileURL, data = {} } = options;
271
+ const mdastPlugins = normalizePlugins(options.mdastPlugins ?? [], "mdastPlugins", source, fileURL, "markdown", data);
272
+ const hastPlugins = normalizePlugins(options.hastPlugins ?? [], "hastPlugins", source, fileURL, "markdown", data);
195
273
  const hastMayHaveStubs = hastPlugins.length > 0;
196
274
  const { features: nativeFeatures, convertOptions: nativeConvertOptions } = featuresToNative(features);
197
- const result = createHastHandleFromMdast(source, mdastPlugins, false, fileURL, nativeFeatures, nativeConvertOptions, data);
198
- const renderAndDrop = (h, frontmatter) => {
275
+ // Fast path: no plugins → parse, convert, render, and frontmatter all happen
276
+ // inside a single NAPI call. Skips 5 handle-NAPI roundtrips that the
277
+ // plugin-capable path needs to keep the arena live across passes.
278
+ if (mdastPlugins.length === 0 && hastPlugins.length === 0) {
279
+ const { html, frontmatter } = markdownToHtmlFast(source, nativeFeatures, nativeConvertOptions);
280
+ return { html, frontmatter: frontmatter ?? null, data };
281
+ }
282
+ // Track source positions only when some plugin opts in with `position: true`;
283
+ // otherwise the parse skips the LineIndex build and per-node line/column lookups.
284
+ const trackPositions = mdastPlugins.some((p) => p.options?.position) || hastPlugins.some((p) => p.options?.position);
285
+ // Fused tail for MDAST-plugins-only (no HAST plugins): after the MDAST
286
+ // plugin pass returns its pending commands, apply + convert + render all
287
+ // happen inside a single NAPI roundtrip. Saves the convert-to-hast handle
288
+ // create + render + drop crossings the generic path makes separately.
289
+ if (hastPlugins.length === 0) {
290
+ const mdastHandle = createMdastHandle(source, nativeFeatures, trackPositions);
199
291
  try {
200
- const html = renderHandle(h);
201
- return { html, frontmatter, data };
292
+ const mdastResult = runMdastPluginsOnHandle(mdastHandle, mdastPlugins, fileURL, data, "markdown", true);
293
+ const finishMdast = (r) => {
294
+ try {
295
+ // Fused tail: apply pending commands (empty buffer is a Rust no-op),
296
+ // extract frontmatter post-mutation, convert, render, all in one
297
+ // NAPI roundtrip.
298
+ const commands = r.pendingCommands ?? EMPTY_COMMAND_BUFFER;
299
+ const { html, frontmatter, droppedTransforms } = applyMdastCommandsAndConvertAndRender(r.handle, commands, nativeConvertOptions);
300
+ if (droppedTransforms && r.lastPlugin)
301
+ warnDroppedTransforms(r.lastPlugin, droppedTransforms, "mdast");
302
+ return {
303
+ html,
304
+ frontmatter: frontmatter ?? null,
305
+ data,
306
+ };
307
+ }
308
+ finally {
309
+ releaseHandle(r.handle, true);
310
+ }
311
+ };
312
+ if (mdastResult instanceof Promise) {
313
+ return mdastResult.then(finishMdast, (err) => {
314
+ releaseHandle(mdastHandle, true);
315
+ throw err;
316
+ });
317
+ }
318
+ return finishMdast(mdastResult);
202
319
  }
203
- finally {
204
- releaseHandle(h, hastMayHaveStubs);
320
+ catch (err) {
321
+ releaseHandle(mdastHandle, true);
322
+ throw err;
205
323
  }
206
- };
324
+ }
325
+ const result = createHastHandleFromMdast(source, mdastPlugins, false, fileURL, nativeFeatures, nativeConvertOptions, data, trackPositions);
207
326
  const runHastThenRender = (r) => {
208
- let hastResult;
327
+ // Run all but the last HAST plugin normally (each one applies its
328
+ // commands inline); collect the last plugin's commands and fuse apply +
329
+ // render + handle-drop into one NAPI call.
330
+ let collected;
209
331
  try {
210
- hastResult = runHastPluginsOnHandle(r.hastHandle, hastPlugins, source, fileURL, data);
332
+ collected = runHastPluginsCollectLast(r.hastHandle, hastPlugins, source, fileURL, data, "markdown");
211
333
  }
212
334
  catch (err) {
213
335
  releaseHandle(r.hastHandle, hastMayHaveStubs);
214
336
  throw err;
215
337
  }
216
- if (hastResult instanceof Promise) {
217
- return hastResult.then(() => renderAndDrop(r.hastHandle, r.frontmatter), (err) => {
338
+ if (collected instanceof Promise) {
339
+ return collected.then((c) => finishHastRender(r.hastHandle, c, r.frontmatter), (err) => {
218
340
  releaseHandle(r.hastHandle, hastMayHaveStubs);
219
341
  throw err;
220
342
  });
221
343
  }
222
- return renderAndDrop(r.hastHandle, r.frontmatter);
344
+ return finishHastRender(r.hastHandle, collected, r.frontmatter);
345
+ };
346
+ const finishHastRender = (h, collected, frontmatter) => {
347
+ try {
348
+ let html;
349
+ if (collected.commands.length > 0) {
350
+ const result = applyCommandsAndRenderHandle(h, collected.commands);
351
+ if (result.droppedTransforms && collected.lastPlugin)
352
+ warnDroppedTransforms(collected.lastPlugin, result.droppedTransforms, "hast");
353
+ html = result.html;
354
+ }
355
+ else {
356
+ html = renderHandle(h);
357
+ }
358
+ return { html, frontmatter, data };
359
+ }
360
+ finally {
361
+ releaseHandle(h, hastMayHaveStubs);
362
+ }
223
363
  };
224
364
  if (result instanceof Promise)
225
365
  return result.then(runHastThenRender);
226
366
  return runHastThenRender(result);
227
367
  }
228
368
  export function mdxToJs(source, options = {}) {
229
- const { mdastPlugins = [], hastPlugins = [], features, fileURL, data = {}, ...mdxFields } = options;
369
+ return toJsImpl(source, options, true);
370
+ }
371
+ export function markdownToJs(source, options = {}) {
372
+ return toJsImpl(source, options, false);
373
+ }
374
+ /** `mdx` picks the parser; the pipeline is identical from MDAST on. */
375
+ function toJsImpl(source, options, mdx) {
376
+ const { mdastPlugins: mdastInput = [], hastPlugins: hastInput = [], features, fileURL, data = {}, ...mdxFields } = options;
377
+ const sourceFormat = mdx ? "mdx" : "markdown";
378
+ const mdastPlugins = normalizePlugins(mdastInput, "mdastPlugins", source, fileURL, sourceFormat, data);
379
+ const hastPlugins = normalizePlugins(hastInput, "hastPlugins", source, fileURL, sourceFormat, data);
230
380
  const hastMayHaveStubs = hastPlugins.length > 0;
231
381
  const mdxOptions = mdxOptionsToNative(mdxFields);
232
382
  const { features: nativeFeatures, convertOptions: nativeConvertOptions } = featuresToNative(features);
233
- const result = createHastHandleFromMdast(source, mdastPlugins, true, fileURL, nativeFeatures, nativeConvertOptions, data);
234
- const compileAndDrop = (h, frontmatter) => {
383
+ // Fast path: same trick as `markdownToHtml`. Parse → MDAST → HAST → JS plus
384
+ // frontmatter extraction all happen inside a single NAPI call. Skips 5 of
385
+ // the 6 handle-based crossings the plugin-capable path needs.
386
+ if (mdastPlugins.length === 0 && hastPlugins.length === 0) {
387
+ const { code, frontmatter } = (mdx ? mdxToJsFast : markdownToJsFast)(source, nativeFeatures, mdxOptions, nativeConvertOptions);
388
+ return { code, frontmatter: frontmatter ?? null, data };
389
+ }
390
+ // Track positions only when a plugin opts in (see `markdownToHtml`).
391
+ const trackPositions = mdastPlugins.some((p) => p.options?.position) || hastPlugins.some((p) => p.options?.position);
392
+ // MDAST-plugins-only fused tail (no HAST plugins): apply + extract
393
+ // frontmatter + convert + simplify + compile happen in one NAPI call.
394
+ if (hastPlugins.length === 0) {
395
+ const mdastHandle = mdx
396
+ ? createMdxMdastHandle(source, nativeFeatures, trackPositions)
397
+ : createMdastHandle(source, nativeFeatures, trackPositions);
235
398
  try {
236
- const code = compileHandle(h, mdxOptions);
237
- return { code, frontmatter, data };
399
+ const mdastResult = runMdastPluginsOnHandle(mdastHandle, mdastPlugins, fileURL, data, mdx ? "mdx" : "markdown", true);
400
+ const finishMdast = (r) => {
401
+ try {
402
+ const commands = r.pendingCommands ?? EMPTY_COMMAND_BUFFER;
403
+ const { code, frontmatter, droppedTransforms } = applyMdastCommandsAndConvertAndCompile(r.handle, commands, mdxOptions, nativeConvertOptions);
404
+ if (droppedTransforms && r.lastPlugin)
405
+ warnDroppedTransforms(r.lastPlugin, droppedTransforms, "mdast");
406
+ return {
407
+ code,
408
+ frontmatter: frontmatter ?? null,
409
+ data,
410
+ };
411
+ }
412
+ finally {
413
+ releaseHandle(r.handle, true);
414
+ }
415
+ };
416
+ if (mdastResult instanceof Promise) {
417
+ return mdastResult.then(finishMdast, (err) => {
418
+ releaseHandle(mdastHandle, true);
419
+ throw err;
420
+ });
421
+ }
422
+ return finishMdast(mdastResult);
238
423
  }
239
- finally {
240
- releaseHandle(h, hastMayHaveStubs);
424
+ catch (err) {
425
+ releaseHandle(mdastHandle, true);
426
+ throw err;
241
427
  }
242
- };
428
+ }
429
+ const result = createHastHandleFromMdast(source, mdastPlugins, mdx, fileURL, nativeFeatures, nativeConvertOptions, data, trackPositions);
243
430
  const runHastThenCompile = (r) => {
244
- let hastResult;
431
+ let collected;
245
432
  try {
246
- hastResult = runHastPluginsOnHandle(r.hastHandle, hastPlugins, source, fileURL, data);
433
+ collected = runHastPluginsCollectLast(r.hastHandle, hastPlugins, source, fileURL, data, mdx ? "mdx" : "markdown");
247
434
  }
248
435
  catch (err) {
249
436
  releaseHandle(r.hastHandle, hastMayHaveStubs);
250
437
  throw err;
251
438
  }
252
- if (hastResult instanceof Promise) {
253
- return hastResult.then(() => compileAndDrop(r.hastHandle, r.frontmatter), (err) => {
439
+ if (collected instanceof Promise) {
440
+ return collected.then((c) => finishHastCompile(r.hastHandle, c, r.frontmatter), (err) => {
254
441
  releaseHandle(r.hastHandle, hastMayHaveStubs);
255
442
  throw err;
256
443
  });
257
444
  }
258
- return compileAndDrop(r.hastHandle, r.frontmatter);
445
+ return finishHastCompile(r.hastHandle, collected, r.frontmatter);
446
+ };
447
+ const finishHastCompile = (h, collected, frontmatter) => {
448
+ try {
449
+ let code;
450
+ if (collected.commands.length > 0) {
451
+ const result = applyCommandsAndCompileHandle(h, collected.commands, mdxOptions);
452
+ if (result.droppedTransforms && collected.lastPlugin)
453
+ warnDroppedTransforms(collected.lastPlugin, result.droppedTransforms, "hast");
454
+ code = result.code;
455
+ }
456
+ else {
457
+ code = compileHandle(h, mdxOptions);
458
+ }
459
+ return { code, frontmatter, data };
460
+ }
461
+ finally {
462
+ releaseHandle(h, hastMayHaveStubs);
463
+ }
259
464
  };
260
465
  if (result instanceof Promise)
261
466
  return result.then(runHastThenCompile);
@@ -292,10 +497,20 @@ function createHastHandleFromMdast(source, mdastPlugins, mdx, fileURL,
292
497
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
293
498
  nativeFeatures,
294
499
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
295
- nativeConvertOptions, data) {
500
+ nativeConvertOptions, data, trackPositions) {
501
+ if (mdastPlugins.length === 0) {
502
+ const [hastHandle, raw] = mdx
503
+ ? createMdxHastHandleWithFrontmatter(source, nativeFeatures, nativeConvertOptions, trackPositions)
504
+ : createHastHandleWithFrontmatter(source, nativeFeatures, nativeConvertOptions, trackPositions);
505
+ return {
506
+ hastHandle,
507
+ frontmatter: raw ? { kind: raw.kind === "toml" ? "toml" : "yaml", value: raw.value } : null,
508
+ };
509
+ }
296
510
  const mdastHandle = mdx
297
- ? createMdxMdastHandle(source, nativeFeatures)
298
- : createMdastHandle(source, nativeFeatures);
511
+ ? createMdxMdastHandle(source, nativeFeatures, trackPositions)
512
+ : createMdastHandle(source, nativeFeatures, trackPositions);
513
+ const sourceFormat = mdx ? "mdx" : "markdown";
299
514
  const mdastMayHaveStubs = mdastPlugins.length > 0;
300
515
  // finally{release} is intentional: convertMdastToHastHandle empties the arena
301
516
  // on success, but if any step here throws the handle would otherwise leak.
@@ -313,10 +528,7 @@ nativeConvertOptions, data) {
313
528
  }
314
529
  };
315
530
  try {
316
- if (mdastPlugins.length === 0) {
317
- return finalize({ handle: mdastHandle });
318
- }
319
- const mdastResult = runMdastPluginsOnHandle(mdastHandle, mdastPlugins, fileURL, data);
531
+ const mdastResult = runMdastPluginsOnHandle(mdastHandle, mdastPlugins, fileURL, data, sourceFormat);
320
532
  if (mdastResult instanceof Promise) {
321
533
  return mdastResult.then(finalize, (err) => {
322
534
  releaseHandle(mdastHandle, mdastMayHaveStubs);
@@ -330,10 +542,9 @@ nativeConvertOptions, data) {
330
542
  throw err;
331
543
  }
332
544
  }
333
- // Step-by-step API: individual pipeline stages with materialized trees
334
545
  /** Parse Markdown source into a materialized mdast tree. */
335
546
  export function markdownToMdast(source, options = {}) {
336
- const handle = createMdastHandle(source, featuresToNative(options.features).features);
547
+ const handle = createMdastHandle(source, featuresToNative(options.features).features, options.position);
337
548
  try {
338
549
  return materializeMdastTree(new MdastReader(serializeHandle(handle)));
339
550
  }
@@ -343,7 +554,7 @@ export function markdownToMdast(source, options = {}) {
343
554
  }
344
555
  /** Parse MDX source into a materialized mdast tree. */
345
556
  export function mdxToMdast(source, options = {}) {
346
- const handle = createMdxMdastHandle(source, featuresToNative(options.features).features);
557
+ const handle = createMdxMdastHandle(source, featuresToNative(options.features).features, options.position);
347
558
  try {
348
559
  return materializeMdastTree(new MdastReader(serializeHandle(handle)));
349
560
  }
@@ -354,7 +565,7 @@ export function mdxToMdast(source, options = {}) {
354
565
  /** Convert Markdown source to a materialized hast tree. */
355
566
  export function markdownToHast(source, options = {}) {
356
567
  const { features: nativeFeatures, convertOptions } = featuresToNative(options.features);
357
- const handle = createHastHandle(source, nativeFeatures, convertOptions);
568
+ const handle = createHastHandle(source, nativeFeatures, convertOptions, options.position);
358
569
  try {
359
570
  return materializeHastTree(new HastReader(serializeHandle(handle)));
360
571
  }
@@ -365,7 +576,21 @@ export function markdownToHast(source, options = {}) {
365
576
  /** Convert MDX source to a materialized hast tree. */
366
577
  export function mdxToHast(source, options = {}) {
367
578
  const { features: nativeFeatures, convertOptions } = featuresToNative(options.features);
368
- const handle = createMdxHastHandle(source, nativeFeatures, convertOptions);
579
+ const handle = createMdxHastHandle(source, nativeFeatures, convertOptions, options.position);
580
+ try {
581
+ return materializeHastTree(new HastReader(serializeHandle(handle)));
582
+ }
583
+ finally {
584
+ releaseHandle(handle, true);
585
+ }
586
+ }
587
+ /**
588
+ * Parse an HTML string into a materialized hast tree: a `root` whose children
589
+ * are the doctype (if any) and the implied `<html>` subtree. Only available
590
+ * in builds that include the `from-html` feature.
591
+ */
592
+ export function htmlToHast(html) {
593
+ const handle = createHastHandleFromHtml(html);
369
594
  try {
370
595
  return materializeHastTree(new HastReader(serializeHandle(handle)));
371
596
  }
@@ -0,0 +1,2 @@
1
+ /** Freeze JSON-shaped data recursively; never call on objects with lazy getters. */
2
+ export declare function deepFreeze<T>(value: T): T;
package/dist/freeze.js ADDED
@@ -0,0 +1,10 @@
1
+ /** Freeze JSON-shaped data recursively; never call on objects with lazy getters. */
2
+ export function deepFreeze(value) {
3
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
4
+ Object.freeze(value);
5
+ for (const key of Object.keys(value)) {
6
+ deepFreeze(value[key]);
7
+ }
8
+ }
9
+ return value;
10
+ }
@@ -35,9 +35,9 @@ export declare const CMD_WRAP = 9;
35
35
  export declare const CMD_REPLACE = 11;
36
36
  export declare const CMD_SET_PROPERTY = 12;
37
37
  export declare const CMD_SET_CHILDREN = 13;
38
- export declare const PAYLOAD_RAW_MARKDOWN = 16;
39
- export declare const PAYLOAD_RAW_HTML = 17;
38
+ export declare const PAYLOAD_RAW = 16;
40
39
  export declare const PAYLOAD_OPSTREAM = 20;
40
+ export declare const RAW_LITERAL_BRACES = 1;
41
41
  export declare const PROP_STRING = 0;
42
42
  export declare const PROP_BOOL_TRUE = 1;
43
43
  export declare const PROP_BOOL_FALSE = 2;
@@ -45,6 +45,7 @@ export declare const PROP_SPACE_SEP = 3;
45
45
  export declare const PROP_COMMA_SEP = 4;
46
46
  export declare const PROP_INT = 5;
47
47
  export declare const PROP_NULL = 6;
48
+ export declare const PROP_COMMA_SEP_NUM = 7;
48
49
  export declare const MDX_ATTR_BOOLEAN_PROP = 0;
49
50
  export declare const MDX_ATTR_LITERAL_PROP = 1;
50
51
  export declare const MDX_ATTR_EXPRESSION_PROP = 2;
@@ -45,9 +45,10 @@ export const CMD_REPLACE = 0x0b;
45
45
  export const CMD_SET_PROPERTY = 0x0c; // [valueType: u8][name str][value str], PROP_* value kinds
46
46
  export const CMD_SET_CHILDREN = 0x0d; // payload is a Root-wrapped child list
47
47
  // Structural-command payload types (0x10+, a range distinct from commands).
48
- export const PAYLOAD_RAW_MARKDOWN = 0x10; // [len: u32 LE][utf8] — re-parsed as markdown
49
- export const PAYLOAD_RAW_HTML = 0x11; // [len: u32 LE][utf8] — re-parsed as HTML/MDX
48
+ export const PAYLOAD_RAW = 0x10; // [flags: u8][len: u32 LE][utf8] — re-parsed as markdown; see RAW_* flags
50
49
  export const PAYLOAD_OPSTREAM = 0x14; // [len: u32 LE][op bytes] — replayed straight into the arena
50
+ // Flag bits for the `PAYLOAD_RAW` payload.
51
+ export const RAW_LITERAL_BRACES = 0x01; // keep MDX `{…}` as literal text (only affects MDX output)
51
52
  // Property value kinds (HAST element properties and SET_PROPERTY commands).
52
53
  export const PROP_STRING = 0; // UTF-8 value
53
54
  export const PROP_BOOL_TRUE = 1; // no value bytes
@@ -56,6 +57,7 @@ export const PROP_SPACE_SEP = 3; // space-separated list (UTF-8)
56
57
  export const PROP_COMMA_SEP = 4; // comma-separated list (UTF-8)
57
58
  export const PROP_INT = 5; // decimal string, parsed to i64
58
59
  export const PROP_NULL = 6; // no value bytes
60
+ export const PROP_COMMA_SEP_NUM = 7; // comma-separated list, numeric items parsed to numbers
59
61
  // MDX JSX attribute kinds (MDAST and HAST MDX JSX element type_data).
60
62
  export const MDX_ATTR_BOOLEAN_PROP = 0; // name only, no value
61
63
  export const MDX_ATTR_LITERAL_PROP = 1; // name="literal"
@@ -5,9 +5,8 @@ type HastResolver = LazyChildResolver<HastReader, HastNode>;
5
5
  /**
6
6
  * Walk-path child stub: arena id + `type` eagerly, every other field a lazy
7
7
  * forward to the materialized node (first read snapshots the arena via
8
- * `materializeOne`, which enforces the handle epoch — the pass seal is checked
9
- * where the stubs are built). Spread/identity rules are enforced by `nid()`
10
- * in hast-visitor.ts.
8
+ * `materializeOne`, which enforces the handle epoch). Spread/identity rules
9
+ * are enforced by `nid()` in hast-visitor.ts.
11
10
  */
12
11
  export declare class HastChildStub {
13
12
  _resolver: HastResolver;
@@ -1,16 +1,17 @@
1
- import { flatByTag, stubDescriptors } from "../child-stub.js";
1
+ import { flatByTag, installStubDescriptors, stubDescriptors, } from "../child-stub.js";
2
2
  import { NAME_TO_TYPE, TYPE_NAMES } from "./generated/node-types.js";
3
+ import { HAST_CONTAINER_TYPES } from "./hast-materializer.js";
3
4
  const N = NAME_TO_TYPE;
4
- /** Per-type stub fields, mirroring `materializeHastNode`'s switch. */
5
+ /** Per-type stub fields; must mirror `materializeHastNode`'s populate switch. */
5
6
  const HAST_STUB_FIELDS = {
6
- [N.root]: ["children"],
7
- [N.element]: ["tagName", "properties", "children"],
7
+ [N.root]: [],
8
+ [N.element]: ["tagName", "properties"],
8
9
  [N.text]: ["value"],
9
10
  [N.comment]: ["value"],
10
11
  [N.doctype]: [],
11
12
  [N.raw]: ["value"],
12
- [N.mdxJsxFlowElement]: ["name", "attributes", "children"],
13
- [N.mdxJsxTextElement]: ["name", "attributes", "children"],
13
+ [N.mdxJsxFlowElement]: ["name", "attributes"],
14
+ [N.mdxJsxTextElement]: ["name", "attributes"],
14
15
  [N.mdxFlowExpression]: ["value"],
15
16
  [N.mdxTextExpression]: ["value"],
16
17
  [N.mdxjsEsm]: ["value"],
@@ -19,16 +20,18 @@ const TYPE_NAME_BY_TAG = flatByTag(TYPE_NAMES);
19
20
  const HAST_STUB_DESCRIPTORS = [];
20
21
  for (const tag of Object.keys(HAST_STUB_FIELDS)) {
21
22
  const nodeType = Number(tag);
22
- HAST_STUB_DESCRIPTORS[nodeType] = stubDescriptors(HAST_STUB_FIELDS[nodeType]);
23
+ const fields = [...HAST_STUB_FIELDS[nodeType]];
24
+ if (HAST_CONTAINER_TYPES.has(nodeType))
25
+ fields.push("children");
26
+ HAST_STUB_DESCRIPTORS[nodeType] = stubDescriptors(fields);
23
27
  }
24
28
  /** Unknown node types still expose the prelude-backed lazy fields. */
25
29
  const FALLBACK_DESCRIPTORS = stubDescriptors([]);
26
30
  /**
27
31
  * Walk-path child stub: arena id + `type` eagerly, every other field a lazy
28
32
  * forward to the materialized node (first read snapshots the arena via
29
- * `materializeOne`, which enforces the handle epoch — the pass seal is checked
30
- * where the stubs are built). Spread/identity rules are enforced by `nid()`
31
- * in hast-visitor.ts.
33
+ * `materializeOne`, which enforces the handle epoch). Spread/identity rules
34
+ * are enforced by `nid()` in hast-visitor.ts.
32
35
  */
33
36
  export class HastChildStub {
34
37
  _resolver;
@@ -38,6 +41,10 @@ export class HastChildStub {
38
41
  this._resolver = resolver;
39
42
  this._id = id;
40
43
  this.type = TYPE_NAME_BY_TAG[nodeType] ?? `unknown(${nodeType})`;
41
- Object.defineProperties(this, HAST_STUB_DESCRIPTORS[nodeType] ?? FALLBACK_DESCRIPTORS);
44
+ installStubDescriptors(this, HAST_STUB_DESCRIPTORS[nodeType] ?? FALLBACK_DESCRIPTORS);
45
+ }
46
+ /** @internal */
47
+ get _refs() {
48
+ return this._resolver.refs;
42
49
  }
43
50
  }
@@ -1,2 +1,2 @@
1
- export type HastPropertyValue = string | number | boolean | string[];
1
+ export type HastPropertyValue = string | number | boolean | (string | number)[];
2
2
  export declare function decodeElementProp(kind: number, value: string): HastPropertyValue;