/* * This file is part of esp name: (), var_type: (), params: ()anso. * * 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 . */ use std::collections::{HashMap, HashSet}; use crate::{ CasingStyle, Context, Extension, ExtensionOutput, ExtensionResult, RenderOptions, RenderResult, Renderer, Scope, Template, Value, Variable }; use log::{error, warn}; use regex::{Captures, Regex}; use thiserror::Error; use util::get_body_variable_names; mod util; lazy_static! { pub(crate) static ref VAR_REGEX: Regex = Regex::new(r"\{\{\s*((?P\w+)(\.(?P(\w+)))?)\s*\}\}").unwrap(); } pub(crate) struct DefaultRenderer { extensions: HashMap>, } impl DefaultRenderer { pub fn new(extensions: Vec>) -> Self { let extensions = extensions .into_iter() .map(|ext| (ext.name().to_string(), ext)) .collect(); Self { extensions } } } impl Renderer for DefaultRenderer { fn render( &self, template: &Template, context: &Context, options: &RenderOptions, ) -> RenderResult { 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 let local_variables: Vec<&Variable> = if template.vars.iter().any(|var| var.var_type == "global") { let global_vars: HashMap<&str, &Variable> = context .global_vars .iter() .map(|var| (&*var.name, *var)) .collect(); template .vars .iter() .filter_map(|var| { if var.var_type == "global" { global_vars.get(&*var.name).copied() } else { Some(var) } }) .collect() } else { template.vars.iter().map(|var| var).collect() }; // The implicit global variables will be evaluated first, followed by the local vars let mut variables: Vec<&Variable> = Vec::new(); variables.extend(missing_global_variables); variables.extend(local_variables.iter()); // Compute the variable outputs let mut scope = Scope::new(); for variable in variables { if variable.var_type == "match" { // Recursive call // Call render recursively if let Some(sub_template) = get_matching_template(variable, context.templates.as_slice()) { match self.render(sub_template, context, options) { RenderResult::Success(output) => { scope.insert(&variable.name, ExtensionOutput::Single(output)); } result => return result, } } else { error!("unable to find sub-match: {}", variable.name); return RenderResult::Error(RendererError::MissingSubMatch.into()); } } else if let Some(extension) = self.extensions.get(&variable.var_type) { match extension.calculate(context, &scope, &variable.params) { ExtensionResult::Success(output) => { scope.insert(&variable.name, output); } ExtensionResult::Aborted => { warn!( "rendering was aborted by extension: {}, on var: {}", variable.var_type, variable.name ); return RenderResult::Aborted; } ExtensionResult::Error(err) => { warn!( "extension '{}' on var: '{}' reported an error: {}", variable.var_type, variable.name, err ); return RenderResult::Error(err); } } } else { error!( "no extension found for variable type: {}", variable.var_type ); } } // Replace the variables let mut replacing_error = None; let output = VAR_REGEX .replace_all(&template.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 RenderResult::Error(error.into()); } output } else { template.body.clone() }; // Process the casing style let body_with_casing = match options.casing_style { CasingStyle::None => body, CasingStyle::Uppercase => body.to_uppercase(), CasingStyle::Capitalize => { // Capitalize the first letter let mut v: Vec = body.chars().collect(); v[0] = v[0].to_uppercase().next().unwrap(); v.into_iter().collect() } }; RenderResult::Success(body_with_casing) } } fn get_matching_template<'a>( variable: &Variable, templates: &'a [&Template], ) -> Option<&'a Template> { // Find matching template let id = variable.params.get("trigger")?; if let Value::String(id) = id { templates .iter() .find(|template| template.ids.contains(id)) .copied() } else { None } } #[derive(Error, Debug)] pub enum RendererError { #[error("missing variable: `{0}`")] MissingVariable(String), #[error("missing sub match")] MissingSubMatch, } #[cfg(test)] mod tests { use super::*; use crate::Params; use std::iter::FromIterator; struct MockExtension {} impl Extension for MockExtension { fn name(&self) -> &str { "mock" } fn calculate( &self, _context: &Context, scope: &Scope, params: &crate::Params, ) -> ExtensionResult { if let Some(value) = params.get("echo") { if let Value::String(string) = value { return ExtensionResult::Success(ExtensionOutput::Single(string.clone())); } } // If the "read" param is present, echo the value of the corresponding result in the scope if let Some(value) = params.get("read") { if let Value::String(string) = value { if let Some(ExtensionOutput::Single(value)) = scope.get(string.as_str()) { return ExtensionResult::Success(ExtensionOutput::Single(value.to_string())); } } } if params.get("abort").is_some() { return ExtensionResult::Aborted; } if params.get("error").is_some() { return ExtensionResult::Error(RendererError::MissingVariable("missing".to_string()).into()) } ExtensionResult::Aborted } } pub fn get_renderer() -> impl Renderer { DefaultRenderer::new(vec![Box::new(MockExtension {})]) } pub fn template_for_str(str: &str) -> Template { Template { ids: vec!["id".to_string()], body: str.to_string(), vars: Vec::new(), } } pub fn template(body: &str, vars: &[(&str, &str)]) -> Template { let vars = vars.iter().map(|(name, value)| { Variable { name: (*name).to_string(), var_type: "mock".to_string(), params: Params::from_iter(vec![("echo".to_string(), Value::String((*value).to_string()))].into_iter()) } }).collect(); Template { ids: vec!["id".to_string()], body: body.to_string(), vars, } } #[test] fn no_variable_no_styling() { let renderer = get_renderer(); let res = renderer.render(&template_for_str("plain body"), &Default::default(), &Default::default()); assert!(matches!(res, RenderResult::Success(str) if str == "plain body")); } #[test] fn no_variable_capitalize() { let renderer = get_renderer(); let res = renderer.render(&template_for_str("plain body"), &Default::default(), &RenderOptions { casing_style: CasingStyle::Capitalize, }); assert!(matches!(res, RenderResult::Success(str) if str == "Plain body")); } #[test] fn no_variable_uppercase() { let renderer = get_renderer(); let res = renderer.render(&template_for_str("plain body"), &Default::default(), &RenderOptions { casing_style: CasingStyle::Uppercase, }); assert!(matches!(res, RenderResult::Success(str) if str == "PLAIN BODY")); } #[test] fn basic_variable() { 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 world")); } #[test] fn missing_variable() { let renderer = get_renderer(); let template = template_for_str("hello {{var}}"); let res = renderer.render(&template, &Default::default(), &Default::default()); assert!(matches!(res, RenderResult::Error(_))); } #[test] fn global_variable() { 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()))]) } ], ..Default::default() }, &Default::default()); assert!(matches!(res, RenderResult::Success(str) if str == "hello world")); } #[test] fn global_variable_explicit_ordering() { let renderer = get_renderer(); let template = Template { body: "hello {{var}} {{local}}".to_string(), vars: vec![ Variable { name: "local".to_string(), var_type: "mock".to_string(), params: Params::from_iter(vec![("echo".to_string(), Value::String("Bob".to_string()))].into_iter()) }, Variable { name: "var".to_string(), var_type: "global".to_string(), ..Default::default() } ], ..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![("read".to_string(), Value::String("local".to_string()))]) } ], ..Default::default() }, &Default::default()); assert!(matches!(res, RenderResult::Success(str) if str == "hello Bob Bob")); } #[test] fn nested_match() { let renderer = get_renderer(); let template = Template { body: "hello {{var}}".to_string(), vars: vec![ Variable { name: "var".to_string(), var_type: "match".to_string(), params: Params::from_iter(vec![("trigger".to_string(), Value::String("nested".to_string()))].into_iter()) }, ], ..Default::default() }; let nested_template = Template { ids: vec!["nested".to_string()], body: "world".to_string(), ..Default::default() }; let res = renderer.render(&template, &Context { templates: vec![&nested_template], ..Default::default() }, &Default::default()); assert!(matches!(res, RenderResult::Success(str) if str == "hello world")); } #[test] fn missing_nested_match() { let renderer = get_renderer(); let template = Template { body: "hello {{var}}".to_string(), vars: vec![ Variable { name: "var".to_string(), var_type: "match".to_string(), params: Params::from_iter(vec![("trigger".to_string(), Value::String("nested".to_string()))].into_iter()) }, ], ..Default::default() }; let res = renderer.render(&template, &Context { ..Default::default() }, &Default::default()); assert!(matches!(res, RenderResult::Error(_))); } #[test] fn extension_aborting_propagates() { 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: Params::from_iter(vec![("abort".to_string(), Value::Null)].into_iter()), } ], ..Default::default() }; let res = renderer.render(&template, &Default::default(), &Default::default()); assert!(matches!(res, RenderResult::Aborted)); } #[test] fn extension_error_propagates() { 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: Params::from_iter(vec![("error".to_string(), Value::Null)].into_iter()), } ], ..Default::default() }; let res = renderer.render(&template, &Default::default(), &Default::default()); assert!(matches!(res, RenderResult::Error(_))); } }