linkmarks_core/parser/
mod.rs1#[must_use]
15pub fn decode_html_entities(input: &str) -> String {
16 let mut out = String::with_capacity(input.len());
17 let mut chars = input.chars().peekable();
18 while let Some(c) = chars.next() {
19 if c != '&' {
20 out.push(c);
21 continue;
22 }
23 let mut buf = String::new();
25 let mut ended = false;
26 while let Some(&next) = chars.peek() {
27 if next == ';' {
28 chars.next();
29 ended = true;
30 break;
31 }
32 if buf.len() > 8 {
33 break;
34 }
35 buf.push(next);
36 chars.next();
37 }
38 if !ended {
39 out.push('&');
40 out.push_str(&buf);
41 continue;
42 }
43 match buf.as_str() {
44 "amp" => out.push('&'),
45 "lt" => out.push('<'),
46 "gt" => out.push('>'),
47 "quot" => out.push('"'),
48 "apos" => out.push('\''),
49 "nbsp" => out.push('\u{00A0}'),
50 other if other.starts_with('#') => {
51 let body = &other[1..];
52 let code =
53 if let Some(hex) = body.strip_prefix('x').or_else(|| body.strip_prefix('X')) {
54 u32::from_str_radix(hex, 16).ok()
55 } else {
56 body.parse::<u32>().ok()
57 };
58 if let Some(code) = code {
59 if let Some(ch) = char::from_u32(code) {
60 out.push(ch);
61 }
62 }
63 }
64 _ => {
65 out.push('&');
66 out.push_str(&buf);
67 out.push(';');
68 }
69 }
70 }
71 out
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn decodes_named_entities() {
80 assert_eq!(decode_html_entities("a & b"), "a & b");
81 assert_eq!(decode_html_entities("<tag>"), "<tag>");
82 assert_eq!(decode_html_entities(""x""), "\"x\"");
83 assert_eq!(decode_html_entities("'x'"), "'x'");
84 }
85
86 #[test]
87 fn decodes_numeric_entities() {
88 assert_eq!(decode_html_entities("A"), "A");
89 assert_eq!(decode_html_entities("A"), "A");
90 }
91
92 #[test]
93 fn passes_through_unknown() {
94 assert_eq!(decode_html_entities("&unknown;"), "&unknown;");
95 }
96
97 #[test]
98 fn handles_orphan_ampersand() {
99 assert_eq!(decode_html_entities("a & b"), "a & b");
100 }
101}