1use std::env;
5use std::error::Error;
6
7use std::fs;
8use std::fs::File;
9
10use std::io::prelude::*;
11use std::io::BufReader;
12use std::path::Path;
13use std::path::PathBuf;
14
15use serde::{Deserialize, Serialize};
16
17use crate::content::{
18 init_dir_contents, init_dir_sections, init_entry_contents, Content, ContentType,
19};
20use crate::utils::{build_title_for_dir, is_ext};
21
22#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
25pub struct Data {
26 pub full_page: Option<bool>,
28 pub title: Option<String>,
30 pub subtitle: Option<String>,
32 pub author: Option<String>,
34 pub icon: Option<String>,
36 pub main: Option<Content>,
38 pub contents: Option<Vec<Content>>,
40 pub script: Option<String>,
42 pub style: Option<String>,
44 pub links: Option<Vec<Link>>,
46 pub header: Option<Content>,
48 pub footer: Option<Content>,
50}
51
52#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
54pub struct Link {
55 pub link_type: Option<LinkType>,
57 pub src: Option<String>,
59 pub integrity: Option<String>,
61 pub crossorigin: Option<String>,
63}
64
65#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
67#[serde(rename_all = "lowercase")]
68pub enum LinkType {
69 Style,
70 Script,
71}
72
73impl LinkType {
74 pub fn from_str(s: &str) -> Option<LinkType> {
75 match s {
76 "style" => Some(LinkType::Style),
77 "stylesheet" => Some(LinkType::Style),
78 "script" => Some(LinkType::Script),
79 _ => None,
80 }
81 }
82
83 pub fn as_str(&self) -> &'static str {
84 match self {
85 LinkType::Style => "stylesheet",
86 LinkType::Script => "script",
87 }
88 }
89}
90
91impl Default for Data {
92 fn default() -> Data {
93 Data {
94 full_page: Some(false),
95 title: None,
96 subtitle: None,
97 author: None,
98 icon: None,
99 main: None,
100 contents: None,
101 script: None,
102 style: None,
103 links: None,
104 footer: None,
105 header: None,
106 }
107 }
108}
109
110impl Data {
111 fn build(&mut self, root: &Path) -> Result<(), Box<dyn Error>> {
112 self.init(root)?;
113
114 self.build_contents(root)?;
115
116 Ok(())
117 }
118
119 fn init(&mut self, root: &Path) -> Result<(), Box<dyn Error>> {
120 if self.title.is_none() {
121 self.title = Some(build_title_for_dir(
122 root,
123 fs::read_dir(root).map_err(|err| {
124 format!("Error reading dir: {}. {}", root.display(), err.to_string())
125 })?,
126 true,
127 )?);
128 }
129
130 if self.main.is_some() {
131 self.main.as_mut().unwrap().init_from_file(root);
132 }
133
134 if self.header.is_some() {
135 self.header.as_mut().unwrap().init_from_file(root);
136 }
137
138 if self.footer.is_some() {
139 self.footer.as_mut().unwrap().init_from_file(root);
140 }
141
142 let mut main = None;
143 let mut header = None;
144 let mut footer = None;
145
146 let paths = fs::read_dir(root)
147 .map_err(|err| format!("Error reading dir: {}. {}", root.display(), err.to_string()))?;
148
149 let mut res = paths
150 .filter_map(|p| {
151 if let Ok(entry) = p {
152 if let Ok(file_type) = entry.file_type() {
153 if file_type.is_file() {
154 return init_entry_contents(root, entry, true).and_then(|(c, ct)| {
155 match ct {
156 ContentType::Main => {
157 main = Some(c);
158 None
159 }
160 ContentType::Footer => {
161 footer = Some(c);
162 None
163 }
164 ContentType::Header => {
165 header = Some(c);
166 None
167 }
168 _ => Some(vec![c]),
169 }
170 });
171 }
172 }
173 }
174 None
175 })
176 .flatten()
177 .collect::<Vec<Content>>();
178
179 res.sort_by(|a, b| a.file.cmp(&b.file));
180
181 let mut sections = init_dir_sections(root)?;
182
183 if !res.is_empty() {
184 res.push(Content::new_break());
185 }
186 res.append(&mut sections);
187
188 if self.main.is_none() {
189 self.main = main;
190 }
191
192 if self.footer.is_none() {
193 self.footer = footer;
194 }
195
196 if self.header.is_none() {
197 self.header = header;
198 }
199
200 if self.contents.is_none() {
201 self.contents = Some(res);
202 }
203
204 Ok(())
205 }
206
207 fn build_contents(&mut self, root: &Path) -> Result<(), Box<dyn Error>> {
208 if self.contents.is_some() {
209 let mut contents = self.contents.as_mut().unwrap();
210
211 let has_dir = contents.iter().any(|c| c.dir.is_some());
212 if has_dir {
213 let mut expanded_contents = Vec::new();
214 let mut index = 0;
215 while index < contents.len() {
216 if contents[index].dir.is_some() {
218 let mut pathbuf = contents[index].dir.clone().unwrap();
219
220 if root.has_root() && pathbuf.is_relative() {
221 pathbuf = root.join(&pathbuf).canonicalize().unwrap_or_else(|_| {
222 panic!(
223 "could not resolve path. root: {} path: {}",
224 root.display(),
225 pathbuf.display()
226 )
227 });
228 }
229
230 if pathbuf.is_dir() {
231 let mut dir_contents = Vec::new();
232
233 if let Some(mut root_dir_contents) = init_dir_contents(root, &pathbuf) {
235 dir_contents.append(&mut root_dir_contents);
236 }
237
238 let mut sub_dir_contents = init_dir_sections(&pathbuf)?;
240 dir_contents.append(&mut sub_dir_contents);
241
242 let mut di = 0;
244 while di < dir_contents.len() {
245 expanded_contents.push(dir_contents[di].clone());
246 di += 1;
247 }
248 }
249 } else {
250 expanded_contents.push(contents[index].clone());
251 }
252
253 index += 1;
254 }
255
256 self.contents = Some(expanded_contents);
257 }
258
259 contents = self.contents.as_mut().unwrap();
260
261 for c in contents {
262 crate::content::fill_content(c, root)?;
263 }
264 }
265
266 if self.main.is_some() {
267 crate::content::fill_content(self.main.as_mut().unwrap(), root)?;
268 }
269
270 if self.header.is_some() {
271 crate::content::fill_content(self.header.as_mut().unwrap(), root)?;
272 }
273
274 if self.footer.is_some() {
275 crate::content::fill_content(self.footer.as_mut().unwrap(), root)?;
276 }
277
278 Ok(())
279 }
280}
281
282fn config_file(root: &Path) -> Option<PathBuf> {
283 let mut r = Path::new(root);
284 let json_config = r.join("mdpage.json");
285 if json_config.as_path().exists() {
286 Some(json_config)
287 } else {
288 r = Path::new(root);
289 let toml_config = r.join("mdpage.toml");
290 if toml_config.as_path().exists() {
291 Some(toml_config)
292 } else {
293 None
294 }
295 }
296}
297
298pub fn build(root: &Path, initial_value: Option<Data>) -> Result<Data, Box<dyn Error>> {
300 let mut r = root;
301 let current_dir = env::current_dir()?;
302 let abs;
303 if root.is_relative() {
304 abs = current_dir
305 .as_path()
306 .join(root)
307 .canonicalize()
308 .map_err(|err| {
309 format!(
310 "could not join current dir {} with path: {}. {}",
311 current_dir.display(),
312 root.display(),
313 err.to_string()
314 )
315 })?;
316 r = abs.as_path();
317 }
318
319 let path = config_file(r);
320 let mut data = initial_value.unwrap_or_default();
321
322 if let Some(file_path) = path {
323 info!("reading config: {}", file_path.display());
324 let mut file = File::open(file_path.as_path()).map_err(|err| {
325 format!(
326 "Error reading file: {}. {}",
327 file_path.display(),
328 err.to_string()
329 )
330 })?;
331 if is_ext(&file_path, "json") {
332 let reader = BufReader::new(file);
333 data = serde_json::from_reader(reader).map_err(|err| {
334 format!(
335 "Error reading json: {}. {}",
336 file_path.display(),
337 err.to_string()
338 )
339 })?;
340 } else if is_ext(&file_path, "toml") {
341 let mut content = String::new();
342 file.read_to_string(&mut content).map_err(|err| {
343 format!(
344 "Error reading file: {}. {}",
345 file_path.display(),
346 err.to_string()
347 )
348 })?;
349 data = toml::from_str(&content).map_err(|err| {
350 format!(
351 "Error reading toml: {}. {}",
352 file_path.display(),
353 err.to_string()
354 )
355 })?;
356 }
357 }
358
359 match data.build(r) {
360 Ok(()) => Ok(data),
361 Err(e) => Err(e),
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn test_init() {
371 let mut root = PathBuf::from("tests");
373 let mut data = Data::default();
374 assert!(data.init(&root).is_ok());
375 let mut expected = Data::default();
376 expected.title = Some(String::from("tests"));
377 expected.contents = Some(vec![]); assert_eq!(data, expected);
379
380 root = PathBuf::from("tests/fixtures/data");
382 data = Data::default();
383 assert!(data.init(&root).is_ok());
384 let mut expected_file =
385 File::open("tests/fixtures/data/init_expected1.json").expect("could not open file");
386 let mut reader = BufReader::new(expected_file);
387 expected = serde_json::from_reader(reader).expect("could not read expected data");
388 assert_eq!(data, expected);
389
390 root = PathBuf::from("tests/fixtures/data/dir2");
392 data = Data::default();
393 assert!(data.init(&root).is_ok());
394 expected_file =
395 File::open("tests/fixtures/data/init_expected2.json").expect("could not open file");
396 reader = BufReader::new(expected_file);
397 expected = serde_json::from_reader(reader).expect("could not read expected data");
398 assert_eq!(data, expected);
399
400 let seed_file =
402 File::open("tests/fixtures/data/init_seed1.json").expect("could not open file");
403 reader = BufReader::new(seed_file);
404 data = serde_json::from_reader(reader).expect("could not read seed data");
405 root = PathBuf::from("tests/fixtures/data/dir1");
406 assert!(data.init(&root).is_ok());
407 expected_file =
408 File::open("tests/fixtures/data/init_expected3.json").expect("could not open file");
409 reader = BufReader::new(expected_file);
410 expected = serde_json::from_reader(reader).expect("could not read expected data");
411 assert_eq!(data, expected);
412
413 root = PathBuf::from("docs/examples/single_index");
415 data = Data::default();
416 assert!(data.init(&root).is_ok());
417 expected_file = File::open("tests/fixtures/data/init_expected_single.json")
418 .expect("could not open file");
419 reader = BufReader::new(expected_file);
420 expected = serde_json::from_reader(reader).expect("could not read expected data");
421 assert_eq!(data, expected);
422 }
423}