From fe4872da98fb4b24b175d5cfed9951873a70aae0 Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 13 May 2024 19:17:02 -0400 Subject: [PATCH 1/9] feat(whiskers): add read_file function --- whiskers/src/functions.rs | 19 ++++++++++++++++++- whiskers/src/main.rs | 14 ++++++++++++++ whiskers/src/templating.rs | 6 ++++++ whiskers/tests/cli.rs | 12 ++++++++++++ whiskers/tests/fixtures/read_file/abc.txt | 1 + .../tests/fixtures/read_file/read_file.tera | 6 ++++++ 6 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 whiskers/tests/fixtures/read_file/abc.txt create mode 100644 whiskers/tests/fixtures/read_file/read_file.tera diff --git a/whiskers/src/functions.rs b/whiskers/src/functions.rs index ef5334bf..05db7a7e 100644 --- a/whiskers/src/functions.rs +++ b/whiskers/src/functions.rs @@ -1,4 +1,8 @@ -use std::collections::{BTreeMap, HashMap}; +use std::{ + collections::{BTreeMap, HashMap}, + fs, + path::Path, +}; use crate::models::Color; @@ -67,3 +71,16 @@ pub fn css_hsla(args: &HashMap) -> Result) -> Result { + let absolute = std::env::var("WHISKERS_CWD").unwrap(); + let path: String = tera::from_value( + args.get("path") + .ok_or_else(|| tera::Error::msg("path is required"))? + .clone(), + )?; + let contents = fs::read_to_string(Path::new(&absolute).join(path)) + .or_else(|_| Err(tera::Error::msg("failed to open file"))) + .unwrap(); + Ok(tera::to_value(contents)?) +} diff --git a/whiskers/src/main.rs b/whiskers/src/main.rs index 425cac04..aa2599ae 100644 --- a/whiskers/src/main.rs +++ b/whiskers/src/main.rs @@ -83,6 +83,10 @@ fn main() -> anyhow::Result<()> { .expect("args.template is guaranteed by clap to be set"); let template_from_stdin = matches!(template.source, clap_stdin::Source::Stdin); let template_name = template_name(template); + match template_cwd(template) { + None => {} + Some(path) => std::env::set_var("WHISKERS_CWD", path), + }; let mut decoder = DecodeReaderBytes::new( template @@ -250,6 +254,16 @@ fn template_name(template: &clap_stdin::FileOrStdin) -> String { } } +fn template_cwd(template: &clap_stdin::FileOrStdin) -> Option { + match &template.source { + clap_stdin::Source::Stdin => None, + clap_stdin::Source::Arg(arg) => { + let parent_dir = Path::new(&arg).canonicalize().ok()?.parent()?.to_owned(); + Some(parent_dir) + } + } +} + fn template_is_compatible(template_opts: &TemplateOptions) -> bool { let whiskers_version = semver::Version::parse(env!("CARGO_PKG_VERSION")) .expect("CARGO_PKG_VERSION is always valid"); diff --git a/whiskers/src/templating.rs b/whiskers/src/templating.rs index 418bafa0..623276de 100644 --- a/whiskers/src/templating.rs +++ b/whiskers/src/templating.rs @@ -56,6 +56,7 @@ pub fn make_engine() -> tera::Tera { tera.register_function("css_rgba", functions::css_rgba); tera.register_function("css_hsl", functions::css_hsl); tera.register_function("css_hsla", functions::css_hsla); + tera.register_function("read_file", functions::read_file); tera } @@ -99,6 +100,11 @@ pub fn all_functions() -> Vec { description: "Convert a color to an HSLA CSS string".to_string(), examples: vec![function_example!(css_hsla(color=red) => "hsla(347, 87%, 44%, 1.00)")], }, + Function { + name: "read_file".to_string(), + description: "Read and include the contents of a file".to_string(), + examples: vec![function_example!(read_file(path="abc.txt") => "abc")], + }, ] } diff --git a/whiskers/tests/cli.rs b/whiskers/tests/cli.rs index 119009b5..b558dc34 100644 --- a/whiskers/tests/cli.rs +++ b/whiskers/tests/cli.rs @@ -37,6 +37,18 @@ mod happy_path { )); } + /// Test that the CLI can render a template which uses read_file + #[test] + fn test_read_file() { + let mut cmd = Command::cargo_bin("whiskers").expect("binary exists"); + let assert = cmd + .args(["tests/fixtures/read_file/read_file.tera", "-f", "latte"]) + .assert(); + assert + .success() + .stdout(include_str!("fixtures/read_file/abc.txt")); + } + /// Test that the CLI can render a UTF-8 template file #[test] fn test_utf8() { diff --git a/whiskers/tests/fixtures/read_file/abc.txt b/whiskers/tests/fixtures/read_file/abc.txt new file mode 100644 index 00000000..6eb1f8c7 --- /dev/null +++ b/whiskers/tests/fixtures/read_file/abc.txt @@ -0,0 +1 @@ +Aute tempor minim eiusmod. diff --git a/whiskers/tests/fixtures/read_file/read_file.tera b/whiskers/tests/fixtures/read_file/read_file.tera new file mode 100644 index 00000000..81588587 --- /dev/null +++ b/whiskers/tests/fixtures/read_file/read_file.tera @@ -0,0 +1,6 @@ +--- +whiskers: + version: 2.0.0 +--- + +{{ read_file(path="abc.txt") }} From d17204b77030ff26c67446c175df4b967ed7e400 Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 13 May 2024 19:22:22 -0400 Subject: [PATCH 2/9] fix: better error handling/messaging --- whiskers/src/functions.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/whiskers/src/functions.rs b/whiskers/src/functions.rs index 05db7a7e..25afdc51 100644 --- a/whiskers/src/functions.rs +++ b/whiskers/src/functions.rs @@ -79,8 +79,8 @@ pub fn read_file(args: &HashMap) -> Result Date: Mon, 13 May 2024 19:42:24 -0400 Subject: [PATCH 3/9] refactor: use handler function and magic --- whiskers/src/functions.rs | 28 ++++++++++++++++------------ whiskers/src/main.rs | 7 ++----- whiskers/src/templating.rs | 6 ++++-- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/whiskers/src/functions.rs b/whiskers/src/functions.rs index 25afdc51..d7fa62c9 100644 --- a/whiskers/src/functions.rs +++ b/whiskers/src/functions.rs @@ -1,7 +1,7 @@ use std::{ collections::{BTreeMap, HashMap}, fs, - path::Path, + path::{Path, PathBuf}, }; use crate::models::Color; @@ -72,15 +72,19 @@ pub fn css_hsla(args: &HashMap) -> Result) -> Result { - let absolute = std::env::var("WHISKERS_CWD").unwrap(); - let path: String = tera::from_value( - args.get("path") - .ok_or_else(|| tera::Error::msg("path is required"))? - .clone(), - )?; - let file = Path::new(&absolute).join(path); - let contents = - fs::read_to_string(&file).or_else(|_| Err(format!("Failed to open file {:?}", file)))?; - Ok(tera::to_value(contents)?) +pub fn read_file_handler( + cwd: Option, +) -> impl Fn(&HashMap) -> Result { + return move |args| -> Result { + let path: String = tera::from_value( + args.get("path") + .ok_or_else(|| tera::Error::msg("path is required"))? + .clone(), + )?; + let file = + Path::new(& as Clone>::clone(&cwd).unwrap()).join(path); + let contents = fs::read_to_string(&file) + .or_else(|_| Err(format!("Failed to open file {:?}", file)))?; + Ok(tera::to_value(contents)?) + }; } diff --git a/whiskers/src/main.rs b/whiskers/src/main.rs index aa2599ae..ca96ba2b 100644 --- a/whiskers/src/main.rs +++ b/whiskers/src/main.rs @@ -83,10 +83,7 @@ fn main() -> anyhow::Result<()> { .expect("args.template is guaranteed by clap to be set"); let template_from_stdin = matches!(template.source, clap_stdin::Source::Stdin); let template_name = template_name(template); - match template_cwd(template) { - None => {} - Some(path) => std::env::set_var("WHISKERS_CWD", path), - }; + let template_cwd = template_cwd(template); let mut decoder = DecodeReaderBytes::new( template @@ -153,7 +150,7 @@ fn main() -> anyhow::Result<()> { } // build the Tera engine - let mut tera = templating::make_engine(); + let mut tera = templating::make_engine(template_cwd); tera.add_raw_template(&template_name, &doc.body) .context("Template is invalid")?; diff --git a/whiskers/src/templating.rs b/whiskers/src/templating.rs index 623276de..284a550a 100644 --- a/whiskers/src/templating.rs +++ b/whiskers/src/templating.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use indexmap::IndexMap; use crate::{filters, functions}; @@ -42,7 +44,7 @@ macro_rules! filter_example { }; } -pub fn make_engine() -> tera::Tera { +pub fn make_engine(cwd: Option) -> tera::Tera { let mut tera = tera::Tera::default(); tera.register_filter("add", filters::add); tera.register_filter("sub", filters::sub); @@ -56,7 +58,7 @@ pub fn make_engine() -> tera::Tera { tera.register_function("css_rgba", functions::css_rgba); tera.register_function("css_hsl", functions::css_hsl); tera.register_function("css_hsla", functions::css_hsla); - tera.register_function("read_file", functions::read_file); + tera.register_function("read_file", functions::read_file_handler(cwd)); tera } From 44a5ee2e20b6021a1edf016d665cf09ad07376de Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 13 May 2024 19:42:42 -0400 Subject: [PATCH 4/9] test: update tests to include searching parent directories --- whiskers/tests/cli.rs | 2 +- .../tests/fixtures/read_file/read_file.md | 23 +++++++++++++++++++ .../tests/fixtures/read_file/read_file.tera | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 whiskers/tests/fixtures/read_file/read_file.md diff --git a/whiskers/tests/cli.rs b/whiskers/tests/cli.rs index b558dc34..2e3e27a7 100644 --- a/whiskers/tests/cli.rs +++ b/whiskers/tests/cli.rs @@ -46,7 +46,7 @@ mod happy_path { .assert(); assert .success() - .stdout(include_str!("fixtures/read_file/abc.txt")); + .stdout(include_str!("fixtures/read_file/read_file.md")); } /// Test that the CLI can render a UTF-8 template file diff --git a/whiskers/tests/fixtures/read_file/read_file.md b/whiskers/tests/fixtures/read_file/read_file.md new file mode 100644 index 00000000..ea499800 --- /dev/null +++ b/whiskers/tests/fixtures/read_file/read_file.md @@ -0,0 +1,23 @@ +Aute tempor minim eiusmod. + +MIT License + +Copyright (c) 2021 Catppuccin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/whiskers/tests/fixtures/read_file/read_file.tera b/whiskers/tests/fixtures/read_file/read_file.tera index 81588587..e3ec7ea6 100644 --- a/whiskers/tests/fixtures/read_file/read_file.tera +++ b/whiskers/tests/fixtures/read_file/read_file.tera @@ -4,3 +4,4 @@ whiskers: --- {{ read_file(path="abc.txt") }} +{{ read_file(path="../../../LICENSE") }} From 1117d374c2965273aa9c514c79f41a08b43f1d82 Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Wed, 22 May 2024 11:58:50 -0400 Subject: [PATCH 5/9] refactor: replace references to `cwd` with `template_directory` for clarity --- whiskers/src/functions.rs | 8 +++++--- whiskers/src/main.rs | 6 +++--- whiskers/src/templating.rs | 7 +++++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/whiskers/src/functions.rs b/whiskers/src/functions.rs index d7fa62c9..16cea2d4 100644 --- a/whiskers/src/functions.rs +++ b/whiskers/src/functions.rs @@ -73,7 +73,7 @@ pub fn css_hsla(args: &HashMap) -> Result, + template_directory: Option, ) -> impl Fn(&HashMap) -> Result { return move |args| -> Result { let path: String = tera::from_value( @@ -81,8 +81,10 @@ pub fn read_file_handler( .ok_or_else(|| tera::Error::msg("path is required"))? .clone(), )?; - let file = - Path::new(& as Clone>::clone(&cwd).unwrap()).join(path); + let file = Path::new( + & as Clone>::clone(&template_directory).unwrap(), + ) + .join(path); let contents = fs::read_to_string(&file) .or_else(|_| Err(format!("Failed to open file {:?}", file)))?; Ok(tera::to_value(contents)?) diff --git a/whiskers/src/main.rs b/whiskers/src/main.rs index ca96ba2b..8f91097a 100644 --- a/whiskers/src/main.rs +++ b/whiskers/src/main.rs @@ -83,7 +83,7 @@ fn main() -> anyhow::Result<()> { .expect("args.template is guaranteed by clap to be set"); let template_from_stdin = matches!(template.source, clap_stdin::Source::Stdin); let template_name = template_name(template); - let template_cwd = template_cwd(template); + let template_directory = template_directory(template); let mut decoder = DecodeReaderBytes::new( template @@ -150,7 +150,7 @@ fn main() -> anyhow::Result<()> { } // build the Tera engine - let mut tera = templating::make_engine(template_cwd); + let mut tera = templating::make_engine(template_directory); tera.add_raw_template(&template_name, &doc.body) .context("Template is invalid")?; @@ -251,7 +251,7 @@ fn template_name(template: &clap_stdin::FileOrStdin) -> String { } } -fn template_cwd(template: &clap_stdin::FileOrStdin) -> Option { +fn template_directory(template: &clap_stdin::FileOrStdin) -> Option { match &template.source { clap_stdin::Source::Stdin => None, clap_stdin::Source::Arg(arg) => { diff --git a/whiskers/src/templating.rs b/whiskers/src/templating.rs index 284a550a..2805dad5 100644 --- a/whiskers/src/templating.rs +++ b/whiskers/src/templating.rs @@ -44,7 +44,7 @@ macro_rules! filter_example { }; } -pub fn make_engine(cwd: Option) -> tera::Tera { +pub fn make_engine(template_directory: Option) -> tera::Tera { let mut tera = tera::Tera::default(); tera.register_filter("add", filters::add); tera.register_filter("sub", filters::sub); @@ -58,7 +58,10 @@ pub fn make_engine(cwd: Option) -> tera::Tera { tera.register_function("css_rgba", functions::css_rgba); tera.register_function("css_hsl", functions::css_hsl); tera.register_function("css_hsla", functions::css_hsla); - tera.register_function("read_file", functions::read_file_handler(cwd)); + tera.register_function( + "read_file", + functions::read_file_handler(template_directory), + ); tera } From b15a9280cd585997955f16807130390947d5150f Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Wed, 22 May 2024 13:28:10 -0400 Subject: [PATCH 6/9] test(read_file): control whitespace around expressions --- whiskers/tests/fixtures/read_file/read_file.tera | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/whiskers/tests/fixtures/read_file/read_file.tera b/whiskers/tests/fixtures/read_file/read_file.tera index e3ec7ea6..337ae27d 100644 --- a/whiskers/tests/fixtures/read_file/read_file.tera +++ b/whiskers/tests/fixtures/read_file/read_file.tera @@ -3,5 +3,5 @@ whiskers: version: 2.0.0 --- -{{ read_file(path="abc.txt") }} -{{ read_file(path="../../../LICENSE") }} +{{- read_file(path="abc.txt") }} +{{ read_file(path="../../../LICENSE") -}} From 257b4d3a5d68bf0c233d628e31a169f5a050c232 Mon Sep 17 00:00:00 2001 From: backwardspy Date: Mon, 27 May 2024 16:29:20 +0100 Subject: [PATCH 7/9] feat: use cwd when path is stdin this stops the code panicking on `whiskers -` when read_file is used, and as a side-effect also cleans up the clone dance in read_file_handler. --- whiskers/src/functions.rs | 17 +++++++---------- whiskers/src/main.rs | 17 +++++++++-------- whiskers/src/templating.rs | 6 +++--- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/whiskers/src/functions.rs b/whiskers/src/functions.rs index 16cea2d4..b41a60ba 100644 --- a/whiskers/src/functions.rs +++ b/whiskers/src/functions.rs @@ -1,7 +1,7 @@ use std::{ collections::{BTreeMap, HashMap}, fs, - path::{Path, PathBuf}, + path::PathBuf, }; use crate::models::Color; @@ -73,20 +73,17 @@ pub fn css_hsla(args: &HashMap) -> Result, + template_directory: PathBuf, ) -> impl Fn(&HashMap) -> Result { - return move |args| -> Result { + move |args| -> Result { let path: String = tera::from_value( args.get("path") .ok_or_else(|| tera::Error::msg("path is required"))? .clone(), )?; - let file = Path::new( - & as Clone>::clone(&template_directory).unwrap(), - ) - .join(path); - let contents = fs::read_to_string(&file) - .or_else(|_| Err(format!("Failed to open file {:?}", file)))?; + let path = template_directory.join(path); + let contents = fs::read_to_string(&path) + .map_err(|_| format!("Failed to open file {}", path.display()))?; Ok(tera::to_value(contents)?) - }; + } } diff --git a/whiskers/src/main.rs b/whiskers/src/main.rs index 8f91097a..86b143d7 100644 --- a/whiskers/src/main.rs +++ b/whiskers/src/main.rs @@ -83,7 +83,7 @@ fn main() -> anyhow::Result<()> { .expect("args.template is guaranteed by clap to be set"); let template_from_stdin = matches!(template.source, clap_stdin::Source::Stdin); let template_name = template_name(template); - let template_directory = template_directory(template); + let template_directory = template_directory(template)?; let mut decoder = DecodeReaderBytes::new( template @@ -150,7 +150,7 @@ fn main() -> anyhow::Result<()> { } // build the Tera engine - let mut tera = templating::make_engine(template_directory); + let mut tera = templating::make_engine(&template_directory); tera.add_raw_template(&template_name, &doc.body) .context("Template is invalid")?; @@ -251,13 +251,14 @@ fn template_name(template: &clap_stdin::FileOrStdin) -> String { } } -fn template_directory(template: &clap_stdin::FileOrStdin) -> Option { +fn template_directory(template: &clap_stdin::FileOrStdin) -> anyhow::Result { match &template.source { - clap_stdin::Source::Stdin => None, - clap_stdin::Source::Arg(arg) => { - let parent_dir = Path::new(&arg).canonicalize().ok()?.parent()?.to_owned(); - Some(parent_dir) - } + clap_stdin::Source::Stdin => Ok(std::env::current_dir()?), + clap_stdin::Source::Arg(arg) => Ok(Path::new(&arg) + .canonicalize()? + .parent() + .expect("file path must have a parent") + .to_owned()), } } diff --git a/whiskers/src/templating.rs b/whiskers/src/templating.rs index 2805dad5..4fb320fb 100644 --- a/whiskers/src/templating.rs +++ b/whiskers/src/templating.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::Path; use indexmap::IndexMap; @@ -44,7 +44,7 @@ macro_rules! filter_example { }; } -pub fn make_engine(template_directory: Option) -> tera::Tera { +pub fn make_engine(template_directory: &Path) -> tera::Tera { let mut tera = tera::Tera::default(); tera.register_filter("add", filters::add); tera.register_filter("sub", filters::sub); @@ -60,7 +60,7 @@ pub fn make_engine(template_directory: Option) -> tera::Tera { tera.register_function("css_hsla", functions::css_hsla); tera.register_function( "read_file", - functions::read_file_handler(template_directory), + functions::read_file_handler(template_directory.to_owned()), ); tera } From 1aee1410e13aa6f8ba49af960ecf6c3bc681091b Mon Sep 17 00:00:00 2001 From: backwardspy Date: Mon, 27 May 2024 16:38:11 +0100 Subject: [PATCH 8/9] fix: handle missing template file adds context to errors returned from the `template_directory` function. without this, we get a platform-specific error message that makes testing harder. --- whiskers/src/main.rs | 3 ++- whiskers/tests/cli.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/whiskers/src/main.rs b/whiskers/src/main.rs index 86b143d7..9e27dd59 100644 --- a/whiskers/src/main.rs +++ b/whiskers/src/main.rs @@ -83,7 +83,8 @@ fn main() -> anyhow::Result<()> { .expect("args.template is guaranteed by clap to be set"); let template_from_stdin = matches!(template.source, clap_stdin::Source::Stdin); let template_name = template_name(template); - let template_directory = template_directory(template)?; + let template_directory = + template_directory(template).context("Template file does not exist")?; let mut decoder = DecodeReaderBytes::new( template diff --git a/whiskers/tests/cli.rs b/whiskers/tests/cli.rs index 7c992b76..fe340f3d 100644 --- a/whiskers/tests/cli.rs +++ b/whiskers/tests/cli.rs @@ -101,7 +101,7 @@ mod sad_path { cmd.arg("test/file/doesnt/exist"); cmd.assert() .failure() - .stderr(predicate::str::contains("Failed to open template file")); + .stderr(predicate::str::contains("Template file does not exist")); } #[test] From 18c3f870bdc44ba4906c70fc205819227c02dc24 Mon Sep 17 00:00:00 2001 From: backwardspy Date: Mon, 27 May 2024 16:42:07 +0100 Subject: [PATCH 9/9] docs: clarify read_file path, regen readme table --- whiskers/README.md | 1 + whiskers/src/templating.rs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/whiskers/README.md b/whiskers/README.md index e6c4fd0d..5e7b823a 100644 --- a/whiskers/README.md +++ b/whiskers/README.md @@ -166,6 +166,7 @@ These types are designed to closely match the [palette.json](https://github.com/ | `css_rgba` | Convert a color to an RGBA CSS string | `css_rgba(color=red)` => `rgba(210, 15, 57, 1.00)` | | `css_hsl` | Convert a color to an HSL CSS string | `css_hsl(color=red)` => `hsl(347, 87%, 44%)` | | `css_hsla` | Convert a color to an HSLA CSS string | `css_hsla(color=red)` => `hsla(347, 87%, 44%, 1.00)` | +| `read_file` | Read and include the contents of a file, path is relative to the template file | `read_file(path="abc.txt")` => `abc` | ### Filters diff --git a/whiskers/src/templating.rs b/whiskers/src/templating.rs index 4fb320fb..7ae2c6f7 100644 --- a/whiskers/src/templating.rs +++ b/whiskers/src/templating.rs @@ -107,7 +107,9 @@ pub fn all_functions() -> Vec { }, Function { name: "read_file".to_string(), - description: "Read and include the contents of a file".to_string(), + description: + "Read and include the contents of a file, path is relative to the template file" + .to_string(), examples: vec![function_example!(read_file(path="abc.txt") => "abc")], }, ]