sauron-md 0.1.4

A library for parsing markdown into sauron node
Documentation
use pulldown_cmark::{Alignment, CodeBlockKind, Event, Options, Parser, Tag};
use sauron::{
    html::{
        attributes::{attr, checked, class, href, id, src, title, type_},
        *,
    },
    Node,
};
use std::collections::HashMap;

/// parse markdown and return sauron virtual Node
pub fn markdown<MSG>(src: &str) -> Node<MSG> {
    let mut elems = render_markdown(src);
    if elems.len() == 1 {
        elems.pop().unwrap()
    } else {
        div(vec![], elems)
    }
}

/// Renders a string of Markdown to HTML with the default options (footnotes
/// disabled, tables enabled).
pub fn render_markdown<'a, MSG>(src: &'a str) -> Vec<Node<MSG>> {
    let mut elems = vec![];
    let mut spine = vec![];
    let mut numbers: HashMap<String, usize> = HashMap::new();

    // Add a child to the previous encountered element
    macro_rules! add_child {
        ($child:expr) => {{
            let l = spine.len();
            assert_ne!(l, 0);
            spine[l - 1]
                .as_element_mut()
                .expect("expecting an element")
                .add_children(vec![$child]);
        }};
    }

    for ev in Parser::new_ext(src, Options::all()) {
        match ev {
            Event::Start(tag) => {
                spine.push(make_tag(tag, &mut numbers));
            }
            Event::Text(content) => {
                add_child!(text(content));
            }
            Event::SoftBreak => add_child!(text("\n")),
            Event::HardBreak => add_child!(br(vec![], vec![])),
            Event::Code(code_str) => {
                add_child!(code(vec![], vec![text(code_str)]))
            }
            Event::Html(html) => {
                if let Ok(nodes) = sauron_parse::parse_simple(&html) {
                    for node in nodes {
                        add_child!(node);
                    }
                }
            }
            Event::FootnoteReference(name) => {
                let len = numbers.len() + 1;
                let number = numbers.entry(name.to_string()).or_insert(len);
                add_child!(sup(
                    vec![class("footnote-reference")],
                    vec![a(
                        vec![href(format!("#{}", name))],
                        vec![text(number)]
                    )]
                ));
            }
            Event::Rule => {
                add_child!(hr(vec![], vec![]));
            }
            Event::TaskListMarker(value) => {
                add_child!(input(
                    vec![type_("checkbox"), checked(value)],
                    vec![]
                ));
            }
            Event::End(tag) => {
                let l = spine.len();
                assert!(l >= 1);
                let mut top = spine.pop().unwrap();
                match tag {
                    Tag::CodeBlock(_codeblock) => {
                        top = pre(vec![], vec![top]);
                    }
                    Tag::Table(aligns) => {
                        if let Some(element) = top.as_element_mut() {
                            for r in element.children_mut() {
                                if let Some(r) = r.as_element_mut() {
                                    for (i, c) in
                                        r.children_mut().iter_mut().enumerate()
                                    {
                                        if let Some(tag) = c.as_element_mut() {
                                            match aligns[i] {
                                                Alignment::None => {}
                                                Alignment::Left => tag
                                                    .add_attributes(vec![
                                                        class("text-left"),
                                                    ]),
                                                Alignment::Center => tag
                                                    .add_attributes(vec![
                                                        class("text-center"),
                                                    ]),
                                                Alignment::Right => tag
                                                    .add_attributes(vec![
                                                        class("text-right"),
                                                    ]),
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                    Tag::TableHead => {
                        if let Some(element) = top.as_element_mut() {
                            for c in element.children_mut() {
                                if let Some(tag) = c.as_element_mut() {
                                    tag.set_tag("th");
                                    tag.add_attributes(vec![attr(
                                        "scope", "col",
                                    )]);
                                }
                            }
                        }
                    }
                    _ => (),
                }
                if l == 1 {
                    elems.push(top);
                } else {
                    spine[l - 2]
                        .as_element_mut()
                        .expect("expecting element")
                        .add_children(vec![top]);
                }
            }
        }
    }
    elems
}

fn make_tag<MSG>(t: Tag, numbers: &mut HashMap<String, usize>) -> Node<MSG> {
    match t {
        Tag::Paragraph => p(vec![], vec![]),
        Tag::Heading(n) => {
            assert!(n > 0);
            assert!(n < 7);
            match n {
                1 => h1(vec![], vec![]),
                2 => h2(vec![], vec![]),
                3 => h3(vec![], vec![]),
                4 => h4(vec![], vec![]),
                5 => h5(vec![], vec![]),
                6 => h6(vec![], vec![]),
                _ => unreachable!(),
            }
        }
        Tag::BlockQuote => blockquote(vec![class("blockquote")], vec![]),
        Tag::CodeBlock(codeblock) => match codeblock {
            CodeBlockKind::Indented => code(vec![], vec![]),
            CodeBlockKind::Fenced(fence) => {
                code(vec![class(fence.to_string())], vec![])
            }
        },
        Tag::List(None) => ul(vec![], vec![]),
        Tag::List(Some(1)) => ol(vec![], vec![]),
        Tag::List(Some(ref start)) => ol(vec![attr("start", *start)], vec![]),
        Tag::Item => li(vec![], vec![]),
        Tag::Table(_alignment) => table(vec![class("table")], vec![]),
        Tag::TableHead => th(vec![], vec![]),
        Tag::TableRow => tr(vec![], vec![]),
        Tag::TableCell => td(vec![], vec![]),
        Tag::Emphasis => span(vec![class("font-italic")], vec![]),
        Tag::Strong => span(vec![class("font-weight-bold")], vec![]),
        Tag::Strikethrough => s(vec![], vec![]),
        Tag::Link(_, ref _href, ref _title) => a(
            vec![href(_href.to_string()), title(_title.to_string())],
            vec![],
        ),
        Tag::Image(_, ref _src, ref _title) => img(
            vec![src(_src.to_string()), title(_title.to_string())],
            vec![],
        ),
        Tag::FootnoteDefinition(name) => {
            let len = numbers.len() + 1;
            let number = *numbers.entry(name.to_string()).or_insert(len);
            div(
                vec![class("footnote-definition"), id(name.to_string())],
                vec![sup(vec![class("footnote-label")], vec![text(number)])],
            )
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use sauron::Render;

    #[test]
    fn source_code() {
        let md = r#"
```rust
    fn main(){
        println!("Hello world!");
    }
```
        "#;
        let expected = "<pre>\n    <code class=\"rust\">    fn main(){\n        println!(\"Hello world!\");\n    }\n</code>\n</pre>";
        let view: Node<()> = markdown(md);

        let mut buffer = String::new();
        view.render(&mut buffer).unwrap();
        println!("view: {}", buffer);
        assert_eq!(expected, buffer);
    }

    #[test]
    fn code() {
        let md = r#"
This is has some `code` and other..
        "#;
        let expected = "<p>\n    This is has some \n    <code>code</code>\n     and other..\n</p>";
        let view: Node<()> = markdown(md);

        let mut buffer = String::new();
        view.render(&mut buffer).unwrap();
        println!("view: {}", buffer);
        assert_eq!(expected, buffer);
    }

    #[test]
    fn footnotes() {
        let md = r#"
### [Footnotes](https://github.com/markdown-it/markdown-it-footnote)

Footnote 1 link[^first].

Footnote 2 link[^second].

Inline footnote^[Text of inline footnote] definition.

Duplicated footnote reference[^second].

[^first]: Footnote **can have markup**

    and multiple paragraphs.

[^second]: Footnote text.
        "#;

        let expected = "<div>\n    <h3>\n        <a href=\"https://github.com/markdown-it/markdown-it-footnote\" title=\"\">Footnotes</a>\n    </h3>\n    <p>\n        Footnote 1 link\n        <sup class=\"footnote-reference\">\n            <a href=\"#first\">1</a>\n        </sup>\n        .\n    </p>\n    <p>\n        Footnote 2 link\n        <sup class=\"footnote-reference\">\n            <a href=\"#second\">2</a>\n        </sup>\n        .\n    </p>\n    <p>\n        Inline footnote^\n        [\n        Text of inline footnote\n        ]\n         definition.\n    </p>\n    <p>\n        Duplicated footnote reference\n        <sup class=\"footnote-reference\">\n            <a href=\"#second\">2</a>\n        </sup>\n        .\n    </p>\n    <div class=\"footnote-definition\" id=\"first\">\n        <sup class=\"footnote-label\">1</sup>\n        <p>\n            Footnote \n            <span class=\"font-weight-bold\">can have markup</span>\n        </p>\n    </div>\n    <pre>\n        <code>and multiple paragraphs.\n</code>\n    </pre>\n    <div class=\"footnote-definition\" id=\"second\">\n        <sup class=\"footnote-label\">2</sup>\n        <p>Footnote text.</p>\n    </div>\n</div>";
        let view: Node<()> = markdown(md);

        let mut buffer = String::new();
        view.render(&mut buffer).unwrap();
        println!("view: {}", buffer);
        assert_eq!(expected, buffer);
    }

    #[test]
    fn test_md_with_html() {
        let md = r#"
[Hello](link.html)
<img src="img.jpeg"/>"#;

        let expected =
            "<p>\n    <a href=\"link.html\" title=\"\">Hello</a>\n    \n\n    <img src=\"img.jpeg\"></img>\n</p>";
        let view: Node<()> = markdown(md);

        let mut buffer = String::new();
        view.render(&mut buffer).unwrap();
        println!("view: {}", buffer);
        assert_eq!(expected, buffer);
    }

    #[test]
    fn test_md() {
        let md = r#"
An h1 header
============
look like:
  * this one
  * that one
  * the other one"#;
        let view: Node<()> = markdown(md);

        let expected = r#"<div>
    <h1>An h1 header</h1>
    <p>look like:</p>
    <ul>
        <li>this one</li>
        <li>that one</li>
        <li>the other one</li>
    </ul>
</div>"#;

        let mut buffer = String::new();
        view.render(&mut buffer).unwrap();
        println!("view: {}", buffer);
        assert_eq!(expected, buffer);
    }

    #[test]
    fn test_md_links() {
        let md = r#"
[link text](http://dev.nodeca.com)

[link with title](http://nodeca.github.io/pica/demo/ "title text!")"#;
        let view: Node<()> = markdown(md);
        let expected = r#"<div>
    <p>
        <a href="http://dev.nodeca.com" title="">link text</a>
    </p>
    <p>
        <a href="http://nodeca.github.io/pica/demo/" title="title text!">link with title</a>
    </p>
</div>"#;

        let mut buffer = String::new();
        view.render(&mut buffer).unwrap();
        println!("view: {}", buffer);
        assert_eq!(expected, buffer);
    }

    #[test]
    fn test_md_tables() {
        let md = r#"
## Tables

| Option | Description |
|:------ | -----------:|
| data   | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext    | extension to be used for dest files. |
}
"#;
        let view: Node<()> = markdown(md);
        let expected = r#"<div>
    <h2>Tables</h2>
    <table class="table">
        <th>
            <th scope="col" class="text-left">Option</th>
            <th scope="col" class="text-right">Description</th>
        </th>
        <tr>
            <td class="text-left">data</td>
            <td class="text-right">path to data files to supply the data that will be passed into templates.</td>
        </tr>
        <tr>
            <td class="text-left">engine</td>
            <td class="text-right">engine to be used for processing templates. Handlebars is the default.</td>
        </tr>
        <tr>
            <td class="text-left">ext</td>
            <td class="text-right">extension to be used for dest files.</td>
        </tr>
        <tr>
            <td class="text-left">}</td>
            <td class="text-right"></td>
        </tr>
    </table>
</div>"#;

        let mut buffer = String::new();
        view.render(&mut buffer).unwrap();
        println!("view: {}", buffer);
        assert_eq!(expected, buffer);
    }
}