1pub fn tokenize(input: &str) -> Vec<String> {
4 let chars: Vec<char> = input.chars().collect();
8 let n = chars.len();
9 let mut tokens = Vec::new();
10 let mut i = 0;
11
12 while i < n {
13 while i < n && chars[i] == ' ' {
15 i += 1;
16 }
17 if i >= n {
18 break;
19 }
20
21 if chars[i] == '"' {
22 i += 1;
24 let mut token = String::new();
25 while i < n && chars[i] != '"' {
26 if chars[i] == '\\' && i + 1 < n {
27 let next = chars[i + 1];
28 if next == 'n' {
29 token.push('\n');
30 i += 2;
31 } else {
32 i += 1;
33 token.push(chars[i]);
34 i += 1;
35 }
36 } else {
37 token.push(chars[i]);
38 i += 1;
39 }
40 }
41 if i < n {
42 i += 1; }
44 tokens.push(token);
45 } else {
46 let mut token = String::new();
48 while i < n && chars[i] != ' ' {
49 if chars[i] == '"' {
50 token.push('"');
51 i += 1;
52 while i < n && chars[i] != '"' {
53 if chars[i] == '\\' && i + 1 < n {
54 let next = chars[i + 1];
55 if next == 'n' {
56 token.push('\n');
57 i += 2;
58 } else {
59 i += 1;
60 token.push(chars[i]);
61 i += 1;
62 }
63 } else {
64 token.push(chars[i]);
65 i += 1;
66 }
67 }
68 if i < n {
69 token.push('"');
70 i += 1;
71 }
72 } else {
73 token.push(chars[i]);
74 i += 1;
75 }
76 }
77 tokens.push(token.replace("\\n", "\n"));
78 }
79 }
80
81 tokens
82}
83
84pub fn is_key_value(token: &str) -> bool {
86 if token.starts_with('@') {
87 return false;
88 }
89 if is_arrow(token) {
90 return false;
91 }
92 let Some((key, value)) = token.split_once(':') else {
93 return false;
94 };
95 if key.is_empty() || value.is_empty() {
96 return false;
97 }
98 key.chars()
99 .all(|ch| matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-'))
100}
101
102pub fn parse_key_value(token: &str) -> (String, String) {
105 let (key, raw_value) = token.split_once(':').unwrap();
106 let value = raw_value
107 .strip_prefix('"')
108 .and_then(|s| s.strip_suffix('"'))
109 .unwrap_or(raw_value);
110 (key.to_string(), value.to_string())
111}
112
113pub fn parse_key_value_with_meta(token: &str) -> (String, String, bool) {
115 let (key, raw_value) = token.split_once(':').unwrap();
116 if let Some(unquoted) = raw_value.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
117 (key.to_string(), unquoted.to_string(), true)
118 } else {
119 (key.to_string(), raw_value.to_string(), false)
120 }
121}
122
123pub fn is_arrow(token: &str) -> bool {
125 token == "->" || token == "<->" || token == "--"
126}
127
128pub fn is_selector(token: &str) -> bool {
130 token.starts_with('@')
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_tokenize_simple_tokens() {
139 let got = tokenize("add svc AuthService");
140 assert_eq!(got, vec!["add", "svc", "AuthService"]);
141 }
142
143 #[test]
144 fn test_tokenize_quoted_strings() {
145 let got = tokenize(r#"add svc "Auth Service" theme:blue"#);
146 assert_eq!(got, vec!["add", "svc", "Auth Service", "theme:blue"]);
147 }
148
149 #[test]
150 fn test_tokenize_escaped_quotes() {
151 let got = tokenize(r#"label A "say \"hello\"""#);
152 assert_eq!(got, vec!["label", "A", r#"say "hello""#]);
153 }
154
155 #[test]
156 fn test_tokenize_empty_input() {
157 let got = tokenize("");
158 assert!(got.is_empty());
159 }
160
161 #[test]
162 fn test_tokenize_whitespace_only() {
163 let got = tokenize(" ");
164 assert!(got.is_empty());
165 }
166
167 #[test]
168 fn test_tokenize_multiple_spaces() {
169 let got = tokenize("add svc A");
170 assert_eq!(got, vec!["add", "svc", "A"]);
171 }
172
173 #[test]
174 fn test_tokenize_newline_in_unquoted() {
175 let got = tokenize(r"add svc Container\nRegistry");
176 assert_eq!(got, vec!["add", "svc", "Container\nRegistry"]);
177 }
178
179 #[test]
180 fn test_tokenize_newline_in_quoted() {
181 let got = tokenize(r#"add svc "Container\nRegistry""#);
182 assert_eq!(got, vec!["add", "svc", "Container\nRegistry"]);
183 }
184
185 #[test]
186 fn test_tokenize_embedded_quoted_value() {
187 let got = tokenize(r#"label:"Line1\nLine2""#);
188 assert_eq!(got, vec!["label:\"Line1\nLine2\""]);
189 }
190
191 #[test]
192 fn test_tokenize_multiple_newlines() {
193 let got = tokenize(r"add svc A\nB\nC");
194 assert_eq!(got, vec!["add", "svc", "A\nB\nC"]);
195 }
196
197 #[test]
198 fn test_tokenize_single_token() {
199 let got = tokenize("add");
200 assert_eq!(got, vec!["add"]);
201 }
202
203 #[test]
204 fn test_tokenize_empty_quoted_string() {
205 let got = tokenize(r#""""#);
206 assert_eq!(got, vec![""]);
207 }
208
209 #[test]
210 fn test_tokenize_unclosed_quote() {
211 let got = tokenize(r#""hello world"#);
212 assert_eq!(got, vec!["hello world"]);
213 }
214
215 #[test]
216 fn test_tokenize_escaped_backslash() {
217 let got = tokenize(r#""path\\dir""#);
218 assert_eq!(got, vec![r"path\dir"]);
219 }
220
221 #[test]
222 fn test_tokenize_colons_in_value() {
223 let got = tokenize("url:http://example.com");
224 assert_eq!(got, vec!["url:http://example.com"]);
225 }
226
227 #[test]
228 fn test_tokenize_unicode_labels() {
229 let got = tokenize("café 日本語 😀");
233 assert_eq!(got, vec!["café", "日本語", "😀"]);
234 }
235
236 #[test]
237 fn test_tokenize_unicode_quoted_label() {
238 let got = tokenize(r#"label "Café Étudiant""#);
239 assert_eq!(got, vec!["label", "Café Étudiant"]);
240 }
241
242 #[test]
243 fn test_is_key_value() {
244 let tests = vec![
245 ("theme:blue", true),
246 ("url:http://x", true),
247 ("@type:db", false),
248 ("->", false),
249 ("hello", false),
250 ("key:", false),
251 (":value", false),
252 ];
253 for (input, want) in tests {
254 assert_eq!(
255 is_key_value(input),
256 want,
257 "is_key_value({:?}) = {}, want {}",
258 input,
259 is_key_value(input),
260 want
261 );
262 }
263 }
264
265 #[test]
266 fn test_parse_key_value() {
267 let (key, value) = parse_key_value("theme:blue");
268 assert_eq!(key, "theme");
269 assert_eq!(value, "blue");
270
271 let (key, value) = parse_key_value("url:http://x:8080");
272 assert_eq!(key, "url");
273 assert_eq!(value, "http://x:8080");
274 }
275
276 #[test]
277 fn test_parse_key_value_strips_quotes() {
278 let (key, value) = parse_key_value("label:\"Line1\nLine2\"");
279 assert_eq!(key, "label");
280 assert_eq!(value, "Line1\nLine2");
281 }
282
283 #[test]
284 fn test_parse_key_value_with_meta() {
285 let (key, value, was_quoted) = parse_key_value_with_meta(r#"engine_version:"15""#);
286 assert_eq!(key, "engine_version");
287 assert_eq!(value, "15");
288 assert!(was_quoted);
289
290 let (key, value, was_quoted) = parse_key_value_with_meta("port:80");
291 assert_eq!(key, "port");
292 assert_eq!(value, "80");
293 assert!(!was_quoted);
294 }
295
296 #[test]
297 fn test_is_arrow() {
298 assert!(is_arrow("->"));
299 assert!(is_arrow("<->"));
300 assert!(is_arrow("--"));
301 assert!(!is_arrow("=>"));
302 assert!(!is_arrow("add"));
303 }
304
305 #[test]
306 fn test_is_selector() {
307 assert!(is_selector("@type:db"));
308 assert!(is_selector("@all"));
309 assert!(!is_selector("type:db"));
310 assert!(!is_selector("add"));
311 }
312}