krz/orgo
Lightning fast org-mode static site generator.
clone: git clone https://gitbay.org/krz/orgo.git
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}