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