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