//! minijinja environment: the RainTPL templates of Movim were ported to Jinja //! syntax under `templates/`. Helpers available in templates: //! //! * `__("key", args...)` translation (uses the `lang` variable of the context) //! * `route("page", params...)` URL of a page //! * filters: `cleanupId`, `echapJS`, `stringToColor`, `slugify`, `json`, //! `addEmojis`, `implodeCsv`, `mimeToIcon`, `mimeToLabel`, `firstLetter` use minijinja::{value::Rest, Environment, State, Value}; use std::path::Path; use crate::{i18n, utils}; pub fn environment(dir: &Path) -> Environment<'static> { let mut env = Environment::new(); env.set_loader(minijinja::path_loader(dir)); env.set_auto_escape_callback(|name| { if name.ends_with(".html") || name.ends_with(".tpl") { minijinja::AutoEscape::Html } else { minijinja::AutoEscape::None } }); env.set_trim_blocks(true); env.set_lstrip_blocks(true); // Like PHP's htmlspecialchars: '/' is not escaped, so URLs stay usable in scripts env.set_formatter(|out, state, value| { if state.auto_escape() == minijinja::AutoEscape::None || value.is_safe() { return minijinja::escape_formatter(out, state, value); } if value.is_none() || value.is_undefined() { return Ok(()); } let s = match value.as_str() { Some(s) => std::borrow::Cow::Borrowed(s), None => std::borrow::Cow::Owned(value.to_string()), }; out.write_str(&html_escape::encode_quoted_attribute(&s)) .map_err(|_| minijinja::Error::new(minijinja::ErrorKind::WriteFailure, "write failed")) }); env.add_function("__", translate); env.add_function("route", route); env.add_filter("cleanupId", |s: String| utils::cleanup_id(&s)); env.add_filter("cleanupIdHash", |s: String| utils::cleanup_id_hash(&s)); env.add_filter("echapJS", |s: String| utils::echap_js(&s)); env.add_filter("stringToColor", |s: Option| utils::string_to_color(s.as_deref().unwrap_or("")).to_string()); env.add_filter("slugify", |s: String| utils::slugify(&s)); env.add_filter("firstLetter", |s: String| utils::first_letter_capitalize(&s, false)); env.add_filter("mimeToIcon", |s: Option| utils::mime_to_icon(s.as_deref().unwrap_or("")).to_string()); env.add_filter("mimeToLabel", |s: Option| utils::mime_to_label(s.as_deref().unwrap_or(""))); env.add_filter("implodeCsv", |v: Vec| v.join(", ")); env.add_filter("json", |v: Value| serde_json::to_string(&v).unwrap_or_else(|_| "null".into())); env.add_filter("addEmojis", add_emojis); env } fn translate(state: &State, key: String, args: Rest) -> String { let lang = state .lookup("lang") .and_then(|v| v.as_str().map(String::from)) .unwrap_or_else(|| "en".into()); let args: Vec = args .iter() .map(|a| a.as_str().map(String::from).unwrap_or_else(|| a.to_string())) .collect(); i18n::get().translate(&lang, &key, &args) } fn route(state: &State, page: String, params: Rest) -> String { let base = state .lookup("base_uri") .and_then(|v| v.as_str().map(String::from)) .unwrap_or_else(|| "/".into()); let params: Vec = params .iter() .filter(|v| !v.is_none() && !v.is_undefined()) .map(|a| a.as_str().map(String::from).unwrap_or_else(|| a.to_string())) .collect(); crate::app::route_with_base(&base, &page, ¶ms) } fn add_emojis(state: &State, s: String, no_title: Option) -> Value { let base = state .lookup("base_uri") .and_then(|v| v.as_str().map(String::from)) .unwrap_or_else(|| "/".into()); let escaped = utils::html_escape(&s); Value::from_safe_string(utils::replace_emojis(&escaped, &base, no_title.unwrap_or(false)).html) } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn shared_files_drawer() { let root = Path::new(env!("CARGO_MANIFEST_DIR")); i18n::init(&root.join("frontend")); let env = environment(&root.join("templates")); let tpl = env.get_template("ChatActions/_chatactions_files.html").unwrap(); let file = json!({ "mid": 7, "url": "https://up.example.org/a.pdf", "name": "a.pdf", "size": "1 KiB", "date": "today", "published": "2026-01-01 10:00:00", "thumb": "/picture?url=x", }); let data = |lang| json!({ "lang": lang, "jid": "bob@example.org", "images": [file], "videos": [], "recordings": [file], "files": [file] }); let html = tpl.render(data("en")).unwrap(); assert!(html.contains("Recordings") && html.contains("Shared files")); assert!(html.contains("<b>a</b>.pdf") && !html.contains("a")); assert!(html.contains(r#"