minijinja_embed/
lib.rs

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