html_escape/decode/html_entity/
mod.rs1mod tables;
2
3use alloc::{borrow::Cow, string::String, vec::Vec};
4use core::str::from_utf8_unchecked;
5#[cfg(feature = "std")]
6use std::io::{self, Write};
7
8pub use tables::*;
9
10use crate::functions::*;
11
12#[derive(Clone, Copy)]
13enum DecodedEntityValue {
14 Named(&'static str),
15 Character(char),
16}
17
18#[derive(Clone, Copy)]
19struct DecodedEntity {
20 start: usize,
21 end: usize,
22 value: DecodedEntityValue,
23}
24
25#[inline]
26fn decode_named_entity(name: &[u8]) -> Option<DecodedEntityValue> {
27 let common = match name {
29 b"amp" => Some("&"),
30 b"lt" => Some("<"),
31 b"gt" => Some(">"),
32 b"quot" => Some("\""),
33 b"apos" => Some("'"),
34 b"nbsp" => Some("\u{00A0}"),
35 _ => None,
36 };
37
38 if let Some(value) = common {
39 return Some(DecodedEntityValue::Named(value));
40 }
41
42 NAMED_ENTITIES
43 .binary_search_by(|(t_name, _)| t_name.cmp(&name))
44 .ok()
45 .map(|index| DecodedEntityValue::Named(NAMED_ENTITIES[index].1))
46}
47
48#[inline]
50fn decode_character(number: u32) -> Option<char> {
51 let character = char::try_from(number).ok()?;
52
53 match character {
54 '\t' | '\n' | '\u{000C}' | '\r' => Some(character),
56 '\0'..='\u{001F}' => None,
58 _ => Some(character),
59 }
60}
61
62#[inline]
63fn decode_decimal_entity(text: &str, start: usize, end: usize) -> Option<DecodedEntityValue> {
64 let digits = &text[start..end];
65
66 if !digits.as_bytes().first()?.is_ascii_digit() {
68 return None;
69 }
70
71 let character = decode_character(digits.parse::<u32>().ok()?)?;
72
73 Some(DecodedEntityValue::Character(character))
74}
75
76#[inline]
77fn decode_hex_entity(text: &str, start: usize, end: usize) -> Option<DecodedEntityValue> {
78 let digits = &text[start..end];
79
80 if !digits.as_bytes().first()?.is_ascii_hexdigit() {
82 return None;
83 }
84
85 let character = decode_character(u32::from_str_radix(digits, 16).ok()?)?;
86
87 Some(DecodedEntityValue::Character(character))
88}
89
90fn find_decoded_entity(text: &str, start: usize) -> Option<DecodedEntity> {
91 let text_bytes = text.as_bytes();
92 let text_length = text_bytes.len();
93
94 let mut p = start;
95
96 'search: loop {
97 p = find_byte(text_bytes, p, b'&');
98
99 if p == text_length {
100 return None;
101 }
102
103 let entity_start = p;
104
105 p += 1;
106
107 if p == text_length {
108 return None;
109 }
110
111 match text_bytes[p] {
112 b'&' => continue 'search,
113 b';' => {
114 p += 1;
115 },
116 b'#' => {
117 p += 1;
118
119 if p == text_length {
120 return None;
121 }
122
123 match text_bytes[p] {
124 b'&' => continue 'search,
125 b';' => {
126 p += 1;
127 },
128 b'x' | b'X' => {
129 p += 1;
130
131 if p == text_length {
132 return None;
133 }
134
135 match text_bytes[p] {
136 b'&' => continue 'search,
137 b';' => {
138 p += 1;
139 },
140 _ => {
141 let hex_start = p;
142
143 loop {
144 p += 1;
145
146 if p == text_length {
147 return None;
148 }
149
150 match text_bytes[p] {
151 b'&' => continue 'search,
152 b';' => {
153 if let Some(value) =
154 decode_hex_entity(text, hex_start, p)
155 {
156 return Some(DecodedEntity {
157 start: entity_start,
158 end: p + 1,
159 value,
160 });
161 }
162
163 p += 1;
164
165 continue 'search;
166 },
167 _ => (),
168 }
169 }
170 },
171 }
172 },
173 _ => {
174 let number_start = p;
175
176 loop {
177 p += 1;
178
179 if p == text_length {
180 return None;
181 }
182
183 match text_bytes[p] {
184 b'&' => continue 'search,
185 b';' => {
186 if let Some(value) =
187 decode_decimal_entity(text, number_start, p)
188 {
189 return Some(DecodedEntity {
190 start: entity_start,
191 end: p + 1,
192 value,
193 });
194 }
195
196 p += 1;
197
198 continue 'search;
199 },
200 _ => (),
201 }
202 }
203 },
204 }
205 },
206 _ => {
207 let name_start = p;
208
209 loop {
210 p += 1;
211
212 if p == text_length {
213 return None;
214 }
215
216 match text_bytes[p] {
217 b'&' => continue 'search,
218 b';' => {
219 if let Some(value) = decode_named_entity(&text_bytes[name_start..p]) {
220 return Some(DecodedEntity {
221 start: entity_start,
222 end: p + 1,
223 value,
224 });
225 }
226
227 p += 1;
228
229 continue 'search;
230 },
231 _ => (),
232 }
233 }
234 },
235 }
236 }
237}
238
239#[inline]
240fn write_decoded_entity_to_vec(value: DecodedEntityValue, output: &mut Vec<u8>) {
241 match value {
242 DecodedEntityValue::Named(entity) => output.extend_from_slice(entity.as_bytes()),
243 DecodedEntityValue::Character(character) => write_char_to_vec(character, output),
244 }
245}
246
247#[cfg(feature = "std")]
248#[inline]
249fn write_decoded_entity_to_writer<W: Write + ?Sized>(
250 value: DecodedEntityValue,
251 output: &mut W,
252) -> Result<(), io::Error> {
253 match value {
254 DecodedEntityValue::Named(entity) => output.write_all(entity.as_bytes()),
255 DecodedEntityValue::Character(character) => write_char_to_writer(character, output),
256 }
257}
258
259pub fn decode_html_entities<S: ?Sized + AsRef<str>>(text: &S) -> Cow<'_, str> {
265 let text = text.as_ref();
266 let text_bytes = text.as_bytes();
267 let text_length = text_bytes.len();
268
269 let entity = match find_decoded_entity(text, 0) {
270 Some(entity) => entity,
271 None => return Cow::from(text),
272 };
273
274 let mut v = Vec::with_capacity(text_length);
275
276 v.extend_from_slice(&text_bytes[..entity.start]);
277 write_decoded_entity_to_vec(entity.value, &mut v);
278
279 decode_html_entities_to_vec(&text[entity.end..], &mut v);
281
282 Cow::from(unsafe { String::from_utf8_unchecked(v) })
284}
285
286pub fn decode_html_entities_to_string<S: AsRef<str>>(text: S, output: &mut String) -> &str {
294 unsafe { from_utf8_unchecked(decode_html_entities_to_vec(text, output.as_mut_vec())) }
296}
297
298pub fn decode_html_entities_to_vec<S: AsRef<str>>(text: S, output: &mut Vec<u8>) -> &[u8] {
306 let text = text.as_ref();
307 let text_bytes = text.as_bytes();
308 let text_length = text_bytes.len();
309
310 output.reserve(text_length);
311
312 let current_length = output.len();
313
314 let mut start = 0;
315
316 while let Some(entity) = find_decoded_entity(text, start) {
317 output.extend_from_slice(&text_bytes[start..entity.start]);
318 write_decoded_entity_to_vec(entity.value, output);
319 start = entity.end;
320 }
321
322 output.extend_from_slice(&text_bytes[start..]);
323
324 &output[current_length..]
325}
326
327#[cfg(feature = "std")]
328pub fn decode_html_entities_to_writer<S: AsRef<str>, W: Write + ?Sized>(
334 text: S,
335 output: &mut W,
336) -> Result<(), io::Error> {
337 let text = text.as_ref();
338 let text_bytes = text.as_bytes();
339
340 let mut start = 0;
341
342 while let Some(entity) = find_decoded_entity(text, start) {
343 output.write_all(&text_bytes[start..entity.start])?;
344 write_decoded_entity_to_writer(entity.value, output)?;
345 start = entity.end;
346 }
347
348 output.write_all(&text_bytes[start..])
349}