satteri 0.9.2 → 0.9.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 (2) hide show
  1. package/README.md +19 -323
  2. package/package.json +10 -10
package/README.md CHANGED
@@ -17,7 +17,7 @@ pnpm add satteri
17
17
  ```ts
18
18
  import { markdownToHtml } from "satteri";
19
19
 
20
- const html = markdownToHtml("# Hello\n\nWorld");
20
+ const { html } = markdownToHtml("# Hello\n\nWorld");
21
21
  // <h1>Hello</h1>\n<p>World</p>
22
22
  ```
23
23
 
@@ -26,344 +26,40 @@ const html = markdownToHtml("# Hello\n\nWorld");
26
26
  ```ts
27
27
  import { mdxToJs } from "satteri";
28
28
 
29
- const js = mdxToJs("# Hello\n\n<MyComponent />");
29
+ const { code } = mdxToJs("# Hello\n\n<MyComponent />");
30
30
  ```
31
31
 
32
32
  ### With plugins
33
33
 
34
- Both functions accept `mdastPlugins` (operate on the Markdown AST before conversion to HAST) and `hastPlugins` (operate on the HAST before output).
34
+ Both functions accept `mdastPlugins` (operate on the Markdown AST) and `hastPlugins` (operate on the HTML AST). A plugin is an object with a `name` and a visitor per node type; `defineMdastPlugin` / `defineHastPlugin` add type inference.
35
35
 
36
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
- ### Sharing data between plugins
167
-
168
- Each context exposes a `data` object, a document-scoped grab bag shared across every visitor in the compile. Writes from one plugin are visible to later plugins, and the bag persists across the mdast→hast boundary, so hast plugins can read what mdast plugins wrote. After compilation the final state is returned on `result.data`, making it suitable for things like extracting a table of contents.
169
-
170
- ```ts
171
- const collectHeadings = defineMdastPlugin({
172
- name: "collect-headings",
173
- heading(node, ctx) {
174
- const list = (ctx.data.headings as string[]) ?? [];
175
- const first = node.children[0];
176
- if (first && "value" in first) list.push(first.value as string);
177
- ctx.data.headings = list;
178
- },
179
- });
180
-
181
- const { html, data } = markdownToHtml("# A\n\n# B", { mdastPlugins: [collectHeadings] });
182
- console.log(data?.headings); // ["A", "B"]
183
- ```
184
-
185
- The bag lives entirely on the JS side, so any value is allowed, including functions, class instances, and `Map`/`Set`. References are preserved across plugins and across the mdast→hast boundary. A fresh, empty bag is created for every compile unless you seed one with the `data` option.
186
-
187
- To seed data into the bag before plugins run, pass an object to the `data` option. The same object is available in all plugins and returned as `result.data` after compilation, so you can pre-populate it with values or methods that plugins can read and update.
188
-
189
- ```ts
190
- const data = { title: "Hello" };
191
- const rewriteTitle = defineMdastPlugin({
192
- name: "rewrite-title",
193
- heading(_node, ctx) {
194
- ctx.data.title = "Updated";
195
- },
196
- });
37
+ import { markdownToHtml, defineMdastPlugin } from "satteri";
197
38
 
198
- const result = markdownToHtml("# A", { mdastPlugins: [rewriteTitle], data });
199
- console.log(result.data === data); // true, the seeded object is returned
200
- console.log(data.title); // "Updated"
201
- ```
202
-
203
- By default keys are typed as `unknown`. To give a key a type, augment the `DataMap` interface:
204
-
205
- ```ts
206
- declare module "satteri" {
207
- interface DataMap {
208
- headings: string[];
209
- }
210
- }
211
-
212
- // now ctx.data.headings and result.data.headings are typed as string[] | undefined
213
- ```
214
-
215
- Unregistered keys stay `unknown`, so the bag remains open-ended.
216
-
217
- ### How transforms compose
218
-
219
- Unlike remark and rehype, which re-walk the tree until it stops changing, each Sätteri plugin walks the tree **once**. Within that single pass:
220
-
221
- - **Passed-through children keep their identity.** When a visitor returns a replacement that reuses the original node's children (e.g. `{ ...node, children: [...node.children] }`), those children are spliced back unchanged — so a transform the same pass queues on a nested one still applies. This is what lets a `containerDirective` visitor turn both an outer `:::note` and a nested `:::tip` into asides in one go.
222
- - **A plugin's own freshly-built nodes are not re-walked by that plugin.** If a visitor returns a brand-new node (one that didn't come from the tree), the same plugin won't visit it. Produce its final shape directly, or hand it off to a later plugin — every plugin runs over the fully materialized output of the ones before it.
223
- - **Dropping a subtree drops transforms queued inside it.** If one visitor removes or replaces a node while another (in the same pass) had queued a transform on something inside that subtree, the orphaned transform is dropped and a warning is logged. This is usually intentional — you discarded that subtree on purpose — but the warning helps catch the cases where it isn't.
224
-
225
- ### Async plugins
226
-
227
- 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.
228
-
229
- ````ts
230
- const highlighter = await createHighlighter({ themes: ["github-dark"], langs: ["js", "ts"] });
231
-
232
- const asyncHighlight = defineMdastPlugin({
233
- name: "async-highlight",
234
- async code(node) {
235
- const html = await highlighter.codeToHtml(node.value, {
236
- lang: node.lang,
237
- theme: "github-dark",
238
- });
239
- return { rawHtml: html };
240
- },
241
- });
242
-
243
- // Returns Promise<string> when async plugins are used
244
- const html = await markdownToHtml("```js\ncode\n```", {
245
- mdastPlugins: [asyncHighlight],
246
- });
247
- ````
248
-
249
- ## API
250
-
251
- ### `markdownToHtml(source: string, options?: CompileOptions)`
252
-
253
- Parse Markdown and compile to HTML. Returns `string` if all plugins are sync, `Promise<string>` if any are async.
254
-
255
- ```ts
256
- const html = markdownToHtml("# Hello\n\nWorld");
257
- // <h1>Hello</h1>\n<p>World</p>
258
- ```
259
-
260
- ### `mdxToJs(source: string, options?: MdxCompileOptions)`
261
-
262
- Parse MDX and compile to JavaScript module code. Same sync/async return behavior.
263
-
264
- ```ts
265
- const js = mdxToJs("# Hello\n\n<MyComponent />");
266
- ```
267
-
268
- #### Static optimization
269
-
270
- 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.
271
-
272
- ```ts
273
- // Astro-style: wraps static HTML in <Fragment set:html="...">
274
- const js = mdxToJs("# Hello\n\nWorld", {
275
- optimizeStatic: {
276
- component: "Fragment",
277
- prop: "set:html",
39
+ const stripInlineCode = defineMdastPlugin({
40
+ name: "strip-inline-code",
41
+ inlineCode(node, ctx) {
42
+ ctx.replaceNode(node, { type: "text", value: node.value });
278
43
  },
279
44
  });
280
45
 
281
- // React-style: wraps in <div dangerouslySetInnerHTML={{ __html: "..." }}>
282
- const js = mdxToJs("# Hello\n\nWorld", {
283
- optimizeStatic: {
284
- component: "div",
285
- prop: "dangerouslySetInnerHTML",
286
- wrapPropValue: true,
287
- },
46
+ const { html } = markdownToHtml("Use `let` instead of `var`.", {
47
+ mdastPlugins: [stripInlineCode],
288
48
  });
49
+ // <p>Use let instead of var.</p>
289
50
  ```
290
51
 
291
- The `ignoreElements` option can be used to exclude specific elements from collapsing.
292
-
293
- Shoutout to [Bjorn Lu](https://bjornlu.com) for originally developing this optimization for [Astro](https://astro.build/).
294
-
295
- ### `markdownToMdast(source: string)`
296
-
297
- 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.
298
-
299
- ```ts
300
- import { markdownToMdast } from "satteri";
301
-
302
- const tree = markdownToMdast("# Hello\n\nWorld");
303
- // tree.children[0].type === "heading"
304
- // tree.children[0].depth === 1
305
- ```
306
-
307
- ### `mdxToMdast(source: string)`
308
-
309
- Parse MDX and return a complete mdast tree.
310
-
311
- ```ts
312
- const tree = mdxToMdast('<Component foo="bar" />');
313
- // tree.children[0].type === "mdxJsxFlowElement"
314
- // tree.children[0].name === "Component"
315
- ```
316
-
317
- ### `markdownToHast(source: string)`
318
-
319
- Parse Markdown, convert to hast, and return a complete hast tree.
320
-
321
- ```ts
322
- const tree = markdownToHast("# Hello\n\nWorld");
323
- // tree.children[0].type === "element"
324
- // tree.children[0].tagName === "h1"
325
- ```
326
-
327
- ### `mdxToHast(source: string)`
328
-
329
- Parse MDX, convert to hast, and return a complete hast tree.
330
-
331
- ```ts
332
- const tree = mdxToHast("<MyComponent />");
333
- // tree.children[0].type === "mdxJsxFlowElement"
334
- // tree.children[0].name === "MyComponent"
335
- ```
336
-
337
- ### `defineMdastPlugin(definition: MdastPluginDefinition)`
338
-
339
- Type-safe wrapper for MDAST plugin definitions.
340
-
341
- ### `defineHastPlugin(definition: HastPluginDefinition)`
342
-
343
- Type-safe wrapper for HAST plugin definitions.
344
-
345
- ### `CompileOptions`
346
-
347
- ```ts
348
- interface CompileOptions {
349
- mdastPlugins?: MdastPluginDefinition[];
350
- hastPlugins?: HastPluginDefinition[];
351
- features?: Features;
352
- fileURL?: URL;
353
- data?: Data;
354
- }
355
-
356
- // mdxToJs accepts MdxCompileOptions, which extends CompileOptions
357
- interface MdxCompileOptions extends CompileOptions {
358
- optimizeStatic?: OptimizeStaticConfig;
359
- }
360
- ```
52
+ If you're familiar with the unified ecosystem, mdast and hast plugins are similar to remark and rehype plugins, respectively, reusing the same AST shapes.
361
53
 
362
- `fileURL` is the `URL` of the document being processed, surfaced to plugins as `ctx.fileURL` (a `URL`, or `undefined` when omitted). Pass a file URL such as Astro's `fileURL`, or convert a filesystem path with Node's `pathToFileURL`; read it back with `fileURLToPath(ctx.fileURL)`.
54
+ ## Documentation
363
55
 
364
- `data` seeds the document-level data bag before plugins run, surfaced as `ctx.data` and returned as `result.data`. See [Sharing data between plugins](#sharing-data-between-plugins).
56
+ This README covers the basics. The full documentation covers the complete API, the plugin system, every parser feature, and framework integration:
365
57
 
366
- `Features` controls the Markdown extensions Sätteri's parser recognizes: `gfm`, `frontmatter`, `math`, `directive`, `smartPunctuation`, etc. `gfm`, `math`, and `smartPunctuation` also accept granular options. For example, `math: { singleDollarTextMath: false }` keeps single `$` as literal text. See the [Features reference](https://satteri.dev/docs/features/) for the full list.
58
+ - **[Documentation home](https://satteri.bruits.org/docs/)** — start here
59
+ - [Quick start](https://satteri.bruits.org/docs/quick-start/)
60
+ - [Compiling](https://satteri.bruits.org/docs/compile/) — functions, options, and MDX settings
61
+ - [Plugins](https://satteri.bruits.org/docs/plugins/) and [Plugin API](https://satteri.bruits.org/docs/plugin-api/)
62
+ - [Features](https://satteri.bruits.org/docs/features/) — GFM, math, directives, and more
367
63
 
368
64
  ## License
369
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "satteri",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "High-performance Markdown and MDX processing",
5
5
  "repository": {
6
6
  "type": "git",
@@ -84,15 +84,15 @@
84
84
  ]
85
85
  },
86
86
  "optionalDependencies": {
87
- "@bruits/satteri-linux-x64-gnu": "0.9.2",
88
- "@bruits/satteri-linux-arm64-gnu": "0.9.2",
89
- "@bruits/satteri-linux-x64-musl": "0.9.2",
90
- "@bruits/satteri-linux-arm64-musl": "0.9.2",
91
- "@bruits/satteri-darwin-x64": "0.9.2",
92
- "@bruits/satteri-darwin-arm64": "0.9.2",
93
- "@bruits/satteri-win32-x64-msvc": "0.9.2",
94
- "@bruits/satteri-win32-arm64-msvc": "0.9.2",
95
- "@bruits/satteri-wasm32-wasi": "0.9.2"
87
+ "@bruits/satteri-linux-x64-gnu": "0.9.3",
88
+ "@bruits/satteri-linux-arm64-gnu": "0.9.3",
89
+ "@bruits/satteri-linux-x64-musl": "0.9.3",
90
+ "@bruits/satteri-linux-arm64-musl": "0.9.3",
91
+ "@bruits/satteri-darwin-x64": "0.9.3",
92
+ "@bruits/satteri-darwin-arm64": "0.9.3",
93
+ "@bruits/satteri-win32-x64-msvc": "0.9.3",
94
+ "@bruits/satteri-win32-arm64-msvc": "0.9.3",
95
+ "@bruits/satteri-wasm32-wasi": "0.9.3"
96
96
  },
97
97
  "scripts": {
98
98
  "build:binary:wasm": "napi build --manifest-path ../../crates/satteri-napi-binding/Cargo.toml --output-dir . --platform --release --esm --strip --target wasm32-wasip1-threads",