diff --git a/src/config/mod.rs b/src/config/mod.rs index 361751b..32bfb93 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -55,7 +55,8 @@ fn default_word_separators() -> Vec { vec![' ', ',', '.', '\r', '\n', 22u8 fn default_toggle_interval() -> u32 { 230 } fn default_preserve_clipboard() -> bool {false} fn default_passive_match_regex() -> String{ "(?P:\\p{L}+)(/(?P.*)/)?".to_owned() } -fn default_passive_arg_regex() -> String{ "((\\\\/)|[^/])+".to_owned() } +fn default_passive_arg_delimiter() -> char { '/' } +fn default_passive_arg_escape() -> char { '\\' } fn default_backspace_limit() -> i32 { 3 } fn default_exclude_default_matches() -> bool {false} fn default_matches() -> Vec { Vec::new() } @@ -107,8 +108,11 @@ pub struct Configs { #[serde(default = "default_passive_match_regex")] pub passive_match_regex: String, - #[serde(default = "default_passive_arg_regex")] - pub passive_arg_regex: String, + #[serde(default = "default_passive_arg_delimiter")] + pub passive_arg_delimiter: char, + + #[serde(default = "default_passive_arg_escape")] + pub passive_arg_escape: char, #[serde(default)] pub paste_shortcut: PasteShortcut, @@ -159,7 +163,8 @@ impl Configs { validate_field!(result, self.use_system_agent, default_use_system_agent()); validate_field!(result, self.preserve_clipboard, default_preserve_clipboard()); validate_field!(result, self.passive_match_regex, default_passive_match_regex()); - validate_field!(result, self.passive_arg_regex, default_passive_arg_regex()); + validate_field!(result, self.passive_arg_delimiter, default_passive_arg_delimiter()); + validate_field!(result, self.passive_arg_escape, default_passive_arg_escape()); result } diff --git a/src/extension/date.rs b/src/extension/date.rs index 6c81924..431b4e5 100644 --- a/src/extension/date.rs +++ b/src/extension/date.rs @@ -33,7 +33,7 @@ impl super::Extension for DateExtension { String::from("date") } - fn calculate(&self, params: &Mapping) -> Option { + fn calculate(&self, params: &Mapping, _: &Vec) -> Option { let now: DateTime = Local::now(); let format = params.get(&Value::from("format")); diff --git a/src/extension/mod.rs b/src/extension/mod.rs index 4c0f11e..ccb6934 100644 --- a/src/extension/mod.rs +++ b/src/extension/mod.rs @@ -26,7 +26,7 @@ mod random; pub trait Extension { fn name(&self) -> String; - fn calculate(&self, params: &Mapping) -> Option; + fn calculate(&self, params: &Mapping, args: &Vec) -> Option; } pub fn get_extensions() -> Vec> { diff --git a/src/extension/random.rs b/src/extension/random.rs index e44a2e6..909a696 100644 --- a/src/extension/random.rs +++ b/src/extension/random.rs @@ -34,7 +34,7 @@ impl super::Extension for RandomExtension { String::from("random") } - fn calculate(&self, params: &Mapping) -> Option { + fn calculate(&self, params: &Mapping, _: &Vec) -> Option { // TODO: add argument handling let choices = params.get(&Value::from("choices")); if choices.is_none() { warn!("No 'choices' parameter specified for random variable"); @@ -82,7 +82,7 @@ mod tests { params.insert(Value::from("choices"), Value::from(choices.clone())); let extension = RandomExtension::new(); - let output = extension.calculate(¶ms); + let output = extension.calculate(¶ms, &vec![]); assert!(output.is_some()); @@ -90,4 +90,6 @@ mod tests { assert!(choices.iter().any(|x| x == &output)); } + + // TODO: add test with arguments } \ No newline at end of file diff --git a/src/extension/script.rs b/src/extension/script.rs index a5519aa..ecbc1bd 100644 --- a/src/extension/script.rs +++ b/src/extension/script.rs @@ -34,7 +34,7 @@ impl super::Extension for ScriptExtension { String::from("script") } - fn calculate(&self, params: &Mapping) -> Option { + fn calculate(&self, params: &Mapping, _: &Vec) -> Option { // TODO: add argument handling let args = params.get(&Value::from("args")); if args.is_none() { warn!("No 'args' parameter specified for script variable"); diff --git a/src/extension/shell.rs b/src/extension/shell.rs index aa1012a..9736628 100644 --- a/src/extension/shell.rs +++ b/src/extension/shell.rs @@ -34,7 +34,7 @@ impl super::Extension for ShellExtension { String::from("shell") } - fn calculate(&self, params: &Mapping) -> Option { + fn calculate(&self, params: &Mapping, _: &Vec) -> Option { // TODO: add argument handling let cmd = params.get(&Value::from("cmd")); if cmd.is_none() { warn!("No 'cmd' parameter specified for shell variable"); @@ -90,7 +90,7 @@ mod tests { params.insert(Value::from("cmd"), Value::from("echo hello world")); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms); + let output = extension.calculate(¶ms, &vec![]); assert!(output.is_some()); @@ -108,7 +108,7 @@ mod tests { params.insert(Value::from("trim"), Value::from(true)); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms); + let output = extension.calculate(¶ms, &vec![]); assert!(output.is_some()); assert_eq!(output.unwrap(), "hello world"); @@ -126,7 +126,7 @@ mod tests { params.insert(Value::from("trim"), Value::from(true)); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms); + let output = extension.calculate(¶ms, &vec![]); assert!(output.is_some()); assert_eq!(output.unwrap(), "hello world"); @@ -139,7 +139,7 @@ mod tests { params.insert(Value::from("trim"), Value::from("error")); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms); + let output = extension.calculate(¶ms, &vec![]); assert!(output.is_some()); if cfg!(target_os = "windows") { @@ -157,9 +157,11 @@ mod tests { params.insert(Value::from("trim"), Value::from(true)); let extension = ShellExtension::new(); - let output = extension.calculate(¶ms); + let output = extension.calculate(¶ms, &vec![]); assert!(output.is_some()); assert_eq!(output.unwrap(), "hello world"); } + + // TODO: add tests with arguments } \ No newline at end of file diff --git a/src/render/default.rs b/src/render/default.rs index f1ef5f2..2663c0b 100644 --- a/src/render/default.rs +++ b/src/render/default.rs @@ -36,9 +36,6 @@ pub struct DefaultRenderer { // Regex used to identify matches (and arguments) in passive expansions passive_match_regex: Regex, - - // Regex used to separate arguments in passive expansions - passive_arg_regex: Regex, } impl DefaultRenderer { @@ -54,15 +51,10 @@ impl DefaultRenderer { .unwrap_or_else(|e| { panic!("Invalid passive match regex"); }); - let passive_arg_regex = Regex::new(&config.passive_arg_regex) - .unwrap_or_else(|e| { - panic!("Invalid passive arg regex"); - }); DefaultRenderer{ extension_map, passive_match_regex, - passive_arg_regex } } @@ -129,7 +121,7 @@ impl super::Renderer for DefaultRenderer { // TODO: pass the arguments to the extension let extension = self.extension_map.get(&variable.var_type); if let Some(extension) = extension { - let ext_out = extension.calculate(&variable.params); + let ext_out = extension.calculate(&variable.params, &args); if let Some(output) = ext_out { output_map.insert(variable.name.clone(), output); }else{ @@ -142,11 +134,6 @@ impl super::Renderer for DefaultRenderer { } } - // TODO: replace the arguments - // the idea is that every param placeholder, such as $1$, is replaced with - // an ArgExtension when loading a match, which renders the argument as output - // this is only used as syntactic sugar - // Replace the variables let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| { let var_name = caps.name("name").unwrap().as_str(); @@ -159,6 +146,9 @@ impl super::Renderer for DefaultRenderer { content.replace.clone() }; + // Render any argument that may be present + let target_string = utils::render_args(&target_string, &args); + RenderResult::Text(target_string) }, @@ -202,9 +192,9 @@ impl super::Renderer for DefaultRenderer { }else{ "" }; - let args : Vec = self.passive_arg_regex.split(match_args).into_iter().map( - |arg| arg.to_owned() - ).collect(); + let args : Vec = utils::split_args(match_args, + config.passive_arg_delimiter, + config.passive_arg_escape); let m = m.unwrap(); // Render the actual match @@ -242,7 +232,6 @@ mod tests { fn verify_render(rendered: RenderResult, target: &str) { match rendered { RenderResult::Text(rendered) => { - println!("{}", rendered); assert_eq!(rendered, target); }, _ => { @@ -326,4 +315,97 @@ mod tests { verify_render(rendered, result); } + + #[test] + fn test_render_passive_nested_matches_no_args() { + let text = ":greet"; + + let config = get_config_for(r###" + matches: + - trigger: ':greet' + replace: "hi {{name}}" + vars: + - name: name + type: match + params: + trigger: ":name" + + - trigger: ':name' + replace: john + "###); + + let renderer = get_renderer(config.clone()); + + let rendered = renderer.render_passive(text, &config); + + verify_render(rendered, "hi john"); + } + + #[test] + fn test_render_passive_simple_match_with_args() { + let text = ":greet/Jon/"; + + let config = get_config_for(r###" + matches: + - trigger: ':greet' + replace: "Hi $0$" + "###); + + let renderer = get_renderer(config.clone()); + + let rendered = renderer.render_passive(text, &config); + + verify_render(rendered, "Hi Jon"); + } + + #[test] + fn test_render_passive_simple_match_with_multiple_args() { + let text = ":greet/Jon/Snow/"; + + let config = get_config_for(r###" + matches: + - trigger: ':greet' + replace: "Hi $0$, there is $1$ outside" + "###); + + let renderer = get_renderer(config.clone()); + + let rendered = renderer.render_passive(text, &config); + + verify_render(rendered, "Hi Jon, there is Snow outside"); + } + + #[test] + fn test_render_passive_simple_match_with_escaped_args() { + let text = ":greet/Jon/10\\/12/"; + + let config = get_config_for(r###" + matches: + - trigger: ':greet' + replace: "Hi $0$, today is $1$" + "###); + + let renderer = get_renderer(config.clone()); + + let rendered = renderer.render_passive(text, &config); + + verify_render(rendered, "Hi Jon, today is 10/12"); + } + + #[test] + fn test_render_passive_simple_match_with_args_not_closed() { + let text = ":greet/Jon/Snow"; + + let config = get_config_for(r###" + matches: + - trigger: ':greet' + replace: "Hi $0$" + "###); + + let renderer = get_renderer(config.clone()); + + let rendered = renderer.render_passive(text, &config); + + verify_render(rendered, "Hi JonSnow"); + } } \ No newline at end of file diff --git a/src/render/mod.rs b/src/render/mod.rs index d83e982..80bf645 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -22,6 +22,7 @@ use crate::matcher::{Match}; use crate::config::Configs; pub(crate) mod default; +pub(crate) mod utils; pub trait Renderer { // Render a match output diff --git a/src/render/utils.rs b/src/render/utils.rs new file mode 100644 index 0000000..fe486b6 --- /dev/null +++ b/src/render/utils.rs @@ -0,0 +1,130 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2020 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use regex::{Regex, Captures}; + +lazy_static! { + static ref ARG_REGEX: Regex = Regex::new("\\$(?P\\d+)\\$").unwrap(); +} + +pub fn render_args(text: &str, args: &Vec) -> String { + let result = ARG_REGEX.replace_all(text, |caps: &Captures| { + let position_str = caps.name("pos").unwrap().as_str(); + let position = position_str.parse::().unwrap_or(-1); + + if position >= 0 && position < args.len() as i32 { + args[position as usize].to_owned() + }else{ + "".to_owned() + } + }); + + result.to_string() +} + +pub fn split_args(text: &str, delimiter: char, escape: char) -> Vec { + let mut output = vec![]; + + // Make sure the text is not empty + if text.is_empty() { + return output + } + + let mut last = String::from(""); + let mut previous : char = char::from(0); + text.chars().into_iter().for_each(|c| { + if c == delimiter { + if previous != escape { + output.push(last.clone()); + last = String::from(""); + }else{ + last.push(c); + } + }else if c == escape { + if previous == escape { + last.push(c); + } + }else{ + last.push(c); + } + previous = c; + }); + + // Add the last one + output.push(last); + + output +} + +// TESTS + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_render_args_no_args() { + let args = vec!("hello".to_owned()); + assert_eq!(render_args("no args", &args), "no args") + } + + #[test] + fn test_render_args_one_arg() { + let args = vec!("jon".to_owned()); + assert_eq!(render_args("hello $0$", &args), "hello jon") + } + + #[test] + fn test_render_args_one_multiple_args() { + let args = vec!("jon".to_owned(), "snow".to_owned()); + assert_eq!(render_args("hello $0$, the $1$ is white", &args), "hello jon, the snow is white") + } + + #[test] + fn test_render_args_out_of_range() { + let args = vec!("jon".to_owned()); + assert_eq!(render_args("hello $10$", &args), "hello ") + } + + #[test] + fn test_split_args_one_arg() { + assert_eq!(split_args("jon", '/', '\\'), vec!["jon"]) + } + + #[test] + fn test_split_args_two_args() { + assert_eq!(split_args("jon/snow", '/', '\\'), vec!["jon", "snow"]) + } + + #[test] + fn test_split_args_escaping() { + assert_eq!(split_args("jon\\/snow", '/', '\\'), vec!["jon/snow"]) + } + + #[test] + fn test_split_args_escaping_escape() { + assert_eq!(split_args("jon\\\\snow", '/', '\\'), vec!["jon\\snow"]) + } + + #[test] + fn test_split_args_empty() { + let empty_vec : Vec = vec![]; + assert_eq!(split_args("", '/', '\\'), empty_vec) + } +} \ No newline at end of file