1use crate::config::Config;
2use crate::error::{MarkdownlintError, Result};
3use std::path::{Path, PathBuf};
4use std::{fs, iter};
5
6const CONFIG_FILE_NAMES: &[&str] = &["mdlint.toml", ".mdlint.toml"];
7
8pub enum ConfigLoader {
9 Detect,
10 File(PathBuf),
11 None,
12}
13
14impl ConfigLoader {
15 pub fn load(&self) -> Result<Config> {
16 match self {
17 ConfigLoader::Detect => {
18 let start = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
19 discover_config(&start)
20 }
21 ConfigLoader::File(path) => load_config(path),
22 ConfigLoader::None => Ok(Config::default()),
23 }
24 }
25}
26
27pub fn discover_config(start_dir: &Path) -> Result<Config> {
28 let config_file = iter::successors(Some(start_dir.to_path_buf()), |path| {
29 path.parent().map(|p| p.to_path_buf())
30 })
31 .flat_map(|path| CONFIG_FILE_NAMES.iter().map(move |name| path.join(name)))
32 .find(|path| path.exists());
33 match config_file {
34 Some(path) => load_config(&path),
35 None => Ok(Config::default()),
36 }
37}
38
39pub fn find_all_configs(start_dir: &Path) -> Result<Vec<(PathBuf, Config)>> {
40 let mut configs = Vec::new();
41 let mut current = start_dir.to_path_buf();
42
43 loop {
44 for config_file in CONFIG_FILE_NAMES {
45 let config_path = current.join(config_file);
46 if config_path.exists() {
47 let config = load_config(&config_path)?;
48 configs.push((config_path, config));
49 break;
50 }
51 }
52
53 if !current.pop() {
54 break;
55 }
56 }
57
58 configs.reverse();
59 Ok(configs)
60}
61fn load_config(path: &PathBuf) -> Result<Config> {
62 let content = fs::read_to_string(path).map_err(|e| {
63 MarkdownlintError::Config(format!("Failed to read config file {:?}: {}", path, e))
64 })?;
65 parse_toml_config(&content, path)
66}
67
68fn parse_toml_config(content: &str, _path: &Path) -> Result<Config> {
69 toml::from_str(content)
70 .map_err(|e| MarkdownlintError::Config(format!("Failed to parse TOML: {}", e)))
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use std::io::Write;
77 use tempfile::TempDir;
78
79 #[test]
80 fn test_parse_toml() {
81 let content = r#"
82gitignore = true
83default_enabled = true
84
85[rules.MD013]
86line_length = 100
87
88[rules.MD003]
89style = "atx"
90"#;
91
92 let config = parse_toml_config(content, Path::new("test.toml")).unwrap();
93 assert!(config.gitignore);
94 assert!(config.default_enabled);
95 assert_eq!(config.rules.len(), 2);
96 }
97
98 #[test]
99 fn test_load_from_file() {
100 let temp_dir = TempDir::new().unwrap();
101 let config_path = temp_dir.path().join("mdlint.toml");
102
103 let mut file = fs::File::create(&config_path).unwrap();
104 write!(
105 file,
106 r#"
107gitignore = true
108default_enabled = true
109
110[rules.MD013]
111line_length = 80
112"#
113 )
114 .unwrap();
115
116 let config = load_config(&config_path).unwrap();
117 assert!(config.gitignore);
118 assert!(config.default_enabled);
119 }
120
121 #[test]
122 fn test_discover_config() {
123 let temp_dir = TempDir::new().unwrap();
124 let sub_dir = temp_dir.path().join("subdir");
125 fs::create_dir(&sub_dir).unwrap();
126
127 let config_path = temp_dir.path().join("mdlint.toml");
128 let mut file = fs::File::create(&config_path).unwrap();
129 writeln!(file, "gitignore = true").unwrap();
130
131 let config = discover_config(&sub_dir).unwrap();
132 assert!(config.gitignore);
133 }
134}