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 NAMED_ENTITIES
28 .binary_search_by(|(t_name, _)| t_name.cmp(&name))
29 .ok()
30 .map(|index| DecodedEntityValue::Named(NAMED_ENTITIES[index].1))
31}
32
33#[inline]
35fn decode_character(number: u32) -> Option<char> {
36 let character = char::try_from(number).ok()?;
37
38 match character {
39 '\t' | '\n' | '\u{000C}' | '\r' => Some(character),
41 '\0'..='\u{001F}' => None,
43 _ => Some(character),
44 }
45}
46
47#[inline]
48fn decode_decimal_entity(text: &str, start: usize, end: usize) -> Option<DecodedEntityValue> {
49 let digits = &text[start..end];
50
51 if !digits.as_bytes().first()?.is_ascii_digit() {
53 return None;
54 }
55
56 let character = decode_character(digits.parse::<u32>().ok()?)?;
57
58 Some(DecodedEntityValue::Character(character))
59}
60
61#[inline]
62fn decode_hex_entity(text: &str, start: usize, end: usize) -> Option<DecodedEntityValue> {
63 let digits = &text[start..end];
64
65 if !digits.as_bytes().first()?.is_ascii_hexdigit() {
67 return None;
68 }
69
70 let character = decode_character(u32::from_str_radix(digits, 16).ok()?)?;
71
72 Some(DecodedEntityValue::Character(character))
73}
74
75fn find_decoded_entity(text: &str, start: usize) -> Option<DecodedEntity> {
76 let text_bytes = text.as_bytes();
77 let text_length = text_bytes.len();
78
79 let mut p = start;
80
81 'search: while p < text_length {
82 if text_bytes[p] != b'&' {
83 p += 1;
84
85 continue;
86 }
87
88 let entity_start = p;
89
90 p += 1;
91
92 if p == text_length {
93 return None;
94 }
95
96 match text_bytes[p] {
97 b'&' => continue 'search,
98 b';' => {
99 p += 1;
100 },
101 b'#' => {
102 p += 1;
103
104 if p == text_length {
105 return None;
106 }
107
108 match text_bytes[p] {
109 b'&' => continue 'search,
110 b';' => {
111 p += 1;
112 },
113 b'x' | b'X' => {
114 p += 1;
115
116 if p == text_length {
117 return None;
118 }
119
120 match text_bytes[p] {
121 b'&' => continue 'search,
122 b';' => {
123 p += 1;
124 },
125 _ => {
126 let hex_start = p;
127
128 loop {
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 if let Some(value) =
139 decode_hex_entity(text, hex_start, p)
140 {
141 return Some(DecodedEntity {
142 start: entity_start,
143 end: p + 1,
144 value,
145 });
146 }
147
148 p += 1;
149
150 continue 'search;
151 },
152 _ => (),
153 }
154 }
155 },
156 }
157 },
158 _ => {
159 let number_start = p;
160
161 loop {
162 p += 1;
163
164 if p == text_length {
165 return None;
166 }
167
168 match text_bytes[p] {
169 b'&' => continue 'search,
170 b';' => {
171 if let Some(value) =
172 decode_decimal_entity(text, number_start, p)
173 {
174 return Some(DecodedEntity {
175 start: entity_start,
176 end: p + 1,
177 value,
178 });
179 }
180
181 p += 1;
182
183 continue 'search;
184 },
185 _ => (),
186 }
187 }
188 },
189 }
190 },
191 _ => {
192 let name_start = p;
193
194 loop {
195 p += 1;
196
197 if p == text_length {
198 return None;
199 }
200
201 match text_bytes[p] {
202 b'&' => continue 'search,
203 b';' => {
204 if let Some(value) = decode_named_entity(&text_bytes[name_start..p]) {
205 return Some(DecodedEntity {
206 start: entity_start,
207 end: p + 1,
208 value,
209 });
210 }
211
212 p += 1;
213
214 continue 'search;
215 },
216 _ => (),
217 }
218 }
219 },
220 }
221 }
222
223 None
224}
225
226#[inline]
227fn write_decoded_entity_to_vec(value: DecodedEntityValue, output: &mut Vec<u8>) {
228 match value {
229 DecodedEntityValue::Named(entity) => output.extend_from_slice(entity.as_bytes()),
230 DecodedEntityValue::Character(character) => write_char_to_vec(character, output),
231 }
232}
233
234#[cfg(feature = "std")]
235#[inline]
236fn write_decoded_entity_to_writer<W: Write>(
237 value: DecodedEntityValue,
238 output: &mut W,
239) -> Result<(), io::Error> {
240 match value {
241 DecodedEntityValue::Named(entity) => output.write_all(entity.as_bytes()),
242 DecodedEntityValue::Character(character) => write_char_to_writer(character, output),
243 }
244}
245
246pub fn decode_html_entities<S: ?Sized + AsRef<str>>(text: &S) -> Cow<'_, str> {
252 let text = text.as_ref();
253 let text_bytes = text.as_bytes();
254 let text_length = text_bytes.len();
255
256 let entity = match find_decoded_entity(text, 0) {
257 Some(entity) => entity,
258 None => return Cow::from(text),
259 };
260
261 let mut v = Vec::with_capacity(text_length);
262
263 v.extend_from_slice(&text_bytes[..entity.start]);
264 write_decoded_entity_to_vec(entity.value, &mut v);
265
266 decode_html_entities_to_vec(&text[entity.end..], &mut v);
268
269 Cow::from(unsafe { String::from_utf8_unchecked(v) })
271}
272
273pub fn decode_html_entities_to_string<S: AsRef<str>>(text: S, output: &mut String) -> &str {
279 unsafe { from_utf8_unchecked(decode_html_entities_to_vec(text, output.as_mut_vec())) }
281}
282
283pub fn decode_html_entities_to_vec<S: AsRef<str>>(text: S, output: &mut Vec<u8>) -> &[u8] {
289 let text = text.as_ref();
290 let text_bytes = text.as_bytes();
291 let text_length = text_bytes.len();
292
293 output.reserve(text_length);
294
295 let current_length = output.len();
296
297 let mut start = 0;
298
299 while let Some(entity) = find_decoded_entity(text, start) {
300 output.extend_from_slice(&text_bytes[start..entity.start]);
301 write_decoded_entity_to_vec(entity.value, output);
302 start = entity.end;
303 }
304
305 output.extend_from_slice(&text_bytes[start..]);
306
307 &output[current_length..]
308}
309
310#[cfg(feature = "std")]
311pub fn decode_html_entities_to_writer<S: AsRef<str>, W: Write>(
317 text: S,
318 output: &mut W,
319) -> Result<(), io::Error> {
320 let text = text.as_ref();
321 let text_bytes = text.as_bytes();
322
323 let mut start = 0;
324
325 while let Some(entity) = find_decoded_entity(text, start) {
326 output.write_all(&text_bytes[start..entity.start])?;
327 write_decoded_entity_to_writer(entity.value, output)?;
328 start = entity.end;
329 }
330
331 output.write_all(&text_bytes[start..])
332}