Skip to main content

html_escape/decode/html_entity/
mod.rs

1mod tables;
2
3use alloc::{borrow::Cow, string::String, vec::Vec};
4use core::{convert::TryFrom, 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]
34fn decode_decimal_entity(text: &str, start: usize, end: usize) -> Option<DecodedEntityValue> {
35    let number = text[start..end].parse::<u32>().ok()?;
36    let character = char::try_from(number).ok()?;
37
38    Some(DecodedEntityValue::Character(character))
39}
40
41#[inline]
42fn decode_hex_entity(text: &str, start: usize, end: usize) -> Option<DecodedEntityValue> {
43    let number = u32::from_str_radix(&text[start..end], 16).ok()?;
44    let character = char::try_from(number).ok()?;
45
46    Some(DecodedEntityValue::Character(character))
47}
48
49fn find_decoded_entity(text: &str, start: usize) -> Option<DecodedEntity> {
50    let text_bytes = text.as_bytes();
51    let text_length = text_bytes.len();
52
53    let mut p = start;
54
55    'search: while p < text_length {
56        if text_bytes[p] != b'&' {
57            p += 1;
58
59            continue;
60        }
61
62        let entity_start = p;
63
64        p += 1;
65
66        if p == text_length {
67            return None;
68        }
69
70        match text_bytes[p] {
71            b'&' => continue 'search,
72            b';' => {
73                p += 1;
74            },
75            b'#' => {
76                p += 1;
77
78                if p == text_length {
79                    return None;
80                }
81
82                match text_bytes[p] {
83                    b'&' => continue 'search,
84                    b';' => {
85                        p += 1;
86                    },
87                    b'x' | b'X' => {
88                        p += 1;
89
90                        if p == text_length {
91                            return None;
92                        }
93
94                        match text_bytes[p] {
95                            b'&' => continue 'search,
96                            b';' => {
97                                p += 1;
98                            },
99                            _ => {
100                                let hex_start = p;
101
102                                loop {
103                                    p += 1;
104
105                                    if p == text_length {
106                                        return None;
107                                    }
108
109                                    match text_bytes[p] {
110                                        b'&' => continue 'search,
111                                        b';' => {
112                                            if let Some(value) =
113                                                decode_hex_entity(text, hex_start, p)
114                                            {
115                                                return Some(DecodedEntity {
116                                                    start: entity_start,
117                                                    end: p + 1,
118                                                    value,
119                                                });
120                                            }
121
122                                            p += 1;
123
124                                            continue 'search;
125                                        },
126                                        _ => (),
127                                    }
128                                }
129                            },
130                        }
131                    },
132                    _ => {
133                        let number_start = p;
134
135                        loop {
136                            p += 1;
137
138                            if p == text_length {
139                                return None;
140                            }
141
142                            match text_bytes[p] {
143                                b'&' => continue 'search,
144                                b';' => {
145                                    if let Some(value) =
146                                        decode_decimal_entity(text, number_start, p)
147                                    {
148                                        return Some(DecodedEntity {
149                                            start: entity_start,
150                                            end: p + 1,
151                                            value,
152                                        });
153                                    }
154
155                                    p += 1;
156
157                                    continue 'search;
158                                },
159                                _ => (),
160                            }
161                        }
162                    },
163                }
164            },
165            _ => {
166                let name_start = p;
167
168                loop {
169                    p += 1;
170
171                    if p == text_length {
172                        return None;
173                    }
174
175                    match text_bytes[p] {
176                        b'&' => continue 'search,
177                        b';' => {
178                            if let Some(value) = decode_named_entity(&text_bytes[name_start..p]) {
179                                return Some(DecodedEntity {
180                                    start: entity_start,
181                                    end: p + 1,
182                                    value,
183                                });
184                            }
185
186                            p += 1;
187
188                            continue 'search;
189                        },
190                        _ => (),
191                    }
192                }
193            },
194        }
195    }
196
197    None
198}
199
200#[inline]
201fn write_decoded_entity_to_vec(value: DecodedEntityValue, output: &mut Vec<u8>) {
202    match value {
203        DecodedEntityValue::Named(entity) => output.extend_from_slice(entity.as_bytes()),
204        DecodedEntityValue::Character(character) => write_char_to_vec(character, output),
205    }
206}
207
208#[cfg(feature = "std")]
209#[inline]
210fn write_decoded_entity_to_writer<W: Write>(
211    value: DecodedEntityValue,
212    output: &mut W,
213) -> Result<(), io::Error> {
214    match value {
215        DecodedEntityValue::Named(entity) => output.write_all(entity.as_bytes()),
216        DecodedEntityValue::Character(character) => write_char_to_writer(character, output),
217    }
218}
219
220/// Decode html entities in a given string.
221pub fn decode_html_entities<S: ?Sized + AsRef<str>>(text: &S) -> Cow<'_, str> {
222    let text = text.as_ref();
223    let text_bytes = text.as_bytes();
224    let text_length = text_bytes.len();
225
226    let entity = match find_decoded_entity(text, 0) {
227        Some(entity) => entity,
228        None => return Cow::from(text),
229    };
230
231    let mut v = Vec::with_capacity(text_length);
232    let mut start = entity.end;
233
234    v.extend_from_slice(&text_bytes[..entity.start]);
235    write_decoded_entity_to_vec(entity.value, &mut v);
236
237    while let Some(entity) = find_decoded_entity(text, start) {
238        v.extend_from_slice(&text_bytes[start..entity.start]);
239        write_decoded_entity_to_vec(entity.value, &mut v);
240        start = entity.end;
241    }
242
243    v.extend_from_slice(&text_bytes[start..]);
244
245    Cow::from(unsafe { String::from_utf8_unchecked(v) })
246}
247
248/// Decode html entities in a given string to a mutable `String` reference and return the decoded string slice.
249pub fn decode_html_entities_to_string<S: AsRef<str>>(text: S, output: &mut String) -> &str {
250    unsafe { from_utf8_unchecked(decode_html_entities_to_vec(text, output.as_mut_vec())) }
251}
252
253/// Decode html entities in a given string to a mutable `Vec<u8>` reference and return the decoded data slice.
254pub fn decode_html_entities_to_vec<S: AsRef<str>>(text: S, output: &mut Vec<u8>) -> &[u8] {
255    let text = text.as_ref();
256    let text_bytes = text.as_bytes();
257    let text_length = text_bytes.len();
258
259    output.reserve(text_length);
260
261    let current_length = output.len();
262
263    let mut start = 0;
264
265    while let Some(entity) = find_decoded_entity(text, start) {
266        output.extend_from_slice(&text_bytes[start..entity.start]);
267        write_decoded_entity_to_vec(entity.value, output);
268        start = entity.end;
269    }
270
271    output.extend_from_slice(&text_bytes[start..]);
272
273    &output[current_length..]
274}
275
276#[cfg(feature = "std")]
277/// Decode html entities in a given string to a writer.
278pub fn decode_html_entities_to_writer<S: AsRef<str>, W: Write>(
279    text: S,
280    output: &mut W,
281) -> Result<(), io::Error> {
282    let text = text.as_ref();
283    let text_bytes = text.as_bytes();
284
285    let mut start = 0;
286
287    while let Some(entity) = find_decoded_entity(text, start) {
288        output.write_all(&text_bytes[start..entity.start])?;
289        write_decoded_entity_to_writer(entity.value, output)?;
290        start = entity.end;
291    }
292
293    output.write_all(&text_bytes[start..])
294}