#[derive(Debug, Clone)]
pub enum UnquoteError {
UnterminatedSingleQuote {
char_cursor: usize,
byte_cursor: usize,
},
UnterminatedDoubleQuote {
char_cursor: usize,
byte_cursor: usize,
},
}
impl std::fmt::Display for UnquoteError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for UnquoteError { }
pub fn quote(source: &str) -> String {
let mut acc = String::with_capacity(source.len() + 2);
let mut parts = source.split('\'');
acc.push('\'');
if let Some(part) = parts.next() {
acc.push_str(part);
}
parts.fold(&mut acc, |acc, part| {
acc.push_str("\'\\\'\'");
acc.push_str(part);
acc
});
acc.push('\'');
acc
}
fn unquote_open_single(acc: &mut String, cursor: &mut std::iter::Enumerate<std::str::CharIndices>) -> bool {
for i in cursor {
match i {
(_, (_, c)) if c == '\'' => return true,
(_, (_, c)) => acc.push(c),
}
}
false
}
fn unquote_open_double(acc: &mut String, cursor: &mut std::iter::Enumerate<std::str::CharIndices>) -> bool {
loop {
match cursor.next() {
Some((_, (_, inner_ch))) if inner_ch == '"' => {
return true;
},
Some((_, (_, inner_ch))) if inner_ch == '\\' => {
match cursor.next() {
Some((_, (_, esc_ch))) if esc_ch == '"' ||
esc_ch == '\\' ||
esc_ch == '`' ||
esc_ch == '$' ||
esc_ch == '\n' => {
acc.push(esc_ch);
},
Some((_, (_, esc_ch))) => {
acc.push('\\');
acc.push(esc_ch);
},
None => {
return false;
},
}
},
Some ((_, (_, inner_ch))) => {
acc.push(inner_ch);
},
None => {
return false;
},
}
}
}
fn unquote_open_escape(acc: &mut String, cursor: &mut std::iter::Enumerate<std::str::CharIndices>) {
if let Some((_, (_, esc_ch))) = cursor.next() {
if esc_ch != '\n' {
acc.push(esc_ch);
}
}
}
pub fn unquote(source: &str) -> Result<String, UnquoteError> {
let mut acc = String::with_capacity(source.len());
let mut cursor = source.char_indices().enumerate();
loop {
match cursor.next() {
Some((next_idx, (next_pos, next_ch))) if next_ch == '\'' => {
if !unquote_open_single(&mut acc, &mut cursor) {
break Err(
UnquoteError::UnterminatedSingleQuote {
char_cursor: next_idx,
byte_cursor: next_pos,
}
);
}
},
Some((next_idx, (next_pos, next_ch))) if next_ch == '"' => {
if !unquote_open_double(&mut acc, &mut cursor) {
break Err(
UnquoteError::UnterminatedDoubleQuote {
char_cursor: next_idx,
byte_cursor: next_pos,
}
);
}
},
Some((_, (_, next_ch))) if next_ch == '\\' => {
unquote_open_escape(&mut acc, &mut cursor);
},
Some((_, (_, next_ch))) => {
acc.push(next_ch);
},
None => {
break Ok(acc);
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic() {
assert_eq!(quote("foobar"), "'foobar'");
assert_eq!(quote(""), "''");
assert_eq!(quote("'"), "''\\'''");
assert_eq!(unquote("foobar").unwrap(), "foobar");
assert_eq!(unquote("foo'bar'").unwrap(), "foobar");
assert_eq!(unquote("foo\"bar\"").unwrap(), "foobar");
assert_eq!(unquote("\\foobar\\").unwrap(), "foobar");
assert_eq!(unquote("\\'foobar\\'").unwrap(), "'foobar'");
}
}