#[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);
}
}
}