use anyhow::Error;
use chrono::Local;
use colored::Colorize;
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::io::Read;
use std::path::PathBuf;
use subprocess::Exec;
pub mod errors;
pub mod lisp;
pub mod transformers;
pub static OPTIONAL_RENDER_CHAR: char = '?';
pub static TIME_FORMAT_CHAR: char = '%';
pub static LISP_START_CHAR: char = '=';
pub static VAR_TRANSFORM_SEP_CHAR: char = ':';
pub static LITERAL_VALUE_QUOTE_CHAR: char = '"';
pub static ESCAPE_CHAR: char = '\\';
static LITERAL_REPLACEMENTS: [&str; 3] = [
"", "{", "}", ];
fn cmd_output(cmd: &str, wd: &PathBuf) -> Result<String, Error> {
let mut out: String = String::new();
Exec::shell(cmd)
.cwd(wd)
.stream_stdout()?
.read_to_string(&mut out)?;
Ok(out)
}
#[derive(Debug, Clone)]
pub enum TemplatePart {
Lit(String),
Var(String, String),
Time(String),
Lisp(String, String, Vec<(usize, usize)>),
Cmd(Vec<TemplatePart>),
Any(Vec<TemplatePart>),
}
lazy_static! {
pub static ref TEMPLATE_PAIRS_START: [char; 3] = ['{', '"', '('];
pub static ref TEMPLATE_PAIRS_END: [char; 3] = ['}', '"', ')'];
pub static ref TEMPLATE_PAIRS: HashMap<char, char> = TEMPLATE_PAIRS_START
.iter()
.zip(TEMPLATE_PAIRS_END.iter())
.map(|(k, v)| (*k, *v))
.collect();
}
impl TemplatePart {
pub fn lit(part: &str) -> Self {
Self::Lit(part.to_string())
}
pub fn var(part: &str) -> Self {
if let Some((part, fstr)) = part.split_once(VAR_TRANSFORM_SEP_CHAR) {
Self::Var(part.to_string(), fstr.to_string())
} else {
Self::Var(part.to_string(), "".to_string())
}
}
pub fn lisp(part: &str) -> Self {
let (part, fstr) = if let Some((part, fstr)) = part.split_once(VAR_TRANSFORM_SEP_CHAR) {
(part.to_string(), fstr.to_string())
} else {
(part.to_string(), "".to_string())
};
let variables = part
.match_indices("(st+")
.filter_map(|(loc, _)| {
let end = Self::find_end(')', &part, loc + 1).ok()?;
part[loc..end].find(' ').map(|s| {
let p = &part[(s + 1 + loc)..end];
if p.starts_with('"') {
(s + 2 + loc, end - 1)
} else if p.starts_with('\'') {
(s + 2 + loc, end)
} else {
(s + 1 + loc, end)
}
})
})
.collect();
Self::Lisp(part, fstr, variables)
}
pub fn time(part: &str) -> Self {
Self::Time(part.to_string())
}
pub fn maybe_var(part: &str) -> Self {
if LITERAL_REPLACEMENTS.contains(&part) {
Self::lit(part)
} else if part.starts_with(LITERAL_VALUE_QUOTE_CHAR)
&& part.ends_with(LITERAL_VALUE_QUOTE_CHAR)
{
Self::lit(&part[1..(part.len() - 1)])
} else if part.starts_with(TIME_FORMAT_CHAR) {
Self::time(part)
} else if part.starts_with(LISP_START_CHAR) {
Self::lisp(&part[1..])
} else {
Self::var(part)
}
}
pub fn cmd(parts: Vec<TemplatePart>) -> Self {
Self::Cmd(parts)
}
pub fn parse_cmd(part: &str) -> Result<Self, errors::RenderTemplateError> {
Self::tokenize(part).map(Self::cmd)
}
pub fn any(parts: Vec<TemplatePart>) -> Self {
Self::Any(parts)
}
pub fn maybe_any(part: &str) -> Self {
if part.contains(OPTIONAL_RENDER_CHAR) {
let parts = part
.split(OPTIONAL_RENDER_CHAR)
.map(|s| s.trim())
.map(Self::maybe_var)
.collect();
Self::any(parts)
} else {
Self::maybe_var(part)
}
}
fn find_end(
end: char,
templ: &str,
offset: usize,
) -> Result<usize, errors::RenderTemplateError> {
if end == '"' {
return templ[offset..].find(end).map(|i| i + offset).ok_or(
errors::RenderTemplateError::InvalidFormat(
templ.to_string(),
"Quote not closed".to_string(),
),
);
}
let mut nest: Vec<char> = Vec::new();
for (i, c) in templ[offset..].chars().enumerate() {
if c == end && nest.is_empty() {
return Ok(offset + i);
} else if TEMPLATE_PAIRS_START.contains(&c) {
if c == '"' && nest.contains(&c) {
while Some('"') != nest.pop() {}
continue;
}
nest.push(c);
} else if TEMPLATE_PAIRS_END.contains(&c) {
if let Some(last) = nest.pop() {
if c != TEMPLATE_PAIRS[&last] {
return Err(errors::RenderTemplateError::InvalidFormat(
templ.to_string(),
format!("Extra {} at [{}] in template", c, offset + i),
));
}
} else {
return Err(errors::RenderTemplateError::InvalidFormat(
templ.to_string(),
format!("Extra {} at [{}] in template", c, offset + i),
));
}
}
}
Err(errors::RenderTemplateError::InvalidFormat(
templ.to_string(),
format!(
"Closing {} not found from [{}] onwards in template",
end, offset,
),
))
}
pub fn tokenize(templ: &str) -> Result<Vec<Self>, errors::RenderTemplateError> {
let mut parts: Vec<TemplatePart> = Vec::new();
let mut last = 0usize;
let mut i = 0usize;
let mut escape = false;
while i < templ.len() {
if templ[i..].starts_with(ESCAPE_CHAR) && !escape {
if i > last {
parts.push(Self::lit(&templ[last..i]));
}
i += 1;
last = i;
escape = true;
continue;
}
if escape {
parts.push(Self::lit(&templ[i..(i + 1)]));
last = i + 1;
i += 1;
escape = false;
continue;
}
if templ[i..].starts_with("$(") {
let end = Self::find_end(')', templ, i + 2)?;
if i > last {
parts.push(Self::lit(&templ[last..i]));
}
last = end + 1;
parts.push(Self::parse_cmd(&templ[(i + 2)..end])?);
i = end;
} else if templ[i..].starts_with("=(") {
let end = Self::find_end(')', templ, i + 2)?;
if i > last {
parts.push(Self::lit(&templ[last..i]));
}
last = end + 1;
parts.push(Self::lisp(&templ[(i + 1)..=end]));
i = end;
} else if templ[i..].starts_with('{') {
let end = Self::find_end('}', templ, i + 1)?;
if i > last {
parts.push(Self::lit(&templ[last..i]));
}
last = end + 1;
parts.push(Self::maybe_any(&templ[(i + 1)..end]));
i = end;
} else if templ[i..].starts_with('"') {
let end = Self::find_end('"', templ, i + 1)?;
if i > last {
parts.push(Self::lit(&templ[last..i]));
}
last = end + 1;
parts.push(Self::lit(&templ[(i + 1)..end]));
i = end;
}
i += 1;
}
if templ.len() > last {
parts.push(Self::lit(&templ[last..]));
}
Ok(parts)
}
pub fn variables(&self) -> Vec<&str> {
match self {
TemplatePart::Var(v, _) => vec![v.as_str()],
TemplatePart::Lisp(expr, _, vars) => vars.iter().map(|(s, e)| &expr[*s..*e]).collect(),
TemplatePart::Any(any) => any.iter().flat_map(|p| p.variables()).collect(),
TemplatePart::Cmd(cmd) => cmd.iter().flat_map(|p| p.variables()).collect(),
_ => vec![],
}
}
}
impl ToString for TemplatePart {
fn to_string(&self) -> String {
match self {
Self::Lit(s) => format!("{0}{1}{0}", LITERAL_VALUE_QUOTE_CHAR, s),
Self::Var(s, _) => s.to_string(),
Self::Time(s) => s.to_string(),
Self::Lisp(e, _, _) => e.to_string(),
Self::Cmd(v) => v
.iter()
.map(|p| p.to_string())
.collect::<Vec<String>>()
.join(""),
Self::Any(v) => v
.iter()
.map(|p| p.to_string())
.collect::<Vec<String>>()
.join(OPTIONAL_RENDER_CHAR.to_string().as_str()),
}
}
}
#[derive(Debug, Clone)]
pub struct Template {
original: String,
parts: Vec<TemplatePart>,
}
impl Template {
pub fn parse_template(templ_str: &str) -> Result<Template, Error> {
let template_parts = TemplatePart::tokenize(templ_str)?;
Ok(Self {
original: templ_str.to_string(),
parts: template_parts,
})
}
pub fn parts(&self) -> &Vec<TemplatePart> {
&self.parts
}
pub fn original(&self) -> &str {
&self.original
}
pub fn lit(&self) -> Option<String> {
let mut lit = String::new();
for part in &self.parts {
if let TemplatePart::Lit(l) = part {
lit.push_str(l);
} else {
return None;
}
}
Some(lit)
}
}
pub trait Render {
fn render(&self, op: &RenderOptions) -> Result<String, Error>;
fn print(&self);
}
#[derive(Default, Debug, Clone)]
pub struct RenderOptions {
pub wd: PathBuf,
pub variables: HashMap<String, String>,
pub shell_commands: bool,
}
impl RenderOptions {
pub fn render(&self, templ: &Template) -> Result<String, Error> {
templ.render(self)
}
pub fn render_iter<'a>(&'a self, templ: &'a Template) -> RenderIter<'a> {
RenderIter {
template: templ,
options: self,
count: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct RenderIter<'a> {
template: &'a Template,
options: &'a RenderOptions,
count: usize,
}
impl<'a> RenderIter<'a> {
pub fn new(template: &'a Template, options: &'a RenderOptions) -> Self {
Self {
template,
options,
count: 0,
}
}
}
impl<'a> Iterator for RenderIter<'a> {
type Item = String;
fn next(&mut self) -> Option<String> {
self.template.render(self.options).ok().map(|t| {
self.count += 1;
format!("{}-{}", t, self.count)
})
}
}
impl Render for TemplatePart {
fn render(&self, op: &RenderOptions) -> Result<String, Error> {
match self {
TemplatePart::Lit(l) => Ok(l.to_string()),
TemplatePart::Var(v, f) => op
.variables
.get(v)
.ok_or(errors::RenderTemplateError::VariableNotFound(v.to_string()))
.map(|s| -> Result<String, Error> { Ok(transformers::apply_tranformers(s, f)?) })?,
TemplatePart::Time(t) => Ok(Local::now().format(t).to_string()),
TemplatePart::Lisp(e, f, _) => Ok(transformers::apply_tranformers(
&lisp::calculate(&op.variables, e)?,
f,
)?),
TemplatePart::Cmd(c) => {
let cmd = c.render(op)?;
if op.shell_commands {
cmd_output(&cmd, &op.wd)
} else {
Ok(format!("$({cmd})"))
}
}
TemplatePart::Any(a) => a.iter().find_map(|p| p.render(op).ok()).ok_or(
errors::RenderTemplateError::AllVariablesNotFound(
a.iter().map(|p| p.to_string()).collect(),
)
.into(),
),
}
}
fn print(&self) {
match self {
Self::Lit(s) => print!("{}", s),
Self::Var(s, sf) => print!("{}", {
if sf.is_empty() {
s.on_blue()
} else {
format!("{}:{}", s, sf.on_bright_blue()).on_blue()
}
}),
Self::Time(s) => print!("{}", s.on_yellow()),
Self::Lisp(expr, sf, vars) => {
let mut last = 0;
for (s, e) in vars {
print!("{}", expr[last..*s].on_purple());
print!("{}", expr[*s..*e].on_blue());
last = *e;
}
print!("{}", expr[last..expr.len()].on_purple());
if !sf.is_empty() {
print!("{}", format!(":{}", sf).on_bright_purple())
}
}
Self::Cmd(v) => {
print!("\x1B[53m");
print!("{}", "$(".on_red());
v.iter().for_each(|p| {
print!("\x1B[53m");
p.print();
});
print!("\x1B[53m");
print!("{}", ")".on_red());
}
Self::Any(v) => {
v[..(v.len() - 1)].iter().for_each(|p| {
print!("\x1B[4m");
p.print();
print!("\x1B[4m");
print!("{}", OPTIONAL_RENDER_CHAR.to_string().on_yellow());
});
print!("\x1B[4m");
v.iter().last().unwrap().print();
print!("\x1B[0m");
}
}
}
}
impl Render for Vec<TemplatePart> {
fn render(&self, op: &RenderOptions) -> Result<String, Error> {
self.iter()
.map(|p| p.render(op))
.collect::<Result<Vec<String>, Error>>()
.map(|v| v.join(""))
}
fn print(&self) {
self.iter().for_each(|p| p.print());
}
}
impl Render for Template {
fn render(&self, op: &RenderOptions) -> Result<String, Error> {
self.parts.render(op)
}
fn print(&self) {
self.parts.print();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lit() {
let templ = Template::parse_template("hello name").unwrap();
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("name".into(), "world".into());
let rendered = templ
.render(&RenderOptions {
variables: vars,
..Default::default()
})
.unwrap();
assert_eq!(rendered, "hello name");
}
#[test]
fn test_vars() {
let templ = Template::parse_template("hello {name}").unwrap();
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("name".into(), "world".into());
let rendered = templ
.render(&RenderOptions {
variables: vars,
..Default::default()
})
.unwrap();
assert_eq!(rendered, "hello world");
}
#[test]
fn test_vars_format() {
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("length".into(), "120.1234".into());
vars.insert("name".into(), "joHN".into());
vars.insert("job".into(), "assistant manager of company".into());
let options = RenderOptions {
variables: vars,
..Default::default()
};
let cases = [
("L={length}", "L=120.1234"),
("L={length:calc(+100)}", "L=220.1234"),
("L={length:count(.):calc(+1)}", "L=2"),
("L={length:f(.2)} ({length:f(3)})", "L=120.12 (120.123)"),
("hi {name:case(up)}", "hi JOHN"),
(
"hi {name:case(proper)}, {job:case(title)}",
"hi John, Assistant Manager of Company",
),
("hi {name:case(down)}", "hi john"),
];
for (t, r) in cases {
let templ = Template::parse_template(t).unwrap();
let rendered = templ.render(&options).unwrap();
assert_eq!(rendered, r);
}
}
#[test]
#[should_panic]
fn test_novars() {
let templ = Template::parse_template("hello {name}").unwrap();
let vars: HashMap<String, String> = HashMap::new();
templ
.render(&RenderOptions {
variables: vars,
..Default::default()
})
.unwrap();
}
#[test]
fn test_novars_opt() {
let templ = Template::parse_template("hello {name?}").unwrap();
let vars: HashMap<String, String> = HashMap::new();
let rendered = templ
.render(&RenderOptions {
variables: vars,
..Default::default()
})
.unwrap();
assert_eq!(rendered, "hello ");
}
#[test]
fn test_optional() {
let templ = Template::parse_template("hello {age?name}").unwrap();
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("name".into(), "world".into());
let rendered = templ
.render(&RenderOptions {
variables: vars,
..Default::default()
})
.unwrap();
assert_eq!(rendered, "hello world");
}
#[test]
fn test_special_chars() {
let templ = Template::parse_template("$hello {}? \\{\\}%").unwrap();
let rendered = templ.render(&RenderOptions::default()).unwrap();
assert_eq!(rendered, "$hello ? {}%");
}
#[test]
fn test_special_chars2() {
let templ = Template::parse_template("$hello {}? \"{\"\"}\"%").unwrap();
let rendered = templ.render(&RenderOptions::default()).unwrap();
assert_eq!(rendered, "$hello ? {}%");
}
#[test]
fn test_optional_lit() {
let templ = Template::parse_template("hello {age?\"20\"}").unwrap();
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("name".into(), "world".into());
let rendered = templ
.render(&RenderOptions {
variables: vars,
..Default::default()
})
.unwrap();
assert_eq!(rendered, "hello 20");
}
#[test]
fn test_command() {
let templ = Template::parse_template("hello $(echo {name})").unwrap();
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("name".into(), "world".into());
let rendered = templ
.render(&RenderOptions {
wd: PathBuf::from("."),
variables: vars,
shell_commands: true,
})
.unwrap();
assert_eq!(rendered, "hello world\n");
}
#[test]
fn test_command_quote() {
let templ = Template::parse_template("hello $(printf \\\"%s %d\\\" {name} {age})").unwrap();
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("name".into(), "world".into());
vars.insert("age".into(), "1".into());
let rendered = templ
.render(&RenderOptions {
wd: PathBuf::from("."),
variables: vars,
shell_commands: true,
})
.unwrap();
assert_eq!(rendered, "hello world 1");
}
#[test]
fn test_time() {
let templ = Template::parse_template("hello {name} at {%Y-%m-%d}").unwrap();
let timefmt = Local::now().format("%Y-%m-%d");
let output = format!("hello world at {}", timefmt);
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("name".into(), "world".into());
let rendered = templ
.render(&RenderOptions {
wd: PathBuf::from("."),
variables: vars,
shell_commands: false,
})
.unwrap();
assert_eq!(rendered, output);
}
#[test]
fn test_var_or_time() {
let templ = Template::parse_template("hello {name} at {age?%Y-%m-%d}").unwrap();
let timefmt = Local::now().format("%Y-%m-%d");
let output = format!("hello world at {}", timefmt);
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("name".into(), "world".into());
let rendered = templ
.render(&RenderOptions {
wd: PathBuf::from("."),
variables: vars,
shell_commands: false,
})
.unwrap();
assert_eq!(rendered, output);
}
#[test]
fn test_render_iter() {
let templ = Template::parse_template("hello {name}").unwrap();
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("name".into(), "world".into());
let options = RenderOptions {
variables: vars,
..Default::default()
};
let mut names = options.render_iter(&templ);
assert_eq!("hello world-1", names.next().unwrap());
assert_eq!("hello world-2", names.next().unwrap());
assert_eq!("hello world-3", names.next().unwrap());
}
}