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<'source, 'buf>(
buffer: &'buf mut String,
input: &'source str,
) -> &'buf str
where
'source: 'buf,
{
let bytes = input.as_bytes();
let Some(first_ampersand) = memchr(b'&', bytes) else {
return input;
};
buffer.clear();
if buffer.capacity() < input.len() {
buffer.reserve(input.len() - buffer.capacity());
}
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..]);
&buffer[..]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_replace_html_entities() {
let b = &mut String::new();
assert_eq!(replace_html_entities(b, "you & I"), "you & I");
assert_eq!(replace_html_entities(b, "<hello>"), "<hello>");
assert_eq!(replace_html_entities(b, "no entities"), "no entities");
assert_eq!(replace_html_entities(b, ""quoted""), "\"quoted\"");
assert_eq!(replace_html_entities(b, "'single'"), "'single'");
assert_eq!(
replace_html_entities(b, "mix & <match>"),
"mix & <match>"
);
assert_eq!(
replace_html_entities(b, "incomplete &"),
"incomplete &"
);
assert_eq!(
replace_html_entities(b, "unknown entity"),
"unknown entity"
);
assert_eq!(replace_html_entities(b, "at end &"), "at end &");
assert_eq!(replace_html_entities(b, "you && I"), "you && I");
}
}