use alloc::string::String;
use crate::entities::named_character_reference;
pub(crate) fn decode_escapes_and_references(input: &str) -> String {
decode_selected_escapes_and_references(input, |char| char.is_ascii_punctuation())
}
pub(crate) fn decode_selected_escapes_and_references(
input: &str,
escapable: impl Fn(char) -> bool,
) -> String {
let mut output = String::with_capacity(input.len());
let mut cursor = 0;
while let Some(char) = input[cursor..].chars().next() {
if char == '&' {
if let Some((end, value)) = parse_character_reference(input, cursor) {
output.push_str(&value);
cursor = end;
continue;
}
}
cursor += char.len_utf8();
if char == '\\' {
if let Some(escaped) = input[cursor..].chars().next() {
if !escapable(escaped) {
output.push(char);
}
output.push(escaped);
cursor += escaped.len_utf8();
continue;
}
}
output.push(char);
}
output
}
pub(crate) fn decode_character_reference(reference: &str) -> Option<String> {
parse_character_reference(reference, 0)
.filter(|(end, _)| *end == reference.len())
.map(|(_, value)| value)
}
pub(crate) fn parse_character_reference(input: &str, index: usize) -> Option<(usize, String)> {
let rest = input.get(index..)?;
if let Some(rest) = rest
.strip_prefix("&#x")
.or_else(|| rest.strip_prefix("&#X"))
{
let digits = find_reference_terminator(rest, 6)?;
if digits == 0 || !rest[..digits].bytes().all(|byte| byte.is_ascii_hexdigit()) {
return None;
}
let value = u32::from_str_radix(&rest[..digits], 16).ok()?;
return Some((
index + 3 + digits + 1,
character_reference_value(value).into(),
));
}
if let Some(rest) = rest.strip_prefix("&#") {
let digits = find_reference_terminator(rest, 7)?;
if digits == 0 || !rest[..digits].bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let value = rest[..digits].parse::<u32>().ok()?;
return Some((
index + 2 + digits + 1,
character_reference_value(value).into(),
));
}
let name_end = find_reference_terminator(rest, 32)?;
if name_end == 0 {
return None;
}
let name = &rest[1..name_end];
named_character_reference(name).map(|value| (index + name_end + 1, value.into()))
}
fn find_reference_terminator(rest: &str, max_len: usize) -> Option<usize> {
rest.as_bytes()
.iter()
.take(max_len + 1)
.position(|byte| *byte == b';')
}
pub(crate) fn character_reference_value(value: u32) -> char {
if value == 0 {
'\u{FFFD}'
} else {
char::from_u32(value).unwrap_or('\u{FFFD}')
}
}