satteri 0.1.0 → 0.1.3

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 (55) hide show
  1. package/README.md +298 -4
  2. package/dist/command-buffer.js +1 -14
  3. package/dist/compile.d.ts +14 -9
  4. package/dist/compile.js +44 -36
  5. package/dist/hast/hast-materializer.js +0 -1
  6. package/dist/hast/hast-reader.d.ts +1 -1
  7. package/dist/hast/hast-reader.js +1 -2
  8. package/dist/hast/hast-visitor.d.ts +15 -15
  9. package/dist/hast/hast-visitor.js +7 -12
  10. package/dist/hast-types.d.ts +1 -3
  11. package/dist/hast-types.js +0 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.js +2 -3
  14. package/dist/lazy-props.js +0 -1
  15. package/dist/mdast/mdast-materializer.js +0 -1
  16. package/dist/mdast/mdast-reader.js +0 -4
  17. package/dist/mdast/mdast-visitor.d.ts +19 -14
  18. package/dist/mdast/mdast-visitor.js +24 -36
  19. package/dist/mdast-types.d.ts +1 -3
  20. package/dist/mdast-types.js +0 -1
  21. package/dist/mdx-types.d.ts +140 -0
  22. package/dist/mdx-types.js +7 -0
  23. package/dist/plugin.d.ts +4 -6
  24. package/dist/plugin.js +0 -7
  25. package/dist/types.d.ts +4 -6
  26. package/dist/types.js +0 -5
  27. package/index.d.ts +21 -4
  28. package/index.js +54 -53
  29. package/package.json +16 -14
  30. package/satteri_napi.wasi-browser.js +2 -0
  31. package/satteri_napi.wasi.cjs +1 -0
  32. package/dist/command-buffer.js.map +0 -1
  33. package/dist/compile.js.map +0 -1
  34. package/dist/data-map.d.ts +0 -10
  35. package/dist/data-map.js +0 -26
  36. package/dist/data-map.js.map +0 -1
  37. package/dist/hast/hast-materializer.js.map +0 -1
  38. package/dist/hast/hast-reader.js.map +0 -1
  39. package/dist/hast/hast-visitor.js.map +0 -1
  40. package/dist/hast-types.js.map +0 -1
  41. package/dist/index.js.map +0 -1
  42. package/dist/lazy-props.js.map +0 -1
  43. package/dist/mdast/mdast-materializer.js.map +0 -1
  44. package/dist/mdast/mdast-reader.js.map +0 -1
  45. package/dist/mdast/mdast-visitor.js.map +0 -1
  46. package/dist/mdast-types.js.map +0 -1
  47. package/dist/pipeline.d.ts +0 -29
  48. package/dist/pipeline.js +0 -87
  49. package/dist/pipeline.js.map +0 -1
  50. package/dist/plugin.js.map +0 -1
  51. package/dist/processor.d.ts +0 -33
  52. package/dist/processor.js +0 -49
  53. package/dist/processor.js.map +0 -1
  54. package/dist/types.js.map +0 -1
  55. package/satteri_napi.linux-x64-gnu.node +0 -0
package/README.md CHANGED
@@ -1,7 +1,301 @@
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
+ ### `markdownToMdast(source: string)`
235
+
236
+ 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.
237
+
238
+ ```ts
239
+ import { markdownToMdast } from "satteri";
240
+
241
+ const tree = markdownToMdast("# Hello\n\nWorld");
242
+ // tree.children[0].type === "heading"
243
+ // tree.children[0].depth === 1
244
+ ```
245
+
246
+ ### `mdxToMdast(source: string)`
247
+
248
+ Parse MDX and return a complete mdast tree.
249
+
250
+ ```ts
251
+ const tree = mdxToMdast('<Component foo="bar" />');
252
+ // tree.children[0].type === "mdxJsxFlowElement"
253
+ // tree.children[0].name === "Component"
254
+ ```
255
+
256
+ ### `markdownToHast(source: string)`
257
+
258
+ Parse Markdown, convert to hast, and return a complete hast tree.
259
+
260
+ ```ts
261
+ const tree = markdownToHast("# Hello\n\nWorld");
262
+ // tree.children[0].type === "element"
263
+ // tree.children[0].tagName === "h1"
264
+ ```
265
+
266
+ ### `mdxToHast(source: string)`
267
+
268
+ Parse MDX, convert to hast, and return a complete hast tree.
269
+
270
+ ```ts
271
+ const tree = mdxToHast("<MyComponent />");
272
+ // tree.children[0].type === "mdxJsxFlowElement"
273
+ // tree.children[0].name === "MyComponent"
274
+ ```
275
+
276
+ ### `defineMdastPlugin(definition: MdastPluginDefinition)`
277
+
278
+ Type-safe wrapper for MDAST plugin definitions.
279
+
280
+ ### `defineHastPlugin(definition: HastPluginDefinition)`
281
+
282
+ Type-safe wrapper for HAST plugin definitions.
283
+
284
+ ### `CompileOptions`
285
+
286
+ ```ts
287
+ interface CompileOptions {
288
+ mdastPlugins?: MdastPluginDefinition[];
289
+ hastPlugins?: HastPluginDefinition[];
290
+ filename?: string;
291
+ }
292
+
293
+ // mdxToJs accepts MdxCompileOptions, which extends CompileOptions
294
+ interface MdxCompileOptions extends CompileOptions {
295
+ optimizeStatic?: OptimizeStaticConfig;
296
+ }
297
+ ```
298
+
299
+ ## License
300
+
301
+ MIT
@@ -8,9 +8,7 @@
8
8
  * All multi-byte integers are little-endian to match native x86/ARM layout and
9
9
  * avoid byte-swapping on the Rust side.
10
10
  */
11
- // ---------------------------------------------------------------------------
12
11
  // Command bytes (0x01–0x0F)
13
- // ---------------------------------------------------------------------------
14
12
  const CMD_REMOVE = 0x01;
15
13
  const CMD_INSERT_BEFORE = 0x05;
16
14
  const CMD_INSERT_AFTER = 0x06;
@@ -19,15 +17,11 @@ const CMD_APPEND_CHILD = 0x08;
19
17
  const CMD_WRAP = 0x09;
20
18
  const CMD_REPLACE = 0x0b;
21
19
  const CMD_SET_PROPERTY = 0x0c;
22
- // ---------------------------------------------------------------------------
23
20
  // Payload types (0x10+, distinct range from commands)
24
- // ---------------------------------------------------------------------------
25
21
  const PAYLOAD_RAW_MARKDOWN = 0x10;
26
22
  const PAYLOAD_RAW_HTML = 0x11;
27
23
  const PAYLOAD_SERDE_JSON = 0x12;
28
- // ---------------------------------------------------------------------------
29
24
  // Value types for CMD_SET_PROPERTY (must match commands.rs PROP_* constants)
30
- // ---------------------------------------------------------------------------
31
25
  const PROP_STRING = 0;
32
26
  const PROP_BOOL_TRUE = 1;
33
27
  const PROP_BOOL_FALSE = 2;
@@ -46,9 +40,6 @@ export function classifyReturn(value) {
46
40
  return "structured_node";
47
41
  throw new Error("Invalid return value from visitor: must have raw, rawHtml, or type");
48
42
  }
49
- // ---------------------------------------------------------------------------
50
- // CommandBuffer
51
- // ---------------------------------------------------------------------------
52
43
  const INITIAL_SIZE = 4096;
53
44
  const encoder = new TextEncoder();
54
45
  const EMPTY_U8 = new Uint8Array(0);
@@ -62,7 +53,6 @@ export class CommandBuffer {
62
53
  this.view = new DataView(this.buffer);
63
54
  this.bytes = new Uint8Array(this.buffer);
64
55
  }
65
- // -- Public command methods -----------------------------------------------
66
56
  removeNode(nodeId) {
67
57
  this.ensureCapacity(5);
68
58
  this.writeU8(CMD_REMOVE);
@@ -154,7 +144,6 @@ export class CommandBuffer {
154
144
  this.writeU32(encoded.length);
155
145
  this.writeBytes(encoded);
156
146
  }
157
- // -- Result accessors -----------------------------------------------------
158
147
  /** Return a Uint8Array view of the written bytes (no copy). */
159
148
  getBuffer() {
160
149
  return new Uint8Array(this.buffer, 0, this.offset);
@@ -170,7 +159,6 @@ export class CommandBuffer {
170
159
  this.bytes = new Uint8Array(this.buffer);
171
160
  this.offset = 0;
172
161
  }
173
- // -- Private helpers ------------------------------------------------------
174
162
  writeStructuralCommand(cmd, nodeId, node) {
175
163
  const v = node;
176
164
  if (typeof v.raw === "string") {
@@ -192,7 +180,7 @@ export class CommandBuffer {
192
180
  this.writeBytes(encoded);
193
181
  }
194
182
  else {
195
- // Structured node serialize as JSON
183
+ // Structured node, serialize as JSON
196
184
  const json = JSON.stringify(node);
197
185
  const encoded = encoder.encode(json);
198
186
  this.ensureCapacity(10 + encoded.length);
@@ -228,4 +216,3 @@ export class CommandBuffer {
228
216
  this.offset += data.length;
229
217
  }
230
218
  }
231
- //# sourceMappingURL=command-buffer.js.map
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;
@@ -15,8 +10,18 @@ export interface OptimizeStaticConfig {
15
10
  export interface CompileOptions {
16
11
  mdastPlugins?: MdastPluginDefinition[];
17
12
  hastPlugins?: HastPluginDefinition[];
18
- optimizeStatic?: OptimizeStaticConfig;
19
13
  filename?: string;
20
14
  }
21
- export declare function compileMarkdownToHtml(source: string, options?: CompileOptions): string | Promise<string>;
22
- export declare function compileMdxToJs(source: string, options?: CompileOptions): string | Promise<string>;
15
+ export interface MdxCompileOptions extends CompileOptions {
16
+ optimizeStatic?: OptimizeStaticConfig;
17
+ }
18
+ export declare function markdownToHtml(source: string, options?: CompileOptions): string | Promise<string>;
19
+ export declare function mdxToJs(source: string, options?: MdxCompileOptions): string | Promise<string>;
20
+ /** Parse Markdown source into a materialized mdast tree. */
21
+ export declare function markdownToMdast(source: string): MdastNode;
22
+ /** Parse MDX source into a materialized mdast tree. */
23
+ export declare function mdxToMdast(source: string): MdastNode;
24
+ /** Convert Markdown source to a materialized hast tree. */
25
+ export declare function markdownToHast(source: string): HastNode;
26
+ /** Convert MDX source to a materialized hast tree. */
27
+ export declare function mdxToHast(source: string): HastNode;
package/dist/compile.js CHANGED
@@ -1,39 +1,27 @@
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
- // ---------------------------------------------------------------------------
11
- // Helpers
12
- // ---------------------------------------------------------------------------
13
- function initPlugins(plugins) {
14
- return plugins.map((def) => ({
15
- instance: def.createOnce(),
16
- name: def.name,
17
- }));
18
- }
3
+ import { parseToHtml, compileMdx, createHastHandle, createMdxHastHandle, renderHandle, compileHandle, applyCommandsToHandle, dropHandle, createMdastHandle, createMdxMdastHandle, applyCommandsToMdastHandle, convertMdastToHastHandle, applyCommandsAndConvertToHastHandle, getHandleSource, serializeHandle, serializeMdastHandle, } from "../index.js";
4
+ import { ArenaReader } from "./mdast/mdast-reader.js";
5
+ import { materializeTree } from "./mdast/mdast-materializer.js";
6
+ import { HastReader } from "./hast/hast-reader.js";
7
+ import { materializeHastTree } from "./hast/hast-materializer.js";
19
8
  function runMdastPluginsOnHandle(handle, plugins, filename) {
20
- const instances = initPlugins(plugins);
21
9
  let pendingCommands = null;
22
10
  const source = getHandleSource(handle);
23
11
  let i = 0;
24
12
  const runNext = () => {
25
- while (i < instances.length) {
13
+ while (i < plugins.length) {
26
14
  const idx = i++;
27
- const { instance } = instances[idx];
28
- const subs = resolveMdastSubscriptions(instance);
29
- const result = visitMdastHandle(handle, instance, subs, source, filename);
15
+ const plugin = plugins[idx];
16
+ const subs = resolveMdastSubscriptions(plugin);
17
+ const result = visitMdastHandle(handle, plugin, subs, source, filename);
30
18
  if (result instanceof Promise) {
31
19
  return result.then((r) => {
32
- applyMdastResult(r, idx, instances.length, handle);
20
+ applyMdastResult(r, idx, plugins.length, handle);
33
21
  return runNext();
34
22
  });
35
23
  }
36
- applyMdastResult(result, idx, instances.length, handle);
24
+ applyMdastResult(result, idx, plugins.length, handle);
37
25
  }
38
26
  return { handle, pendingCommands };
39
27
  };
@@ -49,20 +37,16 @@ function runMdastPluginsOnHandle(handle, plugins, filename) {
49
37
  }
50
38
  return runNext();
51
39
  }
52
- // ---------------------------------------------------------------------------
53
- // HAST plugin runner (handle-based)
54
- // ---------------------------------------------------------------------------
55
40
  function runHastPluginsOnHandle(handle, plugins, source, filename) {
56
41
  if (plugins.length === 0)
57
42
  return;
58
- const instances = initPlugins(plugins);
59
43
  let i = 0;
60
44
  const runNext = () => {
61
- while (i < instances.length) {
62
- const { instance } = instances[i];
45
+ while (i < plugins.length) {
46
+ const plugin = plugins[i];
63
47
  i++;
64
- const subs = resolveSubscriptions(instance);
65
- const result = visitHastHandle(handle, instance, subs, source, filename);
48
+ const subs = resolveSubscriptions(plugin);
49
+ const result = visitHastHandle(handle, plugin, subs, source, filename);
66
50
  if (result instanceof Promise) {
67
51
  return result.then(runNext);
68
52
  }
@@ -70,7 +54,7 @@ function runHastPluginsOnHandle(handle, plugins, source, filename) {
70
54
  };
71
55
  return runNext();
72
56
  }
73
- export function compileMarkdownToHtml(source, options = {}) {
57
+ export function markdownToHtml(source, options = {}) {
74
58
  const { mdastPlugins = [], hastPlugins = [], filename = "<unknown>" } = options;
75
59
  if (mdastPlugins.length === 0 && hastPlugins.length === 0) {
76
60
  return parseToHtml(source);
@@ -94,7 +78,7 @@ export function compileMarkdownToHtml(source, options = {}) {
94
78
  }
95
79
  return finish(handleResult);
96
80
  }
97
- export function compileMdxToJs(source, options = {}) {
81
+ export function mdxToJs(source, options = {}) {
98
82
  const { mdastPlugins = [], hastPlugins = [], optimizeStatic, filename = "<unknown>" } = options;
99
83
  const mdxOptions = optimizeStatic ? { optimizeStatic } : undefined;
100
84
  if (mdastPlugins.length === 0 && hastPlugins.length === 0) {
@@ -119,10 +103,8 @@ export function compileMdxToJs(source, options = {}) {
119
103
  }
120
104
  return finish(handleResult);
121
105
  }
122
- // ---------------------------------------------------------------------------
123
106
  // Pipeline: parse → mdast plugins → hast conversion → hast plugins
124
107
  // All arenas stay in Rust. No intermediate buffer copies to JS.
125
- // ---------------------------------------------------------------------------
126
108
  /** Parse + mdast plugins + convert to HAST handle. */
127
109
  function createHastHandleFromMdast(source, mdastPlugins, mdx, filename) {
128
110
  if (mdastPlugins.length === 0) {
@@ -141,4 +123,30 @@ function createHastHandleFromMdast(source, mdastPlugins, mdx, filename) {
141
123
  }
142
124
  return convert(mdastResult);
143
125
  }
144
- //# sourceMappingURL=compile.js.map
126
+ // Step-by-step API: individual pipeline stages with materialized trees
127
+ /** Parse Markdown source into a materialized mdast tree. */
128
+ export function markdownToMdast(source) {
129
+ const handle = createMdastHandle(source);
130
+ const buf = serializeMdastHandle(handle);
131
+ return materializeTree(new ArenaReader(buf));
132
+ }
133
+ /** Parse MDX source into a materialized mdast tree. */
134
+ export function mdxToMdast(source) {
135
+ const handle = createMdxMdastHandle(source);
136
+ const buf = serializeMdastHandle(handle);
137
+ return materializeTree(new ArenaReader(buf));
138
+ }
139
+ /** Convert Markdown source to a materialized hast tree. */
140
+ export function markdownToHast(source) {
141
+ const handle = createHastHandle(source);
142
+ const buf = serializeHandle(handle);
143
+ dropHandle(handle);
144
+ return materializeHastTree(new HastReader(buf));
145
+ }
146
+ /** Convert MDX source to a materialized hast tree. */
147
+ export function mdxToHast(source) {
148
+ const handle = createMdxHastHandle(source);
149
+ const buf = serializeHandle(handle);
150
+ dropHandle(handle);
151
+ return materializeHastTree(new HastReader(buf));
152
+ }
@@ -147,4 +147,3 @@ export function materializeHastNode(reader, nodeId) {
147
147
  export function materializeHastTree(reader) {
148
148
  return materializeHastNode(reader, 0);
149
149
  }
150
- //# sourceMappingURL=hast-materializer.js.map
@@ -1,5 +1,5 @@
1
1
  import type { BufferHeader } from "../types.js";
2
- import type { MdxJsxAttribute, MdxJsxExpressionAttribute } from "mdast-util-mdx-jsx";
2
+ import type { MdxJsxAttribute, MdxJsxExpressionAttribute } from "../mdx-types.js";
3
3
  export type { MdxJsxAttribute, MdxJsxExpressionAttribute };
4
4
  export declare const HAST_ROOT = 0;
5
5
  export declare const HAST_ELEMENT = 1;
@@ -21,7 +21,7 @@ const MDX_ATTR_BOOLEAN_PROP = 0;
21
21
  const MDX_ATTR_LITERAL_PROP = 1;
22
22
  const MDX_ATTR_EXPRESSION_PROP = 2;
23
23
  const MDX_ATTR_SPREAD = 3;
24
- // HastNode field offsets (same layout as MDAST shared binary format)
24
+ // HastNode field offsets (same layout as MDAST, shared binary format)
25
25
  // id: u32 @ 0
26
26
  // node_type: u8 @ 4
27
27
  // _pad: [u8; 3] @ 5
@@ -277,4 +277,3 @@ export class HastReader {
277
277
  return this.getString(ref.offset, ref.len);
278
278
  }
279
279
  }
280
- //# sourceMappingURL=hast-reader.js.map
@@ -1,9 +1,9 @@
1
1
  import { type HastNode } from "./hast-materializer.js";
2
2
  import type { HastRaw } from "../types.js";
3
3
  import type { Element, Text, Comment, Doctype } from "hast";
4
- import type { MdxJsxFlowElementHast, MdxJsxTextElementHast } from "mdast-util-mdx-jsx";
5
- import type { MdxFlowExpressionHast, MdxTextExpressionHast } from "mdast-util-mdx-expression";
6
- import type { MdxjsEsmHast } from "mdast-util-mdxjs-esm";
4
+ import type { MdxJsxFlowElementHast, MdxJsxTextElementHast } from "../mdx-types.js";
5
+ import type { MdxFlowExpressionHast, MdxTextExpressionHast } from "../mdx-types.js";
6
+ import type { MdxjsEsmHast } from "../mdx-types.js";
7
7
  export type HastHandle = any;
8
8
  /** ESTree-compatible Program node returned by `parseExpression()`. */
9
9
  export type EstreeProgram = Record<string, any>;
@@ -15,19 +15,19 @@ export interface HastDiagnostic {
15
15
  export interface HastVisitorContext {
16
16
  readonly source: string;
17
17
  readonly filename: string;
18
- removeNode(node: HastNode): void;
19
- replaceNode(node: HastNode, newNode: HastNode): void;
20
- insertBefore(node: HastNode, newNode: HastNode): void;
21
- insertAfter(node: HastNode, newNode: HastNode): void;
22
- wrapNode(node: HastNode, parentNode: HastNode): void;
23
- prependChild(node: HastNode, childNode: HastNode): void;
24
- appendChild(node: HastNode, childNode: HastNode): void;
25
- setProperty(node: HastNode, key: string, value: unknown): void;
18
+ removeNode(node: Readonly<HastNode>): void;
19
+ replaceNode(node: Readonly<HastNode>, newNode: HastNode): void;
20
+ insertBefore(node: Readonly<HastNode>, newNode: HastNode): void;
21
+ insertAfter(node: Readonly<HastNode>, newNode: HastNode): void;
22
+ wrapNode(node: Readonly<HastNode>, parentNode: HastNode): void;
23
+ prependChild(node: Readonly<HastNode>, childNode: HastNode): void;
24
+ appendChild(node: Readonly<HastNode>, childNode: HastNode): void;
25
+ setProperty(node: Readonly<HastNode>, key: string, value: unknown): void;
26
26
  /** Collect the concatenated text of all descendant text nodes (like DOM textContent). */
27
- textContent(node: HastNode): string;
27
+ textContent(node: Readonly<HastNode>): string;
28
28
  report(opts: {
29
29
  message: string;
30
- node?: HastNode;
30
+ node?: Readonly<HastNode>;
31
31
  severity?: "error" | "warning" | "info";
32
32
  }): void;
33
33
  getDiagnostics(): HastDiagnostic[];
@@ -35,9 +35,9 @@ export interface HastVisitorContext {
35
35
  /** A filtered visitor: Rust filters by tag/component name, only matched nodes cross the boundary. */
36
36
  export interface HastFilteredVisitor<N extends HastNode = HastNode> {
37
37
  filter: string[];
38
- visit(node: N, ctx: HastVisitorContext): HastNode | void | Promise<HastNode | void>;
38
+ visit(node: Readonly<N>, ctx: HastVisitorContext): HastNode | void | Promise<HastNode | void>;
39
39
  }
40
- type HastVisitorFn<N extends HastNode = HastNode> = (node: N, ctx: HastVisitorContext) => HastNode | void | Promise<HastNode | void>;
40
+ type HastVisitorFn<N extends HastNode = HastNode> = (node: Readonly<N>, ctx: HastVisitorContext) => HastNode | void | Promise<HastNode | void>;
41
41
  export interface HastVisitorInstance {
42
42
  element?: HastFilteredVisitor<Element> | HastFilteredVisitor<Element>[];
43
43
  mdxJsxFlowElement?: HastFilteredVisitor<MdxJsxFlowElementHast> | HastFilteredVisitor<MdxJsxFlowElementHast>[];