import { visitHastHandle, resolveSubscriptions } from "./hast/hast-visitor.js"; import { visitMdastHandle, resolveMdastSubscriptions, } from "./mdast/mdast-visitor.js"; import { createHastHandle, createMdxHastHandle, renderHandle, compileHandle, dropHandle, createMdastHandle, createMdxMdastHandle, applyCommandsToMdastHandle, convertMdastToHastHandle, getHandleSource, getMdastFrontmatter, serializeHandle, } from "#binding"; import { MdastReader } from "./mdast/mdast-reader.js"; import { materializeMdastTree } from "./mdast/mdast-materializer.js"; import { markHandleMutated } from "./lazy-child-resolver.js"; import { HastReader } from "./hast/hast-reader.js"; import { materializeHastTree } from "./hast/hast-materializer.js"; /** * Split the user-facing `Features` (with nested unions) into the flat napi * `JsFeatures` shape plus the conversion-side `JsConvertOptions` carrying * the footnote i18n strings. The public API only exposes `features`; the * footnote strings are routed to napi internally. */ function featuresToNative(features) { if (!features) return { features: undefined, convertOptions: undefined }; const result = {}; let convertOptions; if (features.gfm !== undefined) { if (typeof features.gfm === "object") { const g = features.gfm; const gfmOpts = {}; if (g.footnotes !== undefined) { if (typeof g.footnotes === "object") { gfmOpts.footnotes = true; convertOptions = convertOptions ?? {}; if (g.footnotes.label !== undefined) convertOptions.footnoteLabel = g.footnotes.label; if (g.footnotes.backContent !== undefined) convertOptions.footnoteBackContent = g.footnotes.backContent; if (g.footnotes.backLabel !== undefined) convertOptions.footnoteBackLabel = g.footnotes.backLabel; } else { gfmOpts.footnotes = g.footnotes; } } result.gfmOptions = gfmOpts; } else { result.gfm = features.gfm; } } if (features.frontmatter !== undefined) result.frontmatter = features.frontmatter; if (features.math !== undefined) { if (typeof features.math === "object") { const mathOpts = {}; if (features.math.singleDollarTextMath !== undefined) mathOpts.singleDollarTextMath = features.math.singleDollarTextMath; result.mathOptions = mathOpts; } else { result.math = features.math; } } if (features.headingAttributes !== undefined) result.headingAttributes = features.headingAttributes; if (features.directive !== undefined) result.directive = features.directive; if (features.superscript !== undefined) result.superscript = features.superscript; if (features.subscript !== undefined) result.subscript = features.subscript; if (features.wikilinks !== undefined) result.wikilinks = features.wikilinks; if (features.smartPunctuation !== undefined) { if (typeof features.smartPunctuation === "object") { result.smartPunctuationOptions = features.smartPunctuation; } else { result.smartPunctuation = features.smartPunctuation; } } return { features: result, convertOptions }; } /** Free a handle's arena. A plugin's visitor can hand child stubs to user code; * pass `invalidateStubs` so the epoch bump makes any stub retained past this * point hit the designed retention error instead of snapshotting the freed arena. */ function releaseHandle(handle, invalidateStubs) { if (invalidateStubs) markHandleMutated(handle); dropHandle(handle); } function warnDroppedTransforms(plugin, dropped, kind) { const name = plugin.name ?? ""; const noun = dropped === 1 ? "transform" : "transforms"; console.warn(`satteri: plugin "${name}" queued ${dropped} ${kind} ${noun} on node(s) that were removed or ` + `replaced earlier in the same pass; ${dropped === 1 ? "it was" : "they were"} dropped.`); } function runMdastPluginsOnHandle(handle, plugins, fileURL, data) { // Each plugin runs once over the tree. A transform that passes a child // through (returning it inside the replacement) keeps that child's identity, // so a patch the same pass queued on it still applies — nesting composes in // one pass. A plugin's own freshly-built nodes are not re-walked; transform // them up front, or hand off to a later plugin that sees the materialized tree. const runPlugin = (plugin) => { const subs = resolveMdastSubscriptions(plugin); const result = visitMdastHandle(handle, plugin, subs, () => getHandleSource(handle), fileURL, data); const apply = (r) => { if (!r.hasMutations) return; markHandleMutated(handle); const dropped = applyCommandsToMdastHandle(handle, r.commandBuffer); if (dropped) warnDroppedTransforms(plugin, dropped, "mdast"); }; return result instanceof Promise ? result.then(apply) : apply(result); }; let i = 0; const runNext = () => { while (i < plugins.length) { const raw = plugins[i++]; const plugin = typeof raw === "function" ? raw() : raw; const r = runPlugin(plugin); if (r instanceof Promise) return r.then(runNext); } return { handle }; }; return runNext(); } function runHastPluginsOnHandle(handle, plugins, source, fileURL, data) { if (plugins.length === 0) return; let i = 0; const runNext = () => { while (i < plugins.length) { const raw = plugins[i]; i++; const plugin = typeof raw === "function" ? raw() : raw; const subs = resolveSubscriptions(plugin); const result = visitHastHandle(handle, plugin, subs, source, fileURL, data); const warnIfDropped = (dropped) => { if (dropped) warnDroppedTransforms(plugin, dropped, "hast"); }; if (result instanceof Promise) { return result.then((dropped) => { warnIfDropped(dropped); return runNext(); }); } warnIfDropped(result); } }; return runNext(); } // Public API function mdxOptionsToNative(opts) { const hasAny = opts.optimizeStatic || opts.jsxImportSource !== undefined || opts.jsx !== undefined || opts.jsxRuntime !== undefined || opts.development !== undefined || opts.providerImportSource !== undefined || opts.pragma !== undefined || opts.pragmaFrag !== undefined || opts.pragmaImportSource !== undefined || opts.outputFormat !== undefined || opts.elementAttributeNameCase !== undefined || opts.stylePropertyNameCase !== undefined; if (!hasAny) return undefined; const result = {}; if (opts.optimizeStatic) result.optimizeStatic = opts.optimizeStatic; if (opts.jsxImportSource !== undefined) result.jsxImportSource = opts.jsxImportSource; if (opts.jsx !== undefined) result.jsx = opts.jsx; if (opts.jsxRuntime !== undefined) result.jsxRuntime = opts.jsxRuntime; if (opts.development !== undefined) result.development = opts.development; if (opts.providerImportSource !== undefined) result.providerImportSource = opts.providerImportSource; if (opts.pragma !== undefined) result.pragma = opts.pragma; if (opts.pragmaFrag !== undefined) result.pragmaFrag = opts.pragmaFrag; if (opts.pragmaImportSource !== undefined) result.pragmaImportSource = opts.pragmaImportSource; if (opts.outputFormat !== undefined) result.outputFormat = opts.outputFormat; if (opts.elementAttributeNameCase !== undefined) result.elementAttributeNameCase = opts.elementAttributeNameCase; if (opts.stylePropertyNameCase !== undefined) result.stylePropertyNameCase = opts.stylePropertyNameCase; return result; } export function markdownToHtml(source, options = {}) { const { mdastPlugins = [], hastPlugins = [], features, fileURL, data = {} } = options; const hastMayHaveStubs = hastPlugins.length > 0; const { features: nativeFeatures, convertOptions: nativeConvertOptions } = featuresToNative(features); const result = createHastHandleFromMdast(source, mdastPlugins, false, fileURL, nativeFeatures, nativeConvertOptions, data); const renderAndDrop = (h, frontmatter) => { try { const html = renderHandle(h); return { html, frontmatter, data }; } finally { releaseHandle(h, hastMayHaveStubs); } }; const runHastThenRender = (r) => { let hastResult; try { hastResult = runHastPluginsOnHandle(r.hastHandle, hastPlugins, source, fileURL, data); } catch (err) { releaseHandle(r.hastHandle, hastMayHaveStubs); throw err; } if (hastResult instanceof Promise) { return hastResult.then(() => renderAndDrop(r.hastHandle, r.frontmatter), (err) => { releaseHandle(r.hastHandle, hastMayHaveStubs); throw err; }); } return renderAndDrop(r.hastHandle, r.frontmatter); }; if (result instanceof Promise) return result.then(runHastThenRender); return runHastThenRender(result); } export function mdxToJs(source, options = {}) { const { mdastPlugins = [], hastPlugins = [], features, fileURL, data = {}, ...mdxFields } = options; const hastMayHaveStubs = hastPlugins.length > 0; const mdxOptions = mdxOptionsToNative(mdxFields); const { features: nativeFeatures, convertOptions: nativeConvertOptions } = featuresToNative(features); const result = createHastHandleFromMdast(source, mdastPlugins, true, fileURL, nativeFeatures, nativeConvertOptions, data); const compileAndDrop = (h, frontmatter) => { try { const code = compileHandle(h, mdxOptions); return { code, frontmatter, data }; } finally { releaseHandle(h, hastMayHaveStubs); } }; const runHastThenCompile = (r) => { let hastResult; try { hastResult = runHastPluginsOnHandle(r.hastHandle, hastPlugins, source, fileURL, data); } catch (err) { releaseHandle(r.hastHandle, hastMayHaveStubs); throw err; } if (hastResult instanceof Promise) { return hastResult.then(() => compileAndDrop(r.hastHandle, r.frontmatter), (err) => { releaseHandle(r.hastHandle, hastMayHaveStubs); throw err; }); } return compileAndDrop(r.hastHandle, r.frontmatter); }; if (result instanceof Promise) return result.then(runHastThenCompile); return runHastThenCompile(result); } /** * Compile and evaluate MDX in one step. * * Returns the module's exports, including `default` (the MDX component). * Returns a Promise when async plugins are used, otherwise returns synchronously. * * ```ts * import * as runtime from "react/jsx-runtime"; * const { default: Content } = evaluate("# Hello", { ...runtime }); * ``` */ export function evaluate(source, options) { const { Fragment, jsx, jsxs, jsxDEV, useMDXComponents, ...compileOpts } = options; const runtime = { Fragment, jsx, jsxs, jsxDEV, useMDXComponents }; const result = mdxToJs(source, { ...compileOpts, outputFormat: "function-body" }); if (result instanceof Promise) { return result.then((resolved) => new Function(resolved.code)(runtime)); } return new Function(result.code)(runtime); } function readFrontmatter(handle) { const raw = getMdastFrontmatter(handle); return raw ? { kind: raw.kind === "toml" ? "toml" : "yaml", value: raw.value } : null; } /** Parse, run mdast plugins, capture frontmatter, then convert to HAST. * Frontmatter is read from the post-plugin MDAST so visitor mutations to * the yaml/toml node are reflected in the returned value. */ function createHastHandleFromMdast(source, mdastPlugins, mdx, fileURL, // eslint-disable-next-line @typescript-eslint/no-explicit-any nativeFeatures, // eslint-disable-next-line @typescript-eslint/no-explicit-any nativeConvertOptions, data) { const mdastHandle = mdx ? createMdxMdastHandle(source, nativeFeatures) : createMdastHandle(source, nativeFeatures); const mdastMayHaveStubs = mdastPlugins.length > 0; // finally{release} is intentional: convertMdastToHastHandle empties the arena // on success, but if any step here throws the handle would otherwise leak. const finalize = (r) => { try { const frontmatter = readFrontmatter(r.handle); // convert empties the mdast arena, so invalidate any stub held past this point. if (mdastMayHaveStubs) markHandleMutated(r.handle); const hastHandle = convertMdastToHastHandle(r.handle, nativeConvertOptions); return { hastHandle, frontmatter }; } finally { releaseHandle(r.handle, mdastMayHaveStubs); } }; try { if (mdastPlugins.length === 0) { return finalize({ handle: mdastHandle }); } const mdastResult = runMdastPluginsOnHandle(mdastHandle, mdastPlugins, fileURL, data); if (mdastResult instanceof Promise) { return mdastResult.then(finalize, (err) => { releaseHandle(mdastHandle, mdastMayHaveStubs); throw err; }); } return finalize(mdastResult); } catch (err) { releaseHandle(mdastHandle, mdastMayHaveStubs); throw err; } } // Step-by-step API: individual pipeline stages with materialized trees /** Parse Markdown source into a materialized mdast tree. */ export function markdownToMdast(source, options = {}) { const handle = createMdastHandle(source, featuresToNative(options.features).features); try { return materializeMdastTree(new MdastReader(serializeHandle(handle))); } finally { releaseHandle(handle, true); } } /** Parse MDX source into a materialized mdast tree. */ export function mdxToMdast(source, options = {}) { const handle = createMdxMdastHandle(source, featuresToNative(options.features).features); try { return materializeMdastTree(new MdastReader(serializeHandle(handle))); } finally { releaseHandle(handle, true); } } /** Convert Markdown source to a materialized hast tree. */ export function markdownToHast(source, options = {}) { const { features: nativeFeatures, convertOptions } = featuresToNative(options.features); const handle = createHastHandle(source, nativeFeatures, convertOptions); try { return materializeHastTree(new HastReader(serializeHandle(handle))); } finally { releaseHandle(handle, true); } } /** Convert MDX source to a materialized hast tree. */ export function mdxToHast(source, options = {}) { const { features: nativeFeatures, convertOptions } = featuresToNative(options.features); const handle = createMdxHastHandle(source, nativeFeatures, convertOptions); try { return materializeHastTree(new HastReader(serializeHandle(handle))); } finally { releaseHandle(handle, true); } }