MiniJinja Project Scaffold For Site And Page Object Template Calls
This post represents where I ended up after a years of using MiniJinja. The approach creates two structs (Site
and Page
) that are passed into templates during the rendering process. Method calls in the templates are forwared to the functions in the objects.
More notes below the code
use minijinja::context;
use minijinja::value::Object;
use minijinja::Environment;
use minijinja::Error;
use minijinja::Value;
use std::collections::BTreeMap;
use std::fmt::Display;
#[derive(Clone, Debug)]
pub struct Site<'a> {
pub env: Environment<'a>,
pub pages: BTreeMap<String, Page>,
}
impl Site<'_> {
pub fn new() -> Site<'static> {
Site {
env: Environment::<'static>::new(),
pages: BTreeMap::new(),
}
}
}
impl Site<'_> {
pub fn add_tag_from_site(&self, args: &[Value]) -> String {
format!("<site_formatted>{}</site_formatted>", args[0])
}
}
impl Site<'static> {
pub fn build_pages(&self) {
let template = self.env.get_template("a_page_template").unwrap();
self.pages.keys().for_each(|page_id| {
let output = template
.render(context!(
site => Value::from_object(self.clone()),
page => Value::from_object(self.pages.get(page_id).unwrap().clone())
))
.unwrap();
println!("{}", output);
()
});
}
}
impl Site<'_> {
pub fn load_pages(&mut self) {
let pages = vec![
Page {
title: "First Page".to_string(),
id: "a1b2".to_string(),
content: "alfa bravo".to_string(),
},
Page {
title: "Second Page".to_string(),
id: "c3d4".to_string(),
content: "charlie delta".to_string(),
},
];
pages.iter().for_each(|p| {
self.pages.insert(p.id.clone(), p.clone());
});
}
}
impl Site<'_> {
pub fn load_templates(&mut self) {
self.env
.add_template(
"a_page_template",
r#"/-------------------------------------
|
| SITE: {{ site }}
|
| PAGE ID: {{ page }}
|
| PAGE TITLE: {{ page.title() }}
|
| {{ site.add_tag_from_site("Quick Fox") }}
|
| Page count from site method call: {{ site.page_count() }}
|
| Pass arguments to page method call:
|
| - First word: {{ page.get_word(1) }}
|
| - Second word: {{ page.get_word(2) }}
"#,
)
.unwrap();
}
}
impl Site<'_> {
pub fn page_count(&self) -> usize {
self.pages.len()
}
}
impl Display for Site<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", "minijinja.example.com")
}
}
impl Object for Site<'static> {
fn call_method(
&self,
_state: &minijinja::State,
name: &str,
args: &[Value],
) -> Result<Value, Error> {
match name {
"page_count" => Ok(Value::from_serializable(&self.page_count())),
"add_tag_from_site" => Ok(Value::from_serializable(&self.add_tag_from_site(args))),
_ => Ok(Value::from("")),
}
}
}
#[derive(Clone, Debug)]
pub struct Page {
pub id: String,
pub title: String,
pub content: String,
}
impl Page {
pub fn get_word(&self, args: &[Value]) -> String {
let word_num: usize = args[0].to_string().parse().unwrap();
self.content
.split(" ")
.into_iter()
.nth(word_num - 1)
.unwrap()
.to_string()
}
}
impl Page {
pub fn title(&self) -> String {
format!("<h1>{}</h1>", self.title)
}
}
impl Display for Page {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.id)
}
}
impl Object for Page {
fn call_method(
&self,
_state: &minijinja::State,
name: &str,
args: &[Value],
) -> Result<Value, Error> {
match name {
"title" => Ok(Value::from_serializable(&self.title())),
"get_word" => Ok(Value::from_serializable(&self.get_word(args))),
_ => Ok(Value::from("")),
}
}
}
fn main() {
let mut site = Site::new();
site.load_templates();
site.load_pages();
site.build_pages();
}
/-------------------------------------
|
| SITE: minijinja.example.com
|
| PAGE ID: a1b2
|
| PAGE TITLE: <h1>First Page</h1>
|
| <site_formatted>Quick Fox</site_formatted>
|
| Page count from site method call: 2
|
| Pass arguments to page method call:
|
| - First word: alfa
|
| - Second word: bravo
/-------------------------------------
|
| SITE: minijinja.example.com
|
| PAGE ID: c3d4
|
| PAGE TITLE: <h1>Second Page</h1>
|
| <site_formatted>Quick Fox</site_formatted>
|
| Page count from site method call: 2
|
| Pass arguments to page method call:
|
| - First word: charlie
|
| - Second word: delta
Notes
-
This approach is different than the main references I've seen that expect data to have been pre-generated before passing it directly into the template. I like this approach better becuase it's a lot more dynamic (e.g. I can find what categories a page belongs to and request links for those categories specificially)
-
The key to making this work is MiniJinjas's
Value::from_object()
, .call_method()
, and Value::from_serializable()
. They're what let the object be passed into the templates in a way that lets method calls and responses connect
-
I'm cloning the site and page objects to pass them in to the template. Feels like this should be able to be done as a reference, but I haven't figured that out yet and this is working fine.
-
Adding new methods is done by creating the function in the object and then adding an entry in the
fn call_method(){}
of impl Object for Page
or impl Object for Site
-
The impl Display
on Site
and Page
is required, but it's useful. I use it to return the ID of the page which lets me do things like:
{{ site.get_categories_for_page(page) }}