1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
//! This crate adds utilities to embed MiniJinja templates
//! directly in the binary. It is a static version of the
//! `path_loader` function with some optional filtering.
//!
//! First you need to add this as regular and build dependency:
//!
//! ```text
//! cargo add minijinja-embed
//! cargo add minijinja-embed --build
//! ```
//!
//! Afterwards you can embed a template folder in your `build.rs`
//! script. You can also do this conditional based on a feature
//! flag. In this example we just embed all templates in the
//! `src/templates` folder:
//!
//! ```rust
//! fn main() {
//! // ...
//! # if false {
//! minijinja_embed::embed_templates!("src/templates");
//! # }
//! }
//! ```
//!
//! Later when you create the environment you can load the embedded
//! templates:
//!
//! ```rust,ignore
//! use minijinja::Environment;
//!
//! let mut env = Environment::new();
//! minijinja_embed::load_templates!(&mut env);
//! ```
//!
//! For more information see [`embed_templates`].
#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
#![allow(clippy::needless_doctest_main)]
use std::fmt::Write;
use std::fs::{self, DirEntry};
use std::io;
use std::path::Path;
/// Utility macro to store templates in a `build.rs` file.
///
/// This needs to be invoked in `build.rs` file with at least
/// the path to the templates. Optionally it can be filtered
/// by extension and an alternative bundle name can be provided.
///
/// These are all equivalent:
///
/// ```rust
/// # fn foo() {
/// minijinja_embed::embed_templates!("src/templates");
/// minijinja_embed::embed_templates!("src/templates", &[][..]);
/// minijinja_embed::embed_templates!("src/templates", &[][..], "main");
/// # }
/// ```
///
/// To embed diffent folders, alternative bundle names can be provided.
/// Also tempaltes can be filtered down by extension to avoid accidentally
/// including unexpected templates.
///
/// ```rust
/// # fn foo() {
/// minijinja_embed::embed_templates!("src/templates", &[".html", ".txt"]);
/// # }
/// ```
///
/// Later they can then be loaded into a Jinja environment with
/// the [`load_templates!`] macro.
///
/// # Panics
///
/// This function panics if the templates are not valid (eg: invalid syntax).
/// It's not possible to handle this error by design. During development you
/// should be using dynamic template loading instead.
#[macro_export]
macro_rules! embed_templates {
($path:expr, $exts:expr, $bundle_name:expr) => {{
let out_dir = ::std::env::var_os("OUT_DIR").unwrap();
let dst_path = ::std::path::Path::new(&out_dir)
.join(format!("minijinja_templates_{}.rs", $bundle_name));
let generated = $crate::_embed_templates($path, $exts);
println!("cargo:rerun-if-changed={}", $path);
::std::fs::write(dst_path, generated).unwrap();
}};
($path:expr) => {
$crate::embed_templates!($path, &[][..], "main");
};
($path:expr, $exts:expr) => {
$crate::embed_templates!($path, $exts, "main");
};
}
/// Loads embedded templates into the environment.
///
/// This macro takes a MiniJinja environment as argument and optionally
/// also the name of a template bundle. All templates in the bundle are
/// then loaded into the environment. Templates are eagerly loaded into
/// the environment which means that no loader needs to be enabled.
///
/// ```rust,ignore
/// minijinja_embed::load_templates!(&mut env);
/// ```
///
/// By default the `main` bundled is loaded. To load a different one
/// pass it as second argument:
///
/// ```rust,ignore
/// minijinja_embed::load_templates!(&mut env, "other_bundle");
/// ```
#[macro_export]
macro_rules! load_templates {
($env:expr, $bundle_name:literal) => {{
let load_template = include!(concat!(
env!("OUT_DIR"),
"/minijinja_templates_",
$bundle_name,
".rs"
));
load_template(&mut $env);
}};
($env:expr) => {
$crate::load_templates!($env, "main");
};
}
fn visit_dirs(dir: &Path, cb: &mut dyn FnMut(&DirEntry)) -> io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path
.file_name()
.and_then(|x| x.to_str())
.map_or(false, |x| x.starts_with('.'))
{
continue;
}
if path.is_dir() {
visit_dirs(&path, cb)?;
} else {
cb(&entry);
}
}
}
Ok(())
}
#[doc(hidden)]
pub fn _embed_templates<P: AsRef<Path>>(path: P, extensions: &[&str]) -> String
where
P: AsRef<Path>,
{
let path = path.as_ref().canonicalize().unwrap();
let mut gen = String::new();
writeln!(gen, "|env: &mut minijinja::Environment| {{").unwrap();
visit_dirs(&path, &mut |f| {
let p = f.path();
if !extensions.is_empty()
&& !p
.file_name()
.and_then(|x| x.to_str())
.map_or(false, |name| extensions.iter().any(|x| name.ends_with(x)))
{
return;
}
let contents = fs::read_to_string(&p).unwrap();
let name = p.strip_prefix(&path).unwrap();
writeln!(
gen,
"env.add_template({:?}, {:?}).expect(\"Embedded an invalid template\");",
name.to_string_lossy().replace('\\', "/"),
contents
)
.unwrap();
})
.unwrap();
writeln!(gen, "}}").unwrap();
gen
}