1use nom::{
2 branch::alt,
3 bytes::complete::{escaped_transform, is_not, tag, take},
4 character::complete::{alpha1, space0, space1},
5 combinator::{complete, eof, map, opt, recognize, value, verify},
6 multi::{many0, many1, separated_list0, separated_list1},
7 sequence::{delimited, preceded},
8 IResult,
9};
10
11fn unquoted_token(input: &str) -> IResult<&str, String> {
12 let parser = map(recognize(is_not("\t\" ;")), String::from);
13 let mut parser = verify(parser, |t: &str| t != "--");
14
15 parser(input)
16}
17
18fn quoted_token<'a>(input: &'a str) -> IResult<&'a str, String> {
19 let parser = escaped_transform(is_not(r#""\"#), '\\', |control_char: &'a str| {
20 alt((
21 value(r#"""#, tag(r#"""#)),
22 value(r"\", tag(r"\")),
23 value("\r", tag("r")),
24 value("\n", tag("n")),
25 value("\t", tag("t")),
26 take(1usize), ))(control_char)
28 });
29
30 let double_quote = tag("\"");
31 let mut parser = delimited(&double_quote, parser, alt((&double_quote, eof)));
32
33 parser(input)
34}
35
36fn token(input: &str) -> IResult<&str, String> {
37 let mut parser = alt((quoted_token, unquoted_token));
38 parser(input)
39}
40
41fn operation_with_args(input: &str) -> IResult<&str, Vec<String>> {
42 let mut parser = separated_list1(space1, token);
43 parser(input)
44}
45
46fn semicolon(input: &str) -> IResult<&str, &str> {
47 delimited(space0, tag(";"), space0)(input)
48}
49
50fn operation_description(input: &str) -> IResult<&str, String> {
51 let start_token = delimited(space0, tag("--"), space0);
52
53 let string_content = escaped_transform(
54 is_not(r#""\"#),
55 '\\',
56 alt((
57 value("\\", tag("\\")), value("\"", tag("\"")), take(1usize), )),
61 );
62
63 let double_quote = tag("\"");
64 let parser = delimited(&double_quote, string_content, &double_quote);
65
66 let mut parser = preceded(start_token, parser);
67
68 parser(input)
69}
70
71fn operation_sequence(
72 input: &str,
73 allow_description: bool,
74) -> IResult<&str, (Vec<Vec<String>>, Option<String>)> {
75 let (input, _) = space0(input)?;
76 let (input, _) = many0(semicolon)(input)?;
77 let (input, operations) = separated_list0(many1(semicolon), operation_with_args)(input)?;
78 let (input, _) = many0(semicolon)(input)?;
79
80 let (input, optional_description) = if allow_description {
81 opt(operation_description)(input)?
82 } else {
83 (input, None)
84 };
85
86 let (input, _) = space0(input)?;
87 let (input, _) = complete(eof)(input)?;
88
89 Ok((input, (operations, optional_description)))
90}
91
92fn contexts(input: &str) -> IResult<&str, Vec<&str>> {
93 separated_list1(tag(","), alpha1)(input)
94}
95
96fn key_sequence(input: &str) -> IResult<&str, String> {
97 token(input)
98}
99
100fn binding(input: &str) -> IResult<&str, Binding> {
101 let (input, _) = space0(input)?;
102 let (input, key_sequence) = key_sequence(input)?;
103 let (input, _) = space1(input)?;
104 let (input, contexts) = contexts(input)?;
105 let (input, _) = space1(input)?;
106 let (input, (operations, description)) = operation_sequence(input, true)?;
107
108 Ok((
109 input,
110 Binding {
111 key_sequence,
112 contexts: contexts.into_iter().map(|s| s.to_owned()).collect(),
113 operations,
114 description,
115 },
116 ))
117}
118
119pub fn tokenize_operation_sequence(
135 input: &str,
136 allow_description: bool,
137) -> Option<(Vec<Vec<String>>, Option<String>)> {
138 match operation_sequence(input, allow_description) {
139 Ok((_leftovers, tokens)) => Some(tokens),
140 Err(_error) => None,
141 }
142}
143
144#[derive(Debug, PartialEq)]
145pub struct Binding {
146 pub key_sequence: String,
147 pub contexts: Vec<String>,
148 pub operations: Vec<Vec<String>>,
149 pub description: Option<String>,
150}
151
152pub fn tokenize_binding(input: &str) -> Option<Binding> {
153 match binding(input) {
154 Ok((_, binding)) => Some(binding),
155 Err(_error) => None,
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::contexts;
162 use super::key_sequence;
163 use super::tokenize_binding;
164 use super::tokenize_operation_sequence;
165
166 macro_rules! vec_of_strings {
167 ($($x:expr),*) => (vec![$($x.to_string()),*]);
168 }
169
170 #[test]
171 fn t_tokenize_operation_sequence_works_for_all_cpp_inputs() {
172 assert_eq!(
173 tokenize_operation_sequence("", true).unwrap(),
174 (Vec::<Vec<String>>::new(), None)
175 );
176 assert_eq!(
177 tokenize_operation_sequence("open", true).unwrap(),
178 (vec![vec_of_strings!["open"]], None)
179 );
180 assert_eq!(
181 tokenize_operation_sequence("open-all-unread-in-browser-and-mark-read", true).unwrap(),
182 (
183 vec![vec_of_strings!["open-all-unread-in-browser-and-mark-read"]],
184 None
185 )
186 );
187 assert_eq!(
188 tokenize_operation_sequence("; ; ; ;", true).unwrap(),
189 (Vec::<Vec<String>>::new(), None)
190 );
191 assert_eq!(
192 tokenize_operation_sequence("open ; next", true).unwrap(),
193 (vec![vec_of_strings!["open"], vec_of_strings!["next"]], None)
194 );
195 assert_eq!(
196 tokenize_operation_sequence("open ; next ; prev", true).unwrap(),
197 (
198 vec![
199 vec_of_strings!["open"],
200 vec_of_strings!["next"],
201 vec_of_strings!["prev"]
202 ],
203 None
204 )
205 );
206 assert_eq!(
207 tokenize_operation_sequence("open ; next ; prev ; quit", true).unwrap(),
208 (
209 vec![
210 vec_of_strings!["open"],
211 vec_of_strings!["next"],
212 vec_of_strings!["prev"],
213 vec_of_strings!["quit"]
214 ],
215 None
216 )
217 );
218 assert_eq!(
219 tokenize_operation_sequence(r#"set "arg 1""#, true).unwrap(),
220 (vec![vec_of_strings!["set", "arg 1"]], None)
221 );
222 assert_eq!(
223 tokenize_operation_sequence(r#"set "arg 1" ; set "arg 2" "arg 3""#, true).unwrap(),
224 (
225 vec![
226 vec_of_strings!["set", "arg 1"],
227 vec_of_strings!["set", "arg 2", "arg 3"]
228 ],
229 None
230 )
231 );
232 assert_eq!(
233 tokenize_operation_sequence(r#"set browser "firefox"; open-in-browser"#, true).unwrap(),
234 (
235 vec![
236 vec_of_strings!["set", "browser", "firefox"],
237 vec_of_strings!["open-in-browser"]
238 ],
239 None
240 )
241 );
242 assert_eq!(
243 tokenize_operation_sequence("set browser firefox; open-in-browser", true).unwrap(),
244 (
245 vec![
246 vec_of_strings!["set", "browser", "firefox"],
247 vec_of_strings!["open-in-browser"]
248 ],
249 None
250 )
251 );
252 assert_eq!(
253 tokenize_operation_sequence("open-in-browser; quit", true).unwrap(),
254 (
255 vec![vec_of_strings!["open-in-browser"], vec_of_strings!["quit"]],
256 None
257 )
258 );
259 assert_eq!(
260 tokenize_operation_sequence(
261 r#"open; set browser "firefox --private-window"; quit"#,
262 true
263 )
264 .unwrap(),
265 (
266 vec![
267 vec_of_strings!["open"],
268 vec_of_strings!["set", "browser", "firefox --private-window"],
269 vec_of_strings!["quit"]
270 ],
271 None
272 )
273 );
274 assert_eq!(
275 tokenize_operation_sequence(
276 r#"open ;set browser "firefox --private-window" ;quit"#,
277 true
278 )
279 .unwrap(),
280 (
281 vec![
282 vec_of_strings!["open"],
283 vec_of_strings!["set", "browser", "firefox --private-window"],
284 vec_of_strings!["quit"]
285 ],
286 None
287 )
288 );
289 assert_eq!(
290 tokenize_operation_sequence(
291 r#"open;set browser "firefox --private-window";quit"#,
292 true
293 )
294 .unwrap(),
295 (
296 vec![
297 vec_of_strings!["open"],
298 vec_of_strings!["set", "browser", "firefox --private-window"],
299 vec_of_strings!["quit"]
300 ],
301 None
302 )
303 );
304 assert_eq!(
305 tokenize_operation_sequence("; ;; ; open", true).unwrap(),
306 (vec![vec_of_strings!["open"]], None)
307 );
308 assert_eq!(
309 tokenize_operation_sequence(";;; ;; ; open", true).unwrap(),
310 (vec![vec_of_strings!["open"]], None)
311 );
312 assert_eq!(
313 tokenize_operation_sequence(";;; ;; ; open ;", true).unwrap(),
314 (vec![vec_of_strings!["open"]], None)
315 );
316 assert_eq!(
317 tokenize_operation_sequence(";;; ;; ; open ;; ;", true).unwrap(),
318 (vec![vec_of_strings!["open"]], None)
319 );
320 assert_eq!(
321 tokenize_operation_sequence(";;; ;; ; open ; ;;;;", true).unwrap(),
322 (vec![vec_of_strings!["open"]], None)
323 );
324 assert_eq!(
325 tokenize_operation_sequence(";;; open ; ;;;;", true).unwrap(),
326 (vec![vec_of_strings!["open"]], None)
327 );
328 assert_eq!(
329 tokenize_operation_sequence("; open ;; ;; ;", true).unwrap(),
330 (vec![vec_of_strings!["open"]], None)
331 );
332 assert_eq!(
333 tokenize_operation_sequence("open ; ;;; ;;", true).unwrap(),
334 (vec![vec_of_strings!["open"]], None)
335 );
336 assert_eq!(
337 tokenize_operation_sequence(
338 r#"set browser "sleep 3; do-something ; echo hi"; open-in-browser"#,
339 true
340 )
341 .unwrap(),
342 (
343 vec![
344 vec_of_strings!["set", "browser", "sleep 3; do-something ; echo hi"],
345 vec_of_strings!["open-in-browser"]
346 ],
347 None
348 )
349 );
350 }
351
352 #[test]
353 fn t_tokenize_operation_sequence_ignores_escaped_sequences_outside_double_quotes() {
354 assert_eq!(
355 tokenize_operation_sequence(r"\t", true).unwrap(),
356 (vec![vec_of_strings![r"\t"]], None)
357 );
358 assert_eq!(
359 tokenize_operation_sequence(r"\r", true).unwrap(),
360 (vec![vec_of_strings![r"\r"]], None)
361 );
362 assert_eq!(
363 tokenize_operation_sequence(r"\n", true).unwrap(),
364 (vec![vec_of_strings![r"\n"]], None)
365 );
366 assert_eq!(
367 tokenize_operation_sequence(r"\v", true).unwrap(),
368 (vec![vec_of_strings![r"\v"]], None)
369 );
370 assert_eq!(
371 tokenize_operation_sequence(r"\\", true).unwrap(),
372 (vec![vec_of_strings![r"\\"]], None)
373 );
374 }
375
376 #[test]
377 fn t_tokenize_operation_sequence_expands_escaped_sequences_inside_double_quotes() {
378 assert_eq!(
379 tokenize_operation_sequence(r#""\t""#, true).unwrap(),
380 (vec![vec_of_strings!["\t"]], None)
381 );
382 assert_eq!(
383 tokenize_operation_sequence(r#""\r""#, true).unwrap(),
384 (vec![vec_of_strings!["\r"]], None)
385 );
386 assert_eq!(
387 tokenize_operation_sequence(r#""\n""#, true).unwrap(),
388 (vec![vec_of_strings!["\n"]], None)
389 );
390 assert_eq!(
391 tokenize_operation_sequence(r#""\"""#, true).unwrap(),
392 (vec![vec_of_strings!["\""]], None)
393 );
394 assert_eq!(
395 tokenize_operation_sequence(r#""\\""#, true).unwrap(),
396 (vec![vec_of_strings!["\\"]], None)
397 );
398 }
399
400 #[test]
401 fn t_tokenize_operation_sequence_passes_through_unsupported_escaped_chars_inside_double_quotes()
402 {
403 assert_eq!(
404 tokenize_operation_sequence(r#""\1""#, true).unwrap(),
405 (vec![vec_of_strings!["1"]], None)
406 );
407 assert_eq!(
408 tokenize_operation_sequence(r#""\W""#, true).unwrap(),
409 (vec![vec_of_strings!["W"]], None)
410 );
411 assert_eq!(
412 tokenize_operation_sequence(r#""\b""#, true).unwrap(),
413 (vec![vec_of_strings!["b"]], None)
414 );
415 assert_eq!(
416 tokenize_operation_sequence(r#""\d""#, true).unwrap(),
417 (vec![vec_of_strings!["d"]], None)
418 );
419 assert_eq!(
420 tokenize_operation_sequence(r#""\x""#, true).unwrap(),
421 (vec![vec_of_strings!["x"]], None)
422 );
423 }
424
425 #[test]
426 fn t_tokenize_operation_sequence_implicitly_closes_double_quotes_at_end_of_input() {
427 assert_eq!(
428 tokenize_operation_sequence(r#"set "arg 1"#, true).unwrap(),
429 (vec![vec_of_strings!["set", "arg 1"]], None)
430 );
431 }
432
433 #[test]
434 fn t_tokenize_operation_sequence_allows_single_character_unquoted() {
435 assert_eq!(
436 tokenize_operation_sequence(r#"set a b"#, true).unwrap(),
437 (vec![vec_of_strings!["set", "a", "b"]], None)
438 );
439 }
440
441 #[test]
442 fn t_tokenize_operation_sequence_ignores_leading_and_trailing_whitespace() {
443 assert_eq!(
444 tokenize_operation_sequence(" \t set a b \t ", true).unwrap(),
445 (vec![vec_of_strings!["set", "a", "b"]], None)
446 );
447
448 let (operations, description) =
449 tokenize_operation_sequence(" \t set a b -- \"description\" \t ", true).unwrap();
450 assert_eq!(operations, vec![vec_of_strings!["set", "a", "b"]]);
451 assert_eq!(description, Some("description".to_string()));
452 }
453
454 #[test]
455 fn t_tokenize_operation_sequence_allows_tabs_between_arguments() {
456 assert_eq!(
457 tokenize_operation_sequence("\tset\ta\tb\t;\topen\t", true).unwrap(),
458 (
459 vec![vec_of_strings!["set", "a", "b"], vec_of_strings!["open"]],
460 None
461 )
462 );
463 }
464
465 #[test]
466 fn t_tokenize_operation_sequence_supports_optional_description() {
467 let (operations, description) =
468 tokenize_operation_sequence(r#"set a b -- "name of function""#, true).unwrap();
469 assert_eq!(operations, vec![vec_of_strings!["set", "a", "b"]]);
470 assert_eq!(description, Some("name of function".to_string()));
471 }
472
473 #[test]
474 fn t_tokenize_operation_sequence_allows_dashdash_in_quoted_string() {
475 let (operations, description) =
476 tokenize_operation_sequence(r#"set a b "--" "name of function""#, true).unwrap();
477 assert_eq!(
478 operations,
479 vec![vec_of_strings!["set", "a", "b", "--", "name of function"]]
480 );
481 assert!(description.is_none());
482 }
483
484 #[test]
485 fn t_tokenize_operation_sequence_allows_missing_description() {
486 let (operations, description) = tokenize_operation_sequence(r#"set a b"#, true).unwrap();
487 assert_eq!(operations, vec![vec_of_strings!["set", "a", "b"]]);
488 assert!(description.is_none());
489 }
490
491 #[test]
492 fn t_tokenize_operation_sequence_can_disallow_descriptions() {
493 let allow_description = false;
494 assert_eq!(
495 tokenize_operation_sequence(r#"set a b"#, allow_description).unwrap(),
496 (vec![vec_of_strings!["set", "a", "b"]], None)
497 );
498
499 assert_eq!(
500 tokenize_operation_sequence(
501 r#"set a b -- "disallowed description""#,
502 allow_description
503 ),
504 None
505 );
506 }
507
508 fn verify_parsed_description(input: &str, expected_output: &str) {
509 let (_operations, description) = tokenize_operation_sequence(input, true).unwrap();
510 assert_eq!(description, Some(expected_output.to_string()));
511 }
512
513 #[test]
514 fn t_tokenize_operation_sequence_ignores_whitespace_preceding_description() {
515 verify_parsed_description(
516 &format!(r#"set "a" "b"{}--{}"description""#, "", ""),
517 "description",
518 );
519 verify_parsed_description(
520 &format!(r#"set "a" "b"{}--{}"description""#, " \t", ""),
521 "description",
522 );
523 verify_parsed_description(
524 &format!(r#"set "a" "b"{}--{}"description""#, "", " \t"),
525 "description",
526 );
527 verify_parsed_description(
528 &format!(r#"set "a" "b"{}--{}"description""#, " \t", " \t"),
529 "description",
530 );
531 }
532
533 #[test]
534 fn t_tokenize_operation_sequence_includes_whitespace_in_description() {
535 verify_parsed_description(
536 r#"open -- "multi-word description""#,
537 "multi-word description",
538 );
539 verify_parsed_description(
540 r#"open -- " leading and trailing ""#,
541 " leading and trailing ",
542 );
543 }
544
545 #[test]
546 fn t_tokenize_operation_sequence_requires_closing_quote_in_description() {
547 assert_eq!(
548 tokenize_operation_sequence(r#"open -- "description not closed "#, true),
549 None
550 );
551 }
552
553 #[test]
554 fn t_tokenize_operation_sequence_requires_quoted_string_after_delimiter() {
555 assert_eq!(tokenize_operation_sequence(r#"open --"#, true), None);
556 assert_eq!(
557 tokenize_operation_sequence(r#"open -- invalid description"#, true),
558 None
559 );
560 }
561
562 #[test]
563 fn t_tokenize_operation_sequence_handles_escaped_quotes_in_description() {
564 verify_parsed_description(r#"open -- "internal \" quote""#, r#"internal " quote"#);
565 verify_parsed_description(
566 r#"open -- "\"internal \"\"\" quotes\"""#,
567 r#""internal """ quotes""#,
568 );
569 }
570
571 #[test]
572 fn t_tokenize_operation_sequence_handles_escaped_backslashes_in_description() {
573 verify_parsed_description(
574 r#"open -- "internal \\ backslash""#,
575 r"internal \ backslash",
576 );
577 verify_parsed_description(
578 r#"open -- "\"internal \\\\\\ backslashes\"""#,
579 r#""internal \\\ backslashes""#,
580 );
581 }
582
583 #[test]
584 fn t_tokenize_operation_sequence_passes_through_unknown_escaped_characters_in_description() {
585 verify_parsed_description(r#"open -- "\f\o\o\"\\\b\a\r""#, r#"foo"\bar"#);
586 }
587
588 #[test]
589 fn t_key_sequence() {
590 assert_eq!(key_sequence("x").unwrap().1, "x");
591 assert_eq!(key_sequence("\"x\"").unwrap().1, "x");
592 assert_eq!(key_sequence("gg").unwrap().1, "gg");
593 assert_eq!(key_sequence("<ENTER>").unwrap().1, "<ENTER>");
594 assert_eq!(key_sequence("^U<ENTER>").unwrap().1, "^U<ENTER>");
595 }
596
597 #[test]
598 fn t_contexts() {
599 assert_eq!(contexts("everywhere").unwrap().1, vec!["everywhere"]);
600 assert_eq!(contexts("feedlist").unwrap().1, vec!["feedlist"]);
601 assert_eq!(
602 contexts("feedlist,articlelist").unwrap().1,
603 vec!["feedlist", "articlelist"]
604 );
605 }
606
607 #[test]
608 fn t_test_tokenize_binding_no_description() {
609 let input = "q everywhere quit";
610
611 let parsed_binding = tokenize_binding(input).unwrap();
612
613 assert_eq!(parsed_binding.key_sequence, "q");
614 assert_eq!(parsed_binding.contexts, vec!["everywhere"]);
615 assert_eq!(parsed_binding.operations, vec![vec_of_strings!("quit")]);
616 assert_eq!(parsed_binding.description, None);
617 }
618
619 #[test]
620 fn t_test_tokenize_binding_with_description() {
621 let input = "gg feedlist,articlelist home ; open -- \"Open entry at top of list\"";
622
623 let parsed_binding = tokenize_binding(input).unwrap();
624
625 assert_eq!(parsed_binding.key_sequence, "gg");
626 assert_eq!(parsed_binding.contexts, vec!["feedlist", "articlelist"]);
627 assert_eq!(
628 parsed_binding.operations,
629 vec![vec_of_strings!("home"), vec_of_strings!("open")]
630 );
631 assert_eq!(
632 parsed_binding.description,
633 Some("Open entry at top of list".to_string())
634 );
635 }
636
637 #[test]
638 fn t_test_tokenize_binding_missing_parts() {
639 assert_eq!(tokenize_binding(""), None);
640 assert_eq!(tokenize_binding("gg"), None);
641 assert_eq!(tokenize_binding("gg everywhere"), None);
642 }
643
644 #[test]
645 fn t_test_tokenize_binding_incomplete_description_syntax() {
646 assert_eq!(tokenize_binding("q everywhere quit -- "), None);
647 }
648}