use std::borrow::Cow;
use memchr::memchr2;
fn resolve_entity(text: &str) -> Option<(char, &str)> {
let mut peek = text.chars();
let result = match peek.next()? {
'l' if peek.next()? == 't' => '<',
'g' if peek.next()? == 't' => '>',
'a' => match peek.next()? {
'p' if peek.next()? == 'o' && peek.next()? == 's' => '\'',
'm' if peek.next()? == 'p' => '&',
_ => return None,
},
'q' if peek.next()? == 'u' && peek.next()? == 'o' && peek.next()? == 't' => '"',
'#' => {
let mut code = 0;
let mut next = peek.next()?;
let radix = if next == 'x' {
next = peek.next()?;
16
} else {
10
};
while next != ';' {
code *= radix;
code += next.to_digit(radix)?;
next = peek.next()?;
}
let result = char::from_u32(code)?;
return Some((result, peek.as_str()));
}
_ => return None,
};
if peek.next()? != ';' {
None
} else {
Some((result, peek.as_str()))
}
}
pub fn unescape(string: &'_ str) -> Cow<'_, str> {
let mut replaced = String::new();
let mut current = string;
while let Some(next) = memchr2(b'&', b'\0', current.as_bytes()) {
match current.as_bytes()[next] {
b'&' => {
if let Some((chr, rest)) = resolve_entity(¤t[next + 1..]) {
replaced.push_str(¤t[..next]);
if chr == '\0' {
return Cow::Owned(replaced);
}
replaced.push(chr);
current = rest;
} else {
current = ¤t[1..];
}
}
_ => {
return if replaced.is_empty() {
Cow::Borrowed(string)
} else {
replaced.push_str(¤t[..next]);
Cow::Owned(replaced)
};
}
}
}
if replaced.is_empty() {
Cow::Borrowed(string)
} else {
replaced.push_str(current);
Cow::Owned(replaced)
}
}
fn escape(string: &str, next: impl Fn(&str) -> Option<usize>) -> Cow<'_, str> {
let mut replaced = String::new();
let mut current = string;
while let Some(escaped) = next(current) {
replaced.push_str(¤t[..escaped]);
match current.as_bytes()[escaped] {
b'<' => replaced.push_str("<"),
b'>' => replaced.push_str(">"),
b'&' => replaced.push_str("&"),
b'\"' => replaced.push_str("""),
_ => unreachable!(),
};
current = ¤t[escaped + 1..]
}
if replaced.is_empty() {
Cow::Borrowed(string)
} else {
replaced.push_str(current);
Cow::Owned(replaced)
}
}
pub fn attribute_value_escape(string: &'_ str) -> Cow<'_, str> {
escape(string, |text| {
memchr::memchr3(b'<', b'&', b'"', text.as_bytes())
})
}
pub fn content_escape(string: &'_ str) -> Cow<'_, str> {
escape(string, |text| memchr::memchr2(b'<', b'&', text.as_bytes()))
}
pub fn comment_escape(string: &'_ str) -> Cow<'_, str> {
escape(string, |text| memchr::memchr(b'>', text.as_bytes()))
}
#[cfg(test)]
mod test {
use super::{content_escape, unescape};
#[test]
fn simple_unescape_escape() {
const STRINGS: &[(&str, &str, &str)] = &[
(
"" hello & world '",
"\" hello & world '",
"\" hello & world '",
),
(
"⭐ <hello world> ⭐",
"⭐ <hello world> ⭐",
"⭐ <hello world> ⭐",
),
("&haha; &apo", "&haha; &apo", "&haha; &apo"),
];
for (string, expected_unescaped, expected_escaped) in STRINGS {
let unescaped = unescape(string);
assert_eq!(&unescaped, expected_unescaped);
assert_eq!(&content_escape(&unescaped), expected_escaped);
}
}
}