Skip to main content

arc_web/helpers/
template.rs

1use once_cell::sync::Lazy;
2use std::collections::HashMap;
3use std::fs;
4use std::io::Error;
5use tera::{Context, Tera};
6use tracing::error;
7
8/// Cached Tera instance - compiled once at startup
9static TEMPLATES: Lazy<Tera> = Lazy::new(|| {
10    // Try workspace path first, fallback to direct path
11    let patterns = vec![
12        "crates/arc-app/src/resources/views/**/*", // Workspace structure
13        "src/resources/views/**/*",                // Direct run
14    ];
15
16    for pattern in patterns {
17        // A glob matching no files still returns Ok with an empty Tera, so only
18        // accept a pattern that actually loaded at least one template.
19        match Tera::new(pattern) {
20            Ok(t) if t.get_template_names().next().is_some() => return t,
21            _ => continue,
22        }
23    }
24
25    error!("Fatal error: Could not find templates in any expected location");
26    ::std::process::exit(1);
27});
28
29/// Cached manifest assets - parsed once at startup
30static MANIFEST_ASSETS: Lazy<HashMap<String, String>> = Lazy::new(parse_manifest_assets);
31
32/// Renders a Tera template with the given parameters and optional asset list.
33/// Automatically injects `session_message` (empty if not provided) and `assets`.
34pub fn load_template(
35    template: &str,
36    params: Vec<(&str, &str)>,
37    assets: Option<Vec<&str>>,
38) -> String {
39    let mut context: Context = Context::new();
40    for (key, value) in params.into_iter() {
41        context.insert(key, value);
42    }
43
44    if !context.contains_key("session_message") {
45        context.insert("session_message", "");
46    }
47
48    context.insert("assets", &get_assets_string(assets));
49
50    TEMPLATES
51        .render(template, &context)
52        .expect("Failed to render template")
53}
54
55/// Returns the HTML string to add the assets to the template.
56/// If the assets are passed, we only add the assets passed, otherwise we add all the assets from
57/// the manifest.json file.
58fn get_assets_string(assets: Option<Vec<&str>>) -> String {
59    let mut assets_string: String = String::new();
60    if let Some(assets) = assets {
61        for value in assets.into_iter() {
62            let asset_type = value.split('.').next_back().unwrap();
63            if let Some(asset) = MANIFEST_ASSETS.get(value) {
64                if asset_type == "css" {
65                    assets_string.push_str(&format!(
66                        "<link rel=\"stylesheet\" href=\"/public/{}\">",
67                        asset
68                    ));
69                } else if asset_type == "js" {
70                    assets_string.push_str(&format!(
71                        "<script src=\"/public/{}\" defer></script>",
72                        asset
73                    ));
74                }
75            }
76        }
77    } else {
78        for value in MANIFEST_ASSETS.values() {
79            let asset_type = value.split('.').next_back().unwrap();
80            if asset_type == "css" {
81                assets_string.push_str(&format!(
82                    "<link rel=\"stylesheet\" href=\"/public/{}\">",
83                    value
84                ));
85            } else if asset_type == "js" {
86                assets_string.push_str(&format!(
87                    "<script src=\"/public/{}\" defer></script>",
88                    value
89                ));
90            }
91        }
92    }
93
94    assets_string
95}
96
97/// Parse the assets from the manifest.json file (called once at startup)
98fn parse_manifest_assets() -> HashMap<String, String> {
99    let mut assets: HashMap<String, String> = HashMap::new();
100    let manifest: Result<String, Error> = fs::read_to_string("dist/.vite/manifest.json");
101    if let Ok(manifest) = manifest {
102        let manifest_json: serde_json::Value =
103            serde_json::from_str(&manifest).expect("Failed to parse manifest.json");
104
105        for (key, value) in manifest_json.as_object().unwrap().iter() {
106            if let Some(asset) = value.get("file") {
107                assets.insert(key.to_string(), asset.as_str().unwrap().parse().unwrap());
108
109                // If the asset is a js file, we might add css files to the assets.
110                let asset_type = asset.as_str().unwrap().split('.').next_back().unwrap();
111                if asset_type == "js" {
112                    if let Some(css_array) = value.get("css").and_then(|v| v.as_array()) {
113                        for css_file in css_array {
114                            let css_file_name =
115                                css_file.as_str().unwrap().split('/').next_back().unwrap();
116                            assets.insert(css_file_name.to_string(), css_file_name.to_string());
117                        }
118                    }
119                }
120            }
121        }
122    }
123
124    assets
125}