use std::borrow::Cow;
use memchr::memchr;
static HTML_RESERVED_MAP: phf::Map<&'static [u8], u8> = phf::phf_map! {
b"#34" => b'"',
b"quot" => b'"',
b"#38" => b'&',
b"amp" => b'&',
b"#39" => b'\'',
b"apos" => b'\'',
b"#60" => b'<',
b"lt" => b'<',
b"#62" => b'>',
b"gt" => b'>',
};
pub fn replace_html_entities(input: &str) -> Cow<'_, str> {
let bytes = input.as_bytes();
let Some(first_ampersand) = memchr(b'&', bytes) else {
return Cow::Borrowed(input);
};
let mut buffer = String::with_capacity(input.len());
let mut last_end = 0;
let mut next_start = first_ampersand;
loop {
buffer.push_str(&input[last_end..next_start]);
let entity_start = next_start + 1;
let Some(index) = bytes[entity_start..].iter().position(|&b| b == b';') else {
last_end = next_start;
break;
};
let end = entity_start + index;
if let Some(replacement) = HTML_RESERVED_MAP.get(&bytes[entity_start..end]) {
unsafe {
buffer.push_str(std::str::from_utf8_unchecked(&[*replacement]));
}
} else {
buffer.push_str(&input[next_start..=end]);
}
last_end = end + 1;
match memchr(b'&', &bytes[last_end..]) {
Some(idx) => next_start = last_end + idx,
None => break,
}
}
buffer.push_str(&input[last_end..]);
Cow::Owned(buffer)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_replace_html_entities() {
assert_eq!(replace_html_entities("you & I"), "you & I");
assert_eq!(replace_html_entities("<hello>"), "<hello>");
assert_eq!(replace_html_entities("no entities"), "no entities");
assert_eq!(replace_html_entities(""quoted""), "\"quoted\"");
assert_eq!(replace_html_entities("'single'"), "'single'");
assert_eq!(
replace_html_entities("mix & <match>"),
"mix & <match>"
);
assert_eq!(replace_html_entities("incomplete &"), "incomplete &");
assert_eq!(
replace_html_entities("unknown entity"),
"unknown entity"
);
assert_eq!(replace_html_entities("at end &"), "at end &");
assert_eq!(replace_html_entities("you && I"), "you && I");
}
}