syncdoc_core/
path_utils.rs

1use crate::syncdoc_debug;
2use std::path::{Path, PathBuf};
3
4/// Find the Cargo manifest directory by walking up from a given path
5pub fn find_manifest_dir(start_path: &Path) -> Option<PathBuf> {
6    let mut current = start_path;
7
8    loop {
9        if current.join("Cargo.toml").exists() {
10            return Some(current.to_path_buf());
11        }
12
13        current = current.parent()?;
14    }
15}
16
17/// Convert a doc path to be relative to the Cargo manifest directory
18/// from the perspective of the call site file
19pub fn make_manifest_relative_path(doc_path: &str, call_site_file: &Path) -> String {
20    syncdoc_debug!("make_manifest_relative_path called:");
21    syncdoc_debug!("  doc_path: {}", doc_path);
22    syncdoc_debug!("  call_site_file: {}", call_site_file.display());
23
24    // If path already starts with ../, it's already relative - return as-is
25    if doc_path.starts_with("../") || doc_path.starts_with("..\\") {
26        syncdoc_debug!("  Path already relative, returning as-is");
27        return doc_path.to_string();
28    }
29
30    // Find the manifest directory
31    let manifest_dir = match find_manifest_dir(call_site_file) {
32        Some(dir) => {
33            syncdoc_debug!("  manifest_dir: {}", dir.display());
34            dir
35        }
36        None => {
37            // Fallback: return path as-is if we can't find manifest
38            syncdoc_debug!("  manifest_dir: NOT FOUND (fallback)");
39            return doc_path.to_string();
40        }
41    };
42
43    // Get the directory containing the call site file
44    let call_site_dir = call_site_file.parent().unwrap_or_else(|| Path::new("."));
45    syncdoc_debug!("  call_site_dir: {}", call_site_dir.display());
46
47    // Compute relative path from call site to manifest dir
48    let rel_to_manifest =
49        path_relative_from(&manifest_dir, call_site_dir).unwrap_or_else(|| manifest_dir.clone());
50    syncdoc_debug!("  rel_to_manifest: {}", rel_to_manifest.display());
51
52    // Combine with the doc path
53    let full_path = rel_to_manifest.join(doc_path);
54    syncdoc_debug!("  full_path: {}", full_path.display());
55
56    // Convert to string, using forward slashes for cross-platform compatibility
57    let result = full_path.to_str().unwrap_or(doc_path).replace('\\', "/");
58    syncdoc_debug!("  result: {}", result);
59    result
60}
61
62/// Computes a relative path from `base` to `path`, returning a path with `../` components
63/// if necessary.
64///
65/// This function is vendored from the old Rust standard library implementation
66/// (pre-1.0, removed in RFC 474) and is distributed under the same terms as the
67/// Rust project (MIT/Apache-2.0 dual license).
68///
69/// Unlike `Path::strip_prefix`, this function can handle cases where `path` is not
70/// a descendant of `base`, making it suitable for finding relative paths between
71/// arbitrary directories (e.g., between sibling directories in a workspace).
72fn path_relative_from(path: &Path, base: &Path) -> Option<PathBuf> {
73    use std::path::Component;
74
75    if path.is_absolute() != base.is_absolute() {
76        if path.is_absolute() {
77            Some(PathBuf::from(path))
78        } else {
79            None
80        }
81    } else {
82        let mut ita = path.components();
83        let mut itb = base.components();
84        let mut comps: Vec<Component> = vec![];
85        loop {
86            match (ita.next(), itb.next()) {
87                (None, None) => break,
88                (Some(a), None) => {
89                    comps.push(a);
90                    comps.extend(ita.by_ref());
91                    break;
92                }
93                (None, _) => comps.push(Component::ParentDir),
94                (Some(a), Some(b)) if comps.is_empty() && a == b => {}
95                (Some(a), Some(_b)) => {
96                    comps.push(Component::ParentDir);
97                    for _ in itb {
98                        comps.push(Component::ParentDir);
99                    }
100                    comps.push(a);
101                    comps.extend(ita.by_ref());
102                    break;
103                }
104            }
105        }
106        Some(comps.iter().map(|c| c.as_os_str()).collect())
107    }
108}
109
110/// Extract module path from source file relative to src/
111/// e.g., src/main.rs -> "main", src/foo/mod.rs -> "foo", src/a/b/c.rs -> "a/b/c"
112pub fn extract_module_path(source_file: &str) -> String {
113    let source_path = Path::new(source_file);
114
115    if let Some(manifest_dir) = find_manifest_dir(source_path) {
116        if let Ok(rel) = source_path.strip_prefix(&manifest_dir) {
117            let rel_str = rel.to_string_lossy();
118            let without_src = rel_str
119                .strip_prefix("src/")
120                .or(rel_str.strip_prefix("src\\"))
121                .unwrap_or(&rel_str);
122
123            if without_src == "main.rs" || without_src == "lib.rs" {
124                return without_src.trim_end_matches(".rs").to_string();
125            } else if without_src.ends_with("/mod.rs") || without_src.ends_with("\\mod.rs") {
126                return without_src
127                    .trim_end_matches("/mod.rs")
128                    .trim_end_matches("\\mod.rs")
129                    .replace('\\', "/");
130            } else if without_src.ends_with(".rs") {
131                return without_src.trim_end_matches(".rs").replace('\\', "/");
132            }
133        }
134    }
135
136    String::new()
137}
138
139pub fn apply_module_path(base_path: String) -> String {
140    syncdoc_debug!("apply_module_path called:");
141    syncdoc_debug!("  base_path: {}", base_path);
142
143    let call_site = proc_macro2::Span::call_site();
144    if let Some(source_path) = call_site.local_file() {
145        let source_file = source_path.to_string_lossy().to_string();
146        syncdoc_debug!("  source_file: {}", source_file);
147
148        let module_path = extract_module_path(&source_file);
149        syncdoc_debug!("  module_path: {}", module_path);
150
151        if module_path.is_empty() {
152            syncdoc_debug!("  result: {} (no module path)", base_path);
153            base_path
154        } else {
155            let result = format!("{}/{}", base_path, module_path);
156            syncdoc_debug!("  result: {}", result);
157            result
158        }
159    } else {
160        syncdoc_debug!("  result: {} (no source path)", base_path);
161        base_path
162    }
163}