flatiron 1.0.5

A parser and HTML renderer for the Textile markup language
Documentation
use crate::parse::{attributes::attributes, phrase::phrase};
use crate::structs::InlineTag;
use nom::{
    branch::alt,
    bytes::complete::{escaped_transform, tag},
    character::complete::{char, none_of},
    combinator::{complete, fail, opt, value},
    multi::{fold_many1, many_till},
    sequence::{delimited, preceded, tuple},
    IResult,
};

pub fn link(input: &str) -> IResult<&str, InlineTag> {
    let (rest, (attributes, (content_vec, (title, url)))) = preceded(
        char('"'),
        tuple((
            opt(attributes),
            many_till(
                alt((
                    value('\\', tag("\\\\")),
                    value('"', tag("\\\"")),
                    value('(', tag("\\(")),
                    value(')', tag("\\)")),
                    none_of("\\\"()"),
                )),
                tuple((
                    opt(delimited(
                        tag(" ("),
                        escaped_transform(
                            none_of("\\()"),
                            '\\',
                            alt((
                                value("\\", tag("\\")),
                                value("(", tag("(")),
                                value(")", tag(")")),
                            )),
                        ),
                        char(')'),
                    )),
                    preceded(char('"'), link_suffix),
                )),
            ),
        )),
    )(input)?;
    let content_string = content_vec.into_iter().collect::<String>();
    let content = match complete(phrase)(content_string.as_str()) {
        Ok((_, content)) => content,
        Err(_) => return fail(input),
    };
    Ok((
        rest,
        InlineTag::Link {
            attributes,
            title,
            url,
            content: Box::new(content),
        },
    ))
}

pub fn link_suffix(input: &str) -> IResult<&str, String> {
    preceded(
        char(':'),
        alt((
            delimited(char('('), link_url_parenthesized, char(')')),
            link_url_standard,
        )),
    )(input)
}

fn link_url_parenthesized(input: &str) -> IResult<&str, String> {
    let (rest, url) = escaped_transform(
        none_of("\\() \r\n\t"),
        '\\',
        alt((
            value("\\", tag("\\")),
            value("(", tag("(")),
            value(")", tag(")")),
        )),
    )(input)?;
    Ok((rest, String::from(url)))
}

fn link_url_standard(input: &str) -> IResult<&str, String> {
    fold_many1(none_of(" \r\n\t"), String::new, |mut acc, c| {
        acc.push(c);
        acc
    })(input)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::structs::Attributes;

    #[test]
    fn link_basic() {
        let input = "\"a link\":https://git.sr.ht/~autumnull/flatiron";
        let result = link(input);
        assert_eq!(
            result,
            Ok((
                "",
                InlineTag::Link {
                    attributes: None,
                    title: None,
                    url: String::from("https://git.sr.ht/~autumnull/flatiron"),
                    content: Box::new(InlineTag::Plaintext(String::from(
                        "a link"
                    )))
                }
            ))
        );
    }

    #[test]
    fn link_attributes() {
        let input = "\"(class)a link\":https://git.sr.ht/~autumnull/flatiron";
        let result = link(input);
        assert_eq!(
            result,
            Ok((
                "",
                InlineTag::Link {
                    attributes: Some(Attributes {
                        class: Some(String::from("class")),
                        id: None,
                        style: None,
                        language: None,
                    }),
                    title: None,
                    url: String::from("https://git.sr.ht/~autumnull/flatiron"),
                    content: Box::new(InlineTag::Plaintext(String::from(
                        "a link"
                    )))
                }
            ))
        );
    }

    #[test]
    fn link_attributes_title() {
        let input =
            "\"(class)a link (title)\":https://git.sr.ht/~autumnull/flatiron";
        let result = link(input);
        assert_eq!(
            result,
            Ok((
                "",
                InlineTag::Link {
                    attributes: Some(Attributes {
                        class: Some(String::from("class")),
                        id: None,
                        style: None,
                        language: None,
                    }),
                    title: Some(String::from("title")),
                    url: String::from("https://git.sr.ht/~autumnull/flatiron"),
                    content: Box::new(InlineTag::Plaintext(String::from(
                        "a link"
                    )))
                }
            ))
        );
    }

    #[test]
    fn link_escaped() {
        let input =
            "\"Earvin \\\"Magic\\\" Johnson \\(Basketball Player\\)\":https://en.wikipedia.org/wiki/Magic_Johnson";
        let result = link(input);
        assert_eq!(
            result,
            Ok((
                "",
                InlineTag::Link {
                    attributes: None,
                    title: None,
                    url: String::from(
                        "https://en.wikipedia.org/wiki/Magic_Johnson"
                    ),
                    content: Box::new(InlineTag::Plaintext(String::from(
                        "Earvin “Magic” Johnson (Basketball Player)"
                    )))
                }
            ))
        );
    }

    #[test]
    fn link_parenthesized() {
        let input = "\"this\":(https://git.sr.ht/~autumnull/flatiron)?";
        let result = link(input);
        assert_eq!(
            result,
            Ok((
                "?",
                InlineTag::Link {
                    attributes: None,
                    title: None,
                    url: String::from("https://git.sr.ht/~autumnull/flatiron"),
                    content: Box::new(InlineTag::Plaintext(String::from(
                        "this"
                    )))
                }
            ))
        );
    }
}