//! Diff-aware file writer for ArchDoc //! //! This module handles writing generated documentation to files while preserving //! manual content and only updating generated sections. use crate::errors::ArchDocError; use std::path::Path; use std::fs; use chrono::Utc; #[derive(Debug)] pub struct SectionMarker { pub name: String, pub start_pos: usize, pub end_pos: usize, } #[derive(Debug)] pub struct SymbolMarker { pub symbol_id: String, pub start_pos: usize, pub end_pos: usize, } pub struct DiffAwareWriter { // Configuration } impl Default for DiffAwareWriter { fn default() -> Self { Self::new() } } impl DiffAwareWriter { pub fn new() -> Self { Self {} } pub fn update_file_with_markers( &self, file_path: &Path, generated_content: &str, section_name: &str, ) -> Result<(), ArchDocError> { // Read existing file let existing_content = if file_path.exists() { fs::read_to_string(file_path) .map_err(ArchDocError::Io)? } else { // Create new file with template let template_content = self.create_template_file(file_path, section_name)?; // Write template to file fs::write(file_path, &template_content) .map_err(ArchDocError::Io)?; template_content }; // Find section markers let markers = self.find_section_markers(&existing_content, section_name)?; if let Some(marker) = markers.first() { // Replace content between markers let new_content = self.replace_section_content( &existing_content, marker, generated_content, )?; // Check if content has changed let content_changed = existing_content != new_content; // Only write if content actually changed (optimization) if content_changed { let updated_content = self.update_timestamp(new_content)?; fs::write(file_path, updated_content) .map_err(ArchDocError::Io)?; } // If not changed, skip writing entirely } Ok(()) } pub fn update_symbol_section( &self, file_path: &Path, symbol_id: &str, generated_content: &str, ) -> Result<(), ArchDocError> { // Read existing file let existing_content = if file_path.exists() { fs::read_to_string(file_path) .map_err(ArchDocError::Io)? } else { // If file doesn't exist, create it with a basic template let template_content = self.create_template_file(file_path, "symbol")?; fs::write(file_path, &template_content) .map_err(ArchDocError::Io)?; template_content }; // Find symbol markers let markers = self.find_symbol_markers(&existing_content, symbol_id)?; if let Some(marker) = markers.first() { // Replace content between markers let new_content = self.replace_symbol_content( &existing_content, marker, generated_content, )?; // Check if content has changed let content_changed = existing_content != new_content; // Only write if content actually changed (optimization) if content_changed { let updated_content = self.update_timestamp(new_content)?; fs::write(file_path, updated_content) .map_err(ArchDocError::Io)?; } // If not changed, skip writing entirely } else { eprintln!("Warning: No symbol marker found for {} in {}", symbol_id, file_path.display()); } Ok(()) } fn find_section_markers(&self, content: &str, section_name: &str) -> Result, ArchDocError> { let begin_marker = format!("", section_name); let end_marker = format!("", section_name); let mut markers = Vec::new(); let mut pos = 0; while let Some(begin_pos) = content[pos..].find(&begin_marker) { let absolute_begin = pos + begin_pos; let search_start = absolute_begin + begin_marker.len(); if let Some(end_pos) = content[search_start..].find(&end_marker) { let absolute_end = search_start + end_pos + end_marker.len(); markers.push(SectionMarker { name: section_name.to_string(), start_pos: absolute_begin, end_pos: absolute_end, }); pos = absolute_end; } else { break; } } Ok(markers) } fn find_symbol_markers(&self, content: &str, symbol_id: &str) -> Result, ArchDocError> { let begin_marker = format!("", symbol_id); let end_marker = format!("", symbol_id); let mut markers = Vec::new(); let mut pos = 0; while let Some(begin_pos) = content[pos..].find(&begin_marker) { let absolute_begin = pos + begin_pos; let search_start = absolute_begin + begin_marker.len(); if let Some(end_pos) = content[search_start..].find(&end_marker) { let absolute_end = search_start + end_pos + end_marker.len(); markers.push(SymbolMarker { symbol_id: symbol_id.to_string(), start_pos: absolute_begin, end_pos: absolute_end, }); pos = absolute_end; } else { break; } } Ok(markers) } fn replace_section_content( &self, content: &str, marker: &SectionMarker, new_content: &str, ) -> Result { let before = &content[..marker.start_pos]; let after = &content[marker.end_pos..]; let begin_marker = format!("", marker.name); let end_marker = format!("", marker.name); Ok(format!( "{}{}{}{}{}", before, begin_marker, new_content, end_marker, after )) } fn replace_symbol_content( &self, content: &str, marker: &SymbolMarker, new_content: &str, ) -> Result { let before = &content[..marker.start_pos]; let after = &content[marker.end_pos..]; let begin_marker = format!("", marker.symbol_id); let end_marker = format!("", marker.symbol_id); Ok(format!( "{}{}{}{}{}", before, begin_marker, new_content, end_marker, after )) } fn update_timestamp(&self, content: String) -> Result { // Update the "Updated" field in the document metadata section // Find the metadata section and update the timestamp let today = Utc::now().format("%Y-%m-%d").to_string(); // Look for the "Updated:" line and replace it let lines: Vec<&str> = content.lines().collect(); let mut updated_lines = Vec::new(); for line in lines { if line.trim_start().starts_with("- **Updated:**") { updated_lines.push(format!("- **Updated:** {}", today)); } else { updated_lines.push(line.to_string()); } } Ok(updated_lines.join("\n")) } fn create_template_file(&self, _file_path: &Path, template_type: &str) -> Result { // Create file with appropriate template based on type match template_type { "architecture" => { let template = r#"# ARCHITECTURE — ## Project summary **Name:** **Description:** ## Key decisions (manual) - ## Non-goals (manual) - --- ## Document metadata - **Created:** - **Updated:** - **Generated by:** archdoc (cli) v0.1 --- ## Rails / Tooling > Generated. Do not edit inside this block. --- ## Repository layout (top-level) > Generated. Do not edit inside this block. --- ## Modules index > Generated. Do not edit inside this block. --- ## Critical dependency points > Generated. Do not edit inside this block. --- ## Change notes (manual) - "#; Ok(template.to_string()) } "symbol" => { // Template for symbol documentation files let template = r#"# File: - **Module:** - **Defined symbols:** - **Imports:** ## File intent (manual) --- ## Imports & file-level dependencies > Generated. Do not edit inside this block. --- ## Symbols index > Generated. Do not edit inside this block. --- ## Symbol details "#; Ok(template.to_string()) } _ => { Ok("".to_string()) } } } }