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}