memscope_rs/export/binary/
template_resource_manager.rs

1//! Template resource management for binary to HTML conversion
2//!
3//! This module provides comprehensive resource management for HTML templates,
4//! including CSS/JS embedding, shared resource loading, and placeholder processing.
5
6use crate::export::binary::error::BinaryExportError;
7use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11/// Template resource manager for handling CSS/JS resources and placeholders
12pub struct TemplateResourceManager {
13    /// Base template directory path
14    template_dir: PathBuf,
15    /// Cached CSS content
16    css_cache: HashMap<String, String>,
17    /// Cached JS content
18    js_cache: HashMap<String, String>,
19    /// SVG images cache
20    svg_cache: HashMap<String, String>,
21    /// Placeholder processors
22    placeholder_processors: HashMap<String, Box<dyn PlaceholderProcessor>>,
23}
24
25/// Trait for processing template placeholders
26pub trait PlaceholderProcessor: Send + Sync {
27    /// Process a placeholder with given data
28    fn process(&self, data: &TemplateData) -> Result<String, BinaryExportError>;
29}
30
31/// Template data structure for placeholder processing
32#[derive(Debug, Clone)]
33pub struct TemplateData {
34    /// Project name
35    pub project_name: String,
36    /// Binary analysis data (JSON string)
37    pub binary_data: String,
38    /// Generation timestamp
39    pub generation_time: String,
40    /// CSS content
41    pub css_content: String,
42    /// JavaScript content
43    pub js_content: String,
44    /// SVG images content
45    pub svg_images: String,
46    /// Additional custom data
47    pub custom_data: HashMap<String, String>,
48}
49
50/// Resource embedding configuration
51#[derive(Debug, Clone)]
52pub struct ResourceConfig {
53    /// Whether to embed CSS inline
54    pub embed_css: bool,
55    /// Whether to embed JS inline
56    pub embed_js: bool,
57    /// Whether to embed SVG images
58    pub embed_svg: bool,
59    /// Whether to minify resources
60    pub minify_resources: bool,
61    /// Custom resource paths
62    pub custom_paths: HashMap<String, PathBuf>,
63}
64
65impl Default for ResourceConfig {
66    fn default() -> Self {
67        Self {
68            embed_css: true,
69            embed_js: true,
70            embed_svg: true,
71            minify_resources: false,
72            custom_paths: HashMap::new(),
73        }
74    }
75}
76
77impl TemplateResourceManager {
78    /// Create a new template resource manager
79    pub fn new<P: AsRef<Path>>(template_dir: P) -> Result<Self, BinaryExportError> {
80        let template_dir = template_dir.as_ref().to_path_buf();
81
82        if !template_dir.exists() {
83            return Err(BinaryExportError::CorruptedData(format!(
84                "Template directory does not exist: {}",
85                template_dir.display()
86            )));
87        }
88
89        let mut manager = Self {
90            template_dir,
91            css_cache: HashMap::new(),
92            js_cache: HashMap::new(),
93            svg_cache: HashMap::new(),
94            placeholder_processors: HashMap::new(),
95        };
96
97        // Register default placeholder processors
98        manager.register_default_processors();
99
100        Ok(manager)
101    }
102
103    /// Load and process a template with resources
104    pub fn process_template(
105        &mut self,
106        template_name: &str,
107        data: &TemplateData,
108        config: &ResourceConfig,
109    ) -> Result<String, BinaryExportError> {
110        // Load template content
111        let template_path = self.template_dir.join(template_name);
112        let mut template_content =
113            fs::read_to_string(&template_path).map_err(BinaryExportError::Io)?;
114
115        // Load and embed resources
116        if config.embed_css {
117            let css_content = if !data.css_content.is_empty() {
118                data.css_content.clone()
119            } else {
120                self.load_css_resources(config)?
121            };
122            template_content = template_content.replace("{{CSS_CONTENT}}", &css_content);
123        }
124
125        if config.embed_js {
126            let js_content = if !data.js_content.is_empty() {
127                data.js_content.clone()
128            } else {
129                self.load_js_resources(config)?
130            };
131            template_content = template_content.replace("{{JS_CONTENT}}", &js_content);
132        }
133
134        if config.embed_svg {
135            let svg_content = self.load_svg_resources(config)?;
136            template_content = template_content.replace("{{SVG_IMAGES}}", &svg_content);
137        }
138
139        // Process all placeholders
140        template_content = self.process_placeholders(template_content, data)?;
141
142        Ok(template_content)
143    }
144
145    /// Load CSS resources from templates directory
146    fn load_css_resources(&mut self, config: &ResourceConfig) -> Result<String, BinaryExportError> {
147        let css_files = vec!["styles.css"];
148        let mut combined_css = String::new();
149
150        for css_file in css_files {
151            if let Some(cached) = self.css_cache.get(css_file) {
152                combined_css.push_str(cached);
153                combined_css.push('\n');
154                continue;
155            }
156
157            let css_path = self.template_dir.join(css_file);
158            if css_path.exists() {
159                let css_content = fs::read_to_string(&css_path).map_err(BinaryExportError::Io)?;
160
161                let processed_css = if config.minify_resources {
162                    self.minify_css(&css_content)
163                } else {
164                    css_content
165                };
166
167                self.css_cache
168                    .insert(css_file.to_string(), processed_css.clone());
169                combined_css.push_str(&processed_css);
170                combined_css.push('\n');
171            }
172        }
173
174        Ok(combined_css)
175    }
176
177    /// Load JavaScript resources from templates directory
178    fn load_js_resources(&mut self, config: &ResourceConfig) -> Result<String, BinaryExportError> {
179        let js_files = vec!["script.js"];
180        let mut combined_js = String::new();
181
182        for js_file in js_files {
183            if let Some(cached) = self.js_cache.get(js_file) {
184                combined_js.push_str(cached);
185                combined_js.push('\n');
186                continue;
187            }
188
189            let js_path = self.template_dir.join(js_file);
190            if js_path.exists() {
191                let js_content = fs::read_to_string(&js_path).map_err(BinaryExportError::Io)?;
192
193                let processed_js = if config.minify_resources {
194                    self.minify_js(&js_content)
195                } else {
196                    js_content
197                };
198
199                self.js_cache
200                    .insert(js_file.to_string(), processed_js.clone());
201                combined_js.push_str(&processed_js);
202                combined_js.push('\n');
203            }
204        }
205
206        Ok(combined_js)
207    }
208
209    /// Load SVG resources from templates directory
210    fn load_svg_resources(
211        &mut self,
212        _config: &ResourceConfig,
213    ) -> Result<String, BinaryExportError> {
214        // For now, return empty string as SVG embedding is not implemented
215        // In a real implementation, this would scan for SVG files and embed them
216        Ok(String::new())
217    }
218
219    /// Process all placeholders in template content
220    fn process_placeholders(
221        &self,
222        mut content: String,
223        data: &TemplateData,
224    ) -> Result<String, BinaryExportError> {
225        // Process standard placeholders (handle both with and without spaces)
226        content = content.replace("{{PROJECT_NAME}}", &data.project_name);
227        content = content.replace("{{ PROJECT_NAME }}", &data.project_name);
228        content = content.replace("{{BINARY_DATA}}", &data.binary_data);
229        content = content.replace("{{ BINARY_DATA }}", &data.binary_data);
230        content = content.replace("{{json_data}}", &data.binary_data);
231        content = content.replace("{{ json_data }}", &data.binary_data);
232
233        // Fix the specific template issue - inject data as window.analysisData assignment
234        // Handle all possible variations of the template placeholder
235        content = content.replace(
236            "window.analysisData = {{ json_data }};",
237            &format!("window.analysisData = {};", &data.binary_data),
238        );
239        content = content.replace(
240            "window.analysisData = {{json_data}};",
241            &format!("window.analysisData = {};", &data.binary_data),
242        );
243        content = content.replace(
244            "window.analysisData = {{ json_data}};",
245            &format!("window.analysisData = {};", &data.binary_data),
246        );
247        content = content.replace(
248            "window.analysisData = {{json_data }};",
249            &format!("window.analysisData = {};", &data.binary_data),
250        );
251
252        // Also handle cases where there might be line breaks or spaces
253        content = content.replace(
254            "window.analysisData = {{ json_data",
255            &format!("window.analysisData = {}", &data.binary_data),
256        );
257        content = content.replace(
258            "window.analysisData = {{json_data",
259            &format!("window.analysisData = {}", &data.binary_data),
260        );
261        content = content.replace("{{GENERATION_TIME}}", &data.generation_time);
262        content = content.replace("{{ GENERATION_TIME }}", &data.generation_time);
263
264        // Process additional placeholders from custom data
265        for (key, value) in &data.custom_data {
266            let placeholder_with_spaces = format!("{{{{ {key} }}}}");
267            let placeholder_without_spaces = format!("{{{{{key}}}}}");
268            content = content.replace(&placeholder_with_spaces, value);
269            content = content.replace(&placeholder_without_spaces, value);
270        }
271
272        // Handle common placeholders that might be in custom data
273        if let Some(processing_time) = data.custom_data.get("PROCESSING_TIME") {
274            content = content.replace("{{PROCESSING_TIME}}", processing_time);
275            content = content.replace("{{ PROCESSING_TIME }}", processing_time);
276        }
277
278        if let Some(svg_images) = data.custom_data.get("SVG_IMAGES") {
279            content = content.replace("{{SVG_IMAGES}}", svg_images);
280            content = content.replace("{{ SVG_IMAGES }}", svg_images);
281        }
282
283        // Process custom placeholders using registered processors
284        for (placeholder, processor) in &self.placeholder_processors {
285            let placeholder_pattern = format!("{{{{{placeholder}}}}}");
286            if content.contains(&placeholder_pattern) {
287                let processed_value = processor.process(data)?;
288                content = content.replace(&placeholder_pattern, &processed_value);
289            }
290        }
291
292        Ok(content)
293    }
294
295    /// Register default placeholder processors
296    fn register_default_processors(&mut self) {
297        self.placeholder_processors
298            .insert("MEMORY_DATA".to_string(), Box::new(MemoryDataProcessor));
299        self.placeholder_processors.insert(
300            "COMPLEX_TYPES_DATA".to_string(),
301            Box::new(ComplexTypesProcessor),
302        );
303        self.placeholder_processors
304            .insert("FFI_SAFETY_DATA".to_string(), Box::new(FfiSafetyProcessor));
305        self.placeholder_processors.insert(
306            "RELATIONSHIP_DATA".to_string(),
307            Box::new(RelationshipProcessor),
308        );
309    }
310
311    /// Register a custom placeholder processor
312    pub fn register_processor(
313        &mut self,
314        placeholder: String,
315        processor: Box<dyn PlaceholderProcessor>,
316    ) {
317        self.placeholder_processors.insert(placeholder, processor);
318    }
319
320    /// Simple CSS minification (removes comments and extra whitespace)
321    fn minify_css(&self, css: &str) -> String {
322        css.lines()
323            .map(|line| line.trim())
324            .filter(|line| !line.is_empty() && !line.starts_with("/*"))
325            .collect::<Vec<_>>()
326            .join(" ")
327            .replace("  ", " ")
328    }
329
330    /// Simple JavaScript minification (removes comments and extra whitespace)
331    fn minify_js(&self, js: &str) -> String {
332        js.lines()
333            .map(|line| line.trim())
334            .filter(|line| !line.is_empty() && !line.starts_with("//"))
335            .collect::<Vec<_>>()
336            .join(" ")
337            .replace("  ", " ")
338    }
339
340    /// Get shared resource content for external use
341    pub fn get_shared_css(&mut self, config: &ResourceConfig) -> Result<String, BinaryExportError> {
342        self.load_css_resources(config)
343    }
344
345    /// Get shared JavaScript content for external use
346    pub fn get_shared_js(&mut self, config: &ResourceConfig) -> Result<String, BinaryExportError> {
347        self.load_js_resources(config)
348    }
349
350    /// Clear resource caches
351    pub fn clear_cache(&mut self) {
352        self.css_cache.clear();
353        self.js_cache.clear();
354        self.svg_cache.clear();
355    }
356}
357
358/// Memory data placeholder processor
359struct MemoryDataProcessor;
360
361impl PlaceholderProcessor for MemoryDataProcessor {
362    fn process(&self, data: &TemplateData) -> Result<String, BinaryExportError> {
363        // Extract memory-specific data from binary data
364        // This is a simplified implementation
365        Ok(data.binary_data.clone())
366    }
367}
368
369/// Complex types data placeholder processor
370struct ComplexTypesProcessor;
371
372impl PlaceholderProcessor for ComplexTypesProcessor {
373    fn process(&self, data: &TemplateData) -> Result<String, BinaryExportError> {
374        // Extract complex types data from binary data
375        // This would parse the JSON and extract complex_types section
376        if let Some(complex_types_data) = data.custom_data.get("complex_types") {
377            Ok(complex_types_data.clone())
378        } else {
379            Ok("{}".to_string())
380        }
381    }
382}
383
384/// FFI safety data placeholder processor
385struct FfiSafetyProcessor;
386
387impl PlaceholderProcessor for FfiSafetyProcessor {
388    fn process(&self, data: &TemplateData) -> Result<String, BinaryExportError> {
389        // Extract FFI safety data from binary data
390        if let Some(ffi_data) = data.custom_data.get("unsafe_ffi") {
391            Ok(ffi_data.clone())
392        } else {
393            Ok("{}".to_string())
394        }
395    }
396}
397
398/// Variable relationship data placeholder processor
399struct RelationshipProcessor;
400
401impl PlaceholderProcessor for RelationshipProcessor {
402    fn process(&self, data: &TemplateData) -> Result<String, BinaryExportError> {
403        // Extract relationship data from binary data
404        if let Some(relationship_data) = data.custom_data.get("variable_relationships") {
405            Ok(relationship_data.clone())
406        } else {
407            Ok("{}".to_string())
408        }
409    }
410}
411
412/// Utility function to create template data from binary analysis results
413pub fn create_template_data(
414    project_name: &str,
415    binary_data_json: &str,
416    custom_data: HashMap<String, String>,
417) -> TemplateData {
418    let generation_time = chrono::Utc::now()
419        .format("%Y-%m-%d %H:%M:%S UTC")
420        .to_string();
421
422    TemplateData {
423        project_name: project_name.to_string(),
424        binary_data: binary_data_json.to_string(),
425        generation_time,
426        css_content: String::new(),
427        js_content: String::new(),
428        svg_images: String::new(),
429        custom_data,
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436    use std::fs;
437    use tempfile::TempDir;
438
439    fn create_test_template_dir() -> Result<TempDir, std::io::Error> {
440        let temp_dir = TempDir::new()?;
441
442        // Create test template file
443        let template_content = r#"
444<!DOCTYPE html>
445<html>
446<head>
447    <title>{{PROJECT_NAME}}</title>
448    <style>{{CSS_CONTENT}}</style>
449</head>
450<body>
451    <div id="data">{{BINARY_DATA}}</div>
452    <script>{{JS_CONTENT}}</script>
453</body>
454</html>
455"#;
456        fs::write(temp_dir.path().join("test_template.html"), template_content)?;
457
458        // Create test CSS file
459        let css_content = "body { margin: 0; padding: 0; }";
460        fs::write(temp_dir.path().join("styles.css"), css_content)?;
461
462        // Create test JS file
463        let js_content = "console.log('Test script loaded');";
464        fs::write(temp_dir.path().join("script.js"), js_content)?;
465
466        Ok(temp_dir)
467    }
468
469    #[test]
470    fn test_template_resource_manager_creation() {
471        let temp_dir = create_test_template_dir().expect("Failed to get test value");
472        let manager = TemplateResourceManager::new(temp_dir.path());
473        assert!(manager.is_ok());
474    }
475
476    #[test]
477    fn test_template_processing() {
478        let temp_dir = create_test_template_dir().expect("Failed to get test value");
479        let mut manager =
480            TemplateResourceManager::new(temp_dir.path()).expect("Test operation failed");
481
482        let template_data = TemplateData {
483            project_name: "Test Project".to_string(),
484            binary_data: r#"{"test": "data"}"#.to_string(),
485            generation_time: "2024-01-01 12:00:00 UTC".to_string(),
486            css_content: String::new(),
487            js_content: String::new(),
488            svg_images: String::new(),
489            custom_data: HashMap::new(),
490        };
491
492        let config = ResourceConfig::default();
493        let result = manager.process_template("test_template.html", &template_data, &config);
494
495        assert!(result.is_ok());
496        let processed = result.expect("Test operation failed");
497        assert!(processed.contains("Test Project"));
498        assert!(processed.contains(r#"{"test": "data"}"#));
499        assert!(processed.contains("body { margin: 0; padding: 0; }"));
500        assert!(processed.contains("console.log('Test script loaded');"));
501    }
502
503    #[test]
504    fn test_css_loading() {
505        let temp_dir = create_test_template_dir().expect("Failed to get test value");
506        let mut manager =
507            TemplateResourceManager::new(temp_dir.path()).expect("Test operation failed");
508        let config = ResourceConfig::default();
509
510        let css_content = manager
511            .get_shared_css(&config)
512            .expect("Test operation failed");
513        assert!(css_content.contains("body { margin: 0; padding: 0; }"));
514    }
515
516    #[test]
517    fn test_js_loading() {
518        let temp_dir = create_test_template_dir().expect("Failed to get test value");
519        let mut manager =
520            TemplateResourceManager::new(temp_dir.path()).expect("Test operation failed");
521        let config = ResourceConfig::default();
522
523        let js_content = manager
524            .get_shared_js(&config)
525            .expect("Test operation failed");
526        assert!(js_content.contains("console.log('Test script loaded');"));
527    }
528
529    #[test]
530    fn test_css_minification() {
531        let temp_dir = create_test_template_dir().expect("Failed to get test value");
532        let _manager =
533            TemplateResourceManager::new(temp_dir.path()).expect("Test operation failed");
534
535        let css = "body {\n    margin: 0;\n    padding: 0;\n}";
536        let minified = _manager.minify_css(css);
537        assert!(!minified.contains('\n'));
538        assert!(minified.len() < css.len());
539    }
540
541    #[test]
542    fn test_placeholder_processors() {
543        let temp_dir = create_test_template_dir().expect("Failed to get test value");
544        let _manager =
545            TemplateResourceManager::new(temp_dir.path()).expect("Test operation failed");
546
547        let mut custom_data = HashMap::new();
548        custom_data.insert("complex_types".to_string(), r#"{"types": []}"#.to_string());
549
550        let template_data = TemplateData {
551            project_name: "Test".to_string(),
552            binary_data: "{}".to_string(),
553            generation_time: "2024-01-01".to_string(),
554            css_content: String::new(),
555            js_content: String::new(),
556            svg_images: String::new(),
557            custom_data,
558        };
559
560        let processor = ComplexTypesProcessor;
561        let result = processor
562            .process(&template_data)
563            .expect("Test operation failed");
564        assert_eq!(result, r#"{"types": []}"#);
565    }
566
567    #[test]
568    fn test_cache_functionality() {
569        let temp_dir = create_test_template_dir().expect("Failed to get test value");
570        let mut manager =
571            TemplateResourceManager::new(temp_dir.path()).expect("Test operation failed");
572        let config = ResourceConfig::default();
573
574        // First load should read from file
575        let css1 = manager
576            .get_shared_css(&config)
577            .expect("Test operation failed");
578
579        // Second load should use cache
580        let css2 = manager
581            .get_shared_css(&config)
582            .expect("Test operation failed");
583
584        assert_eq!(css1, css2);
585        assert!(!manager.css_cache.is_empty());
586
587        // Clear cache
588        manager.clear_cache();
589        assert!(manager.css_cache.is_empty());
590    }
591
592    #[test]
593    fn test_template_data_creation() {
594        let mut custom_data = HashMap::new();
595        custom_data.insert("test_key".to_string(), "test_value".to_string());
596
597        let template_data = create_template_data("My Project", r#"{"data": "test"}"#, custom_data);
598
599        assert_eq!(template_data.project_name, "My Project");
600        assert_eq!(template_data.binary_data, r#"{"data": "test"}"#);
601        assert!(template_data.generation_time.contains("UTC"));
602        assert_eq!(
603            template_data.custom_data.get("test_key"),
604            Some(&"test_value".to_string())
605        );
606    }
607}