use std::borrow::Cow;
pub fn decode(s: &str) -> Cow<'_, str> {
if !s.contains('&') {
return Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len());
let mut rest = s;
while let Some(amp) = rest.find('&') {
out.push_str(&rest[..amp]);
let at = &rest[amp..];
match decode_entity(at) {
Some((ch, len)) => {
out.push(ch);
rest = &at[len..];
}
None => {
out.push('&');
rest = &at[1..];
}
}
}
out.push_str(rest);
Cow::Owned(out)
}
fn decode_entity(s: &str) -> Option<(char, usize)> {
let semi = s[1..].find(';')? + 1; let body = &s[1..semi];
if body.is_empty() || body.len() > 10 {
return None;
}
let ch = match body.strip_prefix('#') {
Some(num) => {
let code = match num.strip_prefix(['x', 'X']) {
Some(hex) => u32::from_str_radix(hex, 16).ok()?,
None => num.parse::<u32>().ok()?,
};
char::from_u32(code)?
}
None => named(body)?,
};
let ch = if ch == '\u{00A0}' { ' ' } else { ch };
Some((ch, semi + 1))
}
fn named(name: &str) -> Option<char> {
Some(match name {
"nbsp" => '\u{00A0}', "amp" => '&',
"lt" => '<',
"gt" => '>',
"quot" => '"',
"apos" => '\'',
"ndash" => '\u{2013}',
"mdash" => '\u{2014}',
"minus" => '\u{2212}',
"hellip" => '\u{2026}',
"lsquo" => '\u{2018}',
"rsquo" => '\u{2019}',
"ldquo" => '\u{201C}',
"rdquo" => '\u{201D}',
"times" => '\u{00D7}',
"deg" => '\u{00B0}',
"middot" => '\u{00B7}',
"bull" => '\u{2022}',
"prime" => '\u{2032}',
"Prime" => '\u{2033}',
"frac12" => '\u{00BD}',
"frac14" => '\u{00BC}',
"frac34" => '\u{00BE}',
"copy" => '\u{00A9}',
"reg" => '\u{00AE}',
"trade" => '\u{2122}',
"eacute" => '\u{00E9}',
"egrave" => '\u{00E8}',
"agrave" => '\u{00E0}',
"uuml" => '\u{00FC}',
"ouml" => '\u{00F6}',
"auml" => '\u{00E4}',
"szlig" => '\u{00DF}',
"ntilde" => '\u{00F1}',
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_ampersand_is_borrowed_untouched() {
assert!(matches!(decode("plain text"), Cow::Borrowed("plain text")));
}
#[test]
fn decodes_named_entities() {
assert_eq!(decode("9.02 AU"), "9.02 AU"); assert_eq!(decode("AT&T"), "AT&T");
assert_eq!(decode("<div>"), "<div>");
assert_eq!(decode("café"), "café");
assert_eq!(decode("5 – 10"), "5 \u{2013} 10");
}
#[test]
fn decodes_numeric_entities() {
assert_eq!(decode("AB"), "AB");
assert_eq!(decode(" "), " "); }
#[test]
fn leaves_unknown_and_malformed_literal() {
assert_eq!(decode("Tom & Jerry"), "Tom & Jerry"); assert_eq!(decode("a&bogus;b"), "a&bogus;b"); assert_eq!(decode("R&D"), "R&D"); assert_eq!(decode("�"), "�"); }
}