satteri 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,303 @@
1
1
  # satteri
2
2
 
3
- TypeScript layer with readers, visitors, materializers, and plugin system for Sätteri, a high-performance Markdown and MDX processor.
3
+ Native-enhanced Markdown parsing and processing for JavaScript. Parse and compile in Rust, create flexible plugins in JavaScript.
4
4
 
5
- > [!NOTE]
6
- > **Work in progress** — this package is under active development.
7
- > See the [Contributing Guide](../../CONTRIBUTING.md) if you want to get involved!
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install satteri
9
+ yarn add satteri
10
+ pnpm add satteri
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Markdown to HTML
16
+
17
+ ```ts
18
+ import { markdownToHtml } from "satteri";
19
+
20
+ const html = markdownToHtml("# Hello\n\nWorld");
21
+ // <h1>Hello</h1>\n<p>World</p>
22
+ ```
23
+
24
+ ### MDX to JS
25
+
26
+ ```ts
27
+ import { mdxToJs } from "satteri";
28
+
29
+ const js = mdxToJs("# Hello\n\n<MyComponent />");
30
+ ```
31
+
32
+ ### With plugins
33
+
34
+ Both functions accept `mdastPlugins` (operate on the Markdown AST before conversion to HAST) and `hastPlugins` (operate on the HAST before output).
35
+
36
+ ```ts
37
+ import { markdownToHtml } from "satteri";
38
+ import { removeHeadings } from "./my-mdast-plugins.js";
39
+ import { addLinkClasses } from "./my-hast-plugins.js";
40
+
41
+ const html = markdownToHtml("# Hello\n\n[link](https://example.com)", {
42
+ mdastPlugins: [removeHeadings],
43
+ hastPlugins: [addLinkClasses],
44
+ });
45
+ ```
46
+
47
+ If you're familiar with the unified ecosystem, mdast and hast plugins would be similar to remark and rehype plugins, respectively, and re-uses the same AST shape for both. This project does not currently have an equivalent of micromark or recma plugins.
48
+
49
+ ## Plugins
50
+
51
+ ### MDAST plugins
52
+
53
+ MDAST plugins run on the Markdown syntax tree, allowing you to do things like replace emoji shortcodes, unwrap images from paragraphs, or collect headings for a table of contents before Markdown is transformed to HTML / JS. Define visitor methods named after node types (`heading`, `code`, `link`, `image`, etc.). Each visitor receives the node and a context object for mutations.
54
+
55
+ ```ts
56
+ const emojis = defineMdastPlugin({
57
+ name: "emojis",
58
+ text(node, ctx) {
59
+ if (node.value.includes(":wave:")) {
60
+ ctx.setProperty(node, "value", node.value.replaceAll(":wave:", "\u{1F44B}"));
61
+ }
62
+ },
63
+ });
64
+ ```
65
+
66
+ Visitors can alternatively return a replacement node, raw Markdown, or raw HTML. This is useful when the replacement can't be expressed as property changes on the original node:
67
+
68
+ ```ts
69
+ const highlightCode = defineMdastPlugin({
70
+ name: "highlight-code",
71
+ code(node) {
72
+ return { rawHtml: `<pre class="highlighted">${escape(node.value)}</pre>` };
73
+ },
74
+ });
75
+ ```
76
+
77
+ All standard mdast node types are supported, plus GFM extensions (`table`, `tableRow`, `tableCell`, `delete`, `footnoteDefinition`, `footnoteReference`) and MDX nodes (`mdxJsxFlowElement`, `mdxJsxTextElement`, `mdxFlowExpression`, `mdxTextExpression`, `mdxjsEsm`) if enabled. For more information on the AST shape and node types, see the [mdast spec](https://github.com/syntax-tree/mdast).
78
+
79
+ ### HAST plugins
80
+
81
+ HAST plugins run on the HTML syntax tree after mdast-to-hast conversion, allowing you to do things like add classes to elements, set attributes on links, or wrap HTML elements with other elements, etc. Element visitors use a `filter` array to specify which tag names (or component names for MDX) to match.
82
+
83
+ ```ts
84
+ const addLinkClasses = defineHastPlugin({
85
+ name: "add-link-classes",
86
+ element: {
87
+ filter: ["a"],
88
+ visit(node, ctx) {
89
+ ctx.setProperty(node, "class", "link");
90
+ ctx.setProperty(node, "target", "_blank");
91
+ },
92
+ },
93
+ });
94
+ ```
95
+
96
+ Multiple filter groups on the same node type:
97
+
98
+ ```ts
99
+ const multiFilter = defineHastPlugin({
100
+ name: "multi-filter",
101
+ element: [
102
+ {
103
+ filter: ["h1", "h2", "h3"],
104
+ visit(node, ctx) {
105
+ ctx.setProperty(node, "class", "heading");
106
+ },
107
+ },
108
+ {
109
+ filter: ["a"],
110
+ visit(node, ctx) {
111
+ ctx.setProperty(node, "target", "_blank");
112
+ },
113
+ },
114
+ ],
115
+ });
116
+ ```
117
+
118
+ An empty filter matches all elements, but can quickly become expensive when used on large documents, so use with caution:
119
+
120
+ ```ts
121
+ const allElements = defineHastPlugin({
122
+ name: "all-elements",
123
+ element: {
124
+ filter: [],
125
+ visit(node, ctx) {
126
+ ctx.setProperty(node, "data-visited", "true");
127
+ },
128
+ },
129
+ });
130
+ ```
131
+
132
+ Non-element visitors (`text`, `comment`, `raw`, `doctype`, MDX expression types) use bare functions instead of filter objects:
133
+
134
+ ```ts
135
+ const uppercaseText = defineHastPlugin({
136
+ name: "uppercase-text",
137
+ text(node, ctx) {
138
+ ctx.setProperty(node, "value", node.value.toUpperCase());
139
+ },
140
+ });
141
+ ```
142
+
143
+ For more information on the AST shape and node types, see the [hast spec](https://github.com/syntax-tree/hast).
144
+
145
+ ### Mutating nodes
146
+
147
+ Unlike remark and rehype plugins, nodes in Sätteri inside plugins are read-only. The AST lives in Rust memory and JavaScript only has a "view" over the different nodes, so direct mutations like `node.value = "new text"` have no effect. Use the context methods (`ctx.setProperty`, `ctx.removeNode`, `ctx.replaceNode`, etc.) instead, which send changes back to Rust in an efficient way.
148
+
149
+ ```ts
150
+ // Won't work
151
+ heading(node, ctx) {
152
+ node.depth = 2; // no effect, TypeScript will also complain that the node is readonly
153
+ }
154
+
155
+ // Do this instead
156
+ heading(node, ctx) {
157
+ ctx.setProperty(node, "depth", 2);
158
+ }
159
+
160
+ // Or return a new node to replace it entirely, but this is less efficient and generally not recommended
161
+ heading(node) {
162
+ return { ..node, depth: 2 };
163
+ }
164
+ ```
165
+
166
+ ### Async plugins
167
+
168
+ Visitors can optionally be async. When any visitor is async, `markdownToHtml` and `mdxToJs` return a `Promise<string>` instead of `string`. For performance reasons, it is typically best to avoid async visitors, especially if your visitor matches a large number of nodes.
169
+
170
+ ````ts
171
+ const highlighter = await createHighlighter({ themes: ["github-dark"], langs: ["js", "ts"] });
172
+
173
+ const asyncHighlight = defineMdastPlugin({
174
+ name: "async-highlight",
175
+ async code(node) {
176
+ const html = await highlighter.codeToHtml(node.value, {
177
+ lang: node.lang,
178
+ theme: "github-dark",
179
+ });
180
+ return { rawHtml: html };
181
+ },
182
+ });
183
+
184
+ // Returns Promise<string> when async plugins are used
185
+ const html = await markdownToHtml("```js\ncode\n```", {
186
+ mdastPlugins: [asyncHighlight],
187
+ });
188
+ ````
189
+
190
+ ## API
191
+
192
+ ### `markdownToHtml(source: string, options?: CompileOptions)`
193
+
194
+ Parse Markdown and compile to HTML. Returns `string` if all plugins are sync, `Promise<string>` if any are async.
195
+
196
+ ```ts
197
+ const html = markdownToHtml("# Hello\n\nWorld");
198
+ // <h1>Hello</h1>\n<p>World</p>
199
+ ```
200
+
201
+ ### `mdxToJs(source: string, options?: MdxCompileOptions)`
202
+
203
+ Parse MDX and compile to JavaScript module code. Same sync/async return behavior.
204
+
205
+ ```ts
206
+ const js = mdxToJs("# Hello\n\n<MyComponent />");
207
+ ```
208
+
209
+ #### Static optimization
210
+
211
+ The `optimizeStatic` option for MDX collapses static subtrees into pre-rendered HTML strings, reducing the number of JSX element calls in the output and increasing rendering performance. Dynamic content (JSX components, expressions) is preserved as normal JSX calls.
212
+
213
+ ```ts
214
+ // Astro-style: wraps static HTML in <Fragment set:html="...">
215
+ const js = mdxToJs("# Hello\n\nWorld", {
216
+ optimizeStatic: {
217
+ component: "Fragment",
218
+ prop: "set:html",
219
+ },
220
+ });
221
+
222
+ // React-style: wraps in <div dangerouslySetInnerHTML={{ __html: "..." }}>
223
+ const js = mdxToJs("# Hello\n\nWorld", {
224
+ optimizeStatic: {
225
+ component: "div",
226
+ prop: "dangerouslySetInnerHTML",
227
+ wrapPropValue: true,
228
+ },
229
+ });
230
+ ```
231
+
232
+ The `ignoreElements` option can be used to exclude specific elements from collapsing.
233
+
234
+ Shoutout to [Bjorn Lu](https://bjornlu.com) for originally developing this optimization for [Astro](https://astro.build/).
235
+
236
+ ### `markdownToMdast(source: string)`
237
+
238
+ Parse Markdown and return a complete mdast tree. This can be useful if you wanted to benefit from the fast native parsing of Sätteri, but ultimately wanted another pipeline to handle transformations and compilation, e.g. using remark plugins and `remark-stringify` to convert back to Markdown after processing.
239
+
240
+ ```ts
241
+ import { markdownToMdast } from "satteri";
242
+
243
+ const tree = markdownToMdast("# Hello\n\nWorld");
244
+ // tree.children[0].type === "heading"
245
+ // tree.children[0].depth === 1
246
+ ```
247
+
248
+ ### `mdxToMdast(source: string)`
249
+
250
+ Parse MDX and return a complete mdast tree.
251
+
252
+ ```ts
253
+ const tree = mdxToMdast('<Component foo="bar" />');
254
+ // tree.children[0].type === "mdxJsxFlowElement"
255
+ // tree.children[0].name === "Component"
256
+ ```
257
+
258
+ ### `markdownToHast(source: string)`
259
+
260
+ Parse Markdown, convert to hast, and return a complete hast tree.
261
+
262
+ ```ts
263
+ const tree = markdownToHast("# Hello\n\nWorld");
264
+ // tree.children[0].type === "element"
265
+ // tree.children[0].tagName === "h1"
266
+ ```
267
+
268
+ ### `mdxToHast(source: string)`
269
+
270
+ Parse MDX, convert to hast, and return a complete hast tree.
271
+
272
+ ```ts
273
+ const tree = mdxToHast("<MyComponent />");
274
+ // tree.children[0].type === "mdxJsxFlowElement"
275
+ // tree.children[0].name === "MyComponent"
276
+ ```
277
+
278
+ ### `defineMdastPlugin(definition: MdastPluginDefinition)`
279
+
280
+ Type-safe wrapper for MDAST plugin definitions.
281
+
282
+ ### `defineHastPlugin(definition: HastPluginDefinition)`
283
+
284
+ Type-safe wrapper for HAST plugin definitions.
285
+
286
+ ### `CompileOptions`
287
+
288
+ ```ts
289
+ interface CompileOptions {
290
+ mdastPlugins?: MdastPluginDefinition[];
291
+ hastPlugins?: HastPluginDefinition[];
292
+ filename?: string;
293
+ }
294
+
295
+ // mdxToJs accepts MdxCompileOptions, which extends CompileOptions
296
+ interface MdxCompileOptions extends CompileOptions {
297
+ optimizeStatic?: OptimizeStaticConfig;
298
+ }
299
+ ```
300
+
301
+ ## License
302
+
303
+ MIT
@@ -0,0 +1 @@
1
+ export { applyCommandsAndConvertToHastHandle, applyCommandsToHandle, applyCommandsToMdastHandle, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createMdastHandle, createMdxHastHandle, createMdxMdastHandle, dropHandle, getHandleSource, getNodeData, mdastTextContentHandle, parseExpression, parseToHtml, renderHandle, serializeHandle, serializeMdastHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../satteri_napi.wasi-browser.js";
@@ -0,0 +1,2 @@
1
+ // @ts-nocheck — WASM browser binding has no type declarations
2
+ export { applyCommandsAndConvertToHastHandle, applyCommandsToHandle, applyCommandsToMdastHandle, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createMdastHandle, createMdxHastHandle, createMdxMdastHandle, dropHandle, getHandleSource, getNodeData, mdastTextContentHandle, parseExpression, parseToHtml, renderHandle, serializeHandle, serializeMdastHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../satteri_napi.wasi-browser.js";
@@ -0,0 +1 @@
1
+ export { applyCommandsAndConvertToHastHandle, applyCommandsToHandle, applyCommandsToMdastHandle, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createMdastHandle, createMdxHastHandle, createMdxMdastHandle, dropHandle, getHandleSource, getNodeData, mdastTextContentHandle, parseExpression, parseToHtml, renderHandle, serializeHandle, serializeMdastHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../index.js";
@@ -0,0 +1 @@
1
+ export { applyCommandsAndConvertToHastHandle, applyCommandsToHandle, applyCommandsToMdastHandle, compileHandle, compileMdx, convertMdastToHastHandle, createHastHandle, createMdastHandle, createMdxHastHandle, createMdxMdastHandle, dropHandle, getHandleSource, getNodeData, mdastTextContentHandle, parseExpression, parseToHtml, renderHandle, serializeHandle, serializeMdastHandle, setNodeData, textContentHandle, walkHandle, walkMdastHandle, } from "../index.js";
@@ -40,7 +40,6 @@ export function classifyReturn(value) {
40
40
  return "structured_node";
41
41
  throw new Error("Invalid return value from visitor: must have raw, rawHtml, or type");
42
42
  }
43
- // CommandBuffer
44
43
  const INITIAL_SIZE = 4096;
45
44
  const encoder = new TextEncoder();
46
45
  const EMPTY_U8 = new Uint8Array(0);
package/dist/compile.d.ts CHANGED
@@ -1,10 +1,5 @@
1
- /**
2
- * Top-level compile functions, the primary public API.
3
- *
4
- * Both MDAST and HAST arenas stay in Rust memory via opaque handles.
5
- * Only matched nodes and mutation commands cross the NAPI boundary.
6
- */
7
1
  import type { MdastPluginDefinition, HastPluginDefinition } from "./plugin.js";
2
+ import type { MdastNode, HastNode } from "./types.js";
8
3
  /** Configuration for static subtree collapsing during MDX compilation. */
9
4
  export interface OptimizeStaticConfig {
10
5
  component: string;
@@ -12,11 +7,69 @@ export interface OptimizeStaticConfig {
12
7
  wrapPropValue?: boolean;
13
8
  ignoreElements?: string[];
14
9
  }
10
+ /** Parser feature toggles. All default to their documented value when omitted. */
11
+ export interface Features {
12
+ /** GFM: tables, footnotes, strikethrough, task lists, blockquote tags. Default: true. */
13
+ gfm?: boolean;
14
+ /** Frontmatter: YAML (`--- ... ---`) and TOML (`+++ ... +++`). Default: true. */
15
+ frontmatter?: boolean;
16
+ /** Math blocks and inline math. Default: true. */
17
+ math?: boolean;
18
+ /** Heading attributes (`# text { #id .class }`). Default: true. */
19
+ headingAttributes?: boolean;
20
+ /** Colon-delimited container directive blocks (`:::`). Default: false. */
21
+ directive?: boolean;
22
+ /** Superscript (`^super^`). Default: false. */
23
+ superscript?: boolean;
24
+ /** Subscript (`~sub~`). Default: false. */
25
+ subscript?: boolean;
26
+ /** Obsidian-style wikilinks (`[[link]]`). Default: false. */
27
+ wikilinks?: boolean;
28
+ /** Smart punctuation à la SmartyPants. Default: false. */
29
+ smartPunctuation?: boolean;
30
+ /** Definition lists. Default: false. */
31
+ definitionList?: boolean;
32
+ }
15
33
  export interface CompileOptions {
16
34
  mdastPlugins?: MdastPluginDefinition[];
17
35
  hastPlugins?: HastPluginDefinition[];
18
- optimizeStatic?: OptimizeStaticConfig;
36
+ features?: Features;
19
37
  filename?: string;
20
38
  }
21
- export declare function compileMarkdownToHtml(source: string, options?: CompileOptions): string | Promise<string>;
22
- export declare function compileMdxToJs(source: string, options?: CompileOptions): string | Promise<string>;
39
+ export interface MdxCompileOptions extends CompileOptions {
40
+ optimizeStatic?: OptimizeStaticConfig;
41
+ /** Place to import automatic JSX runtimes from (e.g. "react", "preact"). Default: "react". */
42
+ jsxImportSource?: string;
43
+ /** Whether to keep JSX instead of compiling it to functions. Default: false. */
44
+ jsx?: boolean;
45
+ /** JSX runtime: "automatic" (default) or "classic". */
46
+ jsxRuntime?: "automatic" | "classic";
47
+ /** Enable development mode. Default: false. */
48
+ development?: boolean;
49
+ /** Place to import the component provider from. */
50
+ providerImportSource?: string;
51
+ /** Pragma for JSX in classic runtime (default: "React.createElement"). */
52
+ pragma?: string;
53
+ /** Pragma for JSX fragments in classic runtime (default: "React.Fragment"). */
54
+ pragmaFrag?: string;
55
+ /** Where to import the pragma from in classic runtime (default: "react"). */
56
+ pragmaImportSource?: string;
57
+ }
58
+ export declare function markdownToHtml(source: string, options?: CompileOptions): string | Promise<string>;
59
+ export declare function mdxToJs(source: string, options?: MdxCompileOptions): string | Promise<string>;
60
+ /** Parse Markdown source into a materialized mdast tree. */
61
+ export declare function markdownToMdast(source: string, options?: {
62
+ features?: Features;
63
+ }): MdastNode;
64
+ /** Parse MDX source into a materialized mdast tree. */
65
+ export declare function mdxToMdast(source: string, options?: {
66
+ features?: Features;
67
+ }): MdastNode;
68
+ /** Convert Markdown source to a materialized hast tree. */
69
+ export declare function markdownToHast(source: string, options?: {
70
+ features?: Features;
71
+ }): HastNode;
72
+ /** Convert MDX source to a materialized hast tree. */
73
+ export declare function mdxToHast(source: string, options?: {
74
+ features?: Features;
75
+ }): HastNode;
package/dist/compile.js CHANGED
@@ -1,37 +1,54 @@
1
- /**
2
- * Top-level compile functions, the primary public API.
3
- *
4
- * Both MDAST and HAST arenas stay in Rust memory via opaque handles.
5
- * Only matched nodes and mutation commands cross the NAPI boundary.
6
- */
7
1
  import { visitHastHandle, resolveSubscriptions } from "./hast/hast-visitor.js";
8
2
  import { visitMdastHandle, resolveMdastSubscriptions, } from "./mdast/mdast-visitor.js";
9
- import { parseToHtml, compileMdx, createHastHandle, createMdxHastHandle, renderHandle, compileHandle, applyCommandsToHandle, dropHandle, createMdastHandle, createMdxMdastHandle, applyCommandsToMdastHandle, convertMdastToHastHandle, applyCommandsAndConvertToHastHandle, getHandleSource, } from "../index.js";
10
- // Helpers
11
- function initPlugins(plugins) {
12
- return plugins.map((def) => ({
13
- instance: def.createOnce(),
14
- name: def.name,
15
- }));
3
+ import { parseToHtml, compileMdx, createHastHandle, createMdxHastHandle, renderHandle, compileHandle, applyCommandsToHandle, dropHandle, createMdastHandle, createMdxMdastHandle, applyCommandsToMdastHandle, convertMdastToHastHandle, applyCommandsAndConvertToHastHandle, getHandleSource, serializeHandle, serializeMdastHandle, } from "#binding";
4
+ import { MdastReader } from "./mdast/mdast-reader.js";
5
+ import { materializeMdastTree } from "./mdast/mdast-materializer.js";
6
+ import { HastReader } from "./hast/hast-reader.js";
7
+ import { materializeHastTree } from "./hast/hast-materializer.js";
8
+ function featuresToNative(features) {
9
+ if (!features)
10
+ return undefined;
11
+ // Build object with only defined keys to satisfy exactOptionalPropertyTypes
12
+ const result = {};
13
+ if (features.gfm !== undefined)
14
+ result.gfm = features.gfm;
15
+ if (features.frontmatter !== undefined)
16
+ result.frontmatter = features.frontmatter;
17
+ if (features.math !== undefined)
18
+ result.math = features.math;
19
+ if (features.headingAttributes !== undefined)
20
+ result.headingAttributes = features.headingAttributes;
21
+ if (features.directive !== undefined)
22
+ result.directive = features.directive;
23
+ if (features.superscript !== undefined)
24
+ result.superscript = features.superscript;
25
+ if (features.subscript !== undefined)
26
+ result.subscript = features.subscript;
27
+ if (features.wikilinks !== undefined)
28
+ result.wikilinks = features.wikilinks;
29
+ if (features.smartPunctuation !== undefined)
30
+ result.smartPunctuation = features.smartPunctuation;
31
+ if (features.definitionList !== undefined)
32
+ result.definitionList = features.definitionList;
33
+ return result;
16
34
  }
17
35
  function runMdastPluginsOnHandle(handle, plugins, filename) {
18
- const instances = initPlugins(plugins);
19
36
  let pendingCommands = null;
20
37
  const source = getHandleSource(handle);
21
38
  let i = 0;
22
39
  const runNext = () => {
23
- while (i < instances.length) {
40
+ while (i < plugins.length) {
24
41
  const idx = i++;
25
- const { instance } = instances[idx];
26
- const subs = resolveMdastSubscriptions(instance);
27
- const result = visitMdastHandle(handle, instance, subs, source, filename);
42
+ const plugin = plugins[idx];
43
+ const subs = resolveMdastSubscriptions(plugin);
44
+ const result = visitMdastHandle(handle, plugin, subs, source, filename);
28
45
  if (result instanceof Promise) {
29
46
  return result.then((r) => {
30
- applyMdastResult(r, idx, instances.length, handle);
47
+ applyMdastResult(r, idx, plugins.length, handle);
31
48
  return runNext();
32
49
  });
33
50
  }
34
- applyMdastResult(result, idx, instances.length, handle);
51
+ applyMdastResult(result, idx, plugins.length, handle);
35
52
  }
36
53
  return { handle, pendingCommands };
37
54
  };
@@ -47,18 +64,16 @@ function runMdastPluginsOnHandle(handle, plugins, filename) {
47
64
  }
48
65
  return runNext();
49
66
  }
50
- // HAST plugin runner (handle-based)
51
67
  function runHastPluginsOnHandle(handle, plugins, source, filename) {
52
68
  if (plugins.length === 0)
53
69
  return;
54
- const instances = initPlugins(plugins);
55
70
  let i = 0;
56
71
  const runNext = () => {
57
- while (i < instances.length) {
58
- const { instance } = instances[i];
72
+ while (i < plugins.length) {
73
+ const plugin = plugins[i];
59
74
  i++;
60
- const subs = resolveSubscriptions(instance);
61
- const result = visitHastHandle(handle, instance, subs, source, filename);
75
+ const subs = resolveSubscriptions(plugin);
76
+ const result = visitHastHandle(handle, plugin, subs, source, filename);
62
77
  if (result instanceof Promise) {
63
78
  return result.then(runNext);
64
79
  }
@@ -66,12 +81,47 @@ function runHastPluginsOnHandle(handle, plugins, source, filename) {
66
81
  };
67
82
  return runNext();
68
83
  }
69
- export function compileMarkdownToHtml(source, options = {}) {
70
- const { mdastPlugins = [], hastPlugins = [], filename = "<unknown>" } = options;
84
+ // Public API
85
+ function mdxOptionsToNative(opts) {
86
+ const hasAny = opts.optimizeStatic ||
87
+ opts.jsxImportSource !== undefined ||
88
+ opts.jsx !== undefined ||
89
+ opts.jsxRuntime !== undefined ||
90
+ opts.development !== undefined ||
91
+ opts.providerImportSource !== undefined ||
92
+ opts.pragma !== undefined ||
93
+ opts.pragmaFrag !== undefined ||
94
+ opts.pragmaImportSource !== undefined;
95
+ if (!hasAny)
96
+ return undefined;
97
+ const result = {};
98
+ if (opts.optimizeStatic)
99
+ result.optimizeStatic = opts.optimizeStatic;
100
+ if (opts.jsxImportSource !== undefined)
101
+ result.jsxImportSource = opts.jsxImportSource;
102
+ if (opts.jsx !== undefined)
103
+ result.jsx = opts.jsx;
104
+ if (opts.jsxRuntime !== undefined)
105
+ result.jsxRuntime = opts.jsxRuntime;
106
+ if (opts.development !== undefined)
107
+ result.development = opts.development;
108
+ if (opts.providerImportSource !== undefined)
109
+ result.providerImportSource = opts.providerImportSource;
110
+ if (opts.pragma !== undefined)
111
+ result.pragma = opts.pragma;
112
+ if (opts.pragmaFrag !== undefined)
113
+ result.pragmaFrag = opts.pragmaFrag;
114
+ if (opts.pragmaImportSource !== undefined)
115
+ result.pragmaImportSource = opts.pragmaImportSource;
116
+ return result;
117
+ }
118
+ export function markdownToHtml(source, options = {}) {
119
+ const { mdastPlugins = [], hastPlugins = [], features, filename = "<unknown>" } = options;
120
+ const nativeFeatures = featuresToNative(features);
71
121
  if (mdastPlugins.length === 0 && hastPlugins.length === 0) {
72
- return parseToHtml(source);
122
+ return parseToHtml(source, nativeFeatures);
73
123
  }
74
- const handleResult = createHastHandleFromMdast(source, mdastPlugins, false, filename);
124
+ const handleResult = createHastHandleFromMdast(source, mdastPlugins, false, filename, nativeFeatures);
75
125
  const finish = (hastHandle) => {
76
126
  const asyncResult = runHastPluginsOnHandle(hastHandle, hastPlugins, source, filename);
77
127
  if (asyncResult instanceof Promise) {
@@ -90,13 +140,14 @@ export function compileMarkdownToHtml(source, options = {}) {
90
140
  }
91
141
  return finish(handleResult);
92
142
  }
93
- export function compileMdxToJs(source, options = {}) {
94
- const { mdastPlugins = [], hastPlugins = [], optimizeStatic, filename = "<unknown>" } = options;
95
- const mdxOptions = optimizeStatic ? { optimizeStatic } : undefined;
143
+ export function mdxToJs(source, options = {}) {
144
+ const { mdastPlugins = [], hastPlugins = [], features, filename = "<unknown>", ...mdxFields } = options;
145
+ const mdxOptions = mdxOptionsToNative(mdxFields);
146
+ const nativeFeatures = featuresToNative(features);
96
147
  if (mdastPlugins.length === 0 && hastPlugins.length === 0) {
97
- return compileMdx(source, mdxOptions);
148
+ return compileMdx(source, mdxOptions, nativeFeatures);
98
149
  }
99
- const handleResult = createHastHandleFromMdast(source, mdastPlugins, true, filename);
150
+ const handleResult = createHastHandleFromMdast(source, mdastPlugins, true, filename, nativeFeatures);
100
151
  const finish = (hastHandle) => {
101
152
  const asyncResult = runHastPluginsOnHandle(hastHandle, hastPlugins, source, filename);
102
153
  if (asyncResult instanceof Promise) {
@@ -118,11 +169,17 @@ export function compileMdxToJs(source, options = {}) {
118
169
  // Pipeline: parse → mdast plugins → hast conversion → hast plugins
119
170
  // All arenas stay in Rust. No intermediate buffer copies to JS.
120
171
  /** Parse + mdast plugins + convert to HAST handle. */
121
- function createHastHandleFromMdast(source, mdastPlugins, mdx, filename) {
172
+ function createHastHandleFromMdast(source, mdastPlugins, mdx, filename,
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ nativeFeatures) {
122
175
  if (mdastPlugins.length === 0) {
123
- return mdx ? createMdxHastHandle(source) : createHastHandle(source);
176
+ return mdx
177
+ ? createMdxHastHandle(source, nativeFeatures)
178
+ : createHastHandle(source, nativeFeatures);
124
179
  }
125
- const mdastHandle = mdx ? createMdxMdastHandle(source) : createMdastHandle(source);
180
+ const mdastHandle = mdx
181
+ ? createMdxMdastHandle(source, nativeFeatures)
182
+ : createMdastHandle(source, nativeFeatures);
126
183
  const mdastResult = runMdastPluginsOnHandle(mdastHandle, mdastPlugins, filename);
127
184
  const convert = (r) => {
128
185
  if (r.pendingCommands) {
@@ -135,3 +192,30 @@ function createHastHandleFromMdast(source, mdastPlugins, mdx, filename) {
135
192
  }
136
193
  return convert(mdastResult);
137
194
  }
195
+ // Step-by-step API: individual pipeline stages with materialized trees
196
+ /** Parse Markdown source into a materialized mdast tree. */
197
+ export function markdownToMdast(source, options = {}) {
198
+ const handle = createMdastHandle(source, featuresToNative(options.features));
199
+ const buf = serializeMdastHandle(handle);
200
+ return materializeMdastTree(new MdastReader(buf));
201
+ }
202
+ /** Parse MDX source into a materialized mdast tree. */
203
+ export function mdxToMdast(source, options = {}) {
204
+ const handle = createMdxMdastHandle(source, featuresToNative(options.features));
205
+ const buf = serializeMdastHandle(handle);
206
+ return materializeMdastTree(new MdastReader(buf));
207
+ }
208
+ /** Convert Markdown source to a materialized hast tree. */
209
+ export function markdownToHast(source, options = {}) {
210
+ const handle = createHastHandle(source, featuresToNative(options.features));
211
+ const buf = serializeHandle(handle);
212
+ dropHandle(handle);
213
+ return materializeHastTree(new HastReader(buf));
214
+ }
215
+ /** Convert MDX source to a materialized hast tree. */
216
+ export function mdxToHast(source, options = {}) {
217
+ const handle = createMdxHastHandle(source, featuresToNative(options.features));
218
+ const buf = serializeHandle(handle);
219
+ dropHandle(handle);
220
+ return materializeHastTree(new HastReader(buf));
221
+ }
@@ -51,7 +51,8 @@ export function materializeHastNode(reader, nodeId) {
51
51
  typeName = `unknown(${nodeType})`;
52
52
  break;
53
53
  }
54
- const node = { type: typeName };
54
+ const position = reader.getPosition(nodeId);
55
+ const node = (position ? { type: typeName, position } : { type: typeName });
55
56
  // _nodeId: non-enumerable internal reference
56
57
  Object.defineProperty(node, "_nodeId", {
57
58
  value: nodeId,
@@ -25,6 +25,19 @@ export declare class HastReader {
25
25
  getSource(): string;
26
26
  /** Read a substring from the string pool by byte offset and length. */
27
27
  getString(offset: number, len: number): string;
28
+ /** Get position data for a node. */
29
+ getPosition(nodeId: number): {
30
+ start: {
31
+ offset: number;
32
+ line: number;
33
+ column: number;
34
+ };
35
+ end: {
36
+ offset: number;
37
+ line: number;
38
+ column: number;
39
+ };
40
+ } | undefined;
28
41
  /** Get the node_type byte for a given node ID. */
29
42
  getNodeType(nodeId: number): number;
30
43
  /** Get child node IDs for a given node. */