use std::borrow::Cow;
#[must_use]
pub fn escape_html(input: &str) -> Cow<'_, str> {
if input.contains(['&', '<', '>', '"']) {
let mut out = String::with_capacity(input.len());
escape_html_into(&mut out, input);
Cow::Owned(out)
} else {
Cow::Borrowed(input)
}
}
pub(crate) fn escape_html_into(out: &mut String, input: &str) {
for ch in input.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
other => out.push(other),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escapes_exactly_the_four_characters_markdown_it_escapes() {
assert_eq!(escape_html("&"), "&");
assert_eq!(escape_html("<"), "<");
assert_eq!(escape_html(">"), ">");
assert_eq!(escape_html("\""), """);
}
#[test]
fn leaves_every_other_ascii_character_alone() {
for byte in 0x20u8..0x7f {
let ch = char::from(byte);
if matches!(ch, '&' | '<' | '>' | '"') {
continue;
}
let input = ch.to_string();
assert_eq!(
escape_html(&input),
input,
"U+{byte:04X} must pass through unchanged"
);
}
}
#[test]
fn does_not_escape_the_single_quote() {
assert_eq!(escape_html("it's an 'x'"), "it's an 'x'");
}
#[test]
fn does_not_escape_the_solidus() {
assert_eq!(escape_html("a/b"), "a/b");
}
#[test]
fn does_not_touch_non_ascii() {
assert_eq!(
escape_html("naïve \u{2014} 日本語 \u{1f600}"),
"naïve — 日本語 😀"
);
}
#[test]
fn does_not_double_escape() {
assert_eq!(escape_html("&"), "&amp;");
assert_eq!(escape_html("<"), "&lt;");
}
#[test]
fn replaces_every_occurrence_not_just_the_first() {
assert_eq!(escape_html("<<>>"), "<<>>");
}
#[test]
fn mixed_input_keeps_its_surroundings() {
assert_eq!(
escape_html(r#"<a href="x">tom & jerry</a>"#),
"<a href="x">tom & jerry</a>"
);
}
#[test]
fn borrows_when_there_is_nothing_to_escape() {
assert!(matches!(escape_html("plain text"), Cow::Borrowed(_)));
assert!(matches!(escape_html("a & b"), Cow::Owned(_)));
}
#[test]
fn the_empty_string_is_unchanged() {
assert_eq!(escape_html(""), "");
}
#[test]
fn control_characters_pass_through() {
assert_eq!(escape_html("a\nb\tc\0d"), "a\nb\tc\0d");
}
}