darklua_core/nodes/expressions/
string.rs1use 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("ed)
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("ed)
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("ed).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("ed).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}