comrak 0.30.0

A 100% CommonMark-compatible GitHub Flavored Markdown parser and formatter
Documentation
use crate::nodes::{AstNode, NodeValue, Sourcepos};
use crate::*;
use std::collections::HashMap;
use std::io::{self, Write};
use std::panic;

mod api;
mod autolink;
mod commonmark;
mod core;
mod description_lists;
mod empty;
mod escaped_char_spans;
mod footnotes;
mod fuzz;
mod greentext;
mod header_ids;
mod math;
mod multiline_block_quotes;
mod options;
mod pathological;
mod plugins;
mod regressions;
mod rewriter;
mod shortcodes;
mod spoiler;
mod strikethrough;
mod subscript;
mod superscript;
mod table;
mod tagfilter;
mod tasklist;
mod underline;
mod wikilinks;
mod xml;

#[track_caller]
fn compare_strs(output: &str, expected: &str, kind: &str, original_input: &str) {
    if output != expected {
        println!("Running {} test", kind);
        println!("Original Input:");
        println!("==============================");
        println!("{}", original_input);
        println!("==============================");
        println!("Got:");
        println!("==============================");
        println!("{}", output);
        println!("==============================");
        println!();
        println!("Expected:");
        println!("==============================");
        println!("{}", expected);
        println!("==============================");
        println!();
    }
    assert_eq!(output, expected);
}

#[track_caller]
fn commonmark(input: &str, expected: &str, opts: Option<&Options>) {
    let arena = Arena::new();
    let defaults = Options::default();
    let options = opts.unwrap_or(&defaults);

    let root = parse_document(&arena, input, options);
    let mut output = vec![];
    cm::format_document(root, options, &mut output).unwrap();
    compare_strs(
        &String::from_utf8(output).unwrap(),
        expected,
        "regular",
        input,
    );
}

#[track_caller]
pub fn html(input: &str, expected: &str) {
    html_opts_i(input, expected, true, |_| ());
}

#[track_caller]
fn html_opts_i<F>(input: &str, expected: &str, roundtrip: bool, opts: F)
where
    F: Fn(&mut Options),
{
    let mut options = Options::default();
    opts(&mut options);

    html_opts_w(input, expected, roundtrip, &options);
}

#[track_caller]
fn html_opts_w(input: &str, expected: &str, roundtrip: bool, options: &Options) {
    let arena = Arena::new();

    let root = parse_document(&arena, input, options);
    let mut output = vec![];
    html::format_document(root, options, &mut output).unwrap();
    compare_strs(
        &String::from_utf8(output).unwrap(),
        expected,
        "regular",
        input,
    );

    if options.render.sourcepos || !roundtrip {
        return;
    }

    let mut md = vec![];
    cm::format_document(root, options, &mut md).unwrap();

    let md_string = &String::from_utf8(md).unwrap();
    let root = parse_document(&arena, md_string, options);
    let mut output_from_rt = vec![];
    html::format_document(root, options, &mut output_from_rt).unwrap();
    compare_strs(
        &String::from_utf8(output_from_rt).unwrap(),
        expected,
        "roundtrip",
        md_string,
    );
}

macro_rules! html_opts {
    ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr) => {
        html_opts!([$($optclass.$optname),*], $lhs, $rhs,)
    };
    ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr,) => {
        html_opts!([$($optclass.$optname),*], $lhs, $rhs, roundtrip)
    };
    ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr, $rt:ident) => {
        html_opts!([$($optclass.$optname),*], $lhs, $rhs, $rt,)
    };
    ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr, roundtrip,) => {
        $crate::tests::html_opts_i($lhs, $rhs, true, |opts| {
            $(opts.$optclass.$optname = true;)*
        });
    };
    ([$($optclass:ident.$optname:ident),*], $lhs:expr, $rhs:expr, no_roundtrip,) => {
        $crate::tests::html_opts_i($lhs, $rhs, false, |opts| {
            $(opts.$optclass.$optname = true;)*
        });
    };
}

pub(crate) use html_opts;

#[track_caller]
fn html_plugins(input: &str, expected: &str, plugins: &Plugins) {
    let arena = Arena::new();
    let options = Options::default();

    let root = parse_document(&arena, input, &options);
    let mut output = vec![];
    html::format_document_with_plugins(root, &options, &mut output, plugins).unwrap();
    compare_strs(
        &String::from_utf8(output).unwrap(),
        expected,
        "regular",
        input,
    );

    if options.render.sourcepos {
        return;
    }

    let mut md = vec![];
    cm::format_document(root, &options, &mut md).unwrap();

    let md_string = &String::from_utf8(md).unwrap();
    let root = parse_document(&arena, md_string, &options);
    let mut output_from_rt = vec![];
    html::format_document_with_plugins(root, &options, &mut output_from_rt, plugins).unwrap();
    compare_strs(
        &String::from_utf8(output_from_rt).unwrap(),
        expected,
        "roundtrip",
        md_string,
    );
}

#[track_caller]
fn xml(input: &str, expected: &str) {
    xml_opts(input, expected, |_| ());
}

#[track_caller]
fn xml_opts<F>(input: &str, expected: &str, opts: F)
where
    F: Fn(&mut Options),
{
    let arena = Arena::new();
    let mut options = Options::default();
    opts(&mut options);

    let root = parse_document(&arena, input, &options);
    let mut output = vec![];
    crate::xml::format_document(root, &options, &mut output).unwrap();
    compare_strs(
        &String::from_utf8(output).unwrap(),
        expected,
        "regular",
        input,
    );

    if options.render.sourcepos {
        return;
    }

    let mut md = vec![];
    cm::format_document(root, &options, &mut md).unwrap();

    let md_string = &String::from_utf8(md).unwrap();
    let root = parse_document(&arena, md_string, &options);
    let mut output_from_rt = vec![];
    crate::xml::format_document(root, &options, &mut output_from_rt).unwrap();
    compare_strs(
        &String::from_utf8(output_from_rt).unwrap(),
        expected,
        "roundtrip",
        md_string,
    );
}

fn asssert_node_eq<'a>(node: &'a AstNode<'a>, location: &[usize], expected: &NodeValue) {
    let node = location
        .iter()
        .fold(node, |node, &n| node.children().nth(n).unwrap());

    let data = node.data.borrow();
    let actual = format!("{:?}", data.value);
    let expected = format!("{:?}", expected);

    compare_strs(&actual, &expected, "ast comparison", "ast node");
}

macro_rules! sourcepos {
    (($spsl:literal:$spsc:literal-$spel:literal:$spec:literal)) => {
        ($spsl, $spsc, $spel, $spec).into()
    };
    ((XXX)) => {
        (0, 1, 0, 1).into()
    };
}

pub(crate) use sourcepos;

macro_rules! ast {
    (($name:tt $sp:tt)) => {
        ast!(($name $sp []))
    };
    (($name:tt $sp:tt $content:tt)) => {
        AstMatchTree {
            name: stringify!($name).to_string(),
            sourcepos: sourcepos!($sp),
            content: ast!($content),
        }
    };

    ($text:literal) => {AstMatchContent::Text($text.to_string())};
    ([ $( $children:tt )* ]) => {
        AstMatchContent::Children(vec![ $( ast!($children), )* ])
    };
}

pub(crate) use ast;

#[track_caller]
fn assert_ast_match_i<F>(md: &str, amt: AstMatchTree, opts: F)
where
    F: Fn(&mut Options),
{
    let mut options = Options::default();
    options.render.sourcepos = true;
    opts(&mut options);

    let result = panic::catch_unwind(|| {
        let arena = Arena::new();
        let root = parse_document(&arena, md, &options);

        amt.assert_match(root);
    });

    if let Err(err) = result {
        let arena = Arena::new();
        let root = parse_document(&arena, md, &options);

        let mut output = vec![];
        format_xml(root, &options, &mut output).unwrap();
        eprintln!("{}", std::str::from_utf8(&output).unwrap());

        panic::resume_unwind(err)
    }
}

macro_rules! assert_ast_match {
    ([ $( $optclass:ident.$optname:ident ),* ], $( $md:literal )+, $amt:tt,) => {
        assert_ast_match!(
            [ $( $optclass.$optname ),* ],
            $( $md )+,
            $amt
        )
    };
    ([ $( $optclass:ident.$optname:ident ),* ], $( $md:literal )+, $amt:tt) => {
        crate::tests::assert_ast_match_i(
            concat!( $( $md ),+ ),
            ast!($amt),
            |#[allow(unused_variables)] opts| {$(opts.$optclass.$optname = true;)*},
        );
    };
}

pub(crate) use assert_ast_match;

struct AstMatchTree {
    name: String,
    sourcepos: Sourcepos,
    content: AstMatchContent,
}

enum AstMatchContent {
    Text(String),
    Children(Vec<AstMatchTree>),
}

impl AstMatchTree {
    #[track_caller]
    fn assert_match<'a>(&self, node: &'a AstNode<'a>) {
        let ast = node.data.borrow();
        assert_eq!(self.name, ast.value.xml_node_name(), "node type matches");
        assert_eq!(self.sourcepos, ast.sourcepos, "sourcepos are equal");

        match &self.content {
            AstMatchContent::Text(text) => {
                assert_eq!(
                    0,
                    node.children().count(),
                    "text node should have no children"
                );
                assert_eq!(
                    text,
                    ast.value.text().unwrap(),
                    "text node content should match"
                );
            }
            AstMatchContent::Children(children) => {
                assert_eq!(
                    children.len(),
                    node.children().count(),
                    "children count should match"
                );
                for (e, a) in children.iter().zip(node.children()) {
                    e.assert_match(a);
                }
            }
        }
    }
}