const NAMED: &[(&str, &str)] = &[
("amp", "&"),
("lt", "<"),
("gt", ">"),
("quot", "\""),
("apos", "'"),
("nbsp", "\u{a0}"),
("ensp", "\u{2002}"),
("emsp", "\u{2003}"),
("thinsp", "\u{2009}"),
("shy", "\u{ad}"),
("ndash", "–"),
("mdash", "—"),
("minus", "−"),
("lsquo", "\u{2018}"),
("rsquo", "\u{2019}"),
("sbquo", "\u{201a}"),
("ldquo", "\u{201c}"),
("rdquo", "\u{201d}"),
("bdquo", "\u{201e}"),
("laquo", "«"),
("raquo", "»"),
("lsaquo", "\u{2039}"),
("rsaquo", "\u{203a}"),
("copy", "©"),
("reg", "®"),
("trade", "™"),
("cent", "¢"),
("pound", "£"),
("yen", "¥"),
("euro", "€"),
("curren", "¤"),
("sect", "§"),
("para", "¶"),
("hellip", "…"),
("bull", "•"),
("middot", "·"),
("dagger", "†"),
("Dagger", "‡"),
("prime", "′"),
("Prime", "″"),
("deg", "°"),
("plusmn", "±"),
("times", "×"),
("divide", "÷"),
("frac12", "½"),
("frac14", "¼"),
("frac34", "¾"),
("sup1", "¹"),
("sup2", "²"),
("sup3", "³"),
("micro", "µ"),
("permil", "‰"),
("infin", "∞"),
("ne", "≠"),
("le", "≤"),
("ge", "≥"),
("larr", "←"),
("rarr", "→"),
("harr", "↔"),
("iexcl", "¡"),
("iquest", "¿"),
("brvbar", "¦"),
("uml", "¨"),
("macr", "¯"),
("acute", "´"),
("cedil", "¸"),
("ordf", "ª"),
("ordm", "º"),
("not", "¬"),
];
pub fn resolve_entity(name: &str) -> String {
if let Some(rest) = name.strip_prefix('#') {
let cp = match rest.strip_prefix(['x', 'X']) {
Some(hex) => u32::from_str_radix(hex, 16).ok(),
None => rest.parse::<u32>().ok(),
};
if let Some(c) = cp.and_then(char::from_u32) {
return c.to_string();
}
return format!("&{name};");
}
match NAMED.iter().find(|(n, _)| *n == name) {
Some((_, text)) => (*text).to_string(),
None => format!("&{name};"),
}
}
pub fn resolve_entity_bytes(raw: &[u8]) -> String {
match std::str::from_utf8(raw) {
Ok(name) => resolve_entity(name),
Err(_) => String::new(),
}
}
macro_rules! read_event_folding_entities {
($reader:expr, $buf:expr, $spill:expr, $is_entity:expr) => {
match $reader.read_event_into($buf) {
Ok(quick_xml::events::Event::GeneralRef(r)) => {
*$spill = $crate::entities::resolve_entity_bytes(r.as_ref());
*$is_entity = true;
Ok(quick_xml::events::Event::Text(
quick_xml::events::BytesText::from_escaped($spill.as_str()),
))
}
other => {
*$is_entity = false;
other
}
}
};
($reader:expr, $buf:expr, $spill:expr) => {{
let mut ignored = false;
read_event_folding_entities!($reader, $buf, $spill, &mut ignored)
}};
}
pub(crate) use read_event_folding_entities;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_the_five_xml_predefined_entities() {
assert_eq!(resolve_entity("amp"), "&");
assert_eq!(resolve_entity("lt"), "<");
assert_eq!(resolve_entity("gt"), ">");
assert_eq!(resolve_entity("quot"), "\"");
assert_eq!(resolve_entity("apos"), "'");
}
#[test]
fn resolves_decimal_and_hex_character_references() {
assert_eq!(resolve_entity("#8212"), "—");
assert_eq!(resolve_entity("#x2014"), "—");
assert_eq!(resolve_entity("#X2014"), "—");
assert_eq!(resolve_entity("#169"), "©");
}
#[test]
fn resolves_html_named_entities_that_appear_in_real_documents() {
assert_eq!(resolve_entity("copy"), "©");
assert_eq!(resolve_entity("nbsp"), "\u{a0}");
assert_eq!(resolve_entity("rsquo"), "\u{2019}");
assert_eq!(resolve_entity("hellip"), "…");
}
#[test]
fn unknown_entities_stay_visible_rather_than_vanishing() {
assert_eq!(resolve_entity("notAnEntity"), "¬AnEntity;");
assert_eq!(resolve_entity("#1114112"), "�");
assert_eq!(resolve_entity("#xZZ"), "&#xZZ;");
}
#[test]
fn surrogate_code_points_are_not_forged_into_chars() {
assert_eq!(resolve_entity("#xD800"), "�");
}
}