cobalt_config/
config.rs

1use std::convert::TryInto;
2use std::fmt;
3use std::path;
4
5use super::*;
6
7#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8#[serde(default)]
9#[serde(rename_all = "snake_case")]
10#[cfg_attr(feature = "unstable", serde(deny_unknown_fields))]
11#[cfg_attr(not(feature = "unstable"), non_exhaustive)]
12pub struct Config {
13    #[serde(skip)]
14    pub root: path::PathBuf,
15    pub source: RelPath,
16    pub destination: RelPath,
17    #[serde(skip)]
18    pub abs_dest: Option<path::PathBuf>,
19    pub include_drafts: bool,
20    pub default: Frontmatter,
21    pub pages: PageCollection,
22    pub posts: PostCollection,
23    pub site: Site,
24    pub template_extensions: Vec<liquid_core::model::KString>,
25    pub ignore: Vec<liquid_core::model::KString>,
26    pub syntax_highlight: SyntaxHighlight,
27    #[serde(skip)]
28    pub layouts_dir: &'static str,
29    #[serde(skip)]
30    pub includes_dir: &'static str,
31    pub assets: Assets,
32    pub minify: Minify,
33}
34
35impl Default for Config {
36    fn default() -> Config {
37        Config {
38            root: Default::default(),
39            source: "./".try_into().unwrap(),
40            destination: "./_site".try_into().unwrap(),
41            abs_dest: Default::default(),
42            include_drafts: false,
43            default: Default::default(),
44            pages: Default::default(),
45            posts: Default::default(),
46            site: Default::default(),
47            template_extensions: vec!["md".into(), "wiki".into(), "liquid".into()],
48            ignore: Default::default(),
49            syntax_highlight: SyntaxHighlight::default(),
50            layouts_dir: "_layouts",
51            includes_dir: "_includes",
52            assets: Assets::default(),
53            minify: Minify::default(),
54        }
55    }
56}
57
58impl Config {
59    pub fn from_file<P: Into<path::PathBuf>>(path: P) -> Result<Config> {
60        Self::from_file_internal(path.into())
61    }
62
63    fn from_file_internal(path: path::PathBuf) -> Result<Config> {
64        let content = std::fs::read_to_string(&path).map_err(|e| {
65            Status::new("Failed to read config")
66                .with_source(e)
67                .context_with(|c| c.insert("Path", path.display().to_string()))
68        })?;
69
70        let mut config = if content.trim().is_empty() {
71            Config::default()
72        } else {
73            serde_yaml::from_str(&content).map_err(|e| {
74                Status::new("Failed to parse config")
75                    .with_source(e)
76                    .context_with(|c| c.insert("Path", path.display().to_string()))
77            })?
78        };
79
80        let mut root = path;
81        root.pop(); // Remove filename
82        if root == path::Path::new("") {
83            root = path::Path::new(".").to_owned();
84        }
85        config.root = root;
86
87        Ok(config)
88    }
89
90    pub fn from_cwd<P: Into<path::PathBuf>>(cwd: P) -> Result<Config> {
91        Self::from_cwd_internal(cwd.into())
92    }
93
94    fn from_cwd_internal(cwd: path::PathBuf) -> Result<Config> {
95        let file_path = find_project_file(&cwd, "_cobalt.yml");
96        let config = file_path
97            .map(|p| {
98                log::debug!("Using config file `{}`", p.display());
99                Self::from_file(&p)
100            })
101            .unwrap_or_else(|| {
102                log::warn!("No _cobalt.yml file found in current directory, using default config.");
103                let config = Config {
104                    root: cwd,
105                    ..Default::default()
106                };
107                Ok(config)
108            })?;
109        Ok(config)
110    }
111}
112
113impl fmt::Display for Config {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        let mut converted = serde_yaml::to_string(self).map_err(|_| fmt::Error)?;
116        converted.drain(..4);
117        write!(f, "{converted}")
118    }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
122#[serde(default)]
123#[serde(rename_all = "snake_case")]
124#[cfg_attr(feature = "unstable", serde(deny_unknown_fields))]
125#[cfg_attr(not(feature = "unstable"), non_exhaustive)]
126pub struct SyntaxHighlight {
127    pub theme: liquid_core::model::KString,
128    pub enabled: bool,
129}
130
131impl Default for SyntaxHighlight {
132    fn default() -> Self {
133        Self {
134            theme: "base16-ocean.dark".into(),
135            enabled: true,
136        }
137    }
138}
139
140#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
141#[serde(default)]
142#[serde(rename_all = "snake_case")]
143#[cfg_attr(feature = "unstable", serde(deny_unknown_fields))]
144#[cfg_attr(not(feature = "unstable"), non_exhaustive)]
145pub struct Minify {
146    pub html: bool,
147    pub css: bool,
148    pub js: bool,
149}
150
151fn find_project_file<P: Into<path::PathBuf>>(dir: P, name: &str) -> Option<path::PathBuf> {
152    find_project_file_internal(dir.into(), name)
153}
154
155fn find_project_file_internal(dir: path::PathBuf, name: &str) -> Option<path::PathBuf> {
156    let mut file_path = dir;
157    file_path.push(name);
158    while !file_path.exists() {
159        file_path.pop(); // filename
160        let hit_bottom = !file_path.pop();
161        if hit_bottom {
162            return None;
163        }
164        file_path.push(name);
165    }
166    Some(file_path)
167}
168
169#[cfg(test)]
170mod test {
171    use super::*;
172
173    #[test]
174    fn test_from_file_ok() {
175        let result = Config::from_file("tests/fixtures/config/_cobalt.yml").unwrap();
176        assert_eq!(
177            result.root,
178            path::Path::new("tests/fixtures/config").to_path_buf()
179        );
180    }
181
182    #[test]
183    fn test_from_file_alternate_name() {
184        let result = Config::from_file("tests/fixtures/config/rss.yml").unwrap();
185        assert_eq!(
186            result.root,
187            path::Path::new("tests/fixtures/config").to_path_buf()
188        );
189    }
190
191    #[test]
192    fn test_from_file_empty() {
193        let result = Config::from_file("tests/fixtures/config/empty.yml").unwrap();
194        assert_eq!(
195            result.root,
196            path::Path::new("tests/fixtures/config").to_path_buf()
197        );
198    }
199
200    #[test]
201    fn test_from_file_invalid_syntax() {
202        let result = Config::from_file("tests/fixtures/config/invalid_syntax.yml");
203        assert!(result.is_err());
204    }
205
206    #[test]
207    fn test_from_file_not_found() {
208        let result = Config::from_file("tests/fixtures/config/config_does_not_exist.yml");
209        assert!(result.is_err());
210    }
211
212    #[test]
213    fn test_from_cwd_ok() {
214        let result = Config::from_cwd("tests/fixtures/config/child").unwrap();
215        assert_eq!(
216            result.root,
217            path::Path::new("tests/fixtures/config").to_path_buf()
218        );
219    }
220
221    #[test]
222    fn test_from_cwd_not_found() {
223        let result = Config::from_cwd("tests/fixtures").unwrap();
224        assert_eq!(result.root, path::Path::new("tests/fixtures").to_path_buf());
225    }
226
227    #[test]
228    fn find_project_file_same_dir() {
229        let actual = find_project_file("tests/fixtures/config", "_cobalt.yml").unwrap();
230        let expected = path::Path::new("tests/fixtures/config/_cobalt.yml");
231        assert_eq!(actual, expected);
232    }
233
234    #[test]
235    fn find_project_file_parent_dir() {
236        let actual = find_project_file("tests/fixtures/config/child", "_cobalt.yml").unwrap();
237        let expected = path::Path::new("tests/fixtures/config/_cobalt.yml");
238        assert_eq!(actual, expected);
239    }
240
241    #[test]
242    fn find_project_file_doesnt_exist() {
243        let expected = path::Path::new("<NOT FOUND>");
244        let actual =
245            find_project_file("tests/fixtures/", "_cobalt.yml").unwrap_or_else(|| expected.into());
246        assert_eq!(actual, expected);
247    }
248}