1use std::path::{Path, PathBuf};
2
3use anyhow::Result;
4use serde::Deserialize;
5
6#[derive(Debug, Default, Deserialize)]
8#[serde(default)]
9pub struct FormatOverrides {
10 pub extra_abbreviations: Vec<String>,
11 pub max_width: Option<usize>,
12}
13
14#[derive(Debug, Default, Deserialize)]
16#[serde(default)]
17pub struct ProjectConfig {
18 pub extra_abbreviations: Vec<String>,
20 #[serde(alias = "ignore")]
22 pub ignore_patterns: Vec<String>,
23 #[serde(alias = "format")]
25 pub default_format: Option<String>,
26 pub max_width: Option<usize>,
28 pub lang: Option<String>,
30
31 pub org: Option<FormatOverrides>,
33 pub latex: Option<FormatOverrides>,
34 pub markdown: Option<FormatOverrides>,
35 pub plaintext: Option<FormatOverrides>,
36}
37
38impl ProjectConfig {
39 pub fn find_and_load(start_dir: &Path) -> Result<Self> {
42 let mut dir = start_dir.to_path_buf();
43 loop {
44 let candidate = dir.join(".snapperrc.toml");
45 if candidate.is_file() {
46 return Self::load(&candidate);
47 }
48 if !dir.pop() {
49 break;
50 }
51 }
52 Ok(Self::default())
53 }
54
55 pub fn load(path: &Path) -> Result<Self> {
57 let contents = std::fs::read_to_string(path)?;
58 Self::parse(&contents)
59 }
60
61 fn parse(toml_str: &str) -> Result<Self> {
62 let config: ProjectConfig = toml::from_str(toml_str)?;
63 Ok(config)
64 }
65
66 pub fn resolve(explicit_path: Option<&PathBuf>) -> Result<Self> {
68 if let Some(path) = explicit_path {
69 Self::load(path)
70 } else {
71 let cwd = std::env::current_dir()?;
72 Self::find_and_load(&cwd)
73 }
74 }
75
76 pub fn abbreviations_for_format(&self, format: &str) -> Vec<String> {
79 let mut abbrevs = self.extra_abbreviations.clone();
80 let overrides = match format {
81 "org" => self.org.as_ref(),
82 "latex" => self.latex.as_ref(),
83 "markdown" => self.markdown.as_ref(),
84 "plaintext" => self.plaintext.as_ref(),
85 _ => None,
86 };
87 if let Some(ov) = overrides {
88 abbrevs.extend(ov.extra_abbreviations.iter().cloned());
89 }
90 abbrevs
91 }
92
93 pub fn max_width_for_format(&self, format: &str) -> Option<usize> {
95 let overrides = match format {
96 "org" => self.org.as_ref(),
97 "latex" => self.latex.as_ref(),
98 "markdown" => self.markdown.as_ref(),
99 "plaintext" => self.plaintext.as_ref(),
100 _ => None,
101 };
102 overrides.and_then(|ov| ov.max_width).or(self.max_width)
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn parse_empty_config() {
112 let config = ProjectConfig::parse("").unwrap();
113 assert!(config.extra_abbreviations.is_empty());
114 assert!(config.ignore_patterns.is_empty());
115 assert!(config.default_format.is_none());
116 assert!(config.max_width.is_none());
117 }
118
119 #[test]
120 fn parse_full_config() {
121 let toml = r#"
122# Project-specific snapper config
123extra_abbreviations = ["Dept", "Univ", "Corp"]
124ignore = ["*.bib", "*.cls"]
125format = "org"
126max_width = 80
127lang = "de"
128"#;
129 let config = ProjectConfig::parse(toml).unwrap();
130 assert_eq!(config.extra_abbreviations, vec!["Dept", "Univ", "Corp"]);
131 assert_eq!(config.ignore_patterns, vec!["*.bib", "*.cls"]);
132 assert_eq!(config.default_format, Some("org".to_string()));
133 assert_eq!(config.max_width, Some(80));
134 assert_eq!(config.lang, Some("de".to_string()));
135 }
136
137 #[test]
138 fn parse_comments_and_blanks() {
139 let toml = "# comment\n\nextra_abbreviations = [\"Fig\"]\n";
140 let config = ProjectConfig::parse(toml).unwrap();
141 assert_eq!(config.extra_abbreviations, vec!["Fig"]);
142 }
143
144 #[test]
145 fn parse_per_format_overrides() {
146 let toml = r#"
147extra_abbreviations = ["Global"]
148max_width = 80
149
150[org]
151extra_abbreviations = ["PROPERTIES", "DEADLINE"]
152
153[latex]
154extra_abbreviations = ["Thm", "Lem"]
155max_width = 100
156"#;
157 let config = ProjectConfig::parse(toml).unwrap();
158 let org_abbrevs = config.abbreviations_for_format("org");
159 assert!(org_abbrevs.contains(&"Global".to_string()));
160 assert!(org_abbrevs.contains(&"PROPERTIES".to_string()));
161 assert_eq!(config.max_width_for_format("org"), Some(80));
162 assert_eq!(config.max_width_for_format("latex"), Some(100));
163 assert_eq!(config.max_width_for_format("plaintext"), Some(80));
164 }
165}