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(); 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(); 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}