Skip to main content

mdlint/config/
loader.rs

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}