const HTML_ENTITIES: &[(char, &str)] = &[
('&', "&"),
('\'', "'"),
('<', "<"),
('>', ">"),
('"', """),
];
#[must_use]
pub fn escape_html(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
push_escaped(ch, &mut out);
}
out
}
#[inline]
pub(crate) fn push_escaped(ch: char, out: &mut String) {
for &(c, entity) in HTML_ENTITIES {
if ch == c {
out.push_str(entity);
return;
}
}
out.push(ch);
}
#[must_use]
pub(crate) fn decode_html_entities(s: &str) -> String {
if !s.contains('&') {
return s.to_string();
}
let mut result = s.to_string();
for &(ch, entity) in HTML_ENTITIES {
let mut buf = [0u8; 4];
let replacement = ch.encode_utf8(&mut buf);
result = result.replace(entity, replacement);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escape_html_all_special_chars() {
let r = escape_html("<div class=\"test\">AT&T 'hello'</div>");
assert_eq!(
r,
"<div class="test">AT&T 'hello'</div>"
);
}
#[test]
fn escape_html_no_special_chars() {
let r = escape_html("hello world 123");
assert_eq!(r, "hello world 123");
}
#[test]
fn escape_html_empty_string() {
let r = escape_html("");
assert_eq!(r, "");
}
#[test]
fn escape_html_only_ampersand() {
let r = escape_html("& < > " '");
assert_eq!(r, "&amp; &lt; &gt; &quot; &#39;");
}
#[test]
fn decode_html_entities_no_change() {
let r = decode_html_entities("hello world 123");
assert_eq!(r, "hello world 123");
assert_eq!(decode_html_entities(""), "");
}
#[test]
fn decode_html_entities_lone_ampersand() {
let r = decode_html_entities("a & b");
assert_eq!(r, "a & b");
}
#[test]
fn decode_html_entities_all() {
let r = decode_html_entities("say "hi" & <tag> 'ok'");
assert_eq!(r, "say \"hi\" & <tag> 'ok'");
}
#[test]
fn decode_html_entities_double_encoded() {
let r = decode_html_entities("&#39;");
assert_eq!(r, "'");
}
#[test]
fn round_trip_all_entities() {
for &(ch, _entity) in HTML_ENTITIES {
let encoded = {
let mut s = String::new();
push_escaped(ch, &mut s);
s
};
let decoded = decode_html_entities(&encoded);
assert_eq!(decoded, ch.to_string(), "round-trip failed for {ch:?}");
}
}
#[test]
fn round_trip_all_entities_in_context() {
let original = "<div class=\"test\">AT&T 'hello'</div>";
let encoded = escape_html(original);
let decoded = decode_html_entities(&encoded);
assert_eq!(decoded, original);
}
}