wayver's git archive


an obsidian renderer
git clone https://git.wayver.dev/sable

sable-renderer/src/templates/mod.rs@337ba67f65eaa17b44e371af7c0f0c761d6aa914

raw
Date Commit Message Author Files + -
2026-02-23 01:55 initial mvp wayverd 139 17808 0
...

1mod func_link_graph;
2mod funcs;
3mod markdown;
4
5use sable_core::config::Config;
6use sable_vault::SharedVault;
7use tera::{Context, Tera};
8
9static DEFAULT_TEMPLATE_NAME: &str = "--sable-default.html";
10static DEFAULT_TEMPLATE: &str = include_str!("default.html");
11
12#[derive(Debug, miette::Diagnostic, thiserror::Error)]
13pub enum TemplatesError {
14    #[error("failed to convert path as its not valid utf-8")]
15    PathNotUtf8,
16    #[error("failed to load tera templates")]
17    TeraInit(#[source] tera::Error),
18    #[error("failed to render template")]
19    RenderTemplate(#[source] tera::Error),
20}
21
22/// A wrapper around [`Tera`] that provides the built-in template and custom filters/functions.
23#[derive(Debug)]
24pub struct Templates {
25    tera: Tera,
26
27    default_template: Option<String>,
28}
29
30impl Templates {
31    pub fn load(config: &Config, vault: SharedVault) -> Result<Self, TemplatesError> {
32        let dir_str = config
33            .templates
34            .to_str()
35            .ok_or(TemplatesError::PathNotUtf8)?;
36        let dir_pattern = format!("{dir_str}/**/*");
37
38        let mut tera = Tera::new(&dir_pattern).map_err(TemplatesError::TeraInit)?;
39
40        tera.add_raw_template(DEFAULT_TEMPLATE_NAME, DEFAULT_TEMPLATE)
41            .expect("failed to add default fallback template");
42
43        tera.register_filter("markdown", markdown::Markdown::new(vault.clone()));
44
45        tera.register_function("link_graph", func_link_graph::LinkGraph::new(vault));
46
47        for template in tera.get_template_names() {
48            tracing::debug!(template=%template, "loaded tera template");
49        }
50
51        Ok(Self {
52            tera,
53
54            default_template: config.default_template.clone(),
55        })
56    }
57
58    // pub fn reload(&mut self) {
59    //     self.tera.full_reload();
60    // }
61
62    /// Returns an iterator over the names of all registered templates in an unspecified order.
63    pub fn get_template_names(&self) -> impl Iterator<Item = &str> {
64        self.tera.get_template_names()
65    }
66
67    /// Checks if the given template is loaded.
68    #[must_use]
69    pub fn has_template(&self, name: &str) -> bool {
70        self.get_template_names().any(|tn| tn == name)
71    }
72
73    pub fn render(&self, template_name: &str, context: &Context) -> Result<String, TemplatesError> {
74        let name = if self.has_template(template_name) {
75            template_name
76        } else if let Some(default_template) = &self.default_template {
77            default_template
78        } else {
79            tracing::warn!(template=%template_name, "requested template does not exist, using fallback");
80
81            DEFAULT_TEMPLATE_NAME
82        };
83
84        self.tera
85            .render(name, context)
86            .map_err(TemplatesError::RenderTemplate)
87    }
88}
89
90#[derive(Debug)]
91struct FilterError {
92    msg: String,
93}
94
95impl FilterError {
96    fn new<M: ToString>(msg: M) -> Self {
97        Self {
98            msg: msg.to_string(),
99        }
100    }
101}
102
103impl std::fmt::Display for FilterError {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(f, "{}", self.msg)
106    }
107}
108
109impl std::error::Error for FilterError {}
110