#[must_use]
pub fn escape_xml(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
c if (c as u32) < 0x20 && !matches!(c, '\t' | '\n' | '\r') => {}
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escapes_ampersand() {
assert_eq!(escape_xml("A&B"), "A&B");
}
#[test]
fn escapes_angle_brackets() {
assert_eq!(escape_xml("A<B>C"), "A<B>C");
}
#[test]
fn escapes_double_quote() {
assert_eq!(escape_xml(r#"say "hi""#), "say "hi"");
}
#[test]
fn escapes_single_quote() {
assert_eq!(escape_xml("it's"), "it's");
}
#[test]
fn no_escaping_needed() {
assert_eq!(escape_xml("Am7"), "Am7");
}
#[test]
fn empty_string() {
assert_eq!(escape_xml(""), "");
}
#[test]
fn all_special_chars() {
assert_eq!(escape_xml("&<>\"'"), "&<>"'");
}
#[test]
fn strips_xml_illegal_c0_controls() {
let input = "a\u{0000}b\u{0007}c\u{000B}d\u{000C}e\u{001B}f";
assert_eq!(escape_xml(input), "abcdef");
}
#[test]
fn preserves_xml_legal_whitespace() {
assert_eq!(escape_xml("a\tb\nc\rd"), "a\tb\nc\rd");
}
}