krz/orgo

Lightning fast org-mode static site generator.

clone: git clone https://gitbay.org/krz/orgo.git

v0.3.0: src/template.rs · raw

 1//! TEMPLATE stage (spec §2.1, §2.4, §3.3): rendered fragment + page metadata → full HTML.
 2//!
 3//! minijinja (Jinja2 semantics, runtime templates: edit-and-rebuild, no recompile).
 4//! Templates are a hashing input for incrementality (spec §4.1): a base-layout edit
 5//! invalidates every page that transitively uses it. Keep the fragment/template
 6//! boundary sharp so content HTML can be snapshot-tested independently of chrome.
 7
 8use minijinja::{context, Environment};
 9use serde::Serialize;
10
11/// A navigation entry: a page title and the URL to reach it from the current page.
12#[derive(Debug, Clone, Serialize)]
13pub struct NavItem {
14    pub title: String,
15    pub url: String,
16}
17
18/// The base layout applied to every page: `<title>`, a nav bar, and the body.
19/// Minimal but real — a single `base` template, no partials yet.
20const BASE_TEMPLATE: &str = r#"<!DOCTYPE html>
21<html lang="en">
22<head>
23<meta charset="utf-8">
24<title>{{ title }}</title>
25</head>
26<body>
27<nav>
28{%- for item in nav %}
29<a href="{{ item.url }}">{{ item.title }}</a>
30{%- endfor %}
31</nav>
32<main>
33{{ body | safe }}</main>
34</body>
35</html>
36"#;
37
38/// The source text of every template that participates in the page layout. Hashed by
39/// the incremental layer (spec §4.1): a base-layout edit invalidates every page that
40/// uses it. There is a single `base` template today; when partials arrive this returns
41/// the transitive closure so a single-partial edit invalidates only its users.
42pub fn template_sources() -> &'static [(&'static str, &'static str)] {
43    &[("base", BASE_TEMPLATE)]
44}
45
46#[derive(Debug, thiserror::Error)]
47pub enum TemplateError {
48    #[error("template error: {0}")]
49    Render(String),
50}
51
52/// Wraps a rendered fragment in its page template.
53pub struct Templater {
54    env: Environment<'static>,
55}
56
57impl Templater {
58    pub fn new() -> Self {
59        let mut env = Environment::new();
60        env.add_template("base", BASE_TEMPLATE)
61            .expect("base template compiles");
62        Templater { env }
63    }
64
65    /// fragment + page metadata → full HTML page.
66    pub fn render_page(
67        &self,
68        title: &str,
69        body: &str,
70        nav: &[NavItem],
71    ) -> Result<String, TemplateError> {
72        let tmpl = self
73            .env
74            .get_template("base")
75            .map_err(|e| TemplateError::Render(e.to_string()))?;
76        tmpl.render(context! { title => title, body => body, nav => nav })
77            .map_err(|e| TemplateError::Render(e.to_string()))
78    }
79}
80
81impl Default for Templater {
82    fn default() -> Self {
83        Self::new()
84    }
85}