femstache 0.1.1

Embrace the feminine mustache to unlock your templating dreams! Or something, I don't know. :3
Documentation
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Token<'a> {
    Str(&'a str),
    Stache(&'a str),
}

pub fn parse<'s>(s: &'s str) -> Vec<Token<'s>> {
    let mut tokens = Vec::new();
    parse_into(s, &mut tokens);
    tokens
}

pub fn parse_into<'s>(s: &'s str, tokens: &mut Vec<Token<'s>>) {
    const STACHE_START_LEN: usize = "{".len();
    const STACHE_STOP_LEN: usize = "}".len();

    enum State {
        Init(usize),
        Stache(usize),
    }

    let mut state = State::Init(0);

    for (i, c) in s.char_indices() {
        match state {
            State::Init(len) => {
                let clen = c.len_utf8();
                let sub = &s[i - len..i];
                match c {
                    '{' if sub.ends_with("{") => {
                        if sub.len() > STACHE_START_LEN {
                            tokens.push(Token::Str(&sub[..sub.len() - STACHE_START_LEN]));
                        }
                        state = State::Stache(0);
                    }
                    _ => {
                        state = State::Init(len + clen);
                    }
                }
            }
            State::Stache(len) => {
                let clen = c.len_utf8();
                let sub = &s[i - len..i];
                match c {
                    '}' if sub.ends_with("}") => {
                        if sub.len() > STACHE_STOP_LEN {
                            tokens.push(Token::Stache(&sub[..sub.len() - STACHE_STOP_LEN]));
                        } else if sub.len() == STACHE_STOP_LEN {
                            tokens.push(Token::Str("{{}}"));
                        }
                        state = State::Init(0);
                    }
                    _ => {
                        state = State::Stache(len + clen);
                    }
                }
            }
        }
    }

    match state {
        State::Init(len) => {
            if len > 0 {
                tokens.push(Token::Str(&s[s.len() - len..s.len()]));
            }
        }
        State::Stache(len) => {
            tokens.push(Token::Str(
                &s[s.len() - len - 2 * STACHE_START_LEN..s.len()],
            ));
        }
    }
}

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

    use proptest::prelude::*;

    fn dump<'s>(tokens: &[Token<'s>]) -> String {
        let mut output = String::new();
        for &token in tokens {
            match token {
                Token::Str(s) => output.push_str(s),
                Token::Stache(s) => {
                    output.push_str("{{");
                    output.push_str(s);
                    output.push_str("}}");
                }
            }
        }
        output
    }

    #[test]
    fn parse_empty_works() {
        assert_eq!(parse(""), &[]);
    }

    #[test]
    fn parse_string_works() {
        assert_eq!(parse("a"), &[Token::Str("a")]);
        assert_eq!(parse(":3c"), &[Token::Str(":3c")]);
        assert_eq!(parse("trans rights!"), &[Token::Str("trans rights!")]);
        assert_eq!(
            parse("give girls their estrogen and guys their testosterone"),
            &[Token::Str(
                "give girls their estrogen and guys their testosterone"
            )]
        );
    }

    #[test]
    fn parse_stache_works() {
        assert_eq!(parse("{{meow}}"), &[Token::Stache("meow")]);
        assert_eq!(parse("{{waf waf}}"), &[Token::Stache("waf waf")]);
    }

    #[test]
    fn parse_full_works() {
        assert_eq!(
            parse("beep beep i'm a {{animal}}. {{sound}} {{sound}} i'm a cow"),
            &[
                Token::Str("beep beep i'm a "),
                Token::Stache("animal"),
                Token::Str(". "),
                Token::Stache("sound"),
                Token::Str(" "),
                Token::Stache("sound"),
                Token::Str(" i'm a cow")
            ]
        );
    }

    #[test]
    fn parse_faulty_works() {
        assert_eq!(parse("{{"), &[Token::Str("{{")]);
        assert_eq!(parse("}}"), &[Token::Str("}}")]);
        assert_eq!(parse("{{}}"), &[Token::Str("{{}}")]);
        assert_eq!(
            parse("hi hello {{ greetings"),
            &[Token::Str("hi hello "), Token::Str("{{ greetings")]
        );
        assert_eq!(
            parse("ebil input {{ woa }"),
            &[Token::Str("ebil input "), Token::Str("{{ woa }")]
        );
        assert_eq!(
            parse("owo {{{ uwu }"),
            &[Token::Str("owo "), Token::Str("{{{ uwu }")]
        );
    }

    proptest! {
        #[test]
        fn parse_dump_rt(s in any::<String>()) {
            let parsed = parse(&s);
            let dumped = dump(&parsed);
            assert_eq!(s, dumped);
        }

        #[test]
        fn parse_dump_rt_specific(s in r#"[{}a]+"#) {
            let parsed = parse(&s);
            let dumped = dump(&parsed);
            assert_eq!(s, dumped);
        }
    }
}