#[inline]
pub fn decode_named(name: &str) -> Option<&'static str> {
ENTITY_MAP.get(name).copied()
}
pub fn decode_numeric(digits: &str, is_hex: bool) -> Option<char> {
let codepoint = if is_hex {
u32::from_str_radix(digits, 16).ok()?
} else {
digits.parse::<u32>().ok()?
};
if codepoint == 0 {
return Some('\u{FFFD}');
}
char::from_u32(codepoint)
}
#[inline]
pub fn escape_text(input: &str, out: &mut String) {
escape_impl::<false>(input, out);
}
#[inline]
pub fn escape_attr(input: &str, out: &mut String) {
escape_impl::<true>(input, out);
}
#[inline(always)]
fn escape_impl<const ESCAPE_QUOTES: bool>(input: &str, out: &mut String) {
out.reserve(input.len());
let bytes = input.as_bytes();
let mut last = 0;
for (i, &b) in bytes.iter().enumerate() {
let replacement = match b {
b'&' => "&",
b'<' => "<",
b'>' => ">",
b'"' if ESCAPE_QUOTES => """,
b'\'' if ESCAPE_QUOTES => "'",
_ => continue,
};
out.push_str(&input[last..i]);
out.push_str(replacement);
last = i + 1;
}
out.push_str(&input[last..]);
}
static ENTITY_MAP: phf::Map<&'static str, &'static str> = phf::phf_map! {
"amp" => "&",
"lt" => "<",
"gt" => ">",
"quot" => "\"",
"apos" => "'",
"nbsp" => "\u{00A0}",
"iexcl" => "\u{00A1}",
"cent" => "\u{00A2}",
"pound" => "\u{00A3}",
"curren" => "\u{00A4}",
"yen" => "\u{00A5}",
"brvbar" => "\u{00A6}",
"sect" => "\u{00A7}",
"uml" => "\u{00A8}",
"copy" => "\u{00A9}",
"ordf" => "\u{00AA}",
"laquo" => "\u{00AB}",
"not" => "\u{00AC}",
"shy" => "\u{00AD}",
"reg" => "\u{00AE}",
"macr" => "\u{00AF}",
"deg" => "\u{00B0}",
"plusmn" => "\u{00B1}",
"sup2" => "\u{00B2}",
"sup3" => "\u{00B3}",
"acute" => "\u{00B4}",
"micro" => "\u{00B5}",
"para" => "\u{00B6}",
"middot" => "\u{00B7}",
"cedil" => "\u{00B8}",
"sup1" => "\u{00B9}",
"ordm" => "\u{00BA}",
"raquo" => "\u{00BB}",
"frac14" => "\u{00BC}",
"frac12" => "\u{00BD}",
"frac34" => "\u{00BE}",
"iquest" => "\u{00BF}",
"Agrave" => "\u{00C0}",
"Aacute" => "\u{00C1}",
"Acirc" => "\u{00C2}",
"Atilde" => "\u{00C3}",
"Auml" => "\u{00C4}",
"Aring" => "\u{00C5}",
"AElig" => "\u{00C6}",
"Ccedil" => "\u{00C7}",
"Egrave" => "\u{00C8}",
"Eacute" => "\u{00C9}",
"Ecirc" => "\u{00CA}",
"Euml" => "\u{00CB}",
"Igrave" => "\u{00CC}",
"Iacute" => "\u{00CD}",
"Icirc" => "\u{00CE}",
"Iuml" => "\u{00CF}",
"ETH" => "\u{00D0}",
"Ntilde" => "\u{00D1}",
"Ograve" => "\u{00D2}",
"Oacute" => "\u{00D3}",
"Ocirc" => "\u{00D4}",
"Otilde" => "\u{00D5}",
"Ouml" => "\u{00D6}",
"times" => "\u{00D7}",
"Oslash" => "\u{00D8}",
"Ugrave" => "\u{00D9}",
"Uacute" => "\u{00DA}",
"Ucirc" => "\u{00DB}",
"Uuml" => "\u{00DC}",
"Yacute" => "\u{00DD}",
"THORN" => "\u{00DE}",
"szlig" => "\u{00DF}",
"agrave" => "\u{00E0}",
"aacute" => "\u{00E1}",
"acirc" => "\u{00E2}",
"atilde" => "\u{00E3}",
"auml" => "\u{00E4}",
"aring" => "\u{00E5}",
"aelig" => "\u{00E6}",
"ccedil" => "\u{00E7}",
"egrave" => "\u{00E8}",
"eacute" => "\u{00E9}",
"ecirc" => "\u{00EA}",
"euml" => "\u{00EB}",
"igrave" => "\u{00EC}",
"iacute" => "\u{00ED}",
"icirc" => "\u{00EE}",
"iuml" => "\u{00EF}",
"eth" => "\u{00F0}",
"ntilde" => "\u{00F1}",
"ograve" => "\u{00F2}",
"oacute" => "\u{00F3}",
"ocirc" => "\u{00F4}",
"otilde" => "\u{00F5}",
"ouml" => "\u{00F6}",
"divide" => "\u{00F7}",
"oslash" => "\u{00F8}",
"ugrave" => "\u{00F9}",
"uacute" => "\u{00FA}",
"ucirc" => "\u{00FB}",
"uuml" => "\u{00FC}",
"yacute" => "\u{00FD}",
"thorn" => "\u{00FE}",
"yuml" => "\u{00FF}",
"Alpha" => "\u{0391}",
"Beta" => "\u{0392}",
"Gamma" => "\u{0393}",
"Delta" => "\u{0394}",
"Epsilon" => "\u{0395}",
"Zeta" => "\u{0396}",
"Eta" => "\u{0397}",
"Theta" => "\u{0398}",
"Iota" => "\u{0399}",
"Kappa" => "\u{039A}",
"Lambda" => "\u{039B}",
"Mu" => "\u{039C}",
"Nu" => "\u{039D}",
"Xi" => "\u{039E}",
"Omicron" => "\u{039F}",
"Pi" => "\u{03A0}",
"Rho" => "\u{03A1}",
"Sigma" => "\u{03A3}",
"Tau" => "\u{03A4}",
"Upsilon" => "\u{03A5}",
"Phi" => "\u{03A6}",
"Chi" => "\u{03A7}",
"Psi" => "\u{03A8}",
"Omega" => "\u{03A9}",
"alpha" => "\u{03B1}",
"beta" => "\u{03B2}",
"gamma" => "\u{03B3}",
"delta" => "\u{03B4}",
"epsilon" => "\u{03B5}",
"zeta" => "\u{03B6}",
"eta" => "\u{03B7}",
"theta" => "\u{03B8}",
"iota" => "\u{03B9}",
"kappa" => "\u{03BA}",
"lambda" => "\u{03BB}",
"mu" => "\u{03BC}",
"nu" => "\u{03BD}",
"xi" => "\u{03BE}",
"omicron" => "\u{03BF}",
"pi" => "\u{03C0}",
"rho" => "\u{03C1}",
"sigmaf" => "\u{03C2}",
"sigma" => "\u{03C3}",
"tau" => "\u{03C4}",
"upsilon" => "\u{03C5}",
"phi" => "\u{03C6}",
"chi" => "\u{03C7}",
"psi" => "\u{03C8}",
"omega" => "\u{03C9}",
"bull" => "\u{2022}",
"hellip" => "\u{2026}",
"prime" => "\u{2032}",
"Prime" => "\u{2033}",
"oline" => "\u{203E}",
"frasl" => "\u{2044}",
"trade" => "\u{2122}",
"larr" => "\u{2190}",
"uarr" => "\u{2191}",
"rarr" => "\u{2192}",
"darr" => "\u{2193}",
"harr" => "\u{2194}",
"lArr" => "\u{21D0}",
"uArr" => "\u{21D1}",
"rArr" => "\u{21D2}",
"dArr" => "\u{21D3}",
"hArr" => "\u{21D4}",
"forall" => "\u{2200}",
"part" => "\u{2202}",
"exist" => "\u{2203}",
"empty" => "\u{2205}",
"nabla" => "\u{2207}",
"isin" => "\u{2208}",
"notin" => "\u{2209}",
"ni" => "\u{220B}",
"prod" => "\u{220F}",
"sum" => "\u{2211}",
"minus" => "\u{2212}",
"lowast" => "\u{2217}",
"radic" => "\u{221A}",
"prop" => "\u{221D}",
"infin" => "\u{221E}",
"ang" => "\u{2220}",
"and" => "\u{2227}",
"or" => "\u{2228}",
"cap" => "\u{2229}",
"cup" => "\u{222A}",
"int" => "\u{222B}",
"there4" => "\u{2234}",
"sim" => "\u{223C}",
"cong" => "\u{2245}",
"asymp" => "\u{2248}",
"ne" => "\u{2260}",
"equiv" => "\u{2261}",
"le" => "\u{2264}",
"ge" => "\u{2265}",
"sub" => "\u{2282}",
"sup" => "\u{2283}",
"nsub" => "\u{2284}",
"sube" => "\u{2286}",
"supe" => "\u{2287}",
"oplus" => "\u{2295}",
"otimes" => "\u{2297}",
"perp" => "\u{22A5}",
"sdot" => "\u{22C5}",
"ensp" => "\u{2002}",
"emsp" => "\u{2003}",
"thinsp" => "\u{2009}",
"zwnj" => "\u{200C}",
"zwj" => "\u{200D}",
"lrm" => "\u{200E}",
"rlm" => "\u{200F}",
"ndash" => "\u{2013}",
"mdash" => "\u{2014}",
"lsquo" => "\u{2018}",
"rsquo" => "\u{2019}",
"sbquo" => "\u{201A}",
"ldquo" => "\u{201C}",
"rdquo" => "\u{201D}",
"bdquo" => "\u{201E}",
"dagger" => "\u{2020}",
"Dagger" => "\u{2021}",
"permil" => "\u{2030}",
"lsaquo" => "\u{2039}",
"rsaquo" => "\u{203A}",
"euro" => "\u{20AC}",
"OElig" => "\u{0152}",
"oelig" => "\u{0153}",
"Scaron" => "\u{0160}",
"scaron" => "\u{0161}",
"Yuml" => "\u{0178}",
"circ" => "\u{02C6}",
"tilde" => "\u{02DC}",
"fnof" => "\u{0192}",
"spades" => "\u{2660}",
"clubs" => "\u{2663}",
"hearts" => "\u{2665}",
"diams" => "\u{2666}",
"loz" => "\u{25CA}",
"lceil" => "\u{2308}",
"rceil" => "\u{2309}",
"lfloor" => "\u{230A}",
"rfloor" => "\u{230B}",
"lang" => "\u{2329}",
"rang" => "\u{232A}",
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn common_named_entities() {
assert_eq!(decode_named("amp"), Some("&"));
assert_eq!(decode_named("lt"), Some("<"));
assert_eq!(decode_named("gt"), Some(">"));
assert_eq!(decode_named("quot"), Some("\""));
assert_eq!(decode_named("apos"), Some("'"));
assert_eq!(decode_named("nbsp"), Some("\u{00A0}"));
}
#[test]
fn unknown_entity() {
assert_eq!(decode_named("nonexistent"), None);
assert_eq!(decode_named(""), None);
}
#[test]
fn numeric_decimal() {
assert_eq!(decode_numeric("60", false), Some('<'));
assert_eq!(decode_numeric("62", false), Some('>'));
assert_eq!(decode_numeric("38", false), Some('&'));
assert_eq!(decode_numeric("128512", false), Some('\u{1F600}'));
}
#[test]
fn numeric_hex() {
assert_eq!(decode_numeric("3C", true), Some('<'));
assert_eq!(decode_numeric("3e", true), Some('>'));
assert_eq!(decode_numeric("1F600", true), Some('\u{1F600}'));
}
#[test]
fn numeric_null_replaced() {
assert_eq!(decode_numeric("0", false), Some('\u{FFFD}'));
assert_eq!(decode_numeric("0", true), Some('\u{FFFD}'));
}
#[test]
fn numeric_invalid() {
assert_eq!(decode_numeric("FFFFFF", true), None); assert_eq!(decode_numeric("abc", false), None); assert_eq!(decode_numeric("", false), None);
}
#[test]
fn greek_entities() {
assert_eq!(decode_named("alpha"), Some("\u{03B1}"));
assert_eq!(decode_named("omega"), Some("\u{03C9}"));
assert_eq!(decode_named("Sigma"), Some("\u{03A3}"));
}
#[test]
fn typography_entities() {
assert_eq!(decode_named("mdash"), Some("\u{2014}"));
assert_eq!(decode_named("euro"), Some("\u{20AC}"));
assert_eq!(decode_named("trade"), Some("\u{2122}"));
}
#[test]
fn escape_text_special_chars() {
let mut buf = String::new();
escape_text("&", &mut buf);
assert_eq!(buf, "&");
buf.clear();
escape_text("<", &mut buf);
assert_eq!(buf, "<");
buf.clear();
escape_text(">", &mut buf);
assert_eq!(buf, ">");
}
#[test]
fn escape_text_mixed() {
let mut buf = String::new();
escape_text("1 < 2 & 3 > 0", &mut buf);
assert_eq!(buf, "1 < 2 & 3 > 0");
}
#[test]
fn escape_text_plain() {
let mut buf = String::new();
escape_text("hello world", &mut buf);
assert_eq!(buf, "hello world");
}
#[test]
fn escape_text_empty() {
let mut buf = String::new();
escape_text("", &mut buf);
assert_eq!(buf, "");
}
#[test]
fn escape_text_all_special() {
let mut buf = String::new();
escape_text("&<>", &mut buf);
assert_eq!(buf, "&<>");
}
#[test]
fn escape_attr_quote() {
let mut buf = String::new();
escape_attr("say \"hello\"", &mut buf);
assert_eq!(buf, "say "hello"");
}
#[test]
fn escape_attr_mixed() {
let mut buf = String::new();
escape_attr("x&y=\"z\"", &mut buf);
assert_eq!(buf, "x&y="z"");
}
#[test]
fn escape_attr_plain() {
let mut buf = String::new();
escape_attr("plain", &mut buf);
assert_eq!(buf, "plain");
}
#[test]
fn escape_attr_single_quote() {
let mut buf = String::new();
escape_attr("it's", &mut buf);
assert_eq!(buf, "it's");
}
#[test]
fn escape_attr_empty() {
let mut buf = String::new();
escape_attr("", &mut buf);
assert_eq!(buf, "");
}
#[test]
fn escape_text_does_not_escape_quotes() {
let mut buf = String::new();
escape_text("say \"hello\" it's", &mut buf);
assert_eq!(buf, "say \"hello\" it's");
}
}