sphinx_ultra/
utils.rs

1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use std::path::Path;
4
5#[derive(Debug)]
6pub struct ProjectStats {
7    pub source_files: usize,
8    pub total_lines: usize,
9    pub avg_file_size_kb: f64,
10    pub largest_file_kb: f64,
11    pub max_depth: usize,
12    pub cross_references: usize,
13}
14
15pub async fn analyze_project(source_dir: &Path) -> Result<ProjectStats> {
16    let mut state = AnalysisState {
17        source_files: 0,
18        total_lines: 0,
19        total_size_bytes: 0,
20        largest_file_kb: 0.0,
21        max_depth: 0,
22        cross_references: 0,
23    };
24
25    // Use synchronous approach to avoid async recursion issues
26    analyze_directory_sync(source_dir, source_dir, 0, &mut state)?;
27
28    let avg_file_size_kb = if state.source_files > 0 {
29        (state.total_size_bytes as f64) / (state.source_files as f64) / 1024.0
30    } else {
31        0.0
32    };
33
34    Ok(ProjectStats {
35        source_files: state.source_files,
36        total_lines: state.total_lines,
37        avg_file_size_kb,
38        largest_file_kb: state.largest_file_kb,
39        max_depth: state.max_depth,
40        cross_references: state.cross_references,
41    })
42}
43
44/// Analysis state for directory traversal
45struct AnalysisState {
46    source_files: usize,
47    total_lines: usize,
48    total_size_bytes: u64,
49    largest_file_kb: f64,
50    max_depth: usize,
51    cross_references: usize,
52}
53
54fn analyze_directory_sync(
55    dir: &Path,
56    _root_dir: &Path,
57    current_depth: usize,
58    state: &mut AnalysisState,
59) -> Result<()> {
60    state.max_depth = state.max_depth.max(current_depth);
61
62    for entry in std::fs::read_dir(dir)? {
63        let entry = entry?;
64        let path = entry.path();
65
66        if path.is_dir() {
67            // Skip hidden directories
68            if let Some(name) = path.file_name() {
69                if name.to_string_lossy().starts_with('.') {
70                    continue;
71                }
72            }
73
74            analyze_directory_sync(&path, _root_dir, current_depth + 1, state)?;
75        } else if is_source_file(&path) {
76            state.source_files += 1;
77
78            let metadata = std::fs::metadata(&path)?;
79            let file_size_bytes = metadata.len();
80            let file_size_kb = file_size_bytes as f64 / 1024.0;
81
82            state.total_size_bytes += file_size_bytes;
83            state.largest_file_kb = state.largest_file_kb.max(file_size_kb);
84
85            // Count lines and cross-references
86            if let Ok(content) = std::fs::read_to_string(&path) {
87                state.total_lines += content.lines().count();
88                state.cross_references += count_cross_references(&content);
89            }
90        }
91    }
92
93    Ok(())
94}
95
96pub fn is_source_file(path: &Path) -> bool {
97    if let Some(ext) = path.extension() {
98        matches!(ext.to_string_lossy().as_ref(), "rst" | "md" | "txt")
99    } else {
100        false
101    }
102}
103
104pub fn count_cross_references(content: &str) -> usize {
105    let patterns = [
106        r":doc:`",
107        r":ref:`",
108        r":func:`",
109        r":class:`",
110        r":meth:`",
111        r":attr:`",
112        r":mod:`",
113        r":py:",
114        r".. _",
115        r"`~",
116    ];
117
118    let mut count = 0;
119    for pattern in &patterns {
120        count += content.matches(pattern).count();
121    }
122    count
123}
124
125pub fn get_file_mtime(path: &Path) -> Result<DateTime<Utc>> {
126    let metadata = std::fs::metadata(path)?;
127    let mtime = metadata.modified()?;
128    Ok(DateTime::from(mtime))
129}
130
131pub async fn calculate_directory_size(dir: &Path) -> Result<u64> {
132    // Use synchronous approach
133    calculate_directory_size_sync(dir)
134}
135
136fn calculate_directory_size_sync(dir: &Path) -> Result<u64> {
137    let mut total_size = 0;
138
139    for entry in std::fs::read_dir(dir)? {
140        let entry = entry?;
141        let path = entry.path();
142
143        if path.is_dir() {
144            total_size += calculate_directory_size_sync(&path)?;
145        } else {
146            let metadata = std::fs::metadata(&path)?;
147            total_size += metadata.len();
148        }
149    }
150
151    Ok(total_size)
152}
153
154pub async fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
155    // Use synchronous approach
156    copy_dir_recursive_sync(src, dst)
157}
158
159fn copy_dir_recursive_sync(src: &Path, dst: &Path) -> Result<()> {
160    std::fs::create_dir_all(dst)?;
161
162    for entry in std::fs::read_dir(src)? {
163        let entry = entry?;
164        let src_path = entry.path();
165        let dst_path = dst.join(entry.file_name());
166
167        if src_path.is_dir() {
168            copy_dir_recursive_sync(&src_path, &dst_path)?;
169        } else {
170            std::fs::copy(&src_path, &dst_path)?;
171        }
172    }
173
174    Ok(())
175}
176
177#[allow(dead_code)]
178pub fn format_duration(duration: std::time::Duration) -> String {
179    let secs = duration.as_secs();
180    let millis = duration.subsec_millis();
181
182    if secs > 0 {
183        format!("{}.{:03}s", secs, millis)
184    } else {
185        format!("{}ms", millis)
186    }
187}
188
189#[allow(dead_code)]
190pub fn format_bytes(bytes: u64) -> String {
191    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
192
193    if bytes == 0 {
194        return "0 B".to_string();
195    }
196
197    let mut size = bytes as f64;
198    let mut unit_index = 0;
199
200    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
201        size /= 1024.0;
202        unit_index += 1;
203    }
204
205    format!("{:.1} {}", size, UNITS[unit_index])
206}
207
208/// Format a date according to the specified format string and language
209#[allow(dead_code)]
210pub fn format_date(fmt: &str, _language: &Option<String>) -> String {
211    let now = chrono::Utc::now();
212
213    match fmt {
214        "%b %d, %Y" => now.format("%b %d, %Y").to_string(),
215        "%B %d, %Y" => now.format("%B %d, %Y").to_string(),
216        "%Y-%m-%d" => now.format("%Y-%m-%d").to_string(),
217        "%Y-%m-%d %H:%M:%S" => now.format("%Y-%m-%d %H:%M:%S").to_string(),
218        _ => {
219            // For custom formats, try to parse and format
220            match chrono::DateTime::parse_from_str(&now.to_rfc3339(), "%+") {
221                Ok(dt) => dt.format(fmt).to_string(),
222                Err(_) => now.format("%Y-%m-%d").to_string(),
223            }
224        }
225    }
226}
227
228/// Ensure a directory exists, creating it if necessary
229#[allow(dead_code)]
230pub async fn ensure_dir(path: &Path) -> Result<()> {
231    use tokio::fs;
232
233    if !path.exists() {
234        fs::create_dir_all(path).await?;
235    }
236    Ok(())
237}
238
239/// Calculate relative URI from one path to another
240#[allow(dead_code)]
241pub fn relative_uri(from: &str, to: &str, suffix: &str) -> String {
242    use std::path::Path;
243
244    let from_path = Path::new(from);
245    let to_path = Path::new(to);
246
247    // Get the relative path
248    if let Some(rel_path) =
249        pathdiff::diff_paths(to_path, from_path.parent().unwrap_or(Path::new("")))
250    {
251        let mut result = rel_path.to_string_lossy().to_string();
252        if !suffix.is_empty() && !result.ends_with(suffix) {
253            result.push_str(suffix);
254        }
255        result.replace('\\', "/") // Ensure forward slashes
256    } else {
257        format!("{}{}", to, suffix)
258    }
259}
260
261/// Copy all files and directories from source to destination
262#[allow(dead_code)]
263pub async fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
264    use tokio::fs;
265
266    ensure_dir(dst).await?;
267
268    let mut entries = fs::read_dir(src).await?;
269
270    while let Some(entry) = entries.next_entry().await? {
271        let entry_path = entry.path();
272        let file_name = entry.file_name();
273        let dest_path = dst.join(file_name);
274
275        if entry_path.is_dir() {
276            Box::pin(copy_dir_all(&entry_path, &dest_path)).await?;
277        } else {
278            if let Some(parent) = dest_path.parent() {
279                ensure_dir(parent).await?;
280            }
281            fs::copy(&entry_path, &dest_path).await?;
282        }
283    }
284
285    Ok(())
286}