flatiron 1.0.5

A parser and HTML renderer for the Textile markup language
Documentation
use crate::structs::InlineTag;
use nom::{
    branch::alt,
    bytes::complete::{escaped_transform, tag, take_while_m_n},
    character::complete::{char, none_of, satisfy},
    combinator::{eof, opt, value},
    multi::fold_many_m_n,
    sequence::{delimited, tuple},
    IResult,
};

pub fn acronym(input: &str) -> IResult<&str, InlineTag> {
    let (rest, (caps, opt_title)) = tuple((
        capitals,
        opt(delimited(char('('), acronym_title, char(')'))),
    ))(input)?;
    alt((eof, take_while_m_n(1, 1, |c: char| !c.is_alphanumeric())))(rest)?;
    Ok((
        rest,
        InlineTag::Acronym {
            title: opt_title,
            content: caps,
        },
    ))
}

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

fn capitals(input: &str) -> IResult<&str, String> {
    let (rest, caps) = alt((
        // acronyms of the form ABC
        fold_many_m_n(
            2,
            usize::MAX,
            single_capital,
            String::new,
            |mut acc, c| {
                acc.push(c);
                acc
            },
        ),
        // acronyms of the form A.B.C.
        fold_many_m_n(
            2,
            usize::MAX,
            capital_with_dot,
            String::new,
            |mut acc, (c, dot)| {
                acc.push(c);
                acc.push(dot);
                acc
            },
        ),
    ))(input)?;
    Ok((rest, String::from(caps)))
}

fn capital_with_dot(input: &str) -> IResult<&str, (char, char)> {
    tuple((single_capital, char('.')))(input)
}

fn single_capital(input: &str) -> IResult<&str, char> {
    satisfy(|c| c.is_ascii_uppercase())(input)
}

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

    #[test]
    fn acronym_basic() {
        let input = "ACLU(American Civil Liberties Union)";
        let result = acronym(input);
        assert_eq!(
            result,
            Ok((
                "",
                InlineTag::Acronym {
                    title: Some(String::from("American Civil Liberties Union")),
                    content: String::from("ACLU")
                }
            ))
        );
    }

    #[test]
    fn acronym_without_title() {
        let input = "ACLU";
        let result = acronym(input);
        assert_eq!(
            result,
            Ok((
                "",
                InlineTag::Acronym {
                    title: None,
                    content: String::from("ACLU")
                }
            ))
        );
    }

    #[test]
    fn acronym_with_parentheses() {
        let input = "ACLU(American Civil Liberties Union \\(a union\\))";
        let result = acronym(input);
        assert_eq!(
            result,
            Ok((
                "",
                InlineTag::Acronym {
                    title: Some(String::from(
                        "American Civil Liberties Union (a union)"
                    )),
                    content: String::from("ACLU")
                }
            ))
        );
    }

    #[test]
    fn acronym_with_dots() {
        let input = "A.C.L.U.(American Civil Liberties Union)";
        let result = acronym(input);
        assert_eq!(
            result,
            Ok((
                "",
                InlineTag::Acronym {
                    title: Some(String::from("American Civil Liberties Union")),
                    content: String::from("A.C.L.U.")
                }
            ))
        );
    }

    #[test]
    #[should_panic]
    fn malformed_acronym_with_dots() {
        let input = "A.CL.U.(American CivilLiberties Union)";
        acronym(input).unwrap();
    }
}