use alloc::borrow::Cow;
use alloc::string::String;
use core::str;
pub fn decode(input: &str) -> String {
let mut output = String::with_capacity(input.len());
let chars = input.as_bytes();
let mut i = 0;
while i < chars.len() {
if chars[i] == b'&' {
if let Some((decoded_char, consumed)) = try_decode_entity(&chars[i..]) {
output.push(decoded_char);
i += consumed;
continue;
}
}
let remaining = &input[i..];
let ch = remaining.chars().next().unwrap();
output.push(ch);
i += ch.len_utf8();
}
output
}
pub fn decode_cow(input: &str) -> Cow<'_, str> {
if !input.contains('&') {
return Cow::Borrowed(input);
}
Cow::Owned(decode(input))
}
#[must_use]
pub fn decode_numeric_only(input: &str) -> String {
let mut output = String::with_capacity(input.len());
let chars = input.as_bytes();
let mut i = 0;
while i < chars.len() {
if chars[i] == b'&' {
if let Some((decoded_char, consumed)) = try_decode_numeric_entity(&chars[i..]) {
output.push(decoded_char);
i += consumed;
continue;
}
}
let remaining = &input[i..];
let ch = remaining.chars().next().unwrap();
output.push(ch);
i += ch.len_utf8();
}
output
}
fn try_decode_numeric_entity(bytes: &[u8]) -> Option<(char, usize)> {
debug_assert!(bytes[0] == b'&');
let semi_pos = bytes.iter().position(|&b| b == b';')?;
if semi_pos < 3 {
return None;
}
let entity_body = &bytes[1..semi_pos];
let consumed = semi_pos + 1;
if entity_body.first() == Some(&b'#') {
let num_body = &entity_body[1..];
let code_point = if num_body.first() == Some(&b'x') || num_body.first() == Some(&b'X') {
let hex_str = str::from_utf8(&num_body[1..]).ok()?;
u32::from_str_radix(hex_str, 16).ok()?
} else {
let dec_str = str::from_utf8(num_body).ok()?;
dec_str.parse::<u32>().ok()?
};
if code_point == 0 {
return None;
}
let c = char::from_u32(code_point)?;
return Some((c, consumed));
}
None
}
fn try_decode_entity(bytes: &[u8]) -> Option<(char, usize)> {
debug_assert!(bytes[0] == b'&');
let semi_pos = bytes.iter().position(|&b| b == b';')?;
if semi_pos < 2 {
return None;
}
let entity_body = &bytes[1..semi_pos];
let consumed = semi_pos + 1;
match entity_body {
b"amp" => return Some(('&', consumed)),
b"lt" => return Some(('<', consumed)),
b"gt" => return Some(('>', consumed)),
b"quot" => return Some(('"', consumed)),
b"apos" => return Some(('\'', consumed)),
_ => {}
}
try_decode_numeric_entity(bytes)
}
pub fn encode_text(input: &str) -> Cow<'_, str> {
if !input.contains(['&', '<', '>']) {
return Cow::Borrowed(input);
}
let mut output = String::with_capacity(input.len() + input.len() / 8);
for ch in input.chars() {
match ch {
'&' => output.push_str("&"),
'<' => output.push_str("<"),
'>' => output.push_str(">"),
other => output.push(other),
}
}
Cow::Owned(output)
}
pub fn encode_attribute(input: &str) -> Cow<'_, str> {
if !input.contains(['&', '<', '>', '"', '\'']) {
return Cow::Borrowed(input);
}
let mut output = String::with_capacity(input.len() + input.len() / 4);
for ch in input.chars() {
match ch {
'&' => output.push_str("&"),
'<' => output.push_str("<"),
'>' => output.push_str(">"),
'"' => output.push_str("""),
'\'' => output.push_str("'"),
other => output.push(other),
}
}
Cow::Owned(output)
}
#[cfg(test)]
mod tests {
use alloc::borrow::Cow;
use super::*;
#[test]
fn decode_named_entities() {
assert_eq!(decode("&"), "&");
assert_eq!(decode("<"), "<");
assert_eq!(decode(">"), ">");
assert_eq!(decode("""), "\"");
assert_eq!(decode("'"), "'");
}
#[test]
fn decode_multiple_entities() {
assert_eq!(decode("a & b < c"), "a & b < c");
assert_eq!(decode("<>&"), "<>&");
}
#[test]
fn decode_no_entities() {
assert_eq!(decode("hello world"), "hello world");
assert_eq!(decode(""), "");
}
#[test]
fn decode_decimal_numeric() {
assert_eq!(decode("A"), "A");
assert_eq!(decode("a"), "a");
assert_eq!(decode("€"), "€");
assert_eq!(decode("😀"), "😀");
}
#[test]
fn decode_hex_numeric() {
assert_eq!(decode("A"), "A");
assert_eq!(decode("a"), "a");
assert_eq!(decode("€"), "€");
assert_eq!(decode("😀"), "😀");
assert_eq!(decode("A"), "A");
}
#[test]
fn decode_invalid_numeric_left_unchanged() {
assert_eq!(decode("�"), "�");
assert_eq!(decode("�"), "�");
assert_eq!(decode("&#;"), "&#;");
}
#[test]
fn decode_unknown_entity_left_unchanged() {
assert_eq!(decode("&unknown;"), "&unknown;");
assert_eq!(decode(" "), " ");
}
#[test]
fn decode_ampersand_without_semicolon() {
assert_eq!(decode("a & b"), "a & b");
assert_eq!(decode("&"), "&");
assert_eq!(decode("&&"), "&&");
}
#[test]
fn decode_nested_entity_reference() {
assert_eq!(decode("&amp;"), "&");
}
#[test]
fn decode_entity_at_end() {
assert_eq!(decode("text&"), "text&");
}
#[test]
fn decode_cow_borrows_when_no_entities() {
let result = decode_cow("no entities here");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn decode_cow_owns_when_entities_present() {
let result = decode_cow("has & entity");
assert!(matches!(result, Cow::Owned(_)));
assert_eq!(result, "has & entity");
}
#[test]
fn encode_text_special_chars() {
assert_eq!(encode_text("a & b"), "a & b");
assert_eq!(encode_text("a < b"), "a < b");
assert_eq!(encode_text("a > b"), "a > b");
assert_eq!(encode_text("a < b & c > d"), "a < b & c > d");
}
#[test]
fn encode_text_no_special_chars() {
let result = encode_text("hello world");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn encode_text_preserves_quotes() {
assert_eq!(encode_text("he said \"hello\""), "he said \"hello\"");
}
#[test]
fn encode_attribute_all_special_chars() {
assert_eq!(encode_attribute("&"), "&");
assert_eq!(encode_attribute("<"), "<");
assert_eq!(encode_attribute(">"), ">");
assert_eq!(encode_attribute("\""), """);
assert_eq!(encode_attribute("'"), "'");
}
#[test]
fn encode_attribute_no_special_chars() {
let result = encode_attribute("hello world");
assert!(matches!(result, Cow::Borrowed(_)));
}
#[test]
fn encode_attribute_mixed() {
assert_eq!(
encode_attribute("val=\"a & b\""),
"val="a & b""
);
}
#[test]
fn roundtrip_text() {
let original = "a < b & c > d";
let encoded = encode_text(original);
let decoded = decode(&encoded);
assert_eq!(decoded, original);
}
#[test]
fn roundtrip_attribute() {
let original = "he said \"it's < 5 & > 3\"";
let encoded = encode_attribute(original);
let decoded = decode(&encoded);
assert_eq!(decoded, original);
}
#[test]
fn roundtrip_empty() {
assert_eq!(decode(&encode_text("")), "");
assert_eq!(decode(&encode_attribute("")), "");
}
#[test]
fn roundtrip_no_special_chars() {
let s = "hello world 12345";
assert_eq!(decode(&encode_text(s)), s);
assert_eq!(decode(&encode_attribute(s)), s);
}
#[test]
fn roundtrip_all_entities() {
let original = "&<>\"'";
let encoded = encode_attribute(original);
assert_eq!(encoded, "&<>"'");
let decoded = decode(&encoded);
assert_eq!(decoded, original);
}
#[test]
fn decode_numeric_only_behavior() {
assert_eq!(decode_numeric_only("a & b"), "a & b");
assert_eq!(decode_numeric_only("a A b"), "a A b");
assert_eq!(decode_numeric_only("a A b"), "a A b");
assert_eq!(decode_numeric_only("a 😀 &"), "a 😀 &");
}
}