use std::iter;
pub fn escape(plain: &str) -> String {
plain
.replace('\\', "\\\\")
.replace('[', "\\[")
.replace(']', "\\]")
}
pub fn unescape(source: &str) -> Option<String> {
let mut plain = String::new();
let mut characters = source.chars();
while let Some(character) = characters.next() {
plain.push(if character == '\\' {
characters.next().filter(|&next| is_escapable(next))?
} else {
character
});
}
Some(plain)
}
pub fn unescaped_characters(source: &str) -> impl Iterator<Item = (usize, char)> {
let mut previous_was_escape = false;
source.char_indices().filter(move |&(_, character)| {
let is_escaped = previous_was_escape && is_escapable(character);
previous_was_escape = character == '\\' && !is_escaped;
!is_escaped
})
}
pub fn invalid_escapes(source: &str) -> impl Iterator<Item = usize> {
let mut characters = source.char_indices().peekable();
iter::from_fn(move || {
while let Some((index, character)) = characters.next() {
if character == '\\'
&& characters
.next_if(|&(_, next_character)| is_escapable(next_character))
.is_none()
{
return Some(index);
}
}
None
})
}
fn is_escapable(character: char) -> bool {
matches!(character, '\\' | '[' | ']')
}
#[cfg(test)]
mod tests {
use super::{escape, invalid_escapes, unescape, unescaped_characters};
#[test]
fn escaping() {
assert_eq!(escape(r"a\[b]\c\"), r"a\\\[b\]\\c\\");
assert_eq!(unescape(r"a\\\[b\]\\c\\"), Some(r"a\[b]\c\".to_owned()));
assert_eq!(unescape(r"\a"), None);
assert_eq!(unescape(r"\#"), None);
assert_eq!(unescape(r"a\\\"), None);
assert_eq!(escape(r"#\#"), r"#\\#");
}
#[test]
fn unescaped_characters_skip_escapes() {
assert_eq!(
unescaped_characters(r"[a\]\\]")
.map(|(_, character)| character)
.collect::<String>(),
r"[a\\]",
);
}
#[test]
fn invalid_escapes_find_lone_backslashes() {
assert_eq!(invalid_escapes(r"\a\\\[\").collect::<Vec<_>>(), vec![0, 6]);
}
}