1use crate::export::binary::error::BinaryExportError;
7use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11pub struct TemplateResourceManager {
13 template_dir: PathBuf,
15 css_cache: HashMap<String, String>,
17 js_cache: HashMap<String, String>,
19 svg_cache: HashMap<String, String>,
21 placeholder_processors: HashMap<String, Box<dyn PlaceholderProcessor>>,
23}
24
25pub trait PlaceholderProcessor: Send + Sync {
27 fn process(&self, data: &TemplateData) -> Result<String, BinaryExportError>;
29}
30
31#[derive(Debug, Clone)]
33pub struct TemplateData {
34 pub project_name: String,
36 pub binary_data: String,
38 pub generation_time: String,
40 pub css_content: String,
42 pub js_content: String,
44 pub svg_images: String,
46 pub custom_data: HashMap<String, String>,
48}
49
50#[derive(Debug, Clone)]
52pub struct ResourceConfig {
53 pub embed_css: bool,
55 pub embed_js: bool,
57 pub embed_svg: bool,
59 pub minify_resources: bool,
61 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 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 manager.register_default_processors();
99
100 Ok(manager)
101 }
102
103 pub fn process_template(
105 &mut self,
106 template_name: &str,
107 data: &TemplateData,
108 config: &ResourceConfig,
109 ) -> Result<String, BinaryExportError> {
110 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 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 template_content = self.process_placeholders(template_content, data)?;
141
142 Ok(template_content)
143 }
144
145 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 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 fn load_svg_resources(
211 &mut self,
212 _config: &ResourceConfig,
213 ) -> Result<String, BinaryExportError> {
214 Ok(String::new())
217 }
218
219 fn process_placeholders(
221 &self,
222 mut content: String,
223 data: &TemplateData,
224 ) -> Result<String, BinaryExportError> {
225 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 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 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 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 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 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 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 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 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 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 pub fn get_shared_css(&mut self, config: &ResourceConfig) -> Result<String, BinaryExportError> {
342 self.load_css_resources(config)
343 }
344
345 pub fn get_shared_js(&mut self, config: &ResourceConfig) -> Result<String, BinaryExportError> {
347 self.load_js_resources(config)
348 }
349
350 pub fn clear_cache(&mut self) {
352 self.css_cache.clear();
353 self.js_cache.clear();
354 self.svg_cache.clear();
355 }
356}
357
358struct MemoryDataProcessor;
360
361impl PlaceholderProcessor for MemoryDataProcessor {
362 fn process(&self, data: &TemplateData) -> Result<String, BinaryExportError> {
363 Ok(data.binary_data.clone())
366 }
367}
368
369struct ComplexTypesProcessor;
371
372impl PlaceholderProcessor for ComplexTypesProcessor {
373 fn process(&self, data: &TemplateData) -> Result<String, BinaryExportError> {
374 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
384struct FfiSafetyProcessor;
386
387impl PlaceholderProcessor for FfiSafetyProcessor {
388 fn process(&self, data: &TemplateData) -> Result<String, BinaryExportError> {
389 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
398struct RelationshipProcessor;
400
401impl PlaceholderProcessor for RelationshipProcessor {
402 fn process(&self, data: &TemplateData) -> Result<String, BinaryExportError> {
403 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
412pub 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 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 let css_content = "body { margin: 0; padding: 0; }";
460 fs::write(temp_dir.path().join("styles.css"), css_content)?;
461
462 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 let css1 = manager
576 .get_shared_css(&config)
577 .expect("Test operation failed");
578
579 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 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}