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 => 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        // Change working directory for the test
128        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        // Restore original directory
135        std::env::set_current_dir(original_dir).unwrap();
136    }
137}