use std::iter;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Context {
Text, Link, }
pub fn escape(plain: &str, context: Context) -> String {
let mut escaped = String::new();
for character in plain.chars() {
if is_escapable(character, context) {
escaped.push('\\');
}
escaped.push(character);
}
escaped
}
pub fn unescape(source: &str, context: Context) -> 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, context))?
} 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, Context::Text);
previous_was_escape = character == '\\' && !is_escaped;
!is_escaped
})
}
pub fn invalid_escapes(source: &str, context: Context) -> 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, context))
.is_none()
{
return Some(index);
}
}
None
})
}
pub fn is_escapable(character: char, context: Context) -> bool {
match context {
Context::Text => matches!(character, '\\' | '[' | ']' | '`'),
Context::Link => matches!(character, '\\' | '[' | ']'),
}
}
pub fn invalid_escape_message(context: Context) -> &'static str {
match context {
Context::Text => {
"A backslash must be followed by `[`, `]`, `` ` ``, or `\\`, so write `\\\\` for a \
backslash itself."
}
Context::Link => {
"In a link, a backslash must be followed by `[`, `]`, or `\\`, so write `\\\\` for a \
backslash itself."
}
}
}
#[cfg(test)]
mod tests {
use super::{Context, escape, invalid_escapes, unescape, unescaped_characters};
#[test]
fn escaping() {
assert_eq!(escape(r"a\[b]\c`", Context::Text), r"a\\\[b\]\\c\`");
assert_eq!(escape(r"a\[b]\c`", Context::Link), r"a\\\[b\]\\c`");
assert_eq!(
unescape(r"a\\\[b\]\\c\\\``", Context::Text),
Some(r"a\[b]\c\``".to_owned()),
);
assert_eq!(
unescape(r"a\\\[b\]\\c\\`", Context::Link),
Some(r"a\[b]\c\`".to_owned()),
);
assert_eq!(unescape(r"\`", Context::Link), None);
assert_eq!(unescape(r"\a", Context::Text), None);
assert_eq!(unescape(r"\#", Context::Text), None);
assert_eq!(unescape(r"a\\\", Context::Text), None);
assert_eq!(escape(r"#\#", Context::Text), 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\\\[\`\", Context::Text).collect::<Vec<_>>(),
vec![0, 8],
);
assert_eq!(
invalid_escapes(r"\a\\\[\`\", Context::Link).collect::<Vec<_>>(),
vec![0, 6, 8],
);
}
}