ironmark 1.12.2

Fast Markdown to HTML parser written in Rust with WebAssembly bindings
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# ironmark

[![CI](https://github.com/ph1p/ironmark/actions/workflows/ci.yml/badge.svg)](https://github.com/ph1p/ironmark/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/ironmark)](https://www.npmjs.com/package/ironmark) [![crates.io](https://img.shields.io/crates/v/ironmark)](https://crates.io/crates/ironmark)

Fast Markdown parser written in Rust with **zero third-party** parsing dependencies. Outputs HTML, AST, ANSI terminal, or Markdown. Fully compliant with [CommonMark 0.31.2](https://spec.commonmark.org/0.31.2/) (652/652 spec tests pass). Available as a Rust crate and as an npm package via WebAssembly.

## Table of Contents

- [Configuration]#configuration
  - [Extensions]#extensions
  - [Security]#security
  - [Other Options]#other-options
- [JavaScript / TypeScript]#javascript--typescript
  - [Node.js]#nodejs
  - [AST Output]#ast-output
  - [HTML to Markdown]#html-to-markdown
  - [AST to Markdown]#ast-to-markdown
  - [ANSI Terminal Output]#ansi-terminal-output
  - [Browser / Bundler]#browser--bundler
- [CLI]#cli
- [Rust]#rust
- [C / C++]#c--c++
- [Benchmarks]#benchmarks
- [Development]#development

## Configuration

### Extensions (default `true`)

| Option              | JS (`camelCase`)           | Rust (`snake_case`)           | Description                                            |
| ------------------- | -------------------------- | ----------------------------- | ------------------------------------------------------ |
| Hard breaks         | `hardBreaks`               | `hard_breaks`                 | Every newline becomes `<br />`                         |
| Highlight           | `enableHighlight`          | `enable_highlight`            | `==text==``<mark>`                                  |
| Strikethrough       | `enableStrikethrough`      | `enable_strikethrough`        | `~~text~~``<del>`                                   |
| Underline           | `enableUnderline`          | `enable_underline`            | `++text++``<u>`                                     |
| Tables              | `enableTables`             | `enable_tables`               | Pipe table syntax                                      |
| Autolink            | `enableAutolink`           | `enable_autolink`             | Bare URLs & emails → `<a>`                             |
| Task lists          | `enableTaskLists`          | `enable_task_lists`           | `- [ ]` / `- [x]` checkboxes                           |
| Indented code       | `enableIndentedCodeBlocks` | `enable_indented_code_blocks` | 4-space indent → `<pre><code>`                         |
| Wiki links          | `enableWikiLinks`          | `enable_wiki_links`           | `[[page]]``<a href="page">`                         |
| LaTeX math          | `enableLateXMath`          | `enable_latex_math`           | `$inline$` and `$$display$$``<span class="math-…">` |
| Heading IDs         | `enableHeadingIds`         | `enable_heading_ids`          | Auto `id=` on headings from slugified text             |
| Heading anchors     | `enableHeadingAnchors`     | `enable_heading_anchors`      | `<a class="anchor">` inside each heading (implies IDs) |
| Permissive headings | `permissiveAtxHeaders`     | `permissive_atx_headers`      | Allow `#Heading` without space after `#`               |

### Security

| Option           | JS (`camelCase`) | Rust (`snake_case`) | Default        | Description                                                        |
| ---------------- | ---------------- | ------------------- | -------------- | ------------------------------------------------------------------ |
| Disable raw HTML | `disableRawHtml` | `disable_raw_html`  | `false`        | Escape **all** HTML blocks and inline HTML                         |
| No HTML blocks   | `noHtmlBlocks`   | `no_html_blocks`    | `false`        | Escape block-level HTML only (more granular than `disableRawHtml`) |
| No HTML spans    | `noHtmlSpans`    | `no_html_spans`     | `false`        | Escape inline HTML only                                            |
| Tag filter       | `tagFilter`      | `tag_filter`        | `false`        | GFM tag filter: escape `<script>`, `<iframe>`, etc.                |
| Max nesting      || `max_nesting_depth` | `128`          | Limit blockquote/list nesting depth (DoS prevention)               |
| Max input size   || `max_input_size`    | `0` (no limit) | Truncate input beyond this byte count                              |

> In the WASM build, `max_nesting_depth` is fixed at `128` and `max_input_size` at `10 MB`.

Dangerous URI schemes (`javascript:`, `vbscript:`, `data:` except `data:image/…`) are **always** stripped from link and image destinations, regardless of options.

### Other Options

| Option              | JS (`camelCase`)     | Rust (`snake_case`)   | Default | Description                                       |
| ------------------- | -------------------- | --------------------- | ------- | ------------------------------------------------- |
| Collapse whitespace | `collapseWhitespace` | `collapse_whitespace` | `false` | Collapse runs of spaces/tabs in text to one space |

## JavaScript / TypeScript

```bash
npm install ironmark
# or
pnpm add ironmark
```

### Node.js

WASM is embedded and loaded synchronously — no `init()` needed:

```ts
import { parse } from "ironmark";

const html = parse("# Hello\n\nThis is **fast**.");

// safe mode for untrusted input
const safe = parse(userInput, { disableRawHtml: true });
```

### AST Output

Use `parseToAst()` when you need the block-level document structure instead of rendered HTML.

```ts
import { parseToAst } from "ironmark";

const astJson = parseToAst("# Hello\n\n- [x] done");
const ast = JSON.parse(astJson);
```

`parseToAst()` returns a JSON string for portability across JS runtimes and WASM boundaries.

### HTML to Markdown

Convert HTML back to Markdown syntax using `htmlToMarkdown()`. Useful for importing content from HTML sources or round-trip conversion.

```ts
import { htmlToMarkdown } from "ironmark";

const md = htmlToMarkdown("<h1>Hello</h1><p><strong>Bold</strong> text</p>");
// Returns: "# Hello\n\n**Bold** text"

// Preserve unknown HTML tags (e.g., <sup>, <sub>) as raw HTML in output
const md = htmlToMarkdown("<p>H<sub>2</sub>O</p>", true);
// Returns: "H<sub>2</sub>O"
```

For AST access, use `parseHtmlToAst()`:

```ts
import { parseHtmlToAst } from "ironmark";

const astJson = parseHtmlToAst("<h1>Hello</h1><p>World</p>");
const ast = JSON.parse(astJson);
```

### AST to Markdown

Render an AST back to Markdown syntax using `renderMarkdown()`. Combined with `parseToAst()` or `parseHtmlToAst()`, this enables round-trip conversion.

```ts
import { parseToAst, renderMarkdown } from "ironmark";

const ast = parseToAst("# Hello\n\n**World**");
const md = renderMarkdown(ast);
// Returns: "# Hello\n\n**World**"
```

### ANSI Terminal Output

Use `renderAnsi()` to render Markdown as coloured terminal output (ANSI 256-colour escape codes). Useful for CLI tools, terminal UIs, or any environment with a TTY.

````ts
import { renderAnsi } from "ironmark";

// Render with defaults (width 80, colour enabled)
const ansi = renderAnsi("# Hello\n\n**bold** and `code`");
process.stdout.write(ansi);

// Custom terminal width and line numbers in code blocks
const ansi = renderAnsi(
  "# Hello\n\n```rust\nfn main() {}\n```",
  {}, // parse options (same as parse())
  { width: 120, lineNumbers: true },
);
process.stdout.write(ansi);

// With padding — adds horizontal spacing on both sides
const ansi = renderAnsi("# Hello\n\n> A quote", {}, { padding: 2 });
process.stdout.write(ansi);

// Plain text — strips all ANSI codes (useful for piping to files)
const plain = renderAnsi("# Hello\n\n> quote", {}, { color: false });
````

#### ANSI options

| Option        | Type      | Default | Description                                                                                   |
| ------------- | --------- | ------- | --------------------------------------------------------------------------------------------- |
| `width`       | `number`  | `80`    | Column width for word-wrap, heading underlines, rule length. `0` = use default.               |
| `color`       | `boolean` | `true`  | Emit ANSI colour codes. `false` = plain text output.                                          |
| `lineNumbers` | `boolean` | `false` | Show line numbers in fenced code blocks.                                                      |
| `padding`     | `number`  | `0`     | Horizontal padding added to both sides of each line, plus ⌈padding/2⌉ blank lines at the top. |

### Browser / Bundler

Call `init()` once before using `parse()`. It's idempotent and optionally accepts a custom `.wasm` URL.

```ts
import { init, parse } from "ironmark";

await init();

const html = parse("# Hello\n\nThis is **fast**.");
```

#### Vite

```ts
import { init, parse } from "ironmark";
import wasmUrl from "ironmark/ironmark.wasm?url";

await init(wasmUrl);

const html = parse("# Hello\n\nThis is **fast**.");
```

## CLI

Render Markdown as coloured terminal output. Two ways to install:

### npm

```bash
npx ironmark --ansi README.md
```

Or install globally:

```bash
npm install -g ironmark
ironmark --ansi README.md
```

### Rust

Native binary — faster startup, auto-detects terminal width via `$COLUMNS` / `tput cols`.

```bash
cargo install ironmark --features cli
ironmark --ansi README.md
```

### Options

Both CLIs support the same flags (the npm CLI requires `--ansi` as the first flag):

```text
OPTIONS:
    --width N            Terminal column width (default: auto-detect, fallback 80)
    --padding N          Horizontal padding added to both sides of each line, plus ceil(padding/2) blank lines at the top (default: 0)
    --no-color           Disable ANSI escape codes (plain text)
    -n, --line-numbers   Show line numbers in fenced code blocks
    --no-hard-breaks     Don't turn soft newlines into hard line breaks
    --no-tables          Disable pipe table syntax
    --no-highlight       Disable ==highlight== syntax
    --no-strikethrough   Disable ~~strikethrough~~ syntax
    --no-underline       Disable ++underline++ syntax
    --no-autolink        Disable bare URL auto-linking
    --no-task-lists      Disable - [x] task list syntax
    --math               Enable $inline$ and $$display$$ math
    --wiki-links         Enable [[wiki link]] syntax
    --max-size N         Truncate input to N bytes (Rust only)
    -h, --help           Print this help and exit
    -V, --version        Print version and exit
```

### Examples

```bash
# npm
npx ironmark --ansi README.md
npx ironmark --ansi --width 120 README.md
echo '# Hello' | npx ironmark --ansi
npx ironmark --ansi --no-color README.md | less

# Rust (after cargo install ironmark --features cli)
ironmark --ansi README.md
ironmark --ansi --width 120 README.md
echo '# Hello' | ironmark --ansi
cat doc.md | ironmark --ansi --math --wiki-links
```

## Rust

```bash
cargo add ironmark
```

```rust
use ironmark::{parse, ParseOptions};

fn main() {
    // with defaults
    let html = parse("# Hello\n\nThis is **fast**.", &ParseOptions::default());

    // with custom options
    let html = parse("line one\nline two", &ParseOptions {
        hard_breaks: false,
        enable_strikethrough: false,
        ..Default::default()
    });

    // safe mode for untrusted input
    let html = parse("<script>alert(1)</script>", &ParseOptions {
        disable_raw_html: true,
        max_input_size: 1_000_000, // 1 MB
        ..Default::default()
    });
}
```

### AST Output

`parse_to_ast()` returns the typed Rust AST (`Block`) directly:

```rust
use ironmark::{Block, ParseOptions, parse_to_ast};

fn main() {
    let ast = parse_to_ast("# Hello", &ParseOptions::default());

    match ast {
        Block::Document { children } => {
            println!("top-level blocks: {}", children.len());
        }
        _ => unreachable!("root nodes always Document"),
    }
}
```

Exported AST types:

- `Block`
- `ListKind`
- `TableData`
- `TableAlignment`

### HTML to Markdown

Convert HTML back to Markdown syntax:

```rust
use ironmark::{html_to_markdown, HtmlParseOptions};

fn main() {
    let md = html_to_markdown(
        "<h1>Hello</h1><p><strong>Bold</strong> text</p>",
        &HtmlParseOptions::default(),
    );
    // Returns: "# Hello\n\n**Bold** text"
}
```

For AST access, use `parse_html_to_ast()`:

```rust
use ironmark::{parse_html_to_ast, HtmlParseOptions, UnknownInlineHandling};

fn main() {
    // Default: strip unknown tags, keep text content
    let ast = parse_html_to_ast("<p>H<sub>2</sub>O</p>", &HtmlParseOptions::default());

    // Preserve unknown tags as raw HTML
    let ast = parse_html_to_ast(
        "<p>H<sub>2</sub>O</p>",
        &HtmlParseOptions {
            unknown_inline_handling: UnknownInlineHandling::PreserveAsHtml,
            ..Default::default()
        },
    );
}
```

`HtmlParseOptions` fields:

| Field                     | Type                    | Default     | Description                           |
| ------------------------- | ----------------------- | ----------- | ------------------------------------- |
| `max_nesting_depth`       | `usize`                 | `128`       | Limit nesting depth (DoS prevention)  |
| `unknown_inline_handling` | `UnknownInlineHandling` | `StripTags` | How to handle unknown HTML tags       |
| `max_input_size`          | `usize`                 | `0`         | Truncate input beyond this byte count |

`UnknownInlineHandling` variants:

- `StripTags` — Remove unknown tags, keep text content (default)
- `PreserveAsHtml` — Keep unknown tags as raw HTML in output

### AST to Markdown

Render an AST back to Markdown syntax:

```rust
use ironmark::{parse_to_ast, render_markdown, ParseOptions};

fn main() {
    let ast = parse_to_ast("# Hello\n\n**World**", &ParseOptions::default());
    let md = render_markdown(&ast);
    // Returns: "# Hello\n\n**World**"
}
```

### ANSI Terminal Output

`render_ansi()` renders Markdown as ANSI-coloured terminal output. Pass `Some(&AnsiOptions { .. })` to control width, colour, and line numbers, or `None` for defaults.

````rust
use ironmark::{AnsiOptions, ParseOptions, render_ansi};

fn main() {
    // Defaults — width 80, colour enabled
    let out = render_ansi("# Hello\n\n**bold** and `code`", &ParseOptions::default(), None);
    print!("{out}");

    // Custom options — 120 columns, line numbers in code blocks
    let out = render_ansi(
        "# Hello\n\n```rust\nfn main() {}\n```",
        &ParseOptions::default(),
        Some(&AnsiOptions { width: 120, line_numbers: true, ..AnsiOptions::default() }),
    );
    print!("{out}");

    // With padding — adds horizontal spacing on both sides
    let out = render_ansi(
        "# Hello\n\n> A quote",
        &ParseOptions::default(),
        Some(&AnsiOptions { padding: 2, ..AnsiOptions::default() }),
    );
    print!("{out}");

    // Plain text — no ANSI codes (e.g. for writing to a file)
    let plain = render_ansi(
        "# Hello",
        &ParseOptions::default(),
        Some(&AnsiOptions { color: false, ..AnsiOptions::default() }),
    );
}
````

`AnsiOptions` fields:

| Field          | Type    | Default | Description                                                                                   |
| -------------- | ------- | ------- | --------------------------------------------------------------------------------------------- |
| `width`        | `usize` | `80`    | Column width for word-wrap, heading underlines, rule length. `0` = disable.                   |
| `color`        | `bool`  | `true`  | Emit ANSI 256-colour escape codes.                                                            |
| `line_numbers` | `bool`  | `false` | Show line numbers in fenced code blocks.                                                      |
| `padding`      | `usize` | `0`     | Horizontal padding added to both sides of each line, plus ⌈padding/2⌉ blank lines at the top. |

## C / C++

The crate compiles to a static library (`libironmark.a`) that exposes two C functions. A header is provided at `include/ironmark.h`.

### Build the library

```bash
cargo build --release
# output: target/release/libironmark.a
```

### Link

```sh
# Linux
cc -o example example.c -L target/release -l ironmark -lpthread -ldl

# macOS
cc -o example example.c -L target/release -l ironmark \
   -framework CoreFoundation -framework Security
```

### Usage

```c
#include "include/ironmark.h"
#include <stdio.h>

int main(void) {
    char *html = ironmark_parse("# Hello\n\nThis is **fast**.");
    if (html) {
        printf("%s\n", html);
        ironmark_free(html);
    }
    return 0;
}
```

**Memory contract**: `ironmark_parse` returns a heap-allocated string. You **must** free it with `ironmark_free`. Passing any other pointer to `ironmark_free` is undefined behaviour. Both functions are null-safe: `ironmark_parse(NULL)` returns `NULL`; `ironmark_free(NULL)` is a no-op.

Parsing always uses the default `ParseOptions` (all extensions enabled, `disable_raw_html` off). Options are not yet configurable through the C API.

## Benchmarks

Compares ironmark against pulldown-cmark, comrak, markdown-it, and markdown-rs. Results are available at [ph1p.js.org/ironmark/#benchmark](https://ph1p.js.org/ironmark/#benchmark).

```bash
cargo bench                          # run all benchmarks
cargo bench --features bench-md4c   # include md4c (requires: brew install md4c)
pnpm bench                          # run + update playground data
```

## Development

This project uses [pnpm](https://pnpm.io/) for package management.

### Build from source

```bash
pnpm setup:wasm
pnpm build
```

| Command           | Description            |
| ----------------- | ---------------------- |
| `pnpm setup:wasm` | Install prerequisites  |
| `pnpm build`      | Release WASM build     |
| `pnpm build:dev`  | Debug WASM build       |
| `pnpm test`       | Run Rust tests         |
| `pnpm check`      | Format check           |
| `pnpm clean`      | Remove build artifacts |

### Troubleshooting

**`wasm32-unknown-unknown target not found`** or **`wasm-bindgen not found`** — run `pnpm setup:wasm` to install all prerequisites