toon_format/utils/
string.rs

1use crate::{
2    types::Delimiter,
3    utils::literal,
4};
5
6/// Escape special characters in a string for quoted output.
7pub fn escape_string(s: &str) -> String {
8    let mut result = String::with_capacity(s.len());
9
10    for ch in s.chars() {
11        match ch {
12            '\n' => result.push_str("\\n"),
13            '\r' => result.push_str("\\r"),
14            '\t' => result.push_str("\\t"),
15            '"' => result.push_str("\\\""),
16            '\\' => result.push_str("\\\\"),
17            _ => result.push(ch),
18        }
19    }
20
21    result
22}
23
24/// Unescape special characters in a quoted string.
25pub fn unescape_string(s: &str) -> String {
26    let mut result = String::with_capacity(s.len());
27    let mut chars = s.chars();
28
29    while let Some(ch) = chars.next() {
30        if ch == '\\' {
31            if let Some(next) = chars.next() {
32                match next {
33                    'n' => result.push('\n'),
34                    'r' => result.push('\r'),
35                    't' => result.push('\t'),
36                    '"' => result.push('"'),
37                    '\\' => result.push('\\'),
38                    _ => {
39                        result.push('\\');
40                        result.push(next);
41                    }
42                }
43            } else {
44                result.push('\\');
45            }
46        } else {
47            result.push(ch);
48        }
49    }
50
51    result
52}
53
54/// Check if a key can be written without quotes (alphanumeric, underscore,
55/// dot).
56pub fn is_valid_unquoted_key(key: &str) -> bool {
57    if key.is_empty() {
58        return false;
59    }
60
61    let mut chars = key.chars();
62    let first = match chars.next() {
63        Some(c) => c,
64        None => return false,
65    };
66
67    if !first.is_alphabetic() && first != '_' {
68        return false;
69    }
70
71    chars.all(|c| c.is_alphanumeric() || c == '_' || c == '.')
72}
73
74/// Determine if a string needs quoting based on content and delimiter.
75pub fn needs_quoting(s: &str, delimiter: char) -> bool {
76    if s.is_empty() {
77        return true;
78    }
79
80    if literal::is_literal_like(s) {
81        return true;
82    }
83
84    if s.chars().any(literal::is_structural_char) {
85        return true;
86    }
87
88    if s.contains('\\') || s.contains('"') {
89        return true;
90    }
91
92    if s.contains(delimiter) {
93        return true;
94    }
95
96    if s.contains('\n') || s.contains('\r') || s.contains('\t') {
97        return true;
98    }
99
100    if s.starts_with(char::is_whitespace) || s.ends_with(char::is_whitespace) {
101        return true;
102    }
103
104    if s.starts_with("-") {
105        return true;
106    }
107
108    // Check for leading zeros (e.g., "05", "007", "0123")
109    // Numbers with leading zeros must be quoted
110    if s.starts_with('0') && s.len() > 1 && s.chars().nth(1).is_some_and(|c| c.is_ascii_digit()) {
111        return true;
112    }
113
114    false
115}
116
117/// Quote and escape a string.
118pub fn quote_string(s: &str) -> String {
119    format!("\"{}\"", escape_string(s))
120}
121
122pub fn split_by_delimiter(s: &str, delimiter: Delimiter) -> Vec<String> {
123    let mut result = Vec::new();
124    let mut current = String::new();
125    let mut in_quotes = false;
126    let chars = s.chars().peekable();
127    let delim_char = delimiter.as_char();
128
129    for ch in chars {
130        if ch == '"' && (current.is_empty() || !current.ends_with('\\')) {
131            in_quotes = !in_quotes;
132            current.push(ch);
133        } else if ch == delim_char && !in_quotes {
134            result.push(current.trim().to_string());
135            current.clear();
136        } else {
137            current.push(ch);
138        }
139    }
140
141    if !current.is_empty() {
142        result.push(current.trim().to_string());
143    }
144
145    result
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_escape_string() {
154        assert_eq!(escape_string("hello"), "hello");
155        assert_eq!(escape_string("hello\nworld"), "hello\\nworld");
156        assert_eq!(escape_string("say \"hi\""), "say \\\"hi\\\"");
157        assert_eq!(escape_string("back\\slash"), "back\\\\slash");
158    }
159
160    #[test]
161    fn test_unescape_string() {
162        assert_eq!(unescape_string("hello"), "hello");
163        assert_eq!(unescape_string("hello\\nworld"), "hello\nworld");
164        assert_eq!(unescape_string("say \\\"hi\\\""), "say \"hi\"");
165        assert_eq!(unescape_string("back\\\\slash"), "back\\slash");
166    }
167
168    #[test]
169    fn test_needs_quoting() {
170        let comma = Delimiter::Comma.as_char();
171
172        assert!(needs_quoting("", comma));
173
174        assert!(needs_quoting("true", comma));
175        assert!(needs_quoting("false", comma));
176        assert!(needs_quoting("null", comma));
177        assert!(needs_quoting("123", comma));
178
179        assert!(needs_quoting("hello[world]", comma));
180        assert!(needs_quoting("key:value", comma));
181
182        assert!(needs_quoting("a,b", comma));
183        assert!(!needs_quoting("a,b", Delimiter::Pipe.as_char()));
184
185        assert!(!needs_quoting("hello world", comma));
186        assert!(needs_quoting(" hello", comma));
187        assert!(needs_quoting("hello ", comma));
188
189        assert!(!needs_quoting("hello", comma));
190        assert!(!needs_quoting("world", comma));
191        assert!(!needs_quoting("helloworld", comma));
192    }
193
194    #[test]
195    fn test_quote_string() {
196        assert_eq!(quote_string("hello"), "\"hello\"");
197        assert_eq!(quote_string("hello\nworld"), "\"hello\\nworld\"");
198    }
199
200    #[test]
201    fn test_split_by_delimiter() {
202        let comma = Delimiter::Comma;
203
204        assert_eq!(split_by_delimiter("a,b,c", comma), vec!["a", "b", "c"]);
205
206        assert_eq!(split_by_delimiter("a, b, c", comma), vec!["a", "b", "c"]);
207
208        assert_eq!(split_by_delimiter("\"a,b\",c", comma), vec!["\"a,b\"", "c"]);
209    }
210
211    #[test]
212    fn test_is_valid_unquoted_key() {
213        // Valid keys (should return true)
214        assert!(is_valid_unquoted_key("normal_key"));
215        assert!(is_valid_unquoted_key("key123"));
216        assert!(is_valid_unquoted_key("key.value"));
217        assert!(is_valid_unquoted_key("_private"));
218        assert!(is_valid_unquoted_key("KeyName"));
219        assert!(is_valid_unquoted_key("key_name"));
220        assert!(is_valid_unquoted_key("key.name.sub"));
221        assert!(is_valid_unquoted_key("a"));
222        assert!(is_valid_unquoted_key("_"));
223        assert!(is_valid_unquoted_key("key_123.value"));
224
225        assert!(!is_valid_unquoted_key(""));
226        assert!(!is_valid_unquoted_key("123"));
227        assert!(!is_valid_unquoted_key("key:value"));
228        assert!(!is_valid_unquoted_key("key-value"));
229        assert!(!is_valid_unquoted_key("key value"));
230        assert!(!is_valid_unquoted_key(".key"));
231        assert!(is_valid_unquoted_key("key.value.sub."));
232        assert!(is_valid_unquoted_key("key."));
233        assert!(!is_valid_unquoted_key("key[value]"));
234        assert!(!is_valid_unquoted_key("key{value}"));
235    }
236}