use std::collections::{BTreeMap, HashMap};
use std::path::Path;
use anyhow::Result;
use tera::{Function, Tera, Value};
use crate::input::Input;
use crate::platform;
pub fn new_engine() -> Tera {
let mut t = Tera::default();
t.register_function("is_windows", os_fn(platform::is_windows));
t.register_function("is_linux", os_fn(platform::is_linux));
t.register_function("is_mac", os_fn(platform::is_mac));
t
}
fn os_fn(check: fn() -> bool) -> impl Function {
move |_args: &HashMap<String, Value>| -> tera::Result<Value> { Ok(Value::Bool(check())) }
}
pub fn render(tera: &mut Tera, template: &str, ctx: &tera::Context) -> Result<String> {
Ok(tera.render_str(template, ctx)?)
}
pub struct Context<'a> {
pub input: Option<&'a Input>,
pub command: &'a str,
pub cwd: &'a str,
pub group: &'a str,
pub rule_name: &'a str,
pub vars: &'a BTreeMap<String, toml::Value>,
}
fn strip_verbatim_str(s: &str) -> String {
s.strip_prefix(r"\\?\").unwrap_or(s).to_string()
}
fn file_vars(p: &Path) -> [(String, String); 5] {
let full = strip_verbatim_str(&p.to_string_lossy());
let dir = p
.parent()
.map(|x| strip_verbatim_str(&x.to_string_lossy()))
.unwrap_or_default();
let name = p
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let stem = p
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
let ext = p
.extension()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
[
("file_path".into(), full),
("file_dir".into(), dir),
("file_name".into(), name),
("file_stem".into(), stem),
("file_ext".into(), ext),
]
}
fn url_vars(u: &url::Url) -> [(String, String); 6] {
[
("url_scheme".into(), u.scheme().to_string()),
("url_host".into(), u.host_str().unwrap_or("").to_string()),
(
"url_port".into(),
u.port().map(|p| p.to_string()).unwrap_or_default(),
),
("url_path".into(), u.path().to_string()),
("url_query".into(), u.query().unwrap_or("").to_string()),
(
"url_fragment".into(),
u.fragment().unwrap_or("").to_string(),
),
]
}
fn command_vars(command: &str) -> [(String, String); 5] {
let p = Path::new(command);
let [(_, path), (_, dir), (_, name), (_, stem), (_, ext)] = file_vars(p);
[
("command_path".into(), path),
("command_dir".into(), dir),
("command_name".into(), name),
("command_stem".into(), stem),
("command_ext".into(), ext),
]
}
const EMPTY_FILE_KEYS: [&str; 5] = [
"file_path",
"file_dir",
"file_name",
"file_stem",
"file_ext",
];
const EMPTY_URL_KEYS: [&str; 6] = [
"url_scheme",
"url_host",
"url_port",
"url_path",
"url_query",
"url_fragment",
];
pub fn build_context(c: Context<'_>) -> tera::Context {
let mut ctx = tera::Context::new();
let (input_str, input_type) = match c.input {
Some(i) => (i.display_string(), i.kind_label()),
None => (String::new(), ""),
};
ctx.insert("input", &input_str);
ctx.insert("input_type", input_type);
match c.input {
Some(Input::File(p)) => {
for (k, v) in file_vars(p) {
ctx.insert(&k, &v);
}
for k in EMPTY_URL_KEYS {
ctx.insert(k, "");
}
}
Some(Input::Url(u)) => {
for k in EMPTY_FILE_KEYS {
ctx.insert(k, "");
}
for (k, v) in url_vars(u) {
ctx.insert(&k, &v);
}
}
None => {
for k in EMPTY_FILE_KEYS {
ctx.insert(k, "");
}
for k in EMPTY_URL_KEYS {
ctx.insert(k, "");
}
}
}
for (k, v) in command_vars(c.command) {
ctx.insert(&k, &v);
}
ctx.insert("cwd", c.cwd);
ctx.insert("group", c.group);
ctx.insert("rule", c.rule_name);
let env_map: HashMap<String, String> = std::env::vars().collect();
ctx.insert("env", &env_map);
let vars_map: HashMap<String, toml::Value> = c.vars.clone().into_iter().collect();
ctx.insert("vars", &vars_map);
ctx
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn build(input: Option<&Input>, vars: &BTreeMap<String, toml::Value>) -> tera::Context {
build_context(Context {
input,
command: "",
cwd: "/cwd",
group: "",
rule_name: "",
vars,
})
}
#[test]
fn file_input_populates_file_vars() {
let i = Input::File(PathBuf::from("/tmp/hello.rs"));
let ctx = build(Some(&i), &BTreeMap::new());
let mut tera = new_engine();
let out = render(
&mut tera,
"{{ file_stem }}.{{ file_ext }} / {{ input_type }}",
&ctx,
)
.unwrap();
assert_eq!(out, "hello.rs / file");
}
#[test]
fn url_input_populates_url_vars() {
let i =
Input::Url(url::Url::parse("https://github.com/yukimemi/todoke?tab=rs#top").unwrap());
let ctx = build(Some(&i), &BTreeMap::new());
let mut tera = new_engine();
let out = render(
&mut tera,
"{{ url_scheme }}://{{ url_host }}{{ url_path }}?{{ url_query }}#{{ url_fragment }} / {{ input_type }}",
&ctx,
)
.unwrap();
assert_eq!(out, "https://github.com/yukimemi/todoke?tab=rs#top / url");
}
#[test]
fn file_keys_empty_for_url_inputs() {
let i = Input::Url(url::Url::parse("https://example.com").unwrap());
let ctx = build(Some(&i), &BTreeMap::new());
let mut tera = new_engine();
let out = render(&mut tera, "[{{ file_path }}]", &ctx).unwrap();
assert_eq!(out, "[]");
}
#[test]
fn os_function_returns_correct_bool() {
let mut tera = new_engine();
let ctx = tera::Context::new();
let rendered = tera
.render_str("{% if is_windows() %}W{% else %}nW{% endif %}", &ctx)
.unwrap();
if cfg!(target_os = "windows") {
assert_eq!(rendered, "W");
} else {
assert_eq!(rendered, "nW");
}
}
#[test]
fn vars_and_env_substitute() {
unsafe { std::env::set_var("TODOKE_TEST_VAR", "test_value") };
let mut vars = BTreeMap::new();
vars.insert("greeting".into(), toml::Value::String("hi".into()));
let ctx = build(None, &vars);
let mut tera = new_engine();
let out = render(
&mut tera,
"{{ vars.greeting }} {{ env.TODOKE_TEST_VAR }}",
&ctx,
)
.unwrap();
assert_eq!(out, "hi test_value");
}
}