toon_format/utils/
string.rs1use crate::{
2 types::Delimiter,
3 utils::literal,
4};
5
6pub 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
24pub 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
54pub 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
74pub 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 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
117pub 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 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}