darklua_core/nodes/expressions/
string.rs

1use std::str::CharIndices;
2
3use crate::nodes::{StringError, Token};
4
5use super::string_utils;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct StringExpression {
9    value: String,
10    token: Option<Token>,
11}
12
13impl StringExpression {
14    pub fn new(string: &str) -> Result<Self, StringError> {
15        if string.starts_with('[') {
16            return string
17                .chars()
18                .skip(1)
19                .enumerate()
20                .find_map(|(indice, character)| if character == '[' { Some(indice) } else { None })
21                .ok_or_else(|| StringError::invalid("unable to find `[` delimiter"))
22                .and_then(|indice| {
23                    let length = 2 + indice;
24                    let start = if string
25                        .get(length..length + 1)
26                        .filter(|char| char == &"\n")
27                        .is_some()
28                    {
29                        length + 1
30                    } else {
31                        length
32                    };
33                    string
34                        .get(start..string.len() - length)
35                        .map(str::to_owned)
36                        .ok_or_else(|| StringError::invalid(""))
37                })
38                .map(Self::from_value);
39        }
40
41        let mut chars = string.char_indices();
42
43        match (chars.next(), chars.next_back()) {
44            (Some((_, first_char)), Some((_, last_char))) if first_char == last_char => {
45                string_utils::read_escaped_string(chars, Some(string.len())).map(Self::from_value)
46            }
47            (None, None) | (None, Some(_)) | (Some(_), None) => {
48                Err(StringError::invalid("missing quotes"))
49            }
50            (Some(_), Some(_)) => Err(StringError::invalid("quotes do not match")),
51        }
52    }
53
54    pub fn empty() -> Self {
55        Self {
56            value: "".to_owned(),
57            token: None,
58        }
59    }
60
61    pub fn from_value<T: Into<String>>(value: T) -> Self {
62        Self {
63            value: value.into(),
64            token: None,
65        }
66    }
67
68    pub fn with_token(mut self, token: Token) -> Self {
69        self.token = Some(token);
70        self
71    }
72
73    #[inline]
74    pub fn set_token(&mut self, token: Token) {
75        self.token = Some(token);
76    }
77
78    #[inline]
79    pub fn get_token(&self) -> Option<&Token> {
80        self.token.as_ref()
81    }
82
83    #[inline]
84    pub fn get_value(&self) -> &str {
85        &self.value
86    }
87
88    #[inline]
89    pub fn into_value(self) -> String {
90        self.value
91    }
92
93    pub fn is_multiline(&self) -> bool {
94        self.value.contains('\n')
95    }
96
97    pub fn has_single_quote(&self) -> bool {
98        self.find_not_escaped('\'').is_some()
99    }
100
101    pub fn has_double_quote(&self) -> bool {
102        self.find_not_escaped('"').is_some()
103    }
104
105    fn find_not_escaped(&self, pattern: char) -> Option<usize> {
106        self.find_not_escaped_from(pattern, &mut self.value.char_indices())
107    }
108
109    fn find_not_escaped_from(&self, pattern: char, chars: &mut CharIndices) -> Option<usize> {
110        let mut escaped = false;
111        chars.find_map(|(index, character)| {
112            if escaped {
113                escaped = false;
114                None
115            } else {
116                match character {
117                    '\\' => {
118                        escaped = true;
119                        None
120                    }
121                    value => {
122                        if value == pattern {
123                            Some(index)
124                        } else {
125                            None
126                        }
127                    }
128                }
129            }
130        })
131    }
132
133    super::impl_token_fns!(iter = [token]);
134}
135
136#[cfg(test)]
137mod test {
138    use super::*;
139
140    macro_rules! test_quoted {
141        ($($name:ident($input:literal) => $value:literal),* $(,)?) => {
142            mod single_quoted {
143                use super::*;
144                $(
145                    #[test]
146                    fn $name() {
147                        let quoted = format!("'{}'", $input);
148                        assert_eq!(
149                            StringExpression::new(&quoted)
150                                .expect("unable to parse string")
151                                .get_value(),
152                            StringExpression::from_value($value).get_value(),
153                        );
154                    }
155                )*
156            }
157
158            mod double_quoted {
159                use super::*;
160                $(
161                    #[test]
162                    fn $name() {
163                        let quoted = format!("\"{}\"", $input);
164                        assert_eq!(
165                            StringExpression::new(&quoted)
166                                .expect("unable to parse string")
167                                .get_value(),
168                            StringExpression::from_value($value).get_value(),
169                        );
170                    }
171                )*
172            }
173        };
174    }
175
176    test_quoted!(
177        empty("") => "",
178        hello("hello") => "hello",
179        escaped_new_line("\\n") => "\n",
180        escaped_tab("\\t") => "\t",
181        escaped_backslash("\\\\") => "\\",
182        escaped_carriage_return("\\r") => "\r",
183        escaped_bell("\\a") => "\u{7}",
184        escaped_backspace("\\b") => "\u{8}",
185        escaped_vertical_tab("\\v") => "\u{B}",
186        escaped_form_feed("\\f") => "\u{C}",
187        escaped_null("\\0") => "\0",
188        escaped_two_digits("\\65") => "A",
189        escaped_three_digits("\\123") => "{",
190        escaped_null_hex("\\x00") => "\0",
191        escaped_uppercase_a_hex("\\x41") => "A",
192        escaped_tilde_hex_uppercase("\\x7E") => "~",
193        escaped_tilde_hex_lowercase("\\x7e") => "~",
194        skips_whitespaces_but_no_spaces("\\z") => "",
195        skips_whitespaces("a\\z   \n\n   \\nb") => "a\nb",
196        escaped_unicode_single_digit("\\u{0}") => "\0",
197        escaped_unicode_two_hex_digits("\\u{AB}") => "\u{AB}",
198        escaped_unicode_three_digit("\\u{123}") => "\u{123}",
199        escaped_unicode_last_value("\\u{10FFFF}") => "\u{10FFFF}",
200    );
201
202    macro_rules! test_quoted_failures {
203        ($($name:ident => $input:literal),* $(,)?) => {
204            mod single_quoted_failures {
205                use super::*;
206                $(
207                    #[test]
208                    fn $name() {
209                        let quoted = format!("'{}'", $input);
210                        assert!(StringExpression::new(&quoted).is_err());
211                    }
212                )*
213            }
214
215            mod double_quoted_failures {
216                use super::*;
217                $(
218                    #[test]
219                    fn $name() {
220                        let quoted = format!("\"{}\"", $input);
221                        assert!(StringExpression::new(&quoted).is_err());
222                    }
223                )*
224            }
225        };
226    }
227
228    test_quoted_failures!(
229        single_backslash => "\\",
230        escaped_too_large_ascii => "\\256",
231        escaped_too_large_unicode => "\\u{110000}",
232        escaped_missing_opening_brace_unicode => "\\uAB",
233        escaped_missing_closing_brace_unicode => "\\u{0p",
234    );
235
236    #[test]
237    fn new_removes_double_quotes() {
238        let string = StringExpression::new(r#""hello""#).unwrap();
239
240        assert_eq!(string.get_value(), "hello");
241    }
242
243    #[test]
244    fn new_removes_single_quotes() {
245        let string = StringExpression::new("'hello'").unwrap();
246
247        assert_eq!(string.get_value(), "hello");
248    }
249
250    #[test]
251    fn new_removes_double_brackets() {
252        let string = StringExpression::new("[[hello]]").unwrap();
253
254        assert_eq!(string.get_value(), "hello");
255    }
256
257    #[test]
258    fn new_removes_double_brackets_and_skip_first_new_line() {
259        let string = StringExpression::new("[[\nhello]]").unwrap();
260
261        assert_eq!(string.get_value(), "hello");
262    }
263
264    #[test]
265    fn new_removes_double_brackets_with_one_equals() {
266        let string = StringExpression::new("[=[hello]=]").unwrap();
267
268        assert_eq!(string.get_value(), "hello");
269    }
270
271    #[test]
272    fn new_removes_double_brackets_with_multiple_equals() {
273        let string = StringExpression::new("[==[hello]==]").unwrap();
274
275        assert_eq!(string.get_value(), "hello");
276    }
277
278    #[test]
279    fn new_skip_invalid_escape_in_double_quoted_string() {
280        let string = StringExpression::new("'\\oo'").unwrap();
281
282        assert_eq!(string.get_value(), "oo");
283    }
284
285    #[test]
286    fn new_skip_invalid_escape_in_single_quoted_string() {
287        let string = StringExpression::new("\"\\oo\"").unwrap();
288
289        assert_eq!(string.get_value(), "oo");
290    }
291
292    #[test]
293    fn has_single_quote_is_false_if_no_single_quotes() {
294        let string = StringExpression::from_value("hello");
295
296        assert!(!string.has_single_quote());
297    }
298
299    #[test]
300    fn has_single_quote_is_true_if_unescaped_single_quotes() {
301        let string = StringExpression::from_value("don't");
302
303        assert!(string.has_single_quote());
304    }
305
306    #[test]
307    fn has_single_quote_is_true_if_unescaped_single_quotes_with_escaped_backslash() {
308        let string = StringExpression::from_value(r"don\\'t");
309
310        assert!(string.has_single_quote());
311    }
312
313    #[test]
314    fn has_single_quote_is_false_if_escaped_single_quotes() {
315        let string = StringExpression::from_value(r"don\'t");
316
317        assert!(!string.has_single_quote());
318    }
319
320    #[test]
321    fn has_double_quote_is_false_if_no_double_quotes() {
322        let string = StringExpression::from_value("hello");
323
324        assert!(!string.has_double_quote());
325    }
326
327    #[test]
328    fn has_double_quote_is_true_if_unescaped_double_quotes() {
329        let string = StringExpression::from_value(r#"Say: "Hi!""#);
330
331        assert!(string.has_double_quote());
332    }
333
334    #[test]
335    fn has_double_quote_is_false_if_escaped_double_quotes() {
336        let string = StringExpression::from_value(r#"hel\"o"#);
337
338        assert!(!string.has_double_quote());
339    }
340}