Files
movim-rs/src/tpl.rs
T
2026-10-24 17:46:20 +07:00

120 lines
5.0 KiB
Rust

//! 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<String>| 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<String>| utils::mime_to_icon(s.as_deref().unwrap_or("")).to_string());
env.add_filter("mimeToLabel", |s: Option<String>| utils::mime_to_label(s.as_deref().unwrap_or("")));
env.add_filter("implodeCsv", |v: Vec<String>| 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<Value>) -> String {
let lang = state
.lookup("lang")
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_else(|| "en".into());
let args: Vec<String> = 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<Value>) -> String {
let base = state
.lookup("base_uri")
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_else(|| "/".into());
let params: Vec<String> = 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, &params)
}
fn add_emojis(state: &State, s: String, no_title: Option<bool>) -> 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": "<b>a</b>.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("&lt;b&gt;a&lt;/b&gt;.pdf") && !html.contains("<b>a</b>"));
assert!(html.contains(r#"<audio controls preload="none" src="https://up.example.org/a.pdf""#));
let html = tpl.render(data("vi")).unwrap();
assert!(html.contains("Ghi âm") && html.contains("Tệp đã chia sẻ"));
}
}