120 lines
5.0 KiB
Rust
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, ¶ms)
|
|
}
|
|
|
|
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("<b>a</b>.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ẻ"));
|
|
}
|
|
}
|