#html-parser #common-mark #render-markdown #markdown-parser #wasm-parser

bin+lib ironmark

Fast Markdown to HTML parser written in Rust with WebAssembly bindings

47 stable releases

Uses new Rust 2024

new 1.12.1 Apr 13, 2026
1.12.0 Apr 12, 2026
1.5.6 Mar 31, 2026
1.3.4 Feb 27, 2026
0.1.0 Feb 24, 2026

#1013 in Parser implementations

MIT license

440KB
11K SLoC

ironmark

CI npm crates.io

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 (652/652 spec tests pass). Available as a Rust crate and as an npm package via WebAssembly.

Table of Contents

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

npm install ironmark
# or
pnpm add ironmark

Node.js

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

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.

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.

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():

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.

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.

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.

import { init, parse } from "ironmark";

await init();

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

Vite

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

npx ironmark --ansi README.md

Or install globally:

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

Rust

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

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):

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

# 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

cargo add ironmark
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:

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:

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():

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:

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.

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

cargo build --release
# output: target/release/libironmark.a
# 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

#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.

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 for package management.

Build from source

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

Dependencies

~0.9–1.7MB
~33K SLoC