Skip to main content

rustbasic_core/
view.rs

1/* ---------------------------------------------------------
2 * 📑 LABEL: VIEW ENGINE (config/view.rs)
3 * Mengatur template engine (Minijinja) dan fungsi render.
4 * --------------------------------------------------------- */
5
6use axum::{
7    http::StatusCode,
8    response::{Html, IntoResponse, Response},
9};
10use minijinja::Environment;
11use chrono::DateTime;
12use chrono_humanize::HumanTime;
13use chrono_tz::Tz;
14use std::sync::LazyLock;
15use crate::requests::Request as AppRequest;
16use crate::Config;
17use serde_json::{json, Value};
18use regex::Regex;
19
20// 1. Load Static Assets from Internal Resources (Embedded in Library)
21static HTMX_SRC: LazyLock<String> = LazyLock::new(|| {
22    include_str!("../resources/js/htmx.min.js").to_string()
23});
24
25static CSS_SRC: LazyLock<String> = LazyLock::new(|| {
26    include_str!("../resources/css/style.css").to_string()
27});
28
29
30// 2. Setup Engine Template (Minijinja)
31pub static JINJA: LazyLock<Environment<'static>> = LazyLock::new(|| {
32    let mut env = Environment::new();
33    
34    // Default Loader: Hanya mencari di disk (untuk development)
35    // Untuk production, app harus menambahkan template secara manual ke JINJA
36    env.set_loader(|name| {
37        let path = format!("src/resources/views/{}", name);
38        if let Ok(content) = std::fs::read_to_string(&path) {
39            return Ok(Some(content));
40        }
41        Ok(None)
42    });
43
44    // --- REGISTER CARBON-LIKE FILTERS ---
45    env.add_filter("diff_for_humans", |value: String| -> String {
46        if let Ok(dt) = DateTime::parse_from_rfc3339(&value) {
47             let ht = HumanTime::from(dt);
48             return ht.to_string();
49        }
50        value
51    });
52
53    env.add_filter("format_date", |value: String, fmt: String| -> String {
54        let cfg = Config::load();
55        let tz_str = cfg.app_timezone.trim();
56        let tz: Tz = tz_str.parse().unwrap_or(chrono_tz::UTC);
57        
58        if let Ok(dt) = DateTime::parse_from_rfc3339(&value) {
59             return dt.with_timezone(&tz).format(&fmt).to_string();
60        }
61        value
62    });
63
64    env.add_function("now", || -> String {
65        let cfg = Config::load();
66        let tz_str = cfg.app_timezone.trim();
67        let tz: Tz = tz_str.parse().unwrap_or(chrono_tz::UTC);
68        
69        chrono::Utc::now().with_timezone(&tz).to_rfc3339()
70    });
71
72    env.add_function("htmx_js", || -> String {
73        HTMX_SRC.clone()
74    });
75
76    env.add_function("app_css", || -> String {
77        CSS_SRC.clone()
78    });
79
80    env
81});
82
83// 3. Fungsi Helper untuk Render HTML
84pub fn render(template: &str, context: minijinja::Value) -> Response {
85    render_internal(template, context)
86}
87
88pub fn render_to_string(template: &str, context: minijinja::Value) -> String {
89    match JINJA.get_template(template) {
90        Ok(tmpl) => tmpl.render(context).unwrap_or_else(|e| format!("Render error: {}", e)),
91        Err(e) => format!("Template error: {}", e),
92    }
93}
94
95// 4. Fungsi Helper untuk Render dengan Session
96pub fn view(req: &AppRequest, template: &str, ctx: minijinja::Value) -> Response {
97    let mut ctx_value = serde_json::to_value(&ctx).unwrap_or_else(|_| json!({}));
98    
99    if !ctx_value.is_object() {
100        ctx_value = json!({});
101    }
102    
103    let obj = ctx_value.as_object_mut().unwrap();
104
105    // Default keys for flash messages
106    if !obj.contains_key("errors") { obj.insert("errors".to_string(), json!({})); }
107    if !obj.contains_key("old") { obj.insert("old".to_string(), json!({})); }
108    if !obj.contains_key("flash_success") { obj.insert("flash_success".to_string(), json!("")); }
109    if !obj.contains_key("flash_error") { obj.insert("flash_error".to_string(), json!("")); }
110
111    if let Some(success) = req.session.get::<String>("flash_success") {
112        obj.insert("flash_success".to_string(), json!(success));
113        req.session.remove("flash_success");
114    }
115    if let Some(error) = req.session.get::<String>("flash_error") {
116        obj.insert("flash_error".to_string(), json!(error));
117        req.session.remove("flash_error");
118    }
119    if let Some(errors) = req.session.get::<Value>("errors") {
120        obj.insert("errors".to_string(), errors);
121        req.session.remove("errors");
122    }
123    if let Some(old) = req.session.get::<Value>("old_input") {
124        obj.insert("old".to_string(), old);
125    }
126
127    if let Some(token) = req.session.get::<String>("_token") {
128        obj.insert("csrf_token".to_string(), json!(token));
129    }
130
131    let is_logged_in = req.session.get::<i64>("user_id").is_some();
132    obj.insert("auth".to_string(), json!(is_logged_in));
133
134    render_internal(template, minijinja::Value::from_serialize(obj))
135}
136
137fn render_internal(template: &str, context: minijinja::Value) -> Response {
138    
139    match JINJA.get_template(template) {
140        Ok(tmpl) => match tmpl.render(context.clone()) {
141            Ok(rendered) => {
142                let re_comments = Regex::new(r"(?s)<!--.*?-->").unwrap();
143                let without_comments = re_comments.replace_all(&rendered, "");
144                
145                let minified = without_comments
146                    .lines()
147                    .map(|line| line.trim())
148                    .filter(|line| !line.is_empty())
149                    .collect::<Vec<_>>()
150                    .join(" ");
151                
152                Html(minified).into_response()
153            },
154            Err(err) => {
155                tracing::error!("Gagal render template: {}", err);
156                (StatusCode::INTERNAL_SERVER_ERROR, format!("Render Error: {}", err)).into_response()
157            }
158        },
159        Err(err) => {
160            tracing::error!("Template tidak ditemukan: {}", err);
161            (StatusCode::NOT_FOUND, format!("Template Not Found: {}", err)).into_response()
162        }
163    }
164}