All formats

pulldown-cmark

LanguageRust
LicenseMIT
Repositorygithub.com/pulldown-cmark/pulldown-cmark
Documentationdocs.rs/pulldown-cmark, guide
Depends onbitflags, memchr, unicase; optional getopts (CLI), pulldown-cmark-escape (HTML), serde
Read / writeparses CommonMark plus optional extensions; renders HTML

pulldown-cmark doesn’t build a syntax tree. Parser is an iterator of Events: Start(Tag), End(TagEnd), Text, Code, Html, SoftBreak, Rule and others. You can map, filter or collect the events and pass them to html::push_html. This design makes it fast and memory-efficient and makes simple transformations (rewriting links, collecting headings) a few lines of iterator code.

It passes the CommonMark spec tests. Extensions are Options flags: ENABLE_TABLES, ENABLE_TASKLISTS, ENABLE_STRIKETHROUGH, ENABLE_FOOTNOTES (or ENABLE_OLD_FOOTNOTES), ENABLE_HEADING_ATTRIBUTES, ENABLE_SMART_PUNCTUATION, ENABLE_MATH, ENABLE_GFM (blockquote alerts), ENABLE_YAML_STYLE_METADATA_BLOCKS, ENABLE_DEFINITION_LIST, ENABLE_SUPERSCRIPT, ENABLE_SUBSCRIPT and ENABLE_WIKILINKS. Raw HTML passes through as Event::Html; filter it out for untrusted input.

Installation#

Shell
cargo add pulldown-cmark

Basic usage: convert Markdown to HTML#

Rust
use pulldown_cmark::{html, Options, Parser};
use std::fs;

fn main() -> std::io::Result<()> {
    let markdown = fs::read_to_string("README.md")?;

    let mut options = Options::empty();
    options.insert(Options::ENABLE_TABLES);
    options.insert(Options::ENABLE_STRIKETHROUGH);
    options.insert(Options::ENABLE_TASKLISTS);
    options.insert(Options::ENABLE_FOOTNOTES);

    let parser = Parser::new_ext(&markdown, options);
    let mut html_out = String::new();
    html::push_html(&mut html_out, parser);

    fs::write("README.html", html_out)?;
    Ok(())
}
Rust
use pulldown_cmark::{Event, Parser, Tag, TagEnd};

let mut in_heading = None;
let mut title = String::new();
for event in Parser::new(&markdown) {
    match event {
        Event::Start(Tag::Heading { level, .. }) => {
            in_heading = Some(level);
            title.clear();
        }
        Event::Text(t) | Event::Code(t) if in_heading.is_some() => title.push_str(&t),
        Event::End(TagEnd::Heading(level)) => {
            println!("{level}: {title}");   // h1, h2, ...
            in_heading = None;
        }
        Event::Start(Tag::Link { dest_url, .. }) => println!("link: {dest_url}"),
        _ => {}
    }
}
Rust
use pulldown_cmark::{html, CowStr, Event, Parser, Tag};

let parser = Parser::new(&markdown).map(|event| match event {
    Event::Start(Tag::Link { link_type, dest_url, title, id }) => {
        let url: CowStr = if dest_url.ends_with(".md") {
            dest_url.replace(".md", ".html").into()
        } else {
            dest_url
        };
        Event::Start(Tag::Link { link_type, dest_url: url, title, id })
    }
    other => other,
});
let mut out = String::new();
html::push_html(&mut out, parser);

How to drop raw HTML from untrusted input#

Rust
let parser = Parser::new(&untrusted)
    .filter(|e| !matches!(e, Event::Html(_) | Event::InlineHtml(_)));

Links can still use javascript: URLs; sanitize the output with a crate like ammonia for full protection.

How to read YAML front matter#

Rust
use pulldown_cmark::{Event, MetadataBlockKind, Options, Parser, Tag};

let mut in_meta = false;
let mut yaml = String::new();
for event in Parser::new_ext(&markdown, Options::ENABLE_YAML_STYLE_METADATA_BLOCKS) {
    match event {
        Event::Start(Tag::MetadataBlock(MetadataBlockKind::YamlStyle)) => in_meta = true,
        Event::End(_) if in_meta => in_meta = false,
        Event::Text(t) if in_meta => yaml.push_str(&t),
        _ => {}
    }
}
// parse `yaml` with a YAML crate

Tips#