pub fn decode_label(raw: &str) -> String {
let t = raw.trim();
let t = strip_quotes(t);
let t = replace_breaks(t);
let t = t.replace("\\n", "\n");
decode_entities(&t)
}
fn strip_quotes(t: &str) -> &str {
let t = match (t.strip_prefix('"'), t.strip_suffix('"')) {
(Some(_), Some(_)) if t.len() >= 2 => &t[1..t.len() - 1],
_ => t,
};
match (t.strip_prefix('`'), t.strip_suffix('`')) {
(Some(_), Some(_)) if t.len() >= 2 => &t[1..t.len() - 1],
_ => t,
}
}
pub fn replace_breaks(t: &str) -> String {
let chars: Vec<char> = t.chars().collect();
let mut out = String::with_capacity(t.len());
let mut i = 0;
while i < chars.len() {
if chars[i] == '<' {
if let Some(end) = match_break_tag(&chars, i) {
out.push('\n');
i = end;
continue;
}
}
out.push(chars[i]);
i += 1;
}
out
}
fn match_break_tag(chars: &[char], i: usize) -> Option<usize> {
let mut p = i + 1;
if chars.get(p) == Some(&'/') {
p += 1;
}
let b = chars.get(p)?;
let r = chars.get(p + 1)?;
if !(b.eq_ignore_ascii_case(&'b') && r.eq_ignore_ascii_case(&'r')) {
return None;
}
p += 2;
while chars.get(p).is_some_and(|c| c.is_whitespace()) {
p += 1;
}
if chars.get(p) == Some(&'/') {
p += 1;
}
if chars.get(p) == Some(&'>') {
Some(p + 1)
} else {
None
}
}
fn decode_entities(t: &str) -> String {
let chars: Vec<char> = t.chars().collect();
let mut out = String::with_capacity(t.len());
let mut i = 0;
while i < chars.len() {
if chars[i] == '#' {
let mut p = i + 1;
while chars
.get(p)
.is_some_and(|c| c.is_alphanumeric() || *c == '_')
{
p += 1;
}
if p > i + 1 && chars.get(p) == Some(&';') {
let name: String = chars[i + 1..p].iter().collect();
if let Some(c) = entity_char(&name) {
out.push(c);
i = p + 1;
continue;
}
}
}
out.push(chars[i]);
i += 1;
}
out
}
fn entity_char(name: &str) -> Option<char> {
if let Some(digits) = name.strip_prefix(|c: char| c == 'x' || c == 'X') {
if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_hexdigit()) {
return u32::from_str_radix(digits, 16)
.ok()
.and_then(char::from_u32);
}
}
if !name.is_empty() && name.chars().all(|c| c.is_ascii_digit()) {
return name.parse::<u32>().ok().and_then(char::from_u32);
}
NAMED_ENTITIES
.iter()
.find(|(n, _)| *n == name)
.map(|(_, c)| *c)
}
const NAMED_ENTITIES: &[(&str, char)] = &[
("quot", '"'),
("apos", '\''),
("amp", '&'),
("lt", '<'),
("gt", '>'),
("nbsp", '\u{a0}'),
("semi", ';'),
("hellip", '…'),
("mdash", '—'),
("ndash", '–'),
("bull", '•'),
("middot", '·'),
("deg", '°'),
("plusmn", '±'),
("times", '×'),
("divide", '÷'),
("copy", '©'),
("reg", '®'),
("trade", '™'),
("laquo", '«'),
("raquo", '»'),
("larr", '←'),
("rarr", '→'),
("uarr", '↑'),
("darr", '↓'),
("harr", '↔'),
("infin", '∞'),
("ne", '≠'),
("le", '≤'),
("ge", '≥'),
("sum", '∑'),
("radic", '√'),
("check", '✓'),
("cross", '✗'),
("star", '★'),
("euro", '€'),
("pound", '£'),
("yen", '¥'),
("cent", '¢'),
("sect", '§'),
("para", '¶'),
("dagger", '†'),
("alpha", 'α'),
("beta", 'β'),
("gamma", 'γ'),
("delta", 'δ'),
("epsilon", 'ε'),
("theta", 'θ'),
("lambda", 'λ'),
("mu", 'μ'),
("pi", 'π'),
("sigma", 'σ'),
("phi", 'φ'),
("omega", 'ω'),
];