string_template_plus/
lib.rs

1/*!
2# Introduction
3
4This is a simple template tool that works with variable names and
5[`HashMap`] of [`String`]. The [`Template`] can be parsed from [`str`]
6and then you can render it using the variables in [`HashMap`] and any
7shell commands running through [`Exec`].
8
9# Features
10- Parse the template from a `str` that's easy to write,
11- Support for alternatives in case some variables are not present,
12  Use `?` to separate the alternatives, uses whichever it can find first. If `?` is at the end, leaves it blank instead of erroring out.
13- Support for literal strings inside the alternative options,
14  You can use a literal string `"string"` enclosed in `"` as an alternative if you want to put something instead of blank at the end.
15- Support for the date time format using `chrono`,
16  You can use any format starting with `%` inside the variable placeholder `{}` to use a date time format supported by chrono.
17- Support for any arbitrary commands, etc.
18You can keep any command inside `$(` and `)` to run it and use the result in the template. You can use other format elements inside it.
19- Support for iterating (incremented with -N) strings with the same template conditions,
20- Limited formatting support like UPCASE, downcase, float significant digits, etc. Look into [`transformers`] for more info.
21
22
23# Usages
24Simple variables:
25```rust
26# use std::error::Error;
27# use std::collections::HashMap;
28# use std::path::PathBuf;
29# use string_template_plus::{Render, RenderOptions, Template};
30#
31# fn main() -> Result<(), Box<dyn Error>> {
32let templ = Template::parse_template("hello {name}").unwrap();
33let mut vars: HashMap<String, String> = HashMap::new();
34vars.insert("name".into(), "world".into());
35let rendered = templ
36.render(&RenderOptions {
37variables: vars,
38..Default::default()
39            })
40            .unwrap();
41assert_eq!(rendered, "hello world");
42# Ok(())
43# }
44```
45
46Safe replace, blank if not present, or literal string if not present:
47```rust
48# use std::error::Error;
49# use std::collections::HashMap;
50# use std::path::PathBuf;
51# use string_template_plus::{Render, RenderOptions, Template};
52#
53# fn main() -> Result<(), Box<dyn Error>> {
54let templ = Template::parse_template("hello {name?} {lastname?\"User\"}").unwrap();
55let vars: HashMap<String, String> = HashMap::new();
56let rendered = templ
57.render(&RenderOptions {
58variables: vars,
59..Default::default()
60            })
61            .unwrap();
62assert_eq!(rendered, "hello  User");
63# Ok(())
64# }
65```
66
67Alternate, whichever variable it finds first will be replaced:
68```rust
69# use std::error::Error;
70# use std::collections::HashMap;
71# use std::path::PathBuf;
72# use string_template_plus::{Render, RenderOptions, Template};
73#
74# fn main() -> Result<(), Box<dyn Error>> {
75let templ = Template::parse_template("hello {nickname?name}").unwrap();
76let mut vars: HashMap<String, String> = HashMap::new();
77vars.insert("name".into(), "world".into());
78let rendered = templ
79.render(&RenderOptions {
80variables: vars,
81..Default::default()
82            })
83            .unwrap();
84        assert_eq!(rendered, "hello world");
85# Ok(())
86# }
87```
88
89Calculations can be written in lisp like language, it supports simple
90functions. Using lisp can also allow you to write more complex
91logic. The lisp implementation is the one from
92https://github.com/brundonsmith/rust_lisp refer to the README there
93for the functionality.
94
95To access the values in lisp you can use the following functions:
96- `st+var` : the value as string,
97- `st+num` the value as a number, and
98- `st+has` true if value is present else false.
99
100You need to quote the symbol to pass to the functions (e.g. (st+num
101'total) or (st+num "total").
102
103Else, you can just write the variables in braces like normal as well.
104
105there are two use cases.
106- Standalone use case: in the format of `=()` that'll be replaced with
107  the results.
108- Inside the `{}` that can be used as alternative to a variable, or
109  with a transformer.
110
111```rust
112# use std::error::Error;
113# use std::collections::HashMap;
114# use std::path::PathBuf;
115# use string_template_plus::{Render, RenderOptions, Template};
116#
117# fn main() -> Result<(), Box<dyn Error>> {
118let templ = Template::parse_template("hello {nickname?name}. You've done =(/ (st+num 'task_done) (st+num 'task_total)) work. {=(- 1 (/ (st+num \"task_done\") (st+num 'task_total))):calc(*100):f(1)}% remains.").unwrap();
119let mut vars: HashMap<String, String> = HashMap::new();
120vars.insert("name".into(), "world".into());
121vars.insert("task_done".into(), "1".into());
122vars.insert("task_total".into(), "4".into());
123let rendered = templ
124.render(&RenderOptions {
125variables: vars,
126..Default::default()
127            })
128            .unwrap();
129        assert_eq!(rendered, "hello world. You've done 0.25 work. 75.0% remains.");
130# Ok(())
131# }
132```
133
134Custom Commands:
135```rust
136# use std::error::Error;
137# use std::collections::HashMap;
138# use std::path::PathBuf;
139# use string_template_plus::{Render, RenderOptions, Template};
140#
141# fn main() -> Result<(), Box<dyn Error>> {
142let templ = Template::parse_template("L=$(printf \"%.2f\" {length})").unwrap();
143let mut vars: HashMap<String, String> = HashMap::new();
144vars.insert("length".into(), "12.342323".into());
145let rendered = templ
146.render(&RenderOptions {
147wd: PathBuf::from("."),
148variables: vars,
149shell_commands: true,
150            })
151            .unwrap();
152        assert_eq!(rendered, "L=12.34");
153# Ok(())
154# }
155```
156
157You can turn off Custom Commands for safety:
158```rust
159# use std::error::Error;
160# use std::collections::HashMap;
161# use std::path::PathBuf;
162# use string_template_plus::{Render, RenderOptions, Template};
163#
164# fn main() -> Result<(), Box<dyn Error>> {
165let templ = Template::parse_template("L=$(printf \"%.2f\" {length})").unwrap();
166let mut vars: HashMap<String, String> = HashMap::new();
167vars.insert("length".into(), "12.342323".into());
168let rendered = templ
169.render(&RenderOptions {
170wd: PathBuf::from("."),
171variables: vars,
172shell_commands: false,
173            })
174            .unwrap();
175        assert_eq!(rendered, "L=$(printf %.2f 12.342323)");
176# Ok(())
177# }
178```
179
180Date Time:
181```rust
182# use std::error::Error;
183# use std::collections::HashMap;
184# use std::path::PathBuf;
185# use chrono::Local;
186# use string_template_plus::{Render, RenderOptions, Template};
187#
188# fn main() -> Result<(), Box<dyn Error>> {
189let templ = Template::parse_template("hello {name} at {%Y-%m-%d}").unwrap();
190let timefmt = Local::now().format("%Y-%m-%d");
191let output = format!("hello world at {}", timefmt);
192let mut vars: HashMap<String, String> = HashMap::new();
193vars.insert("name".into(), "world".into());
194let rendered = templ
195.render(&RenderOptions {
196wd: PathBuf::from("."),
197variables: vars,
198shell_commands: false,
199            })
200            .unwrap();
201        assert_eq!(rendered, output);
202# Ok(())
203# }
204```
205
206# Transformers:
207Although there is no format strings, there are transformer functions that can format for a bit. I'm planning to add more format functions as the need arises.
208
209To apply a tranformer to a variable provide it after [`VAR_TRANSFORM_SEP_CHAR`] (currently ":") to a variable template.
210
211There are a few transformers available:
212
213| Transformer | Funtion                        | Arguments | Function                  | Example                  |
214|-------------|--------------------------------|-----------|---------------------------|--------------------------|
215| f           | [`transformers::float_format`] | [.]N      | only N number of decimal  | {"1.12":f(.1)} ⇒ 1.1     |
216| case        | [`transformers::string_case`]  | up        | UPCASE a string           | {"na":case(up)} ⇒ NA     |
217| case        | [`transformers::string_case`]  | down      | downcase a string         | {"nA":case(down)} ⇒ na   |
218| case        | [`transformers::string_case`]  | proper    | Upcase the first letter   | {"nA":case(proper)} ⇒ Na |
219| case        | [`transformers::string_case`]  | title     | Title Case the string     | {"na":case(title)} ⇒ Na  |
220| calc        | [`transformers::calc`]         | [+-*\/^]N | Airthmatic calculation    | {"1":calc(+1*2^2)} ⇒ 16  |
221| calc        | [`transformers::calc`]         | [+-*\/^]N | Airthmatic calculation    | {"1":calc(+1,-1)} ⇒ 2,0  |
222| count       | [`transformers::count`]        | str       | count str occurance       | {"nata":count(a)} ⇒ 2    |
223| repl        | [`transformers::replace`]      | str1,str2 | replace str1 by str2      | {"nata":rep(a,o)} ⇒ noto |
224| q           | [`transformers::quote`]        | [str1]    | quote with str1, or ""    | {"nata":q()} ⇒ "noto"    |
225| take        | [`transformers::take`]         | str,N     | take Nth group sep by str | {"nata":take(a,2)} ⇒ "t" |
226| trim        | [`transformers::trim`]         | str       | trim the string with str  | {"nata":trim(a)} ⇒ "nat" |
227
228You can chain transformers ones after another for combined actions. For example, `count( ):calc(+1)` will give you total number of words in a sentence.
229
230Examples are in individual functions in [`transformers`].
231
232```rust
233# use std::error::Error;
234# use std::collections::HashMap;
235# use std::path::PathBuf;
236# use chrono::Local;
237# use string_template_plus::{Render, RenderOptions, Template};
238#
239# fn main() -> Result<(), Box<dyn Error>> {
240let mut vars: HashMap<String, String> = HashMap::new();
241vars.insert("length".into(), "120.1234".into());
242vars.insert("name".into(), "joHN".into());
243vars.insert("job".into(), "assistant manager of company".into());
244let options = RenderOptions {
245variables: vars,
246..Default::default()
247        };
248let cases = [
249("L={length}", "L=120.1234"),
250("L={length:calc(+100)}", "L=220.1234"),
251("L={length:f(.2)} ({length:f(3)})", "L=120.12 (120.123)"),
252("hi {name:case(up)}", "hi JOHN"),
253(
254 "hi {name:case(proper)}, {job:case(title)}",
255 "hi John, Assistant Manager of Company",
256),
257 ("hi {name:case(down)}", "hi john"),
258];
259
260for (t, r) in cases {
261 let templ = Template::parse_template(t).unwrap();
262 let rendered = templ.render(&options).unwrap();
263 assert_eq!(rendered, r);
264 }
265# Ok(())
266# }
267```
268
269# Limitations
270- You cannot use positional arguments in this template system, only named ones. `{}` will be replaced with empty string. Although you can use `"0"`, `"1"`, etc as variable names in the template and the render options variables.
271- I haven't tested variety of names, although they should work try to keep the names identifier friendly.
272- Currently doesn't have format specifiers, for now you can use the command options with `printf` bash command to format things the way you want, or use the transformers which have limited formatting capabilities.
273Like a template `this is $(printf "%05.2f" {weight}) kg.` should be rendered with the correct float formatting.
274*/
275use anyhow::Error;
276use chrono::Local;
277use colored::Colorize;
278use lazy_static::lazy_static;
279use std::collections::HashMap;
280use std::io::Read;
281use std::path::PathBuf;
282use subprocess::Exec;
283
284pub mod errors;
285pub mod lisp;
286pub mod transformers;
287
288/// Character to separate the variables. If the first variable is not present it'll use the one behind it and so on. Keep it at the end, if you want a empty string instead of error on missing variable.
289pub static OPTIONAL_RENDER_CHAR: char = '?';
290/// Character that should be in the beginning of the variable to determine it as datetime format.
291pub static TIME_FORMAT_CHAR: char = '%';
292/// Character that indicates that this is a lisp expression from here.
293pub static LISP_START_CHAR: char = '=';
294/// Character that separates variable with format
295pub static VAR_TRANSFORM_SEP_CHAR: char = ':';
296/// Quote characters to use to make a value literal instead of a variable. In combination with [`OPTIONAL_RENDER_CHAR`] it can be used as a default value when variable(s) is/are not present.
297pub static LITERAL_VALUE_QUOTE_CHAR: char = '"';
298/// Character to escape special meaning characters
299pub static ESCAPE_CHAR: char = '\\';
300/// Characters that should be replaced as themselves if presented as a variable
301static LITERAL_REPLACEMENTS: [&str; 3] = [
302    "",  // to replace {} as empty string.
303    "{", // to replace {{} as {
304    "}", // to replace {}} as }
305];
306
307/// Runs a command and returns the output of the command or the error
308fn cmd_output(cmd: &str, wd: &PathBuf) -> Result<String, Error> {
309    let mut out: String = String::new();
310    Exec::shell(cmd)
311        .cwd(wd)
312        .stream_stdout()?
313        .read_to_string(&mut out)?;
314    Ok(out)
315}
316
317/// Parts that make up a [`Template`]. You can have literal strings, variables, time date format, command, or optional format with [`OPTIONAL_RENDER_CHAR`].
318///
319/// [`TemplatePart::Lit`] = Literal Strings like `"hi "` in `"hi {name}"`
320/// [`TemplatePart::Var`] = Variable part like `"name"` in `"hi {name}"` and format specifier
321/// [`TemplatePart::Time`] = Date time format like `"%F"` in `"Today: {%F}"`
322/// [`TemplatePart::Cmd`] = Command like `"echo world"` in `"hello $(echo world)"`
323/// [`TemplatePart::Any`] = Optional format like `"name?age"` in `"hello {name?age}"`
324///
325/// [`TemplatePart::Cmd`] and [`TemplatePart::Any`] can in turn contain other [`TemplatePart`] inside them. Haven't tested on nesting complex ones within each other though.
326#[derive(Debug, Clone)]
327pub enum TemplatePart {
328    /// Literal string, keep them as they are
329    Lit(String),
330    /// Variable and format, uses the variable's value in the rendered String
331    Var(String, String),
332    /// DateTime format, use [`chrono::Local`] in the given format
333    Time(String),
334    /// Lisp expression to calculate with the transformer
335    Lisp(String, String, Vec<(usize, usize)>),
336    /// Shell Command, use the output of command in the rendered String
337    Cmd(Vec<TemplatePart>),
338    /// Multiple variables or [`TemplatePart`]s, use the first one that succeeds
339    Any(Vec<TemplatePart>),
340}
341
342lazy_static! {
343    pub static ref TEMPLATE_PAIRS_START: [char; 3] = ['{', '"', '('];
344    pub static ref TEMPLATE_PAIRS_END: [char; 3] = ['}', '"', ')'];
345    pub static ref TEMPLATE_PAIRS: HashMap<char, char> = TEMPLATE_PAIRS_START
346        .iter()
347        .zip(TEMPLATE_PAIRS_END.iter())
348        .map(|(k, v)| (*k, *v))
349        .collect();
350}
351
352impl TemplatePart {
353    pub fn lit(part: &str) -> Self {
354        Self::Lit(part.to_string())
355    }
356    pub fn var(part: &str) -> Self {
357        if let Some((part, fstr)) = part.split_once(VAR_TRANSFORM_SEP_CHAR) {
358            Self::Var(part.to_string(), fstr.to_string())
359        } else {
360            Self::Var(part.to_string(), "".to_string())
361        }
362    }
363
364    pub fn lisp(part: &str) -> Self {
365        let (part, fstr) = if let Some((part, fstr)) = part.split_once(VAR_TRANSFORM_SEP_CHAR) {
366            (part.to_string(), fstr.to_string())
367        } else {
368            (part.to_string(), "".to_string())
369        };
370        let variables = part
371            .match_indices("(st+")
372            .filter_map(|(loc, _)| {
373                let end = Self::find_end(')', &part, loc + 1).ok()?;
374                part[loc..end].find(' ').map(|s| {
375                    let p = &part[(s + 1 + loc)..end];
376                    if p.starts_with('"') {
377                        (s + 2 + loc, end - 1)
378                    } else if p.starts_with('\'') {
379                        (s + 2 + loc, end)
380                    } else {
381                        (s + 1 + loc, end)
382                    }
383                })
384            })
385            .collect();
386        Self::Lisp(part, fstr, variables)
387    }
388
389    pub fn time(part: &str) -> Self {
390        Self::Time(part.to_string())
391    }
392
393    /// Parse a [`&str`] into [`TemplatePart::Lit`], [`TemplatePart::Time`], or [`TemplatePart::Var`]
394    pub fn maybe_var(part: &str) -> Self {
395        if LITERAL_REPLACEMENTS.contains(&part) {
396            Self::lit(part)
397        } else if part.starts_with(LITERAL_VALUE_QUOTE_CHAR)
398            && part.ends_with(LITERAL_VALUE_QUOTE_CHAR)
399        {
400            Self::lit(&part[1..(part.len() - 1)])
401        } else if part.starts_with(TIME_FORMAT_CHAR) {
402            Self::time(part)
403        } else if part.starts_with(LISP_START_CHAR) {
404            Self::lisp(&part[1..])
405        } else {
406            Self::var(part)
407        }
408    }
409
410    pub fn cmd(parts: Vec<TemplatePart>) -> Self {
411        Self::Cmd(parts)
412    }
413
414    pub fn parse_cmd(part: &str) -> Result<Self, errors::RenderTemplateError> {
415        Self::tokenize(part).map(Self::cmd)
416    }
417
418    pub fn any(parts: Vec<TemplatePart>) -> Self {
419        Self::Any(parts)
420    }
421
422    pub fn maybe_any(part: &str) -> Self {
423        if part.contains(OPTIONAL_RENDER_CHAR) {
424            let parts = part
425                .split(OPTIONAL_RENDER_CHAR)
426                .map(|s| s.trim())
427                .map(Self::maybe_var)
428                .collect();
429
430            Self::any(parts)
431        } else {
432            Self::maybe_var(part)
433        }
434    }
435
436    fn find_end(
437        end: char,
438        templ: &str,
439        offset: usize,
440    ) -> Result<usize, errors::RenderTemplateError> {
441        if end == '"' {
442            return templ[offset..].find(end).map(|i| i + offset).ok_or(
443                errors::RenderTemplateError::InvalidFormat(
444                    templ.to_string(),
445                    "Quote not closed".to_string(),
446                ),
447            );
448        }
449        let mut nest: Vec<char> = Vec::new();
450        for (i, c) in templ[offset..].chars().enumerate() {
451            if c == end && nest.is_empty() {
452                return Ok(offset + i);
453            } else if TEMPLATE_PAIRS_START.contains(&c) {
454                if c == '"' && nest.contains(&c) {
455                    while Some('"') != nest.pop() {}
456                    continue;
457                }
458                nest.push(c);
459            } else if TEMPLATE_PAIRS_END.contains(&c) {
460                if let Some(last) = nest.pop() {
461                    if c != TEMPLATE_PAIRS[&last] {
462                        return Err(errors::RenderTemplateError::InvalidFormat(
463                            templ.to_string(),
464                            format!("Extra {} at [{}] in template", c, offset + i),
465                        ));
466                    }
467                } else {
468                    return Err(errors::RenderTemplateError::InvalidFormat(
469                        templ.to_string(),
470                        format!("Extra {} at [{}] in template", c, offset + i),
471                    ));
472                }
473            }
474        }
475        Err(errors::RenderTemplateError::InvalidFormat(
476            templ.to_string(),
477            format!(
478                "Closing {} not found from [{}] onwards in template",
479                end, offset,
480            ),
481        ))
482    }
483    pub fn tokenize(templ: &str) -> Result<Vec<Self>, errors::RenderTemplateError> {
484        let mut parts: Vec<TemplatePart> = Vec::new();
485        let mut last = 0usize;
486        let mut i = 0usize;
487        let mut escape = false;
488        while i < templ.len() {
489            if templ[i..].starts_with(ESCAPE_CHAR) && !escape {
490                if i > last {
491                    parts.push(Self::lit(&templ[last..i]));
492                }
493                i += 1;
494                last = i;
495                escape = true;
496                continue;
497            }
498            if escape {
499                parts.push(Self::lit(&templ[i..(i + 1)]));
500                last = i + 1;
501                i += 1;
502                escape = false;
503                continue;
504            }
505            if templ[i..].starts_with("$(") {
506                let end = Self::find_end(')', templ, i + 2)?;
507                if i > last {
508                    parts.push(Self::lit(&templ[last..i]));
509                }
510                last = end + 1;
511                parts.push(Self::parse_cmd(&templ[(i + 2)..end])?);
512                i = end;
513            } else if templ[i..].starts_with("=(") {
514                let end = Self::find_end(')', templ, i + 2)?;
515                if i > last {
516                    parts.push(Self::lit(&templ[last..i]));
517                }
518                last = end + 1;
519                // need to include the found ')' for lisp expr to be valid
520                parts.push(Self::lisp(&templ[(i + 1)..=end]));
521                i = end;
522            } else if templ[i..].starts_with('{') {
523                let end = Self::find_end('}', templ, i + 1)?;
524                if i > last {
525                    parts.push(Self::lit(&templ[last..i]));
526                }
527                last = end + 1;
528                parts.push(Self::maybe_any(&templ[(i + 1)..end]));
529                i = end;
530            } else if templ[i..].starts_with('"') {
531                let end = Self::find_end('"', templ, i + 1)?;
532                if i > last {
533                    parts.push(Self::lit(&templ[last..i]));
534                }
535                last = end + 1;
536                parts.push(Self::lit(&templ[(i + 1)..end]));
537                i = end;
538            }
539            i += 1;
540        }
541        if templ.len() > last {
542            parts.push(Self::lit(&templ[last..]));
543        }
544        Ok(parts)
545    }
546
547    pub fn variables(&self) -> Vec<&str> {
548        match self {
549            TemplatePart::Var(v, _) => vec![v.as_str()],
550            TemplatePart::Lisp(expr, _, vars) => vars.iter().map(|(s, e)| &expr[*s..*e]).collect(),
551            TemplatePart::Any(any) => any.iter().flat_map(|p| p.variables()).collect(),
552            TemplatePart::Cmd(cmd) => cmd.iter().flat_map(|p| p.variables()).collect(),
553            _ => vec![],
554        }
555    }
556}
557impl ToString for TemplatePart {
558    fn to_string(&self) -> String {
559        match self {
560            Self::Lit(s) => format!("{0}{1}{0}", LITERAL_VALUE_QUOTE_CHAR, s),
561            Self::Var(s, _) => s.to_string(),
562            Self::Time(s) => s.to_string(),
563            Self::Lisp(e, _, _) => e.to_string(),
564            Self::Cmd(v) => v
565                .iter()
566                .map(|p| p.to_string())
567                .collect::<Vec<String>>()
568                .join(""),
569            Self::Any(v) => v
570                .iter()
571                .map(|p| p.to_string())
572                .collect::<Vec<String>>()
573                .join(OPTIONAL_RENDER_CHAR.to_string().as_str()),
574        }
575    }
576}
577
578/// Main Template that get's passed around, consists of `[Vec`] of [`TemplatePart`]
579///
580/// ```rust
581/// # use std::error::Error;
582/// # use std::collections::HashMap;
583/// # use std::path::PathBuf;
584/// # use string_template_plus::{Render, RenderOptions, Template};
585/// #
586/// # fn main() -> Result<(), Box<dyn Error>> {
587///     let templ = Template::parse_template("hello {nickname?name}. You're $(printf \"%.1f\" {weight})kg").unwrap();
588///     let mut vars: HashMap<String, String> = HashMap::new();
589///     vars.insert("name".into(), "John".into());
590///     vars.insert("weight".into(), "132.3423".into());
591///     let rendered = templ
592///         .render(&RenderOptions {
593///             wd: PathBuf::from("."),
594///             variables: vars,
595///             shell_commands: true,
596///         })
597///         .unwrap();
598///     assert_eq!(rendered, "hello John. You're 132.3kg");
599/// # Ok(())
600/// }
601#[derive(Debug, Clone)]
602pub struct Template {
603    original: String,
604    parts: Vec<TemplatePart>,
605}
606
607impl Template {
608    /// Parses the template from string and makes a [`Template`]. Which you can render later./// Main Template that get's passed around, consists of `[Vec`] of [`TemplatePart`]
609    ///
610    /// ```rust
611    /// # use std::error::Error;
612    /// # use std::collections::HashMap;
613    /// # use std::path::PathBuf;
614    /// # use string_template_plus::{Render, RenderOptions, Template};
615    /// #
616    /// # fn main() -> Result<(), Box<dyn Error>> {
617    ///     let templ = Template::parse_template("hello {nickname?name?}. You're $(printf \\\"%.1f\\\" {weight})kg").unwrap();
618    ///     let parts = concat!("[Lit(\"hello \"), ",
619    ///                  "Any([Var(\"nickname\", \"\"), Var(\"name\", \"\"), Lit(\"\")]), ",
620    ///                  "Lit(\". You're \"), ",
621    ///                  "Cmd([Lit(\"printf \"), Lit(\"\\\"\"), Lit(\"%.1f\"), Lit(\"\\\"\"), Lit(\" \"), Var(\"weight\", \"\")]), ",
622    ///                  "Lit(\"kg\")]");
623    ///     assert_eq!(parts, format!("{:?}", templ.parts()));
624    /// # Ok(())
625    /// }
626    pub fn parse_template(templ_str: &str) -> Result<Template, Error> {
627        let template_parts = TemplatePart::tokenize(templ_str)?;
628        Ok(Self {
629            original: templ_str.to_string(),
630            parts: template_parts,
631        })
632    }
633
634    pub fn parts(&self) -> &Vec<TemplatePart> {
635        &self.parts
636    }
637
638    pub fn original(&self) -> &str {
639        &self.original
640    }
641
642    /// Concatenated String if [`Template`] is only literal strings
643    pub fn lit(&self) -> Option<String> {
644        let mut lit = String::new();
645        for part in &self.parts {
646            if let TemplatePart::Lit(l) = part {
647                lit.push_str(l);
648            } else {
649                return None;
650            }
651        }
652        Some(lit)
653    }
654}
655
656/// Provides the function to render the object with [`RenderOptions`] into [`String`]
657pub trait Render {
658    fn render(&self, op: &RenderOptions) -> Result<String, Error>;
659
660    fn print(&self);
661}
662
663/// Options for the [`Template`] to render into [`String`]
664#[derive(Default, Debug, Clone)]
665pub struct RenderOptions {
666    /// Working Directory for the Shell Commands
667    pub wd: PathBuf,
668    /// Variables to use for the template
669    pub variables: HashMap<String, String>,
670    /// Run Shell Commands for the output or not
671    pub shell_commands: bool,
672}
673
674impl RenderOptions {
675    pub fn render(&self, templ: &Template) -> Result<String, Error> {
676        templ.render(self)
677    }
678
679    /// Makes a [`RenderIter<'a>`] that can generate incremented strings from the given [`Template`] and the [`RenderOptions`]. The Iterator will have `-N` appended where N is the number representing the number of instance.
680    ///
681    /// ```rust
682    /// # use std::error::Error;
683    /// # use std::collections::HashMap;
684    /// # use string_template_plus::{Render, RenderOptions, Template};
685    /// #
686    /// # fn main() -> Result<(), Box<dyn Error>> {
687    ///     let templ = Template::parse_template("hello {name}").unwrap();
688    ///     let mut vars: HashMap<String, String> = HashMap::new();
689    ///     vars.insert("name".into(), "world".into());
690    ///     let options = RenderOptions {
691    ///         variables: vars,
692    ///         ..Default::default()
693    ///     };
694    ///     let mut names = options.render_iter(&templ);
695    ///     assert_eq!("hello world-1", names.next().unwrap());
696    ///     assert_eq!("hello world-2", names.next().unwrap());
697    ///     assert_eq!("hello world-3", names.next().unwrap());
698    /// # Ok(())
699    /// # }
700    pub fn render_iter<'a>(&'a self, templ: &'a Template) -> RenderIter<'a> {
701        RenderIter {
702            template: templ,
703            options: self,
704            count: 0,
705        }
706    }
707}
708
709/// Render option with [`Iterator`] support. You can use this to get
710/// incremented render results. It'll add `-N` to the render
711/// [`Template`] where `N` is the count (1,2,3...). It can be useful
712/// to make files with a given template.
713///
714/// ```rust
715/// # use std::error::Error;
716/// # use std::collections::HashMap;
717/// # use string_template_plus::{Render, RenderOptions, RenderIter, Template};
718/// #
719/// # fn main() -> Result<(), Box<dyn Error>> {
720///     let templ = Template::parse_template("hello {name}").unwrap();
721///     let mut vars: HashMap<String, String> = HashMap::new();
722///     vars.insert("name".into(), "world".into());
723///     let options = RenderOptions {
724///         variables: vars,
725///         ..Default::default()
726///     };
727///     let mut names = RenderIter::new(&templ, &options);
728///     assert_eq!("hello world-1", names.next().unwrap());
729///     assert_eq!("hello world-2", names.next().unwrap());
730///     assert_eq!("hello world-3", names.next().unwrap());
731/// # Ok(())
732/// # }
733#[derive(Debug, Clone)]
734pub struct RenderIter<'a> {
735    template: &'a Template,
736    options: &'a RenderOptions,
737    count: usize,
738}
739
740impl<'a> RenderIter<'a> {
741    /// Creates a new [`RenderIter<'a>`] object
742    pub fn new(template: &'a Template, options: &'a RenderOptions) -> Self {
743        Self {
744            template,
745            options,
746            count: 0,
747        }
748    }
749}
750
751impl<'a> Iterator for RenderIter<'a> {
752    type Item = String;
753    fn next(&mut self) -> Option<String> {
754        self.template.render(self.options).ok().map(|t| {
755            self.count += 1;
756            format!("{}-{}", t, self.count)
757        })
758    }
759}
760
761impl Render for TemplatePart {
762    fn render(&self, op: &RenderOptions) -> Result<String, Error> {
763        match self {
764            TemplatePart::Lit(l) => Ok(l.to_string()),
765            TemplatePart::Var(v, f) => op
766                .variables
767                .get(v)
768                .ok_or(errors::RenderTemplateError::VariableNotFound(v.to_string()))
769                .map(|s| -> Result<String, Error> { Ok(transformers::apply_tranformers(s, f)?) })?,
770            TemplatePart::Time(t) => Ok(Local::now().format(t).to_string()),
771            TemplatePart::Lisp(e, f, _) => Ok(transformers::apply_tranformers(
772                &lisp::calculate(&op.variables, e)?,
773                f,
774            )?),
775            TemplatePart::Cmd(c) => {
776                let cmd = c.render(op)?;
777                if op.shell_commands {
778                    cmd_output(&cmd, &op.wd)
779                } else {
780                    Ok(format!("$({cmd})"))
781                }
782            }
783            TemplatePart::Any(a) => a.iter().find_map(|p| p.render(op).ok()).ok_or(
784                errors::RenderTemplateError::AllVariablesNotFound(
785                    a.iter().map(|p| p.to_string()).collect(),
786                )
787                .into(),
788            ),
789        }
790    }
791    /// Visualize what has been parsed so it's easier to debug
792    fn print(&self) {
793        match self {
794            Self::Lit(s) => print!("{}", s),
795            Self::Var(s, sf) => print!("{}", {
796                if sf.is_empty() {
797                    s.on_blue()
798                } else {
799                    format!("{}:{}", s, sf.on_bright_blue()).on_blue()
800                }
801            }),
802            Self::Time(s) => print!("{}", s.on_yellow()),
803            Self::Lisp(expr, sf, vars) => {
804                let mut last = 0;
805                for (s, e) in vars {
806                    print!("{}", expr[last..*s].on_purple());
807                    print!("{}", expr[*s..*e].on_blue());
808                    last = *e;
809                }
810                print!("{}", expr[last..expr.len()].on_purple());
811                if !sf.is_empty() {
812                    print!("{}", format!(":{}", sf).on_bright_purple())
813                }
814            }
815            Self::Cmd(v) => {
816                // overline; so the literal values are detected
817                print!("\x1B[53m");
818                print!("{}", "$(".on_red());
819                v.iter().for_each(|p| {
820                    print!("\x1B[53m");
821                    p.print();
822                });
823                print!("\x1B[53m");
824                print!("{}", ")".on_red());
825            }
826            Self::Any(v) => {
827                v[..(v.len() - 1)].iter().for_each(|p| {
828                    // underline; so the literal values are detected
829                    print!("\x1B[4m");
830                    p.print();
831                    print!("\x1B[4m");
832                    print!("{}", OPTIONAL_RENDER_CHAR.to_string().on_yellow());
833                });
834                print!("\x1B[4m");
835                v.iter().last().unwrap().print();
836                print!("\x1B[0m");
837            }
838        }
839    }
840}
841
842impl Render for Vec<TemplatePart> {
843    fn render(&self, op: &RenderOptions) -> Result<String, Error> {
844        self.iter()
845            .map(|p| p.render(op))
846            .collect::<Result<Vec<String>, Error>>()
847            .map(|v| v.join(""))
848    }
849
850    fn print(&self) {
851        self.iter().for_each(|p| p.print());
852    }
853}
854
855impl Render for Template {
856    fn render(&self, op: &RenderOptions) -> Result<String, Error> {
857        self.parts.render(op)
858    }
859
860    fn print(&self) {
861        self.parts.print();
862    }
863}
864
865#[cfg(test)]
866mod tests {
867    use super::*;
868
869    #[test]
870    fn test_lit() {
871        let templ = Template::parse_template("hello name").unwrap();
872        let mut vars: HashMap<String, String> = HashMap::new();
873        vars.insert("name".into(), "world".into());
874        let rendered = templ
875            .render(&RenderOptions {
876                variables: vars,
877                ..Default::default()
878            })
879            .unwrap();
880        assert_eq!(rendered, "hello name");
881    }
882
883    #[test]
884    fn test_vars() {
885        let templ = Template::parse_template("hello {name}").unwrap();
886        let mut vars: HashMap<String, String> = HashMap::new();
887        vars.insert("name".into(), "world".into());
888        let rendered = templ
889            .render(&RenderOptions {
890                variables: vars,
891                ..Default::default()
892            })
893            .unwrap();
894        assert_eq!(rendered, "hello world");
895    }
896
897    #[test]
898    fn test_vars_format() {
899        let mut vars: HashMap<String, String> = HashMap::new();
900        vars.insert("length".into(), "120.1234".into());
901        vars.insert("name".into(), "joHN".into());
902        vars.insert("job".into(), "assistant manager of company".into());
903        let options = RenderOptions {
904            variables: vars,
905            ..Default::default()
906        };
907        let cases = [
908            ("L={length}", "L=120.1234"),
909            ("L={length:calc(+100)}", "L=220.1234"),
910            ("L={length:count(.):calc(+1)}", "L=2"),
911            ("L={length:f(.2)} ({length:f(3)})", "L=120.12 (120.123)"),
912            ("hi {name:case(up)}", "hi JOHN"),
913            (
914                "hi {name:case(proper)}, {job:case(title)}",
915                "hi John, Assistant Manager of Company",
916            ),
917            ("hi {name:case(down)}", "hi john"),
918        ];
919
920        for (t, r) in cases {
921            let templ = Template::parse_template(t).unwrap();
922            let rendered = templ.render(&options).unwrap();
923            assert_eq!(rendered, r);
924        }
925    }
926
927    #[test]
928    #[should_panic]
929    fn test_novars() {
930        let templ = Template::parse_template("hello {name}").unwrap();
931        let vars: HashMap<String, String> = HashMap::new();
932        templ
933            .render(&RenderOptions {
934                variables: vars,
935                ..Default::default()
936            })
937            .unwrap();
938    }
939
940    #[test]
941    fn test_novars_opt() {
942        let templ = Template::parse_template("hello {name?}").unwrap();
943        let vars: HashMap<String, String> = HashMap::new();
944        let rendered = templ
945            .render(&RenderOptions {
946                variables: vars,
947                ..Default::default()
948            })
949            .unwrap();
950        assert_eq!(rendered, "hello ");
951    }
952
953    #[test]
954    fn test_optional() {
955        let templ = Template::parse_template("hello {age?name}").unwrap();
956        let mut vars: HashMap<String, String> = HashMap::new();
957        vars.insert("name".into(), "world".into());
958        let rendered = templ
959            .render(&RenderOptions {
960                variables: vars,
961                ..Default::default()
962            })
963            .unwrap();
964        assert_eq!(rendered, "hello world");
965    }
966
967    #[test]
968    fn test_special_chars() {
969        let templ = Template::parse_template("$hello {}? \\{\\}%").unwrap();
970        let rendered = templ.render(&RenderOptions::default()).unwrap();
971        assert_eq!(rendered, "$hello ? {}%");
972    }
973
974    #[test]
975    fn test_special_chars2() {
976        let templ = Template::parse_template("$hello {}? \"{\"\"}\"%").unwrap();
977        let rendered = templ.render(&RenderOptions::default()).unwrap();
978        assert_eq!(rendered, "$hello ? {}%");
979    }
980
981    #[test]
982    fn test_optional_lit() {
983        let templ = Template::parse_template("hello {age?\"20\"}").unwrap();
984        let mut vars: HashMap<String, String> = HashMap::new();
985        vars.insert("name".into(), "world".into());
986        let rendered = templ
987            .render(&RenderOptions {
988                variables: vars,
989                ..Default::default()
990            })
991            .unwrap();
992        assert_eq!(rendered, "hello 20");
993    }
994
995    #[test]
996    fn test_command() {
997        let templ = Template::parse_template("hello $(echo {name})").unwrap();
998        let mut vars: HashMap<String, String> = HashMap::new();
999        vars.insert("name".into(), "world".into());
1000        let rendered = templ
1001            .render(&RenderOptions {
1002                wd: PathBuf::from("."),
1003                variables: vars,
1004                shell_commands: true,
1005            })
1006            .unwrap();
1007        assert_eq!(rendered, "hello world\n");
1008    }
1009
1010    #[test]
1011    fn test_command_quote() {
1012        let templ = Template::parse_template("hello $(printf \\\"%s %d\\\" {name} {age})").unwrap();
1013        let mut vars: HashMap<String, String> = HashMap::new();
1014        vars.insert("name".into(), "world".into());
1015        vars.insert("age".into(), "1".into());
1016        let rendered = templ
1017            .render(&RenderOptions {
1018                wd: PathBuf::from("."),
1019                variables: vars,
1020                shell_commands: true,
1021            })
1022            .unwrap();
1023        assert_eq!(rendered, "hello world 1");
1024    }
1025
1026    #[test]
1027    fn test_time() {
1028        let templ = Template::parse_template("hello {name} at {%Y-%m-%d}").unwrap();
1029        let timefmt = Local::now().format("%Y-%m-%d");
1030        let output = format!("hello world at {}", timefmt);
1031        let mut vars: HashMap<String, String> = HashMap::new();
1032        vars.insert("name".into(), "world".into());
1033        let rendered = templ
1034            .render(&RenderOptions {
1035                wd: PathBuf::from("."),
1036                variables: vars,
1037                shell_commands: false,
1038            })
1039            .unwrap();
1040        assert_eq!(rendered, output);
1041    }
1042
1043    #[test]
1044    fn test_var_or_time() {
1045        let templ = Template::parse_template("hello {name} at {age?%Y-%m-%d}").unwrap();
1046        let timefmt = Local::now().format("%Y-%m-%d");
1047        let output = format!("hello world at {}", timefmt);
1048        let mut vars: HashMap<String, String> = HashMap::new();
1049        vars.insert("name".into(), "world".into());
1050        let rendered = templ
1051            .render(&RenderOptions {
1052                wd: PathBuf::from("."),
1053                variables: vars,
1054                shell_commands: false,
1055            })
1056            .unwrap();
1057        assert_eq!(rendered, output);
1058    }
1059
1060    #[test]
1061    fn test_render_iter() {
1062        let templ = Template::parse_template("hello {name}").unwrap();
1063        let mut vars: HashMap<String, String> = HashMap::new();
1064        vars.insert("name".into(), "world".into());
1065        let options = RenderOptions {
1066            variables: vars,
1067            ..Default::default()
1068        };
1069        let mut names = options.render_iter(&templ);
1070        assert_eq!("hello world-1", names.next().unwrap());
1071        assert_eq!("hello world-2", names.next().unwrap());
1072        assert_eq!("hello world-3", names.next().unwrap());
1073    }
1074}