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.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("ed)
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("ed)
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("ed).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("ed).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}