Skip to main content

libnewsboat/
keymap.rs

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), // all other escaped characters are passed through, unmodified
27        ))(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("\\")), // `\\` -> `\`
58            value("\"", tag("\"")), // `\"` -> `"`
59            take(1usize),           // all other escaped characters are passed through, unmodified
60        )),
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
119/// Split a semicolon-separated list of operations into a vector.
120///
121/// Each operation is represented by a non-empty sub-vector, where the first element is the name of
122/// the operation, and the rest of the elements are operation's arguments.
123///
124/// Tokens can be double-quoted. Such tokens can contain spaces and C-like escaped sequences: `\n`
125/// for newline, `\r` for carriage return, `\t` for tab, `\"` for double quote, `\\` for backslash.
126/// Unsupported sequences are stripped of the escaping, e.g. `\e` turns into `e`.
127///
128/// This function assumes that the input string:
129/// 1. doesn't contain a comment;
130/// 2. doesn't contain backticks that need to be processed.
131///
132/// Returns a vector of operations togeter with an optional description, as a tuple, or `None` if
133/// the input could not be parsed.
134pub 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}