use std::borrow::Cow;
use std::sync::LazyLock;
use regex::{Captures, Regex};
use super::entities_table::NAME2CODEPOINT;
static ENTITY_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"&#?(\w+);").unwrap());
pub fn unescape(text: &str) -> String {
ENTITY_RE
.replace_all(text, |caps: &Captures| {
let whole = caps.get(0).expect("match").as_str();
let code = caps.get(1).expect("group").as_str();
let decoded = if whole.as_bytes()[1] == b'#' {
if code.as_bytes().first() == Some(&b'x') {
u32::from_str_radix(&code[1..], 16).ok()
} else {
code.parse::<u32>().ok()
}
} else {
NAME2CODEPOINT
.binary_search_by_key(&code, |&(name, _)| name)
.ok()
.map(|i| NAME2CODEPOINT[i].1)
};
match decoded.and_then(char::from_u32) {
Some(c) => c.to_string(),
None => whole.to_string(), }
})
.into_owned()
}
pub fn html_escape(text: &str) -> Cow<'_, str> {
if !text.contains(['&', '<', '>']) {
return Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len() + 8);
for c in text.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
_ => out.push(c),
}
}
Cow::Owned(out)
}
pub fn urlencode(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for byte in text.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b'.' | b'-' | b'~' | b'/' => {
out.push(byte as char);
}
_ => out.push_str(&format!("%{byte:02X}")),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entity_table_is_sorted_for_binary_search() {
assert!(NAME2CODEPOINT.windows(2).all(|w| w[0].0 < w[1].0));
assert_eq!(NAME2CODEPOINT.len(), 252);
}
#[test]
fn decodes_named_and_numeric_entities() {
assert_eq!(
unescape("1–5 and 100 km"),
"1\u{2013}5 and 100\u{a0}km"
);
assert_eq!(unescape("AB"), "AB");
assert_eq!(unescape("&lt;"), "<"); }
#[test]
fn leaves_unknown_entities_untouched() {
assert_eq!(
unescape("&bogus; &#xZZ; �"),
"&bogus; &#xZZ; �"
);
assert_eq!(unescape("A"), "A");
}
#[test]
fn escapes_html_without_quotes() {
assert_eq!(
html_escape("a & b < c > \"d\""),
"a & b < c > \"d\""
);
}
#[test]
fn urlencodes_like_python_quote() {
assert_eq!(urlencode("a b&c/d~e"), "a%20b%26c/d~e");
assert_eq!(urlencode("naïve"), "na%C3%AFve");
}
}