rumdl_lib/utils/
mkdocs_config.rs1use serde::Deserialize;
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::{LazyLock, Mutex};
10
11static DOCS_DIR_CACHE: LazyLock<Mutex<HashMap<PathBuf, PathBuf>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
13
14pub fn find_mkdocs_yml(start_path: &Path) -> Option<PathBuf> {
18 let mut current = if start_path.is_file() {
19 start_path.parent()?.to_path_buf()
20 } else {
21 start_path.to_path_buf()
22 };
23
24 loop {
25 for filename in &["mkdocs.yml", "mkdocs.yaml"] {
26 let mkdocs_path = current.join(filename);
27 if mkdocs_path.exists() {
28 return mkdocs_path.canonicalize().ok();
29 }
30 }
31
32 if !current.pop() {
33 break;
34 }
35 }
36
37 None
38}
39
40#[derive(Debug, Deserialize)]
42struct MkDocsYmlPartial {
43 #[serde(default = "default_docs_dir")]
44 docs_dir: String,
45}
46
47fn default_docs_dir() -> String {
48 "docs".to_string()
49}
50
51pub fn resolve_docs_dir(start_path: &Path) -> Option<PathBuf> {
59 let mkdocs_path = find_mkdocs_yml(start_path)?;
60
61 if let Ok(cache) = DOCS_DIR_CACHE.lock()
63 && let Some(docs_dir) = cache.get(&mkdocs_path)
64 {
65 return Some(docs_dir.clone());
66 }
67
68 let content = std::fs::read_to_string(&mkdocs_path).ok()?;
70 let config: MkDocsYmlPartial = serde_yml::from_str(&content).ok()?;
71
72 let mkdocs_dir = mkdocs_path.parent()?;
74 let docs_dir = if Path::new(&config.docs_dir).is_absolute() {
75 PathBuf::from(&config.docs_dir)
76 } else {
77 mkdocs_dir.join(&config.docs_dir)
78 };
79
80 if docs_dir.exists()
82 && let Ok(mut cache) = DOCS_DIR_CACHE.lock()
83 {
84 cache.insert(mkdocs_path, docs_dir.clone());
85 }
86
87 Some(docs_dir)
88}
89
90#[cfg(test)]
92pub fn clear_docs_dir_cache() {
93 if let Ok(mut cache) = DOCS_DIR_CACHE.lock() {
94 cache.clear();
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use std::fs;
102 use tempfile::tempdir;
103
104 #[test]
105 fn test_find_mkdocs_yml() {
106 let temp_dir = tempdir().unwrap();
107 let mkdocs_path = temp_dir.path().join("mkdocs.yml");
108 fs::write(&mkdocs_path, "site_name: test\n").unwrap();
109
110 let sub_dir = temp_dir.path().join("docs");
111 fs::create_dir_all(&sub_dir).unwrap();
112
113 let result = find_mkdocs_yml(&sub_dir);
114 assert!(result.is_some());
115 }
116
117 #[test]
118 fn test_find_mkdocs_yaml_extension() {
119 let temp_dir = tempdir().unwrap();
120 let mkdocs_path = temp_dir.path().join("mkdocs.yaml");
121 fs::write(&mkdocs_path, "site_name: test\n").unwrap();
122
123 let result = find_mkdocs_yml(temp_dir.path());
124 assert!(result.is_some());
125 }
126
127 #[test]
128 fn test_resolve_docs_dir_default() {
129 clear_docs_dir_cache();
130 let temp_dir = tempdir().unwrap();
131 let mkdocs_path = temp_dir.path().join("mkdocs.yml");
132 fs::write(&mkdocs_path, "site_name: test\n").unwrap();
133
134 let docs_dir = temp_dir.path().join("docs");
135 fs::create_dir_all(&docs_dir).unwrap();
136
137 let result = resolve_docs_dir(temp_dir.path());
138 assert!(result.is_some());
139 let result_path = result.unwrap();
140 assert!(result_path.ends_with("docs"));
141 }
142
143 #[test]
144 fn test_resolve_docs_dir_custom() {
145 clear_docs_dir_cache();
146 let temp_dir = tempdir().unwrap();
147 let mkdocs_path = temp_dir.path().join("mkdocs.yml");
148 fs::write(&mkdocs_path, "site_name: test\ndocs_dir: documentation\n").unwrap();
149
150 let docs_dir = temp_dir.path().join("documentation");
151 fs::create_dir_all(&docs_dir).unwrap();
152
153 let result = resolve_docs_dir(temp_dir.path());
154 assert!(result.is_some());
155 let result_path = result.unwrap();
156 assert!(result_path.ends_with("documentation"));
157 }
158
159 #[test]
160 fn test_resolve_docs_dir_no_mkdocs_yml() {
161 clear_docs_dir_cache();
162 let temp_dir = tempdir().unwrap();
163 let result = resolve_docs_dir(temp_dir.path());
164 assert!(result.is_none());
165 }
166}