home ~ projects ~ socials

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

//! ```cargo
//! [dependencies]
//! minijinja = "1.0.11"
//! ```

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();
}
Output:
/-------------------------------------
|
| 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) }}

-- end of line --