mod lexing;
mod stream;
use lexing::Lexeme;
use std::borrow::Cow;
use std::fmt::{self, Write};
pub use stream::TokenStream;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RawLexeme<'a>(&'a str);
impl<'a> RawLexeme<'a> {
pub fn new(text: &'a str) -> Self {
RawLexeme(text)
}
pub fn parse_string(self) -> Cow<'a, str> {
let text = self.0;
let first_char = text.chars().next();
if let Some(quote @ '\'' | quote @ '"') = first_char {
let mut string = String::new();
let mut escaped = false;
let text = if text.ends_with(quote) {
&text[1..text.len() - 1]
} else {
&text[1..]
};
for ch in text.chars() {
match ch {
ch if escaped => {
string.push(ch);
escaped = false;
}
'\\' => escaped = true,
ch => string.push(ch),
}
}
Cow::Owned(string)
} else {
Cow::Borrowed(text)
}
}
}
impl<'a> fmt::Display for RawLexeme<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let first_char = self.0.chars().next();
if let Some(quote @ '\'' | quote @ '"') = first_char {
f.write_str(self.0)?;
if !self.0.ends_with(quote) {
f.write_char(quote)?;
}
Ok(())
} else {
f.write_fmt(format_args!("\"{}\"", self.0))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Token<'a> {
Text(RawLexeme<'a>),
Attribute(RawLexeme<'a>),
}
impl<'a> Token<'a> {
pub fn into_raw_lexeme(self) -> RawLexeme<'a> {
match self {
Token::Text(inner) => inner,
Token::Attribute(inner) => inner,
}
}
pub fn is_attribute(&self) -> bool {
matches!(self, Token::Attribute(_))
}
pub fn is_text(&self) -> bool {
matches!(self, Token::Text(_))
}
fn from_lexeme(lexeme: Lexeme<'a>) -> Result<Self, UnbalancedParenthesis> {
match lexeme {
Lexeme::OpeningParen | Lexeme::ClosingParen => Err(UnbalancedParenthesis),
Lexeme::Text(text) => Ok(Token::Text(RawLexeme::new(text))),
Lexeme::Attribute(attr) => Ok(Token::Attribute(RawLexeme::new(attr))),
}
}
}
#[derive(Debug)]
pub struct UnbalancedParenthesis;
pub fn complete_variants<'a, 'b>(
token: &'b str,
variants: &'b [&'a str],
) -> impl Iterator<Item = &'a str> + 'b {
let index = variants.binary_search(&token).unwrap_or_else(|idx| idx);
variants[index..]
.iter()
.map(move |variant| variant.strip_prefix(token))
.take_while(Option::is_some)
.map(|suggestion| suggestion.unwrap()) .filter(|suggestion| !suggestion.is_empty())
}
#[cfg(test)]
mod tests {
use super::RawLexeme;
mod format_raw_lexeme {
use super::*;
#[test]
fn format_simple() {
assert_eq!(RawLexeme::new("simple").to_string(), "\"simple\"");
}
#[test]
fn format_quoted() {
assert_eq!(RawLexeme::new("'quoted'").to_string(), "'quoted'");
assert_eq!(RawLexeme::new("\"quoted\"").to_string(), "\"quoted\"");
}
#[test]
fn format_quoted_partial() {
assert_eq!(RawLexeme::new("'quoted").to_string(), "'quoted'");
assert_eq!(RawLexeme::new("\"quoted").to_string(), "\"quoted\"");
}
}
mod parse_raw_lexeme {
use super::*;
use std::borrow::Cow;
macro_rules! test_parse {
($name:ident, $text:literal => $variant:ident($result:literal)) => {
#[test]
fn $name() {
let result = RawLexeme::new($text).parse_string();
assert_eq!(result, Cow::Borrowed($result));
assert!(matches!(result, Cow::$variant(_)));
}
};
}
test_parse!(empty, "" => Borrowed(""));
test_parse!(non_empty, "abc" => Borrowed("abc"));
test_parse!(quoted_empty_single, "''" => Owned(""));
test_parse!(quoted_empty_double, "\"\"" => Owned(""));
test_parse!(quoted_non_empty_single, "'abc \\\' def \\\" fgh'" => Owned("abc \' def \" fgh"));
test_parse!(quoted_non_empty_double, "\"abc \\\' def \\\" fgh\"" => Owned("abc \' def \" fgh"));
test_parse!(quoted_not_terminated_single, "'abc" => Owned("abc"));
test_parse!(quoted_not_terminated_double, "\"abc" => Owned("abc"));
}
}