yamd 0.19.0

Yet Another Markdown Document (flavour)
Documentation
use proptest::prelude::*;
use yamd::deserialize;
use yamd::nodes::{
    Anchor, Bold, BoldNodes, Code, CodeSpan, Collapsible, Embed, Emphasis, Heading, HeadingNodes,
    Highlight, Image, Images, Italic, List, ListItem, ListTypes, Paragraph, ParagraphNodes,
    Strikethrough, ThematicBreak, Yamd, YamdNodes,
};

fn arb_text() -> impl Strategy<Value = String> {
    prop_oneof![
        "[a-zA-Z0-9 ]{1,20}",
        prop::collection::vec(
            prop_oneof![
                Just("*".to_string()),
                Just("**".to_string()),
                Just("_".to_string()),
                Just("~~".to_string()),
                Just("`".to_string()),
                Just("```".to_string()),
                Just("#".to_string()),
                Just("!!".to_string()),
                Just("[".to_string()),
                Just("]".to_string()),
                Just("(".to_string()),
                Just(")".to_string()),
                Just("\\".to_string()),
                Just("\n".to_string()),
                Just("|".to_string()),
                Just("}".to_string()),
                Just("{".to_string()),
                Just("%}".to_string()),
                Just("{%".to_string()),
                Just("\n- ".to_string()),
                Just("\n+ ".to_string()),
                Just("\n# ".to_string()),
                Just("\n```".to_string()),
                Just("\n!! ".to_string()),
                Just("\n---".to_string()),
                Just("\n{% ".to_string()),
                Just("\n%}".to_string()),
                Just("\n\n".to_string()),
                "[a-zA-Z0-9]{1,5}",
            ],
            1..=5
        )
        .prop_map(|v| v.join("")),
    ]
    .prop_filter("must not be empty", |s| !s.is_empty())
}

fn arb_inline_text() -> impl Strategy<Value = String> {
    prop_oneof![
        "[a-zA-Z0-9]{1,20}",
        prop::collection::vec(
            prop_oneof![
                Just("*".to_string()),
                Just("**".to_string()),
                Just("_".to_string()),
                Just("~~".to_string()),
                Just("`".to_string()),
                Just("```".to_string()),
                Just("#".to_string()),
                Just("!!".to_string()),
                Just("[".to_string()),
                Just("]".to_string()),
                Just("(".to_string()),
                Just(")".to_string()),
                Just("|".to_string()),
                Just("}".to_string()),
                Just("{".to_string()),
                Just("%}".to_string()),
                Just("{%".to_string()),
                Just("\\".to_string()),
                "[a-zA-Z0-9]{1,5}",
            ],
            1..=5
        )
        .prop_map(|v| v.join("")),
    ]
    .prop_filter(
        "must contain non-whitespace and not start with space",
        |s| !s.trim().is_empty() && !s.starts_with(' '),
    )
}

fn arb_block_text() -> impl Strategy<Value = String> {
    arb_text().prop_filter("safe for block content", |s| {
        !s.trim().is_empty() && !s.ends_with('\n') && !s.starts_with('\n')
    })
}

fn arb_list_item_text() -> impl Strategy<Value = String> {
    arb_block_text().prop_filter(
        "list items cannot start with whitespace (grammar requires exactly one space after marker)",
        |s| !s.starts_with(' '),
    )
}

fn arb_url() -> impl Strategy<Value = String> {
    prop_oneof![
        "[a-zA-Z0-9/:._-]{1,30}",
        prop::collection::vec(
            prop_oneof![
                Just("(".to_string()),
                Just(")".to_string()),
                Just("[".to_string()),
                Just("]".to_string()),
                "[a-zA-Z0-9/:._-]{1,10}",
            ],
            1..=5
        )
        .prop_map(|v| v.join("")),
    ]
    .prop_filter("must not be empty", |s| !s.is_empty())
}

proptest! {
    #[test]
    fn italic_round_trip(text in arb_inline_text()) {
        let yamd = Yamd::new(None, vec![
            Paragraph::new(vec![ParagraphNodes::from(Italic::new(&text))]).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }

    #[test]
    fn emphasis_round_trip(text in arb_inline_text()) {
        let yamd = Yamd::new(None, vec![
            Paragraph::new(vec![ParagraphNodes::from(Emphasis::new(&text))]).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }

    #[test]
    fn strikethrough_round_trip(text in arb_inline_text()) {
        let yamd = Yamd::new(None, vec![
            Paragraph::new(vec![ParagraphNodes::from(Strikethrough::new(&text))]).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }

    #[test]
    fn code_span_round_trip(text in arb_inline_text()) {
        let yamd = Yamd::new(None, vec![
            Paragraph::new(vec![ParagraphNodes::from(CodeSpan::new(&text))]).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }
}

proptest! {
    #[test]
    fn anchor_round_trip(
        text in arb_inline_text(),
        url in arb_url()
    ) {
        let yamd = Yamd::new(None, vec![
            Paragraph::new(vec![ParagraphNodes::from(Anchor::new(&text, &url))]).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }

    #[test]
    fn image_round_trip(
        alt in arb_inline_text(),
        src in arb_url()
    ) {
        let yamd = Yamd::new(None, vec![
            Image::new(&alt, &src).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }

    #[test]
    fn bold_round_trip(text in arb_inline_text()) {
        let yamd = Yamd::new(None, vec![
            Paragraph::new(vec![
                ParagraphNodes::from(Bold::new(vec![BoldNodes::from(text)])),
            ]).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }
}

proptest! {
    #[test]
    fn heading_round_trip(
        level in 1u8..=6u8,
        text in arb_inline_text()
    ) {
        let yamd = Yamd::new(None, vec![
            Heading::new(level, vec![HeadingNodes::from(text)]).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }

    #[test]
    fn code_round_trip(
        lang in arb_inline_text(),
        code in arb_block_text()
    ) {
        let yamd = Yamd::new(None, vec![
            Code::new(&lang, &code).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }

    #[test]
    fn embed_round_trip(
        kind in arb_inline_text(),
        args in arb_block_text()
    ) {
        let yamd = Yamd::new(None, vec![
            Embed::new(&kind, &args).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }
}

proptest! {
    #[test]
    fn highlight_round_trip(
        title in proptest::option::of(arb_inline_text()),
        icon in proptest::option::of(arb_inline_text()),
        body_text in arb_block_text()
    ) {
        let yamd = Yamd::new(None, vec![
            Highlight::new(
                title.as_deref(),
                icon.as_deref(),
                vec![Paragraph::new(vec![ParagraphNodes::from(body_text)])],
            ).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }

    #[test]
    fn list_round_trip(
        item1 in arb_list_item_text(),
        item2 in arb_list_item_text(),
        ordered in any::<bool>()
    ) {
        let list_type = if ordered { ListTypes::Ordered } else { ListTypes::Unordered };
        let yamd = Yamd::new(None, vec![
            List::new(
                list_type,
                0,
                vec![
                    ListItem::new(vec![ParagraphNodes::from(item1)], None),
                    ListItem::new(vec![ParagraphNodes::from(item2)], None),
                ],
            ).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }

    #[test]
    fn collapsible_round_trip(
        title in arb_inline_text(),
        body_text in arb_block_text()
    ) {
        let yamd = Yamd::new(None, vec![
            Collapsible::new(
                &title,
                vec![Paragraph::new(vec![ParagraphNodes::from(body_text)]).into()],
            ).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }

    #[test]
    fn collapsible_round_trip_mixed_body(
        title in arb_inline_text(),
        body in prop::collection::vec(arb_yamd_node(), 1..=4)
    ) {
        let yamd = Yamd::new(None, vec![
            Collapsible::new(&title, body).into(),
        ]);
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }
}

fn arb_paragraph_node() -> impl Strategy<Value = ParagraphNodes> {
    prop_oneof![
        arb_inline_text().prop_map(ParagraphNodes::from),
        arb_inline_text().prop_map(|t| ParagraphNodes::from(Italic::new(t))),
        arb_inline_text().prop_map(|t| ParagraphNodes::from(Emphasis::new(t))),
        arb_inline_text().prop_map(|t| ParagraphNodes::from(Strikethrough::new(t))),
        arb_inline_text().prop_map(|t| ParagraphNodes::from(CodeSpan::new(t))),
        (arb_inline_text(), arb_url()).prop_map(|(t, u)| ParagraphNodes::from(Anchor::new(t, u))),
        "[a-zA-Z0-9]{1,20}".prop_map(|t| ParagraphNodes::from(Bold::new(vec![BoldNodes::from(t)]))),
    ]
}

fn arb_paragraph() -> impl Strategy<Value = Paragraph> {
    prop_oneof![
        arb_paragraph_node().prop_map(|n| Paragraph::new(vec![n])),
        arb_inline_text().prop_map(|t| Paragraph::new(vec![ParagraphNodes::from(t)])),
    ]
}

fn arb_yamd_node() -> impl Strategy<Value = YamdNodes> {
    prop_oneof![
        arb_paragraph().prop_map(YamdNodes::from),
        (1u8..=6u8, arb_inline_text()).prop_map(|(l, t)| Heading::new(
            l,
            vec![HeadingNodes::from(t)]
        )
        .into()),
        (arb_inline_text(), arb_url()).prop_map(|(a, s)| Image::new(a, s).into()),
        prop::collection::vec(
            (arb_inline_text(), arb_url()).prop_map(|(a, s)| Image::new(a, s)),
            2..=3
        )
        .prop_map(|v| Images::new(v).into()),
        (arb_inline_text(), arb_block_text()).prop_map(|(l, c)| Code::new(l, c).into()),
        Just(ThematicBreak::new().into()),
        (arb_inline_text(), arb_block_text()).prop_map(|(k, a)| Embed::new(k, a).into()),
    ]
}

fn arb_yamd() -> impl Strategy<Value = Yamd> {
    (
        proptest::option::of(
            "[a-zA-Z0-9: ]{1,30}".prop_filter("metadata must have non-whitespace", |s| {
                !s.trim().is_empty()
            }),
        ),
        prop::collection::vec(arb_yamd_node(), 1..=5),
    )
        .prop_map(|(metadata, body)| Yamd::new(metadata, body))
}

proptest! {
    #[test]
    fn yamd_document_round_trip(yamd in arb_yamd()) {
        let serialized = yamd.to_string();
        let deserialized = deserialize(&serialized);
        prop_assert_eq!(yamd, deserialized, "Serialized: {:?}", serialized);
    }
}