use std::collections::HashMap; use just_fmt::snake_case; use crate::template::Template; const DISPLAY_BLOCK_BEGIN: &str = "??? >>> "; const DISPLAY_BLOCK_END: &str = "??? <<<"; const IMPL_AREA_BEGIN: &str = "@@@ >>> "; const IMPL_AREA_END: &str = "@@@ <<<"; const IMPL_BEGIN: &str = ">>>>>>>>>>"; const PARAM_BEGIN: &str = "<<<"; const PARAM_BEND: &str = ">>>"; impl Template { pub fn expand(mut self) -> Option { // Extract template text let expanded = std::mem::take(&mut self.template_str); let (expanded, impl_areas) = read_impl_areas(expanded)?; let expanded = apply_impls(&self, expanded, impl_areas)?; let expanded = apply_display_blocks(&self.params, expanded); let expanded = apply_param(&self, expanded)?; Some(expanded.trim().to_string()) } } /// Read all ImplAreas (HashMap) fn read_impl_areas(content: String) -> Option<(String, HashMap)> { let mut striped_content = String::new(); let mut impl_areas: HashMap = HashMap::new(); let mut current_area_name = String::default(); let mut current_area_codes: Vec = Vec::new(); for line in content.split("\n") { let trimmed_line = line.trim(); // Implementation block end if trimmed_line.starts_with(IMPL_AREA_END) { // If the current ImplArea name length is less than 1, it means no block is being matched, // so matching fails, exit early if current_area_name.is_empty() { return None; } // Submit Impl Area let name = std::mem::take(&mut current_area_name); impl_areas.insert(name, current_area_codes.join("\n")); current_area_codes.clear(); continue; } // Implementation block start if trimmed_line.starts_with(IMPL_AREA_BEGIN) { // If the current ImplArea name length is greater than 0, it means we are already inside a block, // since nesting is not allowed, matching fails, exit early if !current_area_name.is_empty() { return None; } // Get a snake_case name let snake_name = snake_case!(line.trim_start_matches(IMPL_AREA_BEGIN).trim()); current_area_name = snake_name; // Continue to next line continue; } // During implementation block if !current_area_name.is_empty() { // Add to current block code current_area_codes.push(line.to_string()); continue; } else { // Add to remaining content striped_content += "\n"; striped_content += line; } } Some((striped_content, impl_areas)) } /// Apply Template parameters to implementation block areas fn apply_impls( template: &Template, content: String, impl_areas: HashMap, ) -> Option { let mut applied_content = String::new(); let mut impled_areas: HashMap> = HashMap::new(); for (impl_area_name, impl_area_template) in impl_areas { // Get user-provided parameters let impl_items = template.impl_params.get(&impl_area_name); // No parameters, return early let Some(impl_items) = impl_items else { impled_areas.insert(impl_area_name, Vec::new()); continue; }; let mut impled_area_code_applied = Vec::new(); // Split items for item in impl_items { // Get base template let mut applied = impl_area_template.clone(); // Merge global params with arm-specific params for display block check let mut display_params = template.params.clone(); for (k, v) in item { display_params.insert(k.clone(), v.clone()); } applied = apply_display_blocks(&display_params, applied); // Extract parameters for (param_name, param_value) in item { // Apply parameter applied = applied.replace( &format!("{}{}{}", PARAM_BEGIN, param_name, PARAM_BEND), param_value, ); } // Add applied template impled_area_code_applied.push(applied); } impled_areas.insert(impl_area_name, impled_area_code_applied); } for line in content.split("\n") { let trimmed_line = line.trim(); // Recognize implementation line if trimmed_line.starts_with(IMPL_BEGIN) { let impl_name = snake_case!(trimmed_line.trim_start_matches(IMPL_BEGIN).trim()); // Try to get implementation code block let Some(impled_code) = impled_areas.get(&impl_name) else { continue; }; if !impled_code.is_empty() { applied_content += "\n"; applied_content += impled_code.join("\n").as_str(); } } else { // Other content directly appended applied_content += "\n"; applied_content += line; } } Some(applied_content) } /// Process display blocks (`??? >>> name` / `??? <<<`). /// /// If `params` contains a key matching the block name, the block content is /// included (with markers removed). Otherwise the entire block is omitted. fn apply_display_blocks(params: &HashMap, content: String) -> String { let mut result = String::new(); let lines: Vec<&str> = content.split("\n").collect(); let mut i = 0; let mut first = true; while i < lines.len() { let line = lines[i]; let trimmed = line.trim(); if trimmed.starts_with(DISPLAY_BLOCK_BEGIN) { let block_name = trimmed.trim_start_matches(DISPLAY_BLOCK_BEGIN).trim(); let show = params.contains_key(block_name); i += 1; while i < lines.len() && !lines[i].trim().starts_with(DISPLAY_BLOCK_END) { if show { if !first { result += "\n"; } result += lines[i]; first = false; } i += 1; } } else if !trimmed.starts_with(DISPLAY_BLOCK_END) { if !first { result += "\n"; } result += line; first = false; } i += 1; } result } fn apply_param(template: &Template, content: String) -> Option { let mut content = content; for (k, v) in template.params.iter() { content = content.replace(&format!("{}{}{}", PARAM_BEGIN, k, PARAM_BEND), v); } Some(content) }