This commit is contained in:
nnduc
2026-10-24 14:03:52 +07:00
commit 4fe046de58
4790 changed files with 311146 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
//! 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)
}