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 into Memory
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: Mencari di disk
35    env.set_loader(|name| {
36        let path = format!("src/resources/views/{}", name);
37        if let Ok(content) = std::fs::read_to_string(&path) {
38            return Ok(Some(content));
39        }
40        Ok(None)
41    });
42
43    // --- REGISTER CARBON-LIKE FILTERS ---
44
45    // Filter: {{ date | diff_for_humans }}
46    env.add_filter("diff_for_humans", |value: String| -> String {
47        if let Ok(dt) = DateTime::parse_from_rfc3339(&value) {
48             let ht = HumanTime::from(dt);
49             return ht.to_string();
50        }
51        value
52    });
53
54    // Filter: {{ date | format_date("%d %b %Y") }}
55    env.add_filter("format_date", |value: String, fmt: String| -> String {
56        let cfg = Config::load();
57        let tz_str = cfg.app_timezone.trim();
58        let tz: Tz = tz_str.parse().unwrap_or(chrono_tz::UTC);
59        
60        if let Ok(dt) = DateTime::parse_from_rfc3339(&value) {
61             return dt.with_timezone(&tz).format(&fmt).to_string();
62        }
63        value
64    });
65
66    // Global Function: {{ now() }}
67    env.add_function("now", || -> String {
68        let cfg = Config::load();
69        let tz_str = cfg.app_timezone.trim();
70        let tz: Tz = tz_str.parse().unwrap_or(chrono_tz::UTC);
71        
72        chrono::Utc::now().with_timezone(&tz).to_rfc3339()
73    });
74
75    // Global Function: {{ htmx_js() }}
76    env.add_function("htmx_js", || -> String {
77        HTMX_SRC.clone()
78    });
79
80    // Global Function: {{ app_css() }}
81    env.add_function("app_css", || -> String {
82        CSS_SRC.clone()
83    });
84
85    env
86});
87
88// 3. Fungsi Helper untuk Render HTML Statis
89pub fn render(template: &str, context: minijinja::Value) -> Response {
90    render_internal(template, context)
91}
92
93pub fn render_to_string(template: &str, context: minijinja::Value) -> String {
94    match JINJA.get_template(template) {
95        Ok(tmpl) => tmpl.render(context).unwrap_or_else(|e| format!("Render error: {}", e)),
96        Err(e) => format!("Template error: {}", e),
97    }
98}
99
100// 4. Fungsi Helper untuk Render dengan Session
101pub fn view(req: &AppRequest, template: &str, ctx: minijinja::Value) -> Response {
102    let mut ctx_value = serde_json::to_value(&ctx).unwrap_or_else(|_| json!({}));
103    
104    if !ctx_value.is_object() {
105        ctx_value = json!({});
106    }
107    
108    let obj = ctx_value.as_object_mut().unwrap();
109
110    // Default keys for flash messages
111    if !obj.contains_key("errors") { obj.insert("errors".to_string(), json!({})); }
112    if !obj.contains_key("old") { obj.insert("old".to_string(), json!({})); }
113    if !obj.contains_key("flash_success") { obj.insert("flash_success".to_string(), json!("")); }
114    if !obj.contains_key("flash_error") { obj.insert("flash_error".to_string(), json!("")); }
115
116    if let Some(success) = req.session.get::<String>("flash_success") {
117        obj.insert("flash_success".to_string(), json!(success));
118        req.session.remove("flash_success");
119    }
120    if let Some(error) = req.session.get::<String>("flash_error") {
121        obj.insert("flash_error".to_string(), json!(error));
122        req.session.remove("flash_error");
123    }
124    if let Some(errors) = req.session.get::<Value>("errors") {
125        obj.insert("errors".to_string(), errors);
126        req.session.remove("errors");
127    }
128    if let Some(old) = req.session.get::<Value>("old_input") {
129        obj.insert("old".to_string(), old);
130    }
131
132    if let Some(token) = req.session.get::<String>("_token") {
133        obj.insert("csrf_token".to_string(), json!(token));
134    }
135
136    let is_logged_in = req.session.get::<i64>("user_id").is_some();
137    obj.insert("auth".to_string(), json!(is_logged_in));
138
139    render_internal(template, minijinja::Value::from_serialize(obj))
140}
141
142fn render_internal(template: &str, context: minijinja::Value) -> Response {
143    let cfg = crate::Config::load();
144    tracing::debug!("Rendering template: {} (APP_DEBUG: {})", template, cfg.app_debug);
145
146    match JINJA.get_template(template) {
147        Ok(tmpl) => match tmpl.render(context.clone()) {
148            Ok(rendered) => {
149                // --- LOGIKA MINIFIKASI ---
150                let re_comments = Regex::new(r"(?s)<!--.*?-->").unwrap();
151                let without_comments = re_comments.replace_all(&rendered, "");
152                
153                let minified = without_comments
154                    .lines()
155                    .map(|line| line.trim())
156                    .filter(|line| !line.is_empty())
157                    .collect::<Vec<_>>()
158                    .join(" ");
159                
160                Html(minified).into_response()
161            },
162            Err(err) => {
163                tracing::error!("Gagal render template: {}", err);
164                
165                if cfg.app_debug {
166                    return (StatusCode::INTERNAL_SERVER_ERROR, format!("Render Error: {}", err)).into_response();
167                }
168
169                (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response()
170            }
171        },
172        Err(err) => {
173            tracing::error!("Template tidak ditemukan: {}", err);
174
175            if cfg.app_debug {
176                return (StatusCode::NOT_FOUND, format!("Template Not Found: {}", err)).into_response();
177            }
178
179            (StatusCode::NOT_FOUND, "Not Found").into_response()
180        }
181    }
182}