repos

darkfurrow.com-rust

mirror archived upstream

A living almanac of seasons, soil, and the quiet knowledge that used to be common. Rust axum with minijinja and Vite.

agriculturealmanacaxumfolk-knowledgegardeningminijinjarustseasonalvite

3.7 KB · 113 lines · Rust Raw History
  1use minijinja::{path_loader, AutoEscape, Environment, Error, Output, State};
  2use minijinja::value::Value;
  3use serde_json::Value as JsonValue;
  4use std::path::Path;
  5
  6/// Custom formatter that matches Jinja2's HTML escape (does NOT escape `/`).
  7/// Without this, minijinja escapes `/` in URLs as `/` which is ugly even
  8/// though browsers parse it the same.
  9fn jinja2_html_formatter(out: &mut Output, state: &State, value: &Value) -> Result<(), Error> {
 10    if value.is_safe() {
 11        write!(out, "{value}").map_err(Error::from)?;
 12        return Ok(());
 13    }
 14    let auto_escape = match state.auto_escape() {
 15        AutoEscape::Html => true,
 16        AutoEscape::None => false,
 17        _ => return minijinja::escape_formatter(out, state, value),
 18    };
 19    if !auto_escape {
 20        write!(out, "{value}").map_err(Error::from)?;
 21        return Ok(());
 22    }
 23    if let Some(s) = value.as_str() {
 24        write_jinja2_html(out, s).map_err(Error::from)?;
 25    } else if value.is_undefined() || value.is_none() {
 26        // emit nothing
 27    } else {
 28        let stringified = value.to_string();
 29        write_jinja2_html(out, &stringified).map_err(Error::from)?;
 30    }
 31    Ok(())
 32}
 33
 34fn write_jinja2_html(out: &mut Output, s: &str) -> std::fmt::Result {
 35    let mut last = 0;
 36    for (i, b) in s.bytes().enumerate() {
 37        let escape = match b {
 38            b'&' => "&amp;",
 39            b'<' => "&lt;",
 40            b'>' => "&gt;",
 41            b'"' => "&#34;",
 42            b'\'' => "&#39;",
 43            _ => continue,
 44        };
 45        if last < i {
 46            out.write_str(&s[last..i])?;
 47        }
 48        out.write_str(escape)?;
 49        last = i + 1;
 50    }
 51    if last < s.len() {
 52        out.write_str(&s[last..])?;
 53    }
 54    Ok(())
 55}
 56
 57fn read_manifest(path: &Path) -> JsonValue {
 58    let text = std::fs::read_to_string(path).unwrap_or_else(|_| "{}".to_string());
 59    serde_json::from_str(&text).unwrap_or(JsonValue::Null)
 60}
 61
 62fn lookup_asset(manifest: &JsonValue, entry: &str, kind: &str) -> String {
 63    if let Some(chunk) = manifest.get(entry) {
 64        if kind == "css" {
 65            if let Some(css_arr) = chunk.get("css").and_then(|v| v.as_array()) {
 66                if let Some(first) = css_arr.first().and_then(|v| v.as_str()) {
 67                    return format!("/static/{first}");
 68                }
 69            }
 70        }
 71        if let Some(file) = chunk.get("file").and_then(|v| v.as_str()) {
 72            return format!("/static/{file}");
 73        }
 74    }
 75    format!("/static/{entry}")
 76}
 77
 78pub fn build_env(templates_dir: &Path, manifest_path: &Path) -> Environment<'static> {
 79    let mut env = Environment::new();
 80    env.set_loader(path_loader(templates_dir));
 81    env.set_formatter(jinja2_html_formatter);
 82
 83    // Vite manifest:
 84    // - debug builds re-read on every call so Vite watcher rebuilds show up
 85    //   immediately
 86    // - release builds load once at startup and reuse the cached value
 87    #[cfg(debug_assertions)]
 88    {
 89        let path = manifest_path.to_path_buf();
 90        env.add_function(
 91            "vite_asset",
 92            move |entry: String, kind: Option<String>| -> Result<String, Error> {
 93                let kind = kind.unwrap_or_else(|| "file".to_string());
 94                let manifest = read_manifest(&path);
 95                Ok(lookup_asset(&manifest, &entry, &kind))
 96            },
 97        );
 98    }
 99    #[cfg(not(debug_assertions))]
100    {
101        let manifest = read_manifest(manifest_path);
102        env.add_function(
103            "vite_asset",
104            move |entry: String, kind: Option<String>| -> Result<String, Error> {
105                let kind = kind.unwrap_or_else(|| "file".to_string());
106                Ok(lookup_asset(&manifest, &entry, &kind))
107            },
108        );
109    }
110
111    env
112}