commit
85f1598cf2
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -572,7 +572,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "2.0.5-alpha"
|
version = "2.1.0-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"caps",
|
"caps",
|
||||||
|
|
|
@ -89,7 +89,7 @@ fn split_config(config: LegacyConfig) -> (LegacyInteropConfig, LegacyMatchGroup)
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|var| {
|
.filter_map(|var| {
|
||||||
let var: YAMLVariable = serde_yaml::from_value(var.clone()).ok()?;
|
let var: YAMLVariable = serde_yaml::from_value(var.clone()).ok()?;
|
||||||
let (var, warnings) = try_convert_into_variable(var).ok()?;
|
let (var, warnings) = try_convert_into_variable(var, true).ok()?;
|
||||||
warnings.into_iter().for_each(|warning| {
|
warnings.into_iter().for_each(|warning| {
|
||||||
warn!("{}", warning);
|
warn!("{}", warning);
|
||||||
});
|
});
|
||||||
|
@ -102,7 +102,7 @@ fn split_config(config: LegacyConfig) -> (LegacyInteropConfig, LegacyMatchGroup)
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|var| {
|
.filter_map(|var| {
|
||||||
let m: YAMLMatch = serde_yaml::from_value(var.clone()).ok()?;
|
let m: YAMLMatch = serde_yaml::from_value(var.clone()).ok()?;
|
||||||
let (m, warnings) = try_convert_into_match(m).ok()?;
|
let (m, warnings) = try_convert_into_match(m, true).ok()?;
|
||||||
warnings.into_iter().for_each(|warning| {
|
warnings.into_iter().for_each(|warning| {
|
||||||
warn!("{}", warning);
|
warn!("{}", warning);
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,6 +43,8 @@ mod util;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)(\\.\\w+)?\\s*\\}\\}").unwrap();
|
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)(\\.\\w+)?\\s*\\}\\}").unwrap();
|
||||||
|
static ref FORM_CONTROL_REGEX: Regex =
|
||||||
|
Regex::new("\\[\\[\\s*(\\w+)(\\.\\w+)?\\s*\\]\\]").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an alias to make the meaning more explicit
|
// Create an alias to make the meaning more explicit
|
||||||
|
@ -72,7 +74,7 @@ impl Importer for YAMLImporter {
|
||||||
|
|
||||||
let mut global_vars = Vec::new();
|
let mut global_vars = Vec::new();
|
||||||
for yaml_global_var in yaml_group.global_vars.as_ref().cloned().unwrap_or_default() {
|
for yaml_global_var in yaml_group.global_vars.as_ref().cloned().unwrap_or_default() {
|
||||||
match try_convert_into_variable(yaml_global_var) {
|
match try_convert_into_variable(yaml_global_var, false) {
|
||||||
Ok((var, warnings)) => {
|
Ok((var, warnings)) => {
|
||||||
global_vars.push(var);
|
global_vars.push(var);
|
||||||
non_fatal_errors.extend(warnings.into_iter().map(ErrorRecord::warn));
|
non_fatal_errors.extend(warnings.into_iter().map(ErrorRecord::warn));
|
||||||
|
@ -85,7 +87,7 @@ impl Importer for YAMLImporter {
|
||||||
|
|
||||||
let mut matches = Vec::new();
|
let mut matches = Vec::new();
|
||||||
for yaml_match in yaml_group.matches.as_ref().cloned().unwrap_or_default() {
|
for yaml_match in yaml_group.matches.as_ref().cloned().unwrap_or_default() {
|
||||||
match try_convert_into_match(yaml_match) {
|
match try_convert_into_match(yaml_match, false) {
|
||||||
Ok((m, warnings)) => {
|
Ok((m, warnings)) => {
|
||||||
matches.push(m);
|
matches.push(m);
|
||||||
non_fatal_errors.extend(warnings.into_iter().map(ErrorRecord::warn));
|
non_fatal_errors.extend(warnings.into_iter().map(ErrorRecord::warn));
|
||||||
|
@ -119,7 +121,10 @@ impl Importer for YAMLImporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_convert_into_match(yaml_match: YAMLMatch) -> Result<(Match, Vec<Warning>)> {
|
pub fn try_convert_into_match(
|
||||||
|
yaml_match: YAMLMatch,
|
||||||
|
use_compatibility_mode: bool,
|
||||||
|
) -> Result<(Match, Vec<Warning>)> {
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
|
|
||||||
if yaml_match.uppercase_style.is_some() && yaml_match.propagate_case.is_none() {
|
if yaml_match.uppercase_style.is_some() && yaml_match.propagate_case.is_none() {
|
||||||
|
@ -203,8 +208,9 @@ pub fn try_convert_into_match(yaml_match: YAMLMatch) -> Result<(Match, Vec<Warni
|
||||||
|
|
||||||
let mut vars: Vec<Variable> = Vec::new();
|
let mut vars: Vec<Variable> = Vec::new();
|
||||||
for yaml_var in yaml_match.vars.unwrap_or_default() {
|
for yaml_var in yaml_match.vars.unwrap_or_default() {
|
||||||
let (var, var_warnings) = try_convert_into_variable(yaml_var.clone())
|
let (var, var_warnings) =
|
||||||
.with_context(|| format!("failed to load variable: {:?}", yaml_var))?;
|
try_convert_into_variable(yaml_var.clone(), use_compatibility_mode)
|
||||||
|
.with_context(|| format!("failed to load variable: {:?}", yaml_var))?;
|
||||||
warnings.extend(var_warnings);
|
warnings.extend(var_warnings);
|
||||||
vars.push(var);
|
vars.push(var);
|
||||||
}
|
}
|
||||||
|
@ -216,21 +222,45 @@ pub fn try_convert_into_match(yaml_match: YAMLMatch) -> Result<(Match, Vec<Warni
|
||||||
force_mode,
|
force_mode,
|
||||||
})
|
})
|
||||||
} else if let Some(form_layout) = yaml_match.form {
|
} else if let Some(form_layout) = yaml_match.form {
|
||||||
// TODO: test form case
|
|
||||||
// Replace all the form fields with actual variables
|
// Replace all the form fields with actual variables
|
||||||
let resolved_layout = VAR_REGEX
|
|
||||||
.replace_all(&form_layout, |caps: &Captures| {
|
// In v2.1.0-alpha the form control syntax was replaced with [[control]]
|
||||||
let var_name = caps.get(1).unwrap().as_str();
|
// instead of {{control}}, so we check if compatibility mode is being used.
|
||||||
format!("{{{{form1.{}}}}}", var_name)
|
// TODO: remove once compatibility mode is removed
|
||||||
})
|
|
||||||
.to_string();
|
let (resolved_replace, resolved_layout) = if use_compatibility_mode {
|
||||||
|
(
|
||||||
|
VAR_REGEX
|
||||||
|
.replace_all(&form_layout, |caps: &Captures| {
|
||||||
|
let var_name = caps.get(1).unwrap().as_str();
|
||||||
|
format!("{{{{form1.{}}}}}", var_name)
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
VAR_REGEX
|
||||||
|
.replace_all(&form_layout, |caps: &Captures| {
|
||||||
|
let var_name = caps.get(1).unwrap().as_str();
|
||||||
|
format!("[[{}]]", var_name)
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
FORM_CONTROL_REGEX
|
||||||
|
.replace_all(&form_layout, |caps: &Captures| {
|
||||||
|
let var_name = caps.get(1).unwrap().as_str();
|
||||||
|
format!("{{{{form1.{}}}}}", var_name)
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
form_layout,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
// Convert escaped brakets in forms
|
// Convert escaped brakets in forms
|
||||||
let resolved_layout = resolved_layout.replace("\\{", "{ ").replace("\\}", " }");
|
let resolved_replace = resolved_replace.replace("\\{", "{ ").replace("\\}", " }");
|
||||||
|
|
||||||
// Convert the form data to valid variables
|
// Convert the form data to valid variables
|
||||||
let mut params = Params::new();
|
let mut params = Params::new();
|
||||||
params.insert("layout".to_string(), Value::String(form_layout));
|
params.insert("layout".to_string(), Value::String(resolved_layout));
|
||||||
|
|
||||||
if let Some(fields) = yaml_match.form_fields {
|
if let Some(fields) = yaml_match.form_fields {
|
||||||
params.insert("fields".to_string(), Value::Object(convert_params(fields)?));
|
params.insert("fields".to_string(), Value::Object(convert_params(fields)?));
|
||||||
|
@ -241,10 +271,11 @@ pub fn try_convert_into_match(yaml_match: YAMLMatch) -> Result<(Match, Vec<Warni
|
||||||
name: "form1".to_owned(),
|
name: "form1".to_owned(),
|
||||||
var_type: "form".to_owned(),
|
var_type: "form".to_owned(),
|
||||||
params,
|
params,
|
||||||
|
..Default::default()
|
||||||
}];
|
}];
|
||||||
|
|
||||||
MatchEffect::Text(TextEffect {
|
MatchEffect::Text(TextEffect {
|
||||||
replace: resolved_layout,
|
replace: resolved_replace,
|
||||||
vars,
|
vars,
|
||||||
format: TextFormat::Plain,
|
format: TextFormat::Plain,
|
||||||
force_mode,
|
force_mode,
|
||||||
|
@ -274,13 +305,18 @@ pub fn try_convert_into_match(yaml_match: YAMLMatch) -> Result<(Match, Vec<Warni
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_convert_into_variable(yaml_var: YAMLVariable) -> Result<(Variable, Vec<Warning>)> {
|
pub fn try_convert_into_variable(
|
||||||
|
yaml_var: YAMLVariable,
|
||||||
|
use_compatibility_mode: bool,
|
||||||
|
) -> Result<(Variable, Vec<Warning>)> {
|
||||||
Ok((
|
Ok((
|
||||||
Variable {
|
Variable {
|
||||||
name: yaml_var.name,
|
name: yaml_var.name,
|
||||||
var_type: yaml_var.var_type,
|
var_type: yaml_var.var_type,
|
||||||
params: convert_params(yaml_var.params)?,
|
params: convert_params(yaml_var.params)?,
|
||||||
id: next_id(),
|
id: next_id(),
|
||||||
|
inject_vars: !use_compatibility_mode && yaml_var.inject_vars.unwrap_or(true),
|
||||||
|
depends_on: yaml_var.depends_on,
|
||||||
},
|
},
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
))
|
))
|
||||||
|
@ -295,9 +331,12 @@ mod tests {
|
||||||
};
|
};
|
||||||
use std::fs::create_dir_all;
|
use std::fs::create_dir_all;
|
||||||
|
|
||||||
fn create_match_with_warnings(yaml: &str) -> Result<(Match, Vec<Warning>)> {
|
fn create_match_with_warnings(
|
||||||
|
yaml: &str,
|
||||||
|
use_compatibility_mode: bool,
|
||||||
|
) -> Result<(Match, Vec<Warning>)> {
|
||||||
let yaml_match: YAMLMatch = serde_yaml::from_str(yaml)?;
|
let yaml_match: YAMLMatch = serde_yaml::from_str(yaml)?;
|
||||||
let (mut m, warnings) = try_convert_into_match(yaml_match)?;
|
let (mut m, warnings) = try_convert_into_match(yaml_match, use_compatibility_mode)?;
|
||||||
|
|
||||||
// Reset the IDs to correctly compare them
|
// Reset the IDs to correctly compare them
|
||||||
m.id = 0;
|
m.id = 0;
|
||||||
|
@ -309,7 +348,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_match(yaml: &str) -> Result<Match> {
|
fn create_match(yaml: &str) -> Result<Match> {
|
||||||
let (m, warnings) = create_match_with_warnings(yaml)?;
|
let (m, warnings) = create_match_with_warnings(yaml, false)?;
|
||||||
if !warnings.is_empty() {
|
if !warnings.is_empty() {
|
||||||
panic!("warnings were detected but not handled: {:?}", warnings);
|
panic!("warnings were detected but not handled: {:?}", warnings);
|
||||||
}
|
}
|
||||||
|
@ -529,6 +568,7 @@ mod tests {
|
||||||
replace: "world"
|
replace: "world"
|
||||||
uppercase_style: "capitalize"
|
uppercase_style: "capitalize"
|
||||||
"#,
|
"#,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -545,6 +585,7 @@ mod tests {
|
||||||
uppercase_style: "invalid"
|
uppercase_style: "invalid"
|
||||||
propagate_case: true
|
propagate_case: true
|
||||||
"#,
|
"#,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -554,6 +595,119 @@ mod tests {
|
||||||
assert_eq!(warnings.len(), 1);
|
assert_eq!(warnings.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_maps_correctly() {
|
||||||
|
let mut params = Params::new();
|
||||||
|
params.insert(
|
||||||
|
"layout".to_string(),
|
||||||
|
Value::String("Hi [[name]]!".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
create_match(
|
||||||
|
r#"
|
||||||
|
trigger: "Hello"
|
||||||
|
form: "Hi [[name]]!"
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
Match {
|
||||||
|
cause: MatchCause::Trigger(TriggerCause {
|
||||||
|
triggers: vec!["Hello".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
effect: MatchEffect::Text(TextEffect {
|
||||||
|
replace: "Hi {{form1.name}}!".to_string(),
|
||||||
|
vars: vec![Variable {
|
||||||
|
id: 0,
|
||||||
|
name: "form1".to_string(),
|
||||||
|
var_type: "form".to_string(),
|
||||||
|
params,
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_maps_correctly_with_variable_injection() {
|
||||||
|
let mut params = Params::new();
|
||||||
|
params.insert(
|
||||||
|
"layout".to_string(),
|
||||||
|
Value::String("Hi [[name]]! {{signature}}".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
create_match(
|
||||||
|
r#"
|
||||||
|
trigger: "Hello"
|
||||||
|
form: "Hi [[name]]! {{signature}}"
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
Match {
|
||||||
|
cause: MatchCause::Trigger(TriggerCause {
|
||||||
|
triggers: vec!["Hello".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
effect: MatchEffect::Text(TextEffect {
|
||||||
|
replace: "Hi {{form1.name}}! {{signature}}".to_string(),
|
||||||
|
vars: vec![Variable {
|
||||||
|
id: 0,
|
||||||
|
name: "form1".to_string(),
|
||||||
|
var_type: "form".to_string(),
|
||||||
|
params,
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_maps_correctly_legacy_format() {
|
||||||
|
let mut params = Params::new();
|
||||||
|
params.insert(
|
||||||
|
"layout".to_string(),
|
||||||
|
Value::String("Hi [[name]]!".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
create_match_with_warnings(
|
||||||
|
r#"
|
||||||
|
trigger: "Hello"
|
||||||
|
form: "Hi {{name}}!"
|
||||||
|
"#,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.0,
|
||||||
|
Match {
|
||||||
|
cause: MatchCause::Trigger(TriggerCause {
|
||||||
|
triggers: vec!["Hello".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
effect: MatchEffect::Text(TextEffect {
|
||||||
|
replace: "Hi {{form1.name}}!".to_string(),
|
||||||
|
vars: vec![Variable {
|
||||||
|
id: 0,
|
||||||
|
name: "form1".to_string(),
|
||||||
|
var_type: "form".to_string(),
|
||||||
|
params,
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vars_maps_correctly() {
|
fn vars_maps_correctly() {
|
||||||
let mut params = Params::new();
|
let mut params = Params::new();
|
||||||
|
@ -592,6 +746,52 @@ mod tests {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vars_inject_vars_and_depends_on() {
|
||||||
|
let vars = vec![
|
||||||
|
Variable {
|
||||||
|
name: "var1".to_string(),
|
||||||
|
var_type: "test".to_string(),
|
||||||
|
depends_on: vec!["test".to_owned()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Variable {
|
||||||
|
name: "var2".to_string(),
|
||||||
|
var_type: "test".to_string(),
|
||||||
|
inject_vars: false,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
create_match(
|
||||||
|
r#"
|
||||||
|
trigger: "Hello"
|
||||||
|
replace: "world"
|
||||||
|
vars:
|
||||||
|
- name: var1
|
||||||
|
type: test
|
||||||
|
depends_on: ["test"]
|
||||||
|
- name: var2
|
||||||
|
type: "test"
|
||||||
|
inject_vars: false
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
Match {
|
||||||
|
cause: MatchCause::Trigger(TriggerCause {
|
||||||
|
triggers: vec!["Hello".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
effect: MatchEffect::Text(TextEffect {
|
||||||
|
replace: "world".to_string(),
|
||||||
|
vars,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vars_no_params_maps_correctly() {
|
fn vars_no_params_maps_correctly() {
|
||||||
let vars = vec![Variable {
|
let vars = vec![Variable {
|
||||||
|
|
|
@ -125,6 +125,12 @@ pub struct YAMLVariable {
|
||||||
|
|
||||||
#[serde(default = "default_params")]
|
#[serde(default = "default_params")]
|
||||||
pub params: Mapping,
|
pub params: Mapping,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub inject_vars: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub depends_on: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_params() -> Mapping {
|
fn default_params() -> Mapping {
|
||||||
|
|
|
@ -205,6 +205,8 @@ pub struct Variable {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub var_type: String,
|
pub var_type: String,
|
||||||
pub params: Params,
|
pub params: Params,
|
||||||
|
pub inject_vars: bool,
|
||||||
|
pub depends_on: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Variable {
|
impl Default for Variable {
|
||||||
|
@ -214,6 +216,8 @@ impl Default for Variable {
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
var_type: String::new(),
|
var_type: String::new(),
|
||||||
params: Params::new(),
|
params: Params::new(),
|
||||||
|
inject_vars: true,
|
||||||
|
depends_on: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ use log::{error, trace, warn};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::event::{HotKeyEvent, InputEvent, Key, KeyboardEvent, Variant};
|
use crate::event::{HotKeyEvent, InputEvent, Key, KeyboardEvent, Status, Variant};
|
||||||
use crate::event::{Key::*, MouseButton, MouseEvent};
|
use crate::event::{Key::*, MouseButton, MouseEvent};
|
||||||
use crate::{event::Status::*, Source, SourceCallback};
|
use crate::{event::Status::*, Source, SourceCallback};
|
||||||
use crate::{event::Variant::*, hotkey::HotKey};
|
use crate::{event::Variant::*, hotkey::HotKey};
|
||||||
|
@ -50,6 +50,7 @@ const INPUT_MOUSE_MIDDLE_BUTTON: i32 = 3;
|
||||||
|
|
||||||
// Take a look at the native.h header file for an explanation of the fields
|
// Take a look at the native.h header file for an explanation of the fields
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct RawInputEvent {
|
pub struct RawInputEvent {
|
||||||
pub event_type: i32,
|
pub event_type: i32,
|
||||||
|
|
||||||
|
@ -58,6 +59,12 @@ pub struct RawInputEvent {
|
||||||
|
|
||||||
pub key_code: i32,
|
pub key_code: i32,
|
||||||
pub status: i32,
|
pub status: i32,
|
||||||
|
|
||||||
|
pub is_caps_lock_pressed: i32,
|
||||||
|
pub is_shift_pressed: i32,
|
||||||
|
pub is_control_pressed: i32,
|
||||||
|
pub is_option_pressed: i32,
|
||||||
|
pub is_command_pressed: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
|
@ -82,15 +89,97 @@ extern "C" {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct ModifierState {
|
||||||
|
is_ctrl_down: bool,
|
||||||
|
is_shift_down: bool,
|
||||||
|
is_command_down: bool,
|
||||||
|
is_option_down: bool,
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CURRENT_SENDER: Arc<Mutex<Option<Sender<InputEvent>>>> = Arc::new(Mutex::new(None));
|
static ref CURRENT_SENDER: Arc<Mutex<Option<Sender<InputEvent>>>> = Arc::new(Mutex::new(None));
|
||||||
|
static ref MODIFIER_STATE: Arc<Mutex<ModifierState>> =
|
||||||
|
Arc::new(Mutex::new(ModifierState::default()));
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn native_callback(raw_event: RawInputEvent) {
|
extern "C" fn native_callback(raw_event: RawInputEvent) {
|
||||||
let lock = CURRENT_SENDER
|
let lock = CURRENT_SENDER
|
||||||
.lock()
|
.lock()
|
||||||
.expect("unable to acquire CocoaSource sender lock");
|
.expect("unable to acquire CocoaSource sender lock");
|
||||||
|
|
||||||
|
// Most of the times, when pressing a modifier key (such as Alt, Ctrl, Shift, Cmd),
|
||||||
|
// we get both a Pressed and Released event. This is important to keep Espanso's
|
||||||
|
// internal representation of modifiers in sync.
|
||||||
|
// Unfortunately, there are times when the corresponding "release" event is not sent,
|
||||||
|
// and this causes Espanso to mistakenly think that the modifier is still pressed.
|
||||||
|
// This can happen for various reasons, such as when using external bluetooth keyboards
|
||||||
|
// or certain keyboard shortcuts.
|
||||||
|
// Luckily, most key events include the "modifiers flag" information, that tells us which
|
||||||
|
// modifier keys were currently pressed at that time.
|
||||||
|
// We use this modifier flag information to detect "inconsistent" states to send the corresponding
|
||||||
|
// modifier release events, keeping espanso's state in sync.
|
||||||
|
// For more info, see:
|
||||||
|
// https://github.com/federico-terzi/espanso/issues/825
|
||||||
|
// https://github.com/federico-terzi/espanso/issues/858
|
||||||
|
let mut compensating_events = Vec::new();
|
||||||
|
if raw_event.event_type == INPUT_EVENT_TYPE_KEYBOARD {
|
||||||
|
let (key_code, _) = key_code_to_key(raw_event.key_code);
|
||||||
|
let mut current_mod_state = MODIFIER_STATE
|
||||||
|
.lock()
|
||||||
|
.expect("unable to acquire modifier state in cocoa detector");
|
||||||
|
|
||||||
|
if let Key::Alt = &key_code {
|
||||||
|
current_mod_state.is_option_down = raw_event.status == INPUT_STATUS_PRESSED;
|
||||||
|
} else if let Key::Meta = &key_code {
|
||||||
|
current_mod_state.is_command_down = raw_event.status == INPUT_STATUS_PRESSED;
|
||||||
|
} else if let Key::Shift = &key_code {
|
||||||
|
current_mod_state.is_shift_down = raw_event.status == INPUT_STATUS_PRESSED;
|
||||||
|
} else if let Key::Control = &key_code {
|
||||||
|
current_mod_state.is_ctrl_down = raw_event.status == INPUT_STATUS_PRESSED;
|
||||||
|
} else {
|
||||||
|
if current_mod_state.is_command_down && raw_event.is_command_pressed == 0 {
|
||||||
|
compensating_events.push((Key::Meta, 0x37));
|
||||||
|
current_mod_state.is_command_down = false;
|
||||||
|
}
|
||||||
|
if current_mod_state.is_ctrl_down && raw_event.is_control_pressed == 0 {
|
||||||
|
compensating_events.push((Key::Control, 0x3B));
|
||||||
|
current_mod_state.is_ctrl_down = false;
|
||||||
|
}
|
||||||
|
if current_mod_state.is_shift_down && raw_event.is_shift_pressed == 0 {
|
||||||
|
compensating_events.push((Key::Shift, 0x38));
|
||||||
|
current_mod_state.is_shift_down = false;
|
||||||
|
}
|
||||||
|
if current_mod_state.is_option_down && raw_event.is_option_pressed == 0 {
|
||||||
|
compensating_events.push((Key::Alt, 0x3A));
|
||||||
|
current_mod_state.is_option_down = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !compensating_events.is_empty() {
|
||||||
|
warn!(
|
||||||
|
"detected inconsistent modifier state for keys {:?}, sending compensating events...",
|
||||||
|
compensating_events
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(sender) = lock.as_ref() {
|
if let Some(sender) = lock.as_ref() {
|
||||||
|
for (key, code) in compensating_events {
|
||||||
|
if let Err(error) = sender.send(InputEvent::Keyboard(KeyboardEvent {
|
||||||
|
key,
|
||||||
|
value: None,
|
||||||
|
status: Status::Released,
|
||||||
|
variant: None,
|
||||||
|
code,
|
||||||
|
})) {
|
||||||
|
error!(
|
||||||
|
"Unable to send compensating event to Cocoa Sender: {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let event: Option<InputEvent> = raw_event.into();
|
let event: Option<InputEvent> = raw_event.into();
|
||||||
if let Some(event) = event {
|
if let Some(event) = event {
|
||||||
if let Err(error) = sender.send(event) {
|
if let Err(error) = sender.send(event) {
|
||||||
|
@ -386,6 +475,11 @@ mod tests {
|
||||||
buffer_len: 0,
|
buffer_len: 0,
|
||||||
key_code: 0,
|
key_code: 0,
|
||||||
status: INPUT_STATUS_PRESSED,
|
status: INPUT_STATUS_PRESSED,
|
||||||
|
is_caps_lock_pressed: 0,
|
||||||
|
is_shift_pressed: 0,
|
||||||
|
is_control_pressed: 0,
|
||||||
|
is_option_pressed: 0,
|
||||||
|
is_command_pressed: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,16 @@ typedef struct {
|
||||||
|
|
||||||
// Pressed or Released status
|
// Pressed or Released status
|
||||||
int32_t status;
|
int32_t status;
|
||||||
|
|
||||||
|
// Modifier keys status, this is needed to "correct" missing modifier release events.
|
||||||
|
// For more info, see the following issues:
|
||||||
|
// https://github.com/federico-terzi/espanso/issues/825
|
||||||
|
// https://github.com/federico-terzi/espanso/issues/858
|
||||||
|
int32_t is_caps_lock_pressed;
|
||||||
|
int32_t is_shift_pressed;
|
||||||
|
int32_t is_control_pressed;
|
||||||
|
int32_t is_option_pressed;
|
||||||
|
int32_t is_command_pressed;
|
||||||
} InputEvent;
|
} InputEvent;
|
||||||
|
|
||||||
typedef void (*EventCallback)(InputEvent data);
|
typedef void (*EventCallback)(InputEvent data);
|
||||||
|
|
|
@ -76,6 +76,19 @@ void * detect_initialize(EventCallback callback, InitializeOptions options) {
|
||||||
strncpy(inputEvent.buffer, chars, 23);
|
strncpy(inputEvent.buffer, chars, 23);
|
||||||
inputEvent.buffer_len = event.characters.length;
|
inputEvent.buffer_len = event.characters.length;
|
||||||
|
|
||||||
|
// We also send the modifier key status to "correct" missing modifier release events
|
||||||
|
if (([event modifierFlags] & NSEventModifierFlagShift) != 0) {
|
||||||
|
inputEvent.is_shift_pressed = 1;
|
||||||
|
} else if (([event modifierFlags] & NSEventModifierFlagControl) != 0) {
|
||||||
|
inputEvent.is_control_pressed = 1;
|
||||||
|
} else if (([event modifierFlags] & NSEventModifierFlagCapsLock) != 0) {
|
||||||
|
inputEvent.is_caps_lock_pressed = 1;
|
||||||
|
} else if (([event modifierFlags] & NSEventModifierFlagOption) != 0) {
|
||||||
|
inputEvent.is_option_pressed = 1;
|
||||||
|
} else if (([event modifierFlags] & NSEventModifierFlagCommand) != 0) {
|
||||||
|
inputEvent.is_command_pressed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
callback(inputEvent);
|
callback(inputEvent);
|
||||||
}else if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown || event.type == NSEventTypeOtherMouseDown ||
|
}else if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown || event.type == NSEventTypeOtherMouseDown ||
|
||||||
event.type == NSEventTypeLeftMouseUp || event.type == NSEventTypeRightMouseUp || event.type == NSEventTypeOtherMouseUp) {
|
event.type == NSEventTypeLeftMouseUp || event.type == NSEventTypeRightMouseUp || event.type == NSEventTypeOtherMouseUp) {
|
||||||
|
@ -106,6 +119,20 @@ void * detect_initialize(EventCallback callback, InitializeOptions options) {
|
||||||
} else if (event.keyCode == kVK_Option || event.keyCode == kVK_RightOption) {
|
} else if (event.keyCode == kVK_Option || event.keyCode == kVK_RightOption) {
|
||||||
inputEvent.status = (([event modifierFlags] & NSEventModifierFlagOption) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED;
|
inputEvent.status = (([event modifierFlags] & NSEventModifierFlagOption) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We also send the modifier key status to "correct" missing modifier release events
|
||||||
|
if (([event modifierFlags] & NSEventModifierFlagShift) != 0) {
|
||||||
|
inputEvent.is_shift_pressed = 1;
|
||||||
|
} else if (([event modifierFlags] & NSEventModifierFlagControl) != 0) {
|
||||||
|
inputEvent.is_control_pressed = 1;
|
||||||
|
} else if (([event modifierFlags] & NSEventModifierFlagCapsLock) != 0) {
|
||||||
|
inputEvent.is_caps_lock_pressed = 1;
|
||||||
|
} else if (([event modifierFlags] & NSEventModifierFlagOption) != 0) {
|
||||||
|
inputEvent.is_option_pressed = 1;
|
||||||
|
} else if (([event modifierFlags] & NSEventModifierFlagCommand) != 0) {
|
||||||
|
inputEvent.is_command_pressed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
callback(inputEvent);
|
callback(inputEvent);
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use regex::{Captures, Regex};
|
||||||
use std::{cmp::Ordering, collections::HashMap, path::PathBuf};
|
use std::{cmp::Ordering, collections::HashMap, path::PathBuf};
|
||||||
use yaml_rust::{yaml::Hash, Yaml};
|
use yaml_rust::{yaml::Hash, Yaml};
|
||||||
|
|
||||||
|
@ -69,24 +70,32 @@ pub fn convert(input_files: HashMap<String, Hash>) -> HashMap<String, ConvertedF
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(global_vars) = yaml_global_vars {
|
if let Some(global_vars) = yaml_global_vars {
|
||||||
|
let mut patched_global_vars: Vec<Yaml> = global_vars.clone();
|
||||||
|
patched_global_vars
|
||||||
|
.iter_mut()
|
||||||
|
.for_each(apply_form_syntax_patch_to_variable);
|
||||||
|
|
||||||
let output_global_vars = output_yaml
|
let output_global_vars = output_yaml
|
||||||
.content
|
.content
|
||||||
.entry(Yaml::String("global_vars".to_string()))
|
.entry(Yaml::String("global_vars".to_string()))
|
||||||
.or_insert(Yaml::Array(Vec::new()));
|
.or_insert(Yaml::Array(Vec::new()));
|
||||||
if let Yaml::Array(out_global_vars) = output_global_vars {
|
if let Yaml::Array(out_global_vars) = output_global_vars {
|
||||||
out_global_vars.extend(global_vars.clone());
|
out_global_vars.extend(patched_global_vars);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("unable to transform global_vars for file: {}", input_path);
|
eprintln!("unable to transform global_vars for file: {}", input_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(matches) = yaml_matches {
|
if let Some(matches) = yaml_matches {
|
||||||
|
let mut patched_matches = matches.clone();
|
||||||
|
apply_form_syntax_patch(&mut patched_matches);
|
||||||
|
|
||||||
let output_matches = output_yaml
|
let output_matches = output_yaml
|
||||||
.content
|
.content
|
||||||
.entry(Yaml::String("matches".to_string()))
|
.entry(Yaml::String("matches".to_string()))
|
||||||
.or_insert(Yaml::Array(Vec::new()));
|
.or_insert(Yaml::Array(Vec::new()));
|
||||||
if let Yaml::Array(out_matches) = output_matches {
|
if let Yaml::Array(out_matches) = output_matches {
|
||||||
out_matches.extend(matches.clone());
|
out_matches.extend(patched_matches);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("unable to transform matches for file: {}", input_path);
|
eprintln!("unable to transform matches for file: {}", input_path);
|
||||||
}
|
}
|
||||||
|
@ -324,3 +333,57 @@ fn map_field_if_present(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is needed to convert the old form's {{control}} syntax to the new [[control]] one.
|
||||||
|
fn apply_form_syntax_patch(matches: &mut Vec<Yaml>) {
|
||||||
|
matches.iter_mut().for_each(|m| {
|
||||||
|
if let Yaml::Hash(fields) = m {
|
||||||
|
if let Some(Yaml::String(form_option)) = fields.get_mut(&Yaml::String("form".to_string())) {
|
||||||
|
let converted = replace_legacy_form_syntax_with_new_one(form_option);
|
||||||
|
if &converted != form_option {
|
||||||
|
form_option.clear();
|
||||||
|
form_option.push_str(&converted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Yaml::Array(vars)) = fields.get_mut(&Yaml::String("vars".to_string())) {
|
||||||
|
vars
|
||||||
|
.iter_mut()
|
||||||
|
.for_each(apply_form_syntax_patch_to_variable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_form_syntax_patch_to_variable(variable: &mut Yaml) {
|
||||||
|
if let Yaml::Hash(fields) = variable {
|
||||||
|
if let Some(Yaml::String(var_type)) = fields.get(&Yaml::String("type".to_string())) {
|
||||||
|
if var_type != "form" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Yaml::Hash(params)) = fields.get_mut(&Yaml::String("params".to_string())) {
|
||||||
|
if let Some(Yaml::String(layout)) = params.get_mut(&Yaml::String("layout".to_string())) {
|
||||||
|
let converted = replace_legacy_form_syntax_with_new_one(layout);
|
||||||
|
if &converted != layout {
|
||||||
|
layout.clear();
|
||||||
|
layout.push_str(&converted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref LEGACY_FIELD_REGEX: Regex = Regex::new(r"\{\{(?P<name>.*?)\}\}").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace_legacy_form_syntax_with_new_one(layout: &str) -> String {
|
||||||
|
LEGACY_FIELD_REGEX
|
||||||
|
.replace_all(layout, |caps: &Captures| {
|
||||||
|
let field_name = caps.name("name").unwrap().as_str();
|
||||||
|
format!("[[{}]]", field_name)
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
|
@ -200,12 +200,14 @@ mod tests {
|
||||||
static BASE_CASE: Dir = include_dir!("test/base");
|
static BASE_CASE: Dir = include_dir!("test/base");
|
||||||
static ALL_PARAMS_CASE: Dir = include_dir!("test/all_params");
|
static ALL_PARAMS_CASE: Dir = include_dir!("test/all_params");
|
||||||
static OTHER_DIRS_CASE: Dir = include_dir!("test/other_dirs");
|
static OTHER_DIRS_CASE: Dir = include_dir!("test/other_dirs");
|
||||||
|
static FORM_SYNTAX: Dir = include_dir!("test/form_syntax");
|
||||||
|
|
||||||
#[allow(clippy::unused_unit)]
|
#[allow(clippy::unused_unit)]
|
||||||
#[test_case(&SIMPLE_CASE; "simple case")]
|
#[test_case(&SIMPLE_CASE; "simple case")]
|
||||||
#[test_case(&BASE_CASE; "base case")]
|
#[test_case(&BASE_CASE; "base case")]
|
||||||
#[test_case(&ALL_PARAMS_CASE; "all config parameters case")]
|
#[test_case(&ALL_PARAMS_CASE; "all config parameters case")]
|
||||||
#[test_case(&OTHER_DIRS_CASE; "other directories case")]
|
#[test_case(&OTHER_DIRS_CASE; "other directories case")]
|
||||||
|
#[test_case(&FORM_SYNTAX; "form syntax")]
|
||||||
fn test_migration(test_data: &Dir) {
|
fn test_migration(test_data: &Dir) {
|
||||||
run_with_temp_dir(test_data, |legacy, expected| {
|
run_with_temp_dir(test_data, |legacy, expected| {
|
||||||
let tmp_out_dir = TempDir::new("espanso-migrate-out").unwrap();
|
let tmp_out_dir = TempDir::new("espanso-migrate-out").unwrap();
|
||||||
|
|
25
espanso-migrate/test/form_syntax/expected/match/base.yml
Normal file
25
espanso-migrate/test/form_syntax/expected/match/base.yml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
global_vars:
|
||||||
|
- name: global_form
|
||||||
|
type: form
|
||||||
|
params:
|
||||||
|
layout: |
|
||||||
|
Reverse [[name]]
|
||||||
|
|
||||||
|
matches:
|
||||||
|
- trigger: ":greet"
|
||||||
|
form: |
|
||||||
|
Hey [[name]],
|
||||||
|
Happy Birthday!
|
||||||
|
|
||||||
|
- trigger: ":rev"
|
||||||
|
replace: "{{reversed}}"
|
||||||
|
vars:
|
||||||
|
- name: form1
|
||||||
|
type: form
|
||||||
|
params:
|
||||||
|
layout: |
|
||||||
|
Reverse [[name]]
|
||||||
|
- name: reversed
|
||||||
|
type: shell
|
||||||
|
params:
|
||||||
|
cmd: "echo $ESPANSO_FORM1_NAME | rev"
|
25
espanso-migrate/test/form_syntax/legacy/default.yml
Normal file
25
espanso-migrate/test/form_syntax/legacy/default.yml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
global_vars:
|
||||||
|
- name: global_form
|
||||||
|
type: form
|
||||||
|
params:
|
||||||
|
layout: |
|
||||||
|
Reverse {{name}}
|
||||||
|
|
||||||
|
matches:
|
||||||
|
- trigger: ":greet"
|
||||||
|
form: |
|
||||||
|
Hey {{name}},
|
||||||
|
Happy Birthday!
|
||||||
|
|
||||||
|
- trigger: ":rev"
|
||||||
|
replace: "{{reversed}}"
|
||||||
|
vars:
|
||||||
|
- name: form1
|
||||||
|
type: form
|
||||||
|
params:
|
||||||
|
layout: |
|
||||||
|
Reverse {{name}}
|
||||||
|
- name: reversed
|
||||||
|
type: shell
|
||||||
|
params:
|
||||||
|
cmd: "echo $ESPANSO_FORM1_NAME | rev"
|
|
@ -21,7 +21,8 @@ use super::split::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref FIELD_REGEX: Regex = Regex::new(r"\{\{(.*?)\}\}").unwrap();
|
// We need to match for both the new [[name]] syntax and the legacy {{name}} one
|
||||||
|
static ref FIELD_REGEX: Regex = Regex::new(r"\{\{(.*?)\}\}|\[\[(.*?)\]\]").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
@ -57,6 +58,9 @@ pub fn parse_layout(layout: &str) -> Vec<Vec<Token>> {
|
||||||
if let Some(name) = caps.get(1) {
|
if let Some(name) = caps.get(1) {
|
||||||
let name = name.as_str().to_owned();
|
let name = name.as_str().to_owned();
|
||||||
row.push(Token::Field(name));
|
row.push(Token::Field(name));
|
||||||
|
} else if let Some(name) = caps.get(2) {
|
||||||
|
let name = name.as_str().to_owned();
|
||||||
|
row.push(Token::Field(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +78,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_layout() {
|
fn test_parse_layout() {
|
||||||
let layout = "Hey {{name}},\nHow are you?\n \nCheers";
|
let layout = "Hey [[name]],\nHow are you?\n \nCheers";
|
||||||
let result = parse_layout(layout);
|
let result = parse_layout(layout);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
|
@ -92,7 +96,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_layout_2() {
|
fn test_parse_layout_2() {
|
||||||
let layout = "Hey {{name}} {{surname}},";
|
let layout = "Hey [[name]] [[surname]],";
|
||||||
let result = parse_layout(layout);
|
let result = parse_layout(layout);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
|
@ -105,4 +109,22 @@ mod tests {
|
||||||
],]
|
],]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_layout_legacy_syntax() {
|
||||||
|
let layout = "Hey {{name}},\nHow are you?\n \nCheers";
|
||||||
|
let result = parse_layout(layout);
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
vec![
|
||||||
|
vec![
|
||||||
|
Token::Text("Hey ".to_owned()),
|
||||||
|
Token::Field("name".to_owned()),
|
||||||
|
Token::Text(",".to_owned())
|
||||||
|
],
|
||||||
|
vec![Token::Text("How are you?".to_owned())],
|
||||||
|
vec![Token::Text("Cheers".to_owned())],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,11 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::renderer::VAR_REGEX;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
|
||||||
renderer::render_variables, Extension, ExtensionOutput, ExtensionResult, Params, Value,
|
|
||||||
};
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref EMPTY_PARAMS: Params = Params::new();
|
static ref EMPTY_PARAMS: Params = Params::new();
|
||||||
|
@ -59,7 +56,7 @@ impl<'a> Extension for FormExtension<'a> {
|
||||||
fn calculate(
|
fn calculate(
|
||||||
&self,
|
&self,
|
||||||
_: &crate::Context,
|
_: &crate::Context,
|
||||||
scope: &crate::Scope,
|
_: &crate::Scope,
|
||||||
params: &Params,
|
params: &Params,
|
||||||
) -> crate::ExtensionResult {
|
) -> crate::ExtensionResult {
|
||||||
let layout = if let Some(Value::String(layout)) = params.get("layout") {
|
let layout = if let Some(Value::String(layout)) = params.get("layout") {
|
||||||
|
@ -68,15 +65,12 @@ impl<'a> Extension for FormExtension<'a> {
|
||||||
return crate::ExtensionResult::Error(FormExtensionError::MissingLayout.into());
|
return crate::ExtensionResult::Error(FormExtensionError::MissingLayout.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut fields = if let Some(Value::Object(fields)) = params.get("fields") {
|
let fields = if let Some(Value::Object(fields)) = params.get("fields") {
|
||||||
fields.clone()
|
fields.clone()
|
||||||
} else {
|
} else {
|
||||||
Params::new()
|
Params::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inject scope variables into fields (if needed)
|
|
||||||
inject_scope(&mut fields, scope);
|
|
||||||
|
|
||||||
match self.provider.show(layout, &fields, &EMPTY_PARAMS) {
|
match self.provider.show(layout, &fields, &EMPTY_PARAMS) {
|
||||||
FormProviderResult::Success(values) => {
|
FormProviderResult::Success(values) => {
|
||||||
ExtensionResult::Success(ExtensionOutput::Multiple(values))
|
ExtensionResult::Success(ExtensionOutput::Multiple(values))
|
||||||
|
@ -87,25 +81,6 @@ impl<'a> Extension for FormExtension<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: test
|
|
||||||
fn inject_scope(fields: &mut HashMap<String, Value>, scope: &HashMap<&str, ExtensionOutput>) {
|
|
||||||
for value in fields.values_mut() {
|
|
||||||
if let Value::Object(field_options) = value {
|
|
||||||
if let Some(Value::String(default_value)) = field_options.get_mut("default") {
|
|
||||||
if VAR_REGEX.is_match(default_value) {
|
|
||||||
match render_variables(default_value, scope) {
|
|
||||||
Ok(rendered) => *default_value = rendered,
|
|
||||||
Err(err) => error!(
|
|
||||||
"error while injecting variable in form default value: {}",
|
|
||||||
err
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum FormExtensionError {
|
pub enum FormExtensionError {
|
||||||
#[error("missing layout parameter")]
|
#[error("missing layout parameter")]
|
||||||
|
|
|
@ -98,7 +98,10 @@ impl Default for Template {
|
||||||
pub struct Variable {
|
pub struct Variable {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub var_type: String,
|
pub var_type: String,
|
||||||
|
pub inject_vars: bool,
|
||||||
pub params: Params,
|
pub params: Params,
|
||||||
|
// Name of the variables this variable depends on
|
||||||
|
pub depends_on: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Variable {
|
impl Default for Variable {
|
||||||
|
@ -106,7 +109,9 @@ impl Default for Variable {
|
||||||
Self {
|
Self {
|
||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
var_type: "".to_string(),
|
var_type: "".to_string(),
|
||||||
|
inject_vars: true,
|
||||||
params: Params::new(),
|
params: Params::new(),
|
||||||
|
depends_on: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* This file is part of esp name: (), var_type: (), params: ()anso.
|
* This file is part of espanso.
|
||||||
*
|
*
|
||||||
* Copyright (C) 2019-2021 Federico Terzi
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
*
|
*
|
||||||
|
@ -17,18 +17,19 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::{borrow::Cow, collections::HashMap};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CasingStyle, Context, Extension, ExtensionOutput, ExtensionResult, RenderOptions, RenderResult,
|
CasingStyle, Context, Extension, ExtensionOutput, ExtensionResult, RenderOptions, RenderResult,
|
||||||
Renderer, Scope, Template, Value, Variable,
|
Renderer, Scope, Template, Value, Variable,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use util::get_body_variable_names;
|
|
||||||
|
|
||||||
|
use self::util::{inject_variables_into_params, render_variables};
|
||||||
|
|
||||||
|
mod resolve;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -59,23 +60,6 @@ impl<'a> Renderer for DefaultRenderer<'a> {
|
||||||
options: &RenderOptions,
|
options: &RenderOptions,
|
||||||
) -> RenderResult {
|
) -> RenderResult {
|
||||||
let body = if VAR_REGEX.is_match(&template.body) {
|
let body = if VAR_REGEX.is_match(&template.body) {
|
||||||
// In order to define a variable evaluation order, we first need to find
|
|
||||||
// the global variables that are being used but for which an explicit order
|
|
||||||
// is not defined.
|
|
||||||
let body_variable_names = get_body_variable_names(&template.body);
|
|
||||||
let local_variable_names: HashSet<&str> =
|
|
||||||
template.vars.iter().map(|var| var.name.as_str()).collect();
|
|
||||||
let missing_global_variable_names: HashSet<&str> = body_variable_names
|
|
||||||
.difference(&local_variable_names)
|
|
||||||
.copied()
|
|
||||||
.collect();
|
|
||||||
let missing_global_variables: Vec<&Variable> = context
|
|
||||||
.global_vars
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|global_var| missing_global_variable_names.contains(&*global_var.name))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Convert "global" variable type aliases when needed
|
// Convert "global" variable type aliases when needed
|
||||||
let local_variables: Vec<&Variable> =
|
let local_variables: Vec<&Variable> =
|
||||||
if template.vars.iter().any(|var| var.var_type == "global") {
|
if template.vars.iter().any(|var| var.var_type == "global") {
|
||||||
|
@ -99,10 +83,16 @@ impl<'a> Renderer for DefaultRenderer<'a> {
|
||||||
template.vars.iter().collect()
|
template.vars.iter().collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
// The implicit global variables will be evaluated first, followed by the local vars
|
// Here we execute a graph dependency resolution algorithm to determine a valid
|
||||||
let mut variables: Vec<&Variable> = Vec::new();
|
// evaluation order for variables.
|
||||||
variables.extend(missing_global_variables);
|
let variables = match resolve::resolve_evaluation_order(
|
||||||
variables.extend(local_variables.iter());
|
&template.body,
|
||||||
|
&local_variables,
|
||||||
|
&context.global_vars,
|
||||||
|
) {
|
||||||
|
Ok(variables) => variables,
|
||||||
|
Err(err) => return RenderResult::Error(err),
|
||||||
|
};
|
||||||
|
|
||||||
// Compute the variable outputs
|
// Compute the variable outputs
|
||||||
let mut scope = Scope::new();
|
let mut scope = Scope::new();
|
||||||
|
@ -123,7 +113,31 @@ impl<'a> Renderer for DefaultRenderer<'a> {
|
||||||
return RenderResult::Error(RendererError::MissingSubMatch.into());
|
return RenderResult::Error(RendererError::MissingSubMatch.into());
|
||||||
}
|
}
|
||||||
} else if let Some(extension) = self.extensions.get(&variable.var_type) {
|
} else if let Some(extension) = self.extensions.get(&variable.var_type) {
|
||||||
match extension.calculate(context, &scope, &variable.params) {
|
let variable_params = if variable.inject_vars {
|
||||||
|
match inject_variables_into_params(&variable.params, &scope) {
|
||||||
|
Ok(augmented_params) => Cow::Owned(augmented_params),
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"unable to inject variables into params of variable '{}': {}",
|
||||||
|
variable.name, err
|
||||||
|
);
|
||||||
|
|
||||||
|
if variable.var_type == "form" {
|
||||||
|
if let Some(RendererError::MissingVariable(_)) =
|
||||||
|
err.downcast_ref::<RendererError>()
|
||||||
|
{
|
||||||
|
log_new_form_syntax_tip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RenderResult::Error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(&variable.params)
|
||||||
|
};
|
||||||
|
|
||||||
|
match extension.calculate(context, &scope, &variable_params) {
|
||||||
ExtensionResult::Success(output) => {
|
ExtensionResult::Success(output) => {
|
||||||
scope.insert(&variable.name, output);
|
scope.insert(&variable.name, output);
|
||||||
}
|
}
|
||||||
|
@ -161,6 +175,8 @@ impl<'a> Renderer for DefaultRenderer<'a> {
|
||||||
template.body.clone()
|
template.body.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let body = util::unescape_variable_inections(&body);
|
||||||
|
|
||||||
// Process the casing style
|
// Process the casing style
|
||||||
let body_with_casing = match options.casing_style {
|
let body_with_casing = match options.casing_style {
|
||||||
CasingStyle::None => body,
|
CasingStyle::None => body,
|
||||||
|
@ -192,52 +208,6 @@ impl<'a> Renderer for DefaultRenderer<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: test
|
|
||||||
pub(crate) fn render_variables(body: &str, scope: &Scope) -> Result<String> {
|
|
||||||
let mut replacing_error = None;
|
|
||||||
let output = VAR_REGEX
|
|
||||||
.replace_all(body, |caps: &Captures| {
|
|
||||||
let var_name = caps.name("name").unwrap().as_str();
|
|
||||||
let var_subname = caps.name("subname");
|
|
||||||
match scope.get(var_name) {
|
|
||||||
Some(output) => match output {
|
|
||||||
ExtensionOutput::Single(output) => output,
|
|
||||||
ExtensionOutput::Multiple(results) => match var_subname {
|
|
||||||
Some(var_subname) => {
|
|
||||||
let var_subname = var_subname.as_str();
|
|
||||||
results.get(var_subname).map_or("", |value| &*value)
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
error!(
|
|
||||||
"nested name missing from multi-value variable: {}",
|
|
||||||
var_name
|
|
||||||
);
|
|
||||||
replacing_error = Some(RendererError::MissingVariable(format!(
|
|
||||||
"nested name missing from multi-value variable: {}",
|
|
||||||
var_name
|
|
||||||
)));
|
|
||||||
""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
replacing_error = Some(RendererError::MissingVariable(format!(
|
|
||||||
"variable '{}' is missing",
|
|
||||||
var_name
|
|
||||||
)));
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if let Some(error) = replacing_error {
|
|
||||||
return Err(error.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_matching_template<'a>(
|
fn get_matching_template<'a>(
|
||||||
variable: &Variable,
|
variable: &Variable,
|
||||||
templates: &'a [&Template],
|
templates: &'a [&Template],
|
||||||
|
@ -254,6 +224,24 @@ fn get_matching_template<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn log_new_form_syntax_tip() {
|
||||||
|
error!("");
|
||||||
|
error!("TIP: This error might be happening because since version 2.1.0-alpha, Espanso changed");
|
||||||
|
error!("the syntax to define form controls. Instead of `{{{{control}}}}` you need to use");
|
||||||
|
error!("[[control]] (using square brackets instead of curly brackets).");
|
||||||
|
error!("");
|
||||||
|
error!("For example, if you have a form defined like the following:");
|
||||||
|
error!(" - trigger: test");
|
||||||
|
error!(" form: |");
|
||||||
|
error!(" Hi {{{{name}}}}!");
|
||||||
|
error!("");
|
||||||
|
error!("You'll need to replace it with:");
|
||||||
|
error!(" - trigger: test");
|
||||||
|
error!(" form: |");
|
||||||
|
error!(" Hi [[name]]!");
|
||||||
|
error!("");
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum RendererError {
|
pub enum RendererError {
|
||||||
#[error("missing variable: `{0}`")]
|
#[error("missing variable: `{0}`")]
|
||||||
|
@ -261,6 +249,9 @@ pub enum RendererError {
|
||||||
|
|
||||||
#[error("missing sub match")]
|
#[error("missing sub match")]
|
||||||
MissingSubMatch,
|
MissingSubMatch,
|
||||||
|
|
||||||
|
#[error("circular dependency: `{0}` -> `{1}`")]
|
||||||
|
CircularDependency(String, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -285,6 +276,13 @@ mod tests {
|
||||||
if let Some(Value::String(string)) = params.get("echo") {
|
if let Some(Value::String(string)) = params.get("echo") {
|
||||||
return ExtensionResult::Success(ExtensionOutput::Single(string.clone()));
|
return ExtensionResult::Success(ExtensionOutput::Single(string.clone()));
|
||||||
}
|
}
|
||||||
|
if let (Some(Value::String(name)), Some(Value::String(value))) =
|
||||||
|
(params.get("name"), params.get("value"))
|
||||||
|
{
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert(name.to_string(), value.to_string());
|
||||||
|
return ExtensionResult::Success(ExtensionOutput::Multiple(map));
|
||||||
|
}
|
||||||
// If the "read" param is present, echo the value of the corresponding result in the scope
|
// If the "read" param is present, echo the value of the corresponding result in the scope
|
||||||
if let Some(Value::String(string)) = params.get("read") {
|
if let Some(Value::String(string)) = params.get("read") {
|
||||||
if let Some(ExtensionOutput::Single(value)) = scope.get(string.as_str()) {
|
if let Some(ExtensionOutput::Single(value)) = scope.get(string.as_str()) {
|
||||||
|
@ -324,6 +322,7 @@ mod tests {
|
||||||
params: vec![("echo".to_string(), Value::String((*value).to_string()))]
|
params: vec![("echo".to_string(), Value::String((*value).to_string()))]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Params>(),
|
.collect::<Params>(),
|
||||||
|
..Default::default()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Template {
|
Template {
|
||||||
|
@ -393,6 +392,28 @@ mod tests {
|
||||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dict_variable_variable() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let template = Template {
|
||||||
|
body: "hello {{var.nested}}".to_string(),
|
||||||
|
vars: vec![Variable {
|
||||||
|
name: "var".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: vec![
|
||||||
|
("name".to_string(), Value::String("nested".to_string())),
|
||||||
|
("value".to_string(), Value::String("dict".to_string())),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Params>(),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||||
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello dict"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_variable() {
|
fn missing_variable() {
|
||||||
let renderer = get_renderer();
|
let renderer = get_renderer();
|
||||||
|
@ -415,6 +436,7 @@ mod tests {
|
||||||
"echo".to_string(),
|
"echo".to_string(),
|
||||||
Value::String("world".to_string()),
|
Value::String("world".to_string()),
|
||||||
)]),
|
)]),
|
||||||
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -423,6 +445,31 @@ mod tests {
|
||||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn global_dict_variable() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let template = template("hello {{var.nested}}", &[]);
|
||||||
|
let res = renderer.render(
|
||||||
|
&template,
|
||||||
|
&Context {
|
||||||
|
global_vars: vec![&Variable {
|
||||||
|
name: "var".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: vec![
|
||||||
|
("name".to_string(), Value::String("nested".to_string())),
|
||||||
|
("value".to_string(), Value::String("dict".to_string())),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Params>(),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&Default::default(),
|
||||||
|
);
|
||||||
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello dict"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn global_variable_explicit_ordering() {
|
fn global_variable_explicit_ordering() {
|
||||||
let renderer = get_renderer();
|
let renderer = get_renderer();
|
||||||
|
@ -435,6 +482,7 @@ mod tests {
|
||||||
params: vec![("echo".to_string(), Value::String("Bob".to_string()))]
|
params: vec![("echo".to_string(), Value::String("Bob".to_string()))]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Params>(),
|
.collect::<Params>(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
Variable {
|
Variable {
|
||||||
name: "var".to_string(),
|
name: "var".to_string(),
|
||||||
|
@ -454,6 +502,7 @@ mod tests {
|
||||||
"read".to_string(),
|
"read".to_string(),
|
||||||
Value::String("local".to_string()),
|
Value::String("local".to_string()),
|
||||||
)]),
|
)]),
|
||||||
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -462,6 +511,147 @@ mod tests {
|
||||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello Bob Bob"));
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello Bob Bob"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_global_variable() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let template = template("hello {{var2}}", &[]);
|
||||||
|
let res = renderer.render(
|
||||||
|
&template,
|
||||||
|
&Context {
|
||||||
|
global_vars: vec![
|
||||||
|
&Variable {
|
||||||
|
name: "var".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("world".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&Variable {
|
||||||
|
name: "var2".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("{{var}}".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&Default::default(),
|
||||||
|
);
|
||||||
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_global_variable_circular_dependency_should_fail() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let template = template("hello {{var}}", &[]);
|
||||||
|
let res = renderer.render(
|
||||||
|
&template,
|
||||||
|
&Context {
|
||||||
|
global_vars: vec![
|
||||||
|
&Variable {
|
||||||
|
name: "var".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("{{var2}}".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&Variable {
|
||||||
|
name: "var2".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("{{var3}}".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&Variable {
|
||||||
|
name: "var3".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("{{var}}".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&Default::default(),
|
||||||
|
);
|
||||||
|
assert!(matches!(res, RenderResult::Error(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn global_variable_depends_on() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let template = template("hello {{var}}", &[]);
|
||||||
|
let res = renderer.render(
|
||||||
|
&template,
|
||||||
|
&Context {
|
||||||
|
global_vars: vec![
|
||||||
|
&Variable {
|
||||||
|
name: "var".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("world".to_string()),
|
||||||
|
)]),
|
||||||
|
depends_on: vec!["var2".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&Variable {
|
||||||
|
name: "var2".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![("abort".to_string(), Value::Null)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&Default::default(),
|
||||||
|
);
|
||||||
|
assert!(matches!(res, RenderResult::Aborted));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_variable_explicit_ordering() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let template = Template {
|
||||||
|
body: "hello {{var}}".to_string(),
|
||||||
|
vars: vec![Variable {
|
||||||
|
name: "var".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: vec![("echo".to_string(), Value::String("something".to_string()))]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Params>(),
|
||||||
|
depends_on: vec!["global".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let res = renderer.render(
|
||||||
|
&template,
|
||||||
|
&Context {
|
||||||
|
global_vars: vec![&Variable {
|
||||||
|
name: "global".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![("abort".to_string(), Value::Null)]),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&Default::default(),
|
||||||
|
);
|
||||||
|
assert!(matches!(res, RenderResult::Aborted));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nested_match() {
|
fn nested_match() {
|
||||||
let renderer = get_renderer();
|
let renderer = get_renderer();
|
||||||
|
@ -473,6 +663,7 @@ mod tests {
|
||||||
params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
|
params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Params>(),
|
.collect::<Params>(),
|
||||||
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
@ -503,6 +694,7 @@ mod tests {
|
||||||
params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
|
params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Params>(),
|
.collect::<Params>(),
|
||||||
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
@ -527,6 +719,7 @@ mod tests {
|
||||||
params: vec![("abort".to_string(), Value::Null)]
|
params: vec![("abort".to_string(), Value::Null)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Params>(),
|
.collect::<Params>(),
|
||||||
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
@ -545,10 +738,213 @@ mod tests {
|
||||||
params: vec![("error".to_string(), Value::Null)]
|
params: vec![("error".to_string(), Value::Null)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Params>(),
|
.collect::<Params>(),
|
||||||
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let res = renderer.render(&template, &Default::default(), &Default::default());
|
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||||
assert!(matches!(res, RenderResult::Error(_)));
|
assert!(matches!(res, RenderResult::Error(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_injection() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let mut template = template_for_str("hello {{fullname}}");
|
||||||
|
template.vars = vec![
|
||||||
|
Variable {
|
||||||
|
name: "firstname".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("John".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Variable {
|
||||||
|
name: "lastname".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("Snow".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Variable {
|
||||||
|
name: "fullname".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("{{firstname}} {{lastname}}".to_string()),
|
||||||
|
)]),
|
||||||
|
inject_vars: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||||
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello John Snow"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disable_variable_injection() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let mut template = template_for_str("hello {{second}}");
|
||||||
|
template.vars = vec![
|
||||||
|
Variable {
|
||||||
|
name: "first".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![("echo".to_string(), Value::String("one".to_string()))]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Variable {
|
||||||
|
name: "second".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("{{first}} two".to_string()),
|
||||||
|
)]),
|
||||||
|
inject_vars: false,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||||
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello {{first}} two"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escaped_variable_injection() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let mut template = template_for_str("hello {{second}}");
|
||||||
|
template.vars = vec![
|
||||||
|
Variable {
|
||||||
|
name: "first".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![("echo".to_string(), Value::String("one".to_string()))]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Variable {
|
||||||
|
name: "second".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("\\{\\{first\\}\\} two".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||||
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello {{first}} two"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_injection_missing_var() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let mut template = template_for_str("hello {{second}}");
|
||||||
|
template.vars = vec![Variable {
|
||||||
|
name: "second".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("the next is {{missing}}".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||||
|
assert!(matches!(res, RenderResult::Error(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_injection_with_global_variable() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let mut template = template_for_str("hello {{output}}");
|
||||||
|
template.vars = vec![
|
||||||
|
Variable {
|
||||||
|
name: "var".to_string(),
|
||||||
|
var_type: "global".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Variable {
|
||||||
|
name: "output".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("{{var}}".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let res = renderer.render(
|
||||||
|
&template,
|
||||||
|
&Context {
|
||||||
|
global_vars: vec![&Variable {
|
||||||
|
name: "var".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("global".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&Default::default(),
|
||||||
|
);
|
||||||
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello global"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_injection_local_var_takes_precedence_over_global() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let mut template = template_for_str("hello {{output}}");
|
||||||
|
template.vars = vec![
|
||||||
|
Variable {
|
||||||
|
name: "var".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("local".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Variable {
|
||||||
|
name: "output".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("{{var}}".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let res = renderer.render(
|
||||||
|
&template,
|
||||||
|
&Context {
|
||||||
|
global_vars: vec![&Variable {
|
||||||
|
name: "var".to_string(),
|
||||||
|
var_type: "mock".to_string(),
|
||||||
|
params: Params::from_iter(vec![(
|
||||||
|
"echo".to_string(),
|
||||||
|
Value::String("global".to_string()),
|
||||||
|
)]),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&Default::default(),
|
||||||
|
);
|
||||||
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello local"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn variable_escape() {
|
||||||
|
let renderer = get_renderer();
|
||||||
|
let template = template("hello \\{\\{var\\}\\}", &[("var", "world")]);
|
||||||
|
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||||
|
assert!(matches!(res, RenderResult::Success(str) if str == "hello {{var}}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
204
espanso-render/src/renderer/resolve.rs
Normal file
204
espanso-render/src/renderer/resolve.rs
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use log::error;
|
||||||
|
|
||||||
|
use crate::Variable;
|
||||||
|
|
||||||
|
use super::RendererError;
|
||||||
|
|
||||||
|
struct Node<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
variable: Option<&'a Variable>,
|
||||||
|
dependencies: Option<HashSet<&'a str>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_evaluation_order<'a>(
|
||||||
|
body: &'a str,
|
||||||
|
local_vars: &'a [&'a Variable],
|
||||||
|
global_vars: &'a [&'a Variable],
|
||||||
|
) -> Result<Vec<&'a Variable>> {
|
||||||
|
let node_map = generate_nodes(body, local_vars, global_vars);
|
||||||
|
|
||||||
|
let body_node = node_map
|
||||||
|
.get("__match_body")
|
||||||
|
.ok_or_else(|| anyhow!("missing body node"))?;
|
||||||
|
|
||||||
|
let eval_order = RefCell::new(Vec::new());
|
||||||
|
let resolved = RefCell::new(HashSet::new());
|
||||||
|
let seen = RefCell::new(HashSet::new());
|
||||||
|
{
|
||||||
|
resolve_dependencies(body_node, &node_map, &eval_order, &resolved, &seen)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let eval_order_ref = eval_order.borrow();
|
||||||
|
|
||||||
|
let mut ordered_variables = Vec::new();
|
||||||
|
for var_name in (*eval_order_ref).iter() {
|
||||||
|
let node = node_map
|
||||||
|
.get(var_name)
|
||||||
|
.ok_or_else(|| anyhow!("could not find dependency node for variable: {}", var_name))?;
|
||||||
|
if let Some(var) = node.variable {
|
||||||
|
ordered_variables.push(var);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ordered_variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_nodes<'a>(
|
||||||
|
body: &'a str,
|
||||||
|
local_vars: &'a [&'a Variable],
|
||||||
|
global_vars: &'a [&'a Variable],
|
||||||
|
) -> HashMap<&'a str, Node<'a>> {
|
||||||
|
let mut local_vars_nodes = Vec::new();
|
||||||
|
for (index, var) in local_vars.iter().enumerate() {
|
||||||
|
let mut dependencies = HashSet::new();
|
||||||
|
if var.inject_vars {
|
||||||
|
dependencies.extend(super::util::get_params_variable_names(&var.params));
|
||||||
|
}
|
||||||
|
dependencies.extend(var.depends_on.iter().map(|v| v.as_str()));
|
||||||
|
|
||||||
|
// Every local variable depends on the one before it.
|
||||||
|
// Needed to guarantee execution order within local vars.
|
||||||
|
if index > 0 {
|
||||||
|
let previous_var = local_vars.get(index - 1);
|
||||||
|
if let Some(previous_var) = previous_var {
|
||||||
|
dependencies.insert(&previous_var.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
local_vars_nodes.push(Node {
|
||||||
|
name: &var.name,
|
||||||
|
variable: Some(var),
|
||||||
|
dependencies: Some(dependencies),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let global_vars_nodes = global_vars.iter().map(|var| create_node_from_var(*var));
|
||||||
|
|
||||||
|
// The body depends on all local variables + the variables read inside it (which might be global)
|
||||||
|
let mut body_dependencies: HashSet<&str> =
|
||||||
|
local_vars_nodes.iter().map(|node| node.name).collect();
|
||||||
|
body_dependencies.extend(super::util::get_body_variable_names(body));
|
||||||
|
|
||||||
|
let body_node = Node {
|
||||||
|
name: "__match_body",
|
||||||
|
variable: None,
|
||||||
|
dependencies: Some(body_dependencies),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut node_map = HashMap::new();
|
||||||
|
|
||||||
|
node_map.insert(body_node.name, body_node);
|
||||||
|
global_vars_nodes.into_iter().for_each(|node| {
|
||||||
|
node_map.insert(node.name, node);
|
||||||
|
});
|
||||||
|
local_vars_nodes.into_iter().for_each(|node| {
|
||||||
|
node_map.insert(node.name, node);
|
||||||
|
});
|
||||||
|
|
||||||
|
node_map
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_node_from_var(var: &Variable) -> Node {
|
||||||
|
let dependencies = if var.inject_vars || !var.depends_on.is_empty() {
|
||||||
|
let mut vars = HashSet::new();
|
||||||
|
|
||||||
|
if var.inject_vars {
|
||||||
|
vars.extend(super::util::get_params_variable_names(&var.params))
|
||||||
|
}
|
||||||
|
|
||||||
|
vars.extend(var.depends_on.iter().map(|s| s.as_str()));
|
||||||
|
|
||||||
|
Some(vars)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Node {
|
||||||
|
name: &var.name,
|
||||||
|
variable: Some(var),
|
||||||
|
dependencies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_dependencies<'a>(
|
||||||
|
node: &'a Node,
|
||||||
|
node_map: &'a HashMap<&'a str, Node<'a>>,
|
||||||
|
eval_order: &'a RefCell<Vec<&'a str>>,
|
||||||
|
resolved: &'a RefCell<HashSet<&'a str>>,
|
||||||
|
seen: &'a RefCell<HashSet<&'a str>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
{
|
||||||
|
let mut seen_ref = seen.borrow_mut();
|
||||||
|
seen_ref.insert(node.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dependencies) = &node.dependencies {
|
||||||
|
for dependency in dependencies.iter() {
|
||||||
|
let has_been_resolved = {
|
||||||
|
let resolved_ref = resolved.borrow();
|
||||||
|
resolved_ref.contains(dependency)
|
||||||
|
};
|
||||||
|
let has_been_seen = {
|
||||||
|
let seen_ref = seen.borrow();
|
||||||
|
seen_ref.contains(dependency)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !has_been_resolved {
|
||||||
|
if has_been_seen {
|
||||||
|
return Err(
|
||||||
|
RendererError::CircularDependency(node.name.to_string(), dependency.to_string()).into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
match node_map.get(dependency) {
|
||||||
|
Some(dependency_node) => {
|
||||||
|
resolve_dependencies(dependency_node, node_map, eval_order, resolved, seen)?
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!("could not resolve variable {:?}", dependency);
|
||||||
|
if let Some(variable) = &node.variable {
|
||||||
|
if variable.var_type == "form" {
|
||||||
|
super::log_new_form_syntax_tip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err(RendererError::MissingVariable(dependency.to_string()).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut eval_order_ref = eval_order.borrow_mut();
|
||||||
|
eval_order_ref.push(node.name);
|
||||||
|
let mut resolved_ref = resolved.borrow_mut();
|
||||||
|
resolved_ref.insert(node.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -17,6 +17,11 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use crate::{renderer::RendererError, ExtensionOutput, Params, Scope, Value};
|
||||||
|
use anyhow::Result;
|
||||||
|
use log::error;
|
||||||
|
use regex::Captures;
|
||||||
|
|
||||||
use super::VAR_REGEX;
|
use super::VAR_REGEX;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
@ -29,10 +34,128 @@ pub(crate) fn get_body_variable_names(body: &str) -> HashSet<&str> {
|
||||||
variables
|
variables
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_params_variable_names(params: &Params) -> HashSet<&str> {
|
||||||
|
let mut names = HashSet::new();
|
||||||
|
|
||||||
|
for (_, value) in params.iter() {
|
||||||
|
let local_names = get_value_variable_names_recursively(value);
|
||||||
|
names.extend(local_names);
|
||||||
|
}
|
||||||
|
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_value_variable_names_recursively(value: &Value) -> HashSet<&str> {
|
||||||
|
match value {
|
||||||
|
Value::String(s_value) => get_body_variable_names(s_value),
|
||||||
|
Value::Array(values) => {
|
||||||
|
let mut local_names: HashSet<&str> = HashSet::new();
|
||||||
|
for value in values {
|
||||||
|
local_names.extend(get_value_variable_names_recursively(value));
|
||||||
|
}
|
||||||
|
local_names
|
||||||
|
}
|
||||||
|
Value::Object(fields) => {
|
||||||
|
let mut local_names: HashSet<&str> = HashSet::new();
|
||||||
|
for value in fields.values() {
|
||||||
|
local_names.extend(get_value_variable_names_recursively(value));
|
||||||
|
}
|
||||||
|
local_names
|
||||||
|
}
|
||||||
|
_ => HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render_variables(body: &str, scope: &Scope) -> Result<String> {
|
||||||
|
let mut replacing_error = None;
|
||||||
|
let output = VAR_REGEX
|
||||||
|
.replace_all(body, |caps: &Captures| {
|
||||||
|
let var_name = caps.name("name").unwrap().as_str();
|
||||||
|
let var_subname = caps.name("subname");
|
||||||
|
match scope.get(var_name) {
|
||||||
|
Some(output) => match output {
|
||||||
|
ExtensionOutput::Single(output) => output,
|
||||||
|
ExtensionOutput::Multiple(results) => match var_subname {
|
||||||
|
Some(var_subname) => {
|
||||||
|
let var_subname = var_subname.as_str();
|
||||||
|
results.get(var_subname).map_or("", |value| &*value)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!(
|
||||||
|
"nested name missing from multi-value variable: {}",
|
||||||
|
var_name
|
||||||
|
);
|
||||||
|
replacing_error = Some(RendererError::MissingVariable(format!(
|
||||||
|
"nested name missing from multi-value variable: {}",
|
||||||
|
var_name
|
||||||
|
)));
|
||||||
|
""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
replacing_error = Some(RendererError::MissingVariable(format!(
|
||||||
|
"variable '{}' is missing",
|
||||||
|
var_name
|
||||||
|
)));
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if let Some(error) = replacing_error {
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let unescaped_output = unescape_variable_inections(&output);
|
||||||
|
Ok(unescaped_output)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unescape_variable_inections(body: &str) -> String {
|
||||||
|
body.replace("\\{\\{", "{{").replace("\\}\\}", "}}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inject_variables_into_params(params: &Params, scope: &Scope) -> Result<Params> {
|
||||||
|
let mut params = params.clone();
|
||||||
|
|
||||||
|
for (_, value) in params.iter_mut() {
|
||||||
|
inject_variables_into_value(value, scope)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_variables_into_value(value: &mut Value, scope: &Scope) -> Result<()> {
|
||||||
|
match value {
|
||||||
|
Value::String(s_value) => {
|
||||||
|
let new_value = render_variables(s_value, scope)?;
|
||||||
|
|
||||||
|
if &new_value != s_value {
|
||||||
|
s_value.clear();
|
||||||
|
s_value.push_str(&new_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Array(values) => {
|
||||||
|
for value in values {
|
||||||
|
inject_variables_into_value(value, scope)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Object(fields) => {
|
||||||
|
for value in fields.values_mut() {
|
||||||
|
inject_variables_into_value(value, scope)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::iter::FromIterator;
|
use std::{collections::HashMap, iter::FromIterator};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get_body_variable_names_no_vars() {
|
fn get_body_variable_names_no_vars() {
|
||||||
|
@ -49,4 +172,44 @@ mod tests {
|
||||||
HashSet::from_iter(vec!["world", "greet"]),
|
HashSet::from_iter(vec!["world", "greet"]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_variables_into_params() {
|
||||||
|
let mut params = Params::new();
|
||||||
|
params.insert(
|
||||||
|
"field1".to_string(),
|
||||||
|
Value::String("this contains {{first}}".to_string()),
|
||||||
|
);
|
||||||
|
params.insert("field2".to_string(), Value::Bool(true));
|
||||||
|
params.insert(
|
||||||
|
"field3".to_string(),
|
||||||
|
Value::Array(vec![Value::String("this contains {{first}}".to_string())]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut nested = HashMap::new();
|
||||||
|
nested.insert(
|
||||||
|
"subfield1".to_string(),
|
||||||
|
Value::String("also contains {{first}}".to_string()),
|
||||||
|
);
|
||||||
|
params.insert("field4".to_string(), Value::Object(nested));
|
||||||
|
|
||||||
|
let mut scope = Scope::new();
|
||||||
|
scope.insert("first", ExtensionOutput::Single("one".to_string()));
|
||||||
|
|
||||||
|
let result = inject_variables_into_params(¶ms, &scope).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 4);
|
||||||
|
assert_eq!(
|
||||||
|
result.get("field1").unwrap(),
|
||||||
|
&Value::String("this contains one".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(result.get("field2").unwrap(), &Value::Bool(true));
|
||||||
|
assert_eq!(
|
||||||
|
result.get("field3").unwrap(),
|
||||||
|
&Value::Array(vec![Value::String("this contains one".to_string())])
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(result.get("field4").unwrap(), Value::Object(fields) if fields.get("subfield1").unwrap() == &Value::String("also contains one".to_string()))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "2.0.5-alpha"
|
version = "2.1.0-alpha"
|
||||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
description = "Cross-platform Text Expander written in Rust"
|
description = "Cross-platform Text Expander written in Rust"
|
||||||
|
|
|
@ -63,14 +63,7 @@ fn convert_fields(fields: &Params) -> HashMap<String, FormField> {
|
||||||
.cloned(),
|
.cloned(),
|
||||||
values: params
|
values: params
|
||||||
.get("values")
|
.get("values")
|
||||||
.and_then(|val| val.as_array())
|
.and_then(|v| extract_values(v, params.get("trim_string_values")))
|
||||||
.map(|arr| {
|
|
||||||
arr
|
|
||||||
.iter()
|
|
||||||
.flat_map(|choice| choice.as_string())
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
}),
|
}),
|
||||||
Some(Value::String(field_type)) if field_type == "list" => Some(FormField::List {
|
Some(Value::String(field_type)) if field_type == "list" => Some(FormField::List {
|
||||||
|
@ -80,14 +73,7 @@ fn convert_fields(fields: &Params) -> HashMap<String, FormField> {
|
||||||
.cloned(),
|
.cloned(),
|
||||||
values: params
|
values: params
|
||||||
.get("values")
|
.get("values")
|
||||||
.and_then(|val| val.as_array())
|
.and_then(|v| extract_values(v, params.get("trim_string_values")))
|
||||||
.map(|arr| {
|
|
||||||
arr
|
|
||||||
.iter()
|
|
||||||
.flat_map(|choice| choice.as_string())
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
}),
|
}),
|
||||||
// By default, it's considered type 'text'
|
// By default, it's considered type 'text'
|
||||||
|
@ -113,3 +99,38 @@ fn convert_fields(fields: &Params) -> HashMap<String, FormField> {
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_values(value: &Value, trim_string_values: Option<&Value>) -> Option<Vec<String>> {
|
||||||
|
let trim_string_values = *trim_string_values
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(&true);
|
||||||
|
|
||||||
|
match value {
|
||||||
|
Value::Array(values) => Some(
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.flat_map(|choice| choice.as_string())
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
Value::String(values) => Some(
|
||||||
|
values
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
if trim_string_values {
|
||||||
|
let trimmed_line = line.trim();
|
||||||
|
if !trimmed_line.is_empty() {
|
||||||
|
Some(trimmed_line)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(line)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(String::from)
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -151,6 +151,8 @@ fn convert_var(var: espanso_config::matches::Variable) -> espanso_render::Variab
|
||||||
name: var.name,
|
name: var.name,
|
||||||
var_type: var.var_type,
|
var_type: var.var_type,
|
||||||
params: convert_params(var.params),
|
params: convert_params(var.params),
|
||||||
|
inject_vars: var.inject_vars,
|
||||||
|
depends_on: var.depends_on,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,6 +227,8 @@ impl<'a> Renderer<'a> for RendererAdapter<'a> {
|
||||||
name,
|
name,
|
||||||
var_type: "echo".to_string(),
|
var_type: "echo".to_string(),
|
||||||
params,
|
params,
|
||||||
|
inject_vars: false,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: espanso
|
name: espanso
|
||||||
version: 2.0.5-alpha
|
version: 2.1.0-alpha
|
||||||
summary: A Cross-platform Text Expander written in Rust
|
summary: A Cross-platform Text Expander written in Rust
|
||||||
description: |
|
description: |
|
||||||
espanso is a Cross-platform, Text Expander written in Rust.
|
espanso is a Cross-platform, Text Expander written in Rust.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user