feat(render): implement default renderer
This commit is contained in:
parent
8df15bba3f
commit
7806a079dc
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -323,6 +323,19 @@ dependencies = [
|
||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "espanso-render"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"enum-as-inner",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "espanso-ui"
|
name = "espanso-ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
@ -9,4 +9,5 @@ members = [
|
||||||
"espanso-config",
|
"espanso-config",
|
||||||
"espanso-match",
|
"espanso-match",
|
||||||
"espanso-clipboard",
|
"espanso-clipboard",
|
||||||
|
"espanso-render",
|
||||||
]
|
]
|
14
espanso-render/Cargo.toml
Normal file
14
espanso-render/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "espanso-render"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
log = "0.4.14"
|
||||||
|
anyhow = "1.0.38"
|
||||||
|
thiserror = "1.0.23"
|
||||||
|
regex = "1.4.3"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
chrono = "0.4.19"
|
||||||
|
enum-as-inner = "0.3.3"
|
20
espanso-render/src/extension/mod.rs
Normal file
20
espanso-render/src/extension/mod.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub mod date;
|
149
espanso-render/src/lib.rs
Normal file
149
espanso-render/src/lib.rs
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use enum_as_inner::EnumAsInner;
|
||||||
|
|
||||||
|
mod renderer;
|
||||||
|
pub mod extension;
|
||||||
|
|
||||||
|
pub trait Renderer {
|
||||||
|
fn render(&self, template: &Template, context: &Context, options: &RenderOptions)
|
||||||
|
-> RenderResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(extensions: Vec<Box<dyn Extension>>) -> impl Renderer {
|
||||||
|
renderer::DefaultRenderer::new(extensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RenderResult {
|
||||||
|
Success(String),
|
||||||
|
Aborted,
|
||||||
|
Error(anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Context<'a> {
|
||||||
|
pub global_vars: Vec<&'a Variable>,
|
||||||
|
pub templates: Vec<&'a Template>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for Context<'a> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
global_vars: Vec::new(),
|
||||||
|
templates: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct RenderOptions {
|
||||||
|
casing_style: CasingStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RenderOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
casing_style: CasingStyle::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum CasingStyle {
|
||||||
|
None,
|
||||||
|
Capitalize,
|
||||||
|
Uppercase,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Template {
|
||||||
|
ids: Vec<String>,
|
||||||
|
body: String,
|
||||||
|
vars: Vec<Variable>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Template {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ids: Vec::new(),
|
||||||
|
body: "".to_string(),
|
||||||
|
vars: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Variable {
|
||||||
|
name: String,
|
||||||
|
var_type: String,
|
||||||
|
params: Params,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Variable {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
name: "".to_string(),
|
||||||
|
var_type: "".to_string(),
|
||||||
|
params: Params::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Params = HashMap<String, Value>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Value {
|
||||||
|
Null,
|
||||||
|
Bool(bool),
|
||||||
|
Number(Number),
|
||||||
|
String(String),
|
||||||
|
Array(Vec<Value>),
|
||||||
|
Object(HashMap<String, Value>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Number {
|
||||||
|
Integer(i64),
|
||||||
|
Float(f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Extension {
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
fn calculate(&self, context: &Context, scope: &Scope, params: &Params) -> ExtensionResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Scope<'a> = HashMap<&'a str, ExtensionOutput>;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum ExtensionOutput {
|
||||||
|
Single(String),
|
||||||
|
Multiple(HashMap<String, String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, EnumAsInner)]
|
||||||
|
pub enum ExtensionResult {
|
||||||
|
Success(ExtensionOutput),
|
||||||
|
Aborted,
|
||||||
|
Error(anyhow::Error),
|
||||||
|
}
|
473
espanso-render/src/renderer/mod.rs
Normal file
473
espanso-render/src/renderer/mod.rs
Normal file
|
@ -0,0 +1,473 @@
|
||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<name>\w+)(\.(?P<subname>(\w+)))?)\s*\}\}").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DefaultRenderer {
|
||||||
|
extensions: HashMap<String, Box<dyn Extension>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultRenderer {
|
||||||
|
pub fn new(extensions: Vec<Box<dyn Extension>>) -> 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<char> = 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(_)));
|
||||||
|
}
|
||||||
|
}
|
52
espanso-render/src/renderer/util.rs
Normal file
52
espanso-render/src/renderer/util.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* 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::collections::HashSet;
|
||||||
|
use super::VAR_REGEX;
|
||||||
|
|
||||||
|
pub(crate) fn get_body_variable_names(body: &str) -> HashSet<&str> {
|
||||||
|
let mut variables = HashSet::new();
|
||||||
|
for caps in VAR_REGEX.captures_iter(&body) {
|
||||||
|
let var_name = caps.name("name").unwrap().as_str();
|
||||||
|
variables.insert(var_name);
|
||||||
|
}
|
||||||
|
variables
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_body_variable_names_no_vars() {
|
||||||
|
assert_eq!(
|
||||||
|
get_body_variable_names("no variables"),
|
||||||
|
HashSet::from_iter(vec![]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_body_variable_names_multiple_vars() {
|
||||||
|
assert_eq!(
|
||||||
|
get_body_variable_names("hello {{world}} name {{greet}}"),
|
||||||
|
HashSet::from_iter(vec!["world", "greet"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user