lualexer 0.1.2

Read Lua code and produce tokens
Documentation
use crate::token::Token;
use crate::lexer::LexerErrorType;
use crate::utils::ParseResult;

// This function assumes that the next character is either a single quote (`'`) or
// a double quote (`"`)
pub fn parse_quoted_string<'a>(input: &'a str) -> ParseResult<'a> {
    let quote = input.chars().next()
        .unwrap_or_else(|| unreachable!("must be called when the next character is a `'` or `\"`"));
    let mut escaped = false;

    input.char_indices()
        .find_map(|(i, character)| {
            if i == 0 {
                None
            } else {
                match character {
                    '\\' => {
                        escaped = !escaped;
                        None
                    },
                    '\n' => {
                        if escaped {
                            escaped = false;
                            None
                        } else {
                            Some(Err(LexerErrorType::UnfinishedString))
                        }
                    },
                    '\r' => None,
                    _ => {
                        if !escaped && character == quote {
                            Some(Ok(i))
                        } else {
                            escaped = false;
                            None
                        }
                    }
                }
            }
        })
        .unwrap_or(Err(LexerErrorType::UnfinishedString))
        .and_then(|i| {
            let (result, rest) = input.split_at(i + 1);

            Ok((Token::new_string(result), rest))
        })
}

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

    mod ok {
        use super::*;

        macro_rules! test_string {
            ($($name:ident : $input:expr),+) => {
                $(
                    #[test]
                    fn $name() {
                        let (token, rest) = parse_quoted_string($input).unwrap();
                        assert_eq!(token, Token::new_string($input));
                        assert_eq!(rest, "");
                    }
                )+
            };
        }

        test_string!(
            double_quoted_word: r#""hello""#,
            single_quote_word: "'hello'",
            double_quoted_with_single_quote: r#""hel'lo""#,
            single_quoted_with_double_quote: r#"'hel"lo'"#,
            escaped_double_quotes: r#""hello \"you\"""#,
            escaped_single_quotes: r#"'hello \'you\''"#,
            escaped_slash_before_double_quote: r#""hello !\\""#,
            escaped_slash_before_single_quote: r#"'hello !\\'"#,
            double_quoted_more_escaped_characters: r#""hello\t !\"\"""#,
            single_quoted_more_escaped_characters: r#"'hello\t !\'\''"#,
            multiline_double_quoted: r#""hello \
multilined""#,
            multiline_single_quoted: r#"'hello \
multilined'"#,
            double_quoted_with_backslash: r#""hey \ you \ ""#,
            single_quoted_with_backslash: r#"'hey \ you \ '"#
        );
    }

    mod err {
        use super::*;

        macro_rules! test_unfinished_string {
            ($($name:ident : $input:expr),+) => {
                $(
                    #[test]
                    fn $name() {
                        let lexer_error = parse_quoted_string($input).unwrap_err();
                        assert_eq!(lexer_error, LexerErrorType::UnfinishedString);
                    }
                )+
            };
        }

        test_unfinished_string!(
            double_quoted_word: r#""hello"#,
            single_quote_word: "'hello",
            double_quoted_with_single_quote: r#""hel'lo"#,
            single_quoted_with_double_quote: r#"'hel"lo"#,
            escaped_double_quotes: r#""hello \"you\""#,
            escaped_slash_before_double_quotes: r#""hello !\\"#,
            double_quoted_more_escaped_characters: r#""hello\t !\"\""#,
            multiline_double_quoted: r#""hello \
multilined"#,
            double_quoted_with_backslash: r#""hey \ you \ "#
        );
    }
}