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