Skip to main content

mdpage/
data.rs

1//! Data serves both as the configuration data for mdPage
2//! as well as the actual template data for generating content.
3
4use 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/// Data serves both as the configuration data for mdPage
23/// as well as the actual template data for generating content.
24#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
25pub struct Data {
26    /// Whether to do full page or not.
27    pub full_page: Option<bool>,
28    /// Title used in header and title of the document.
29    pub title: Option<String>,
30    /// Subtitle used in header.
31    pub subtitle: Option<String>,
32    /// Author used in metadata.
33    pub author: Option<String>,
34    /// The favicon link.
35    pub icon: Option<String>,
36    /// The main content used for the front page.
37    pub main: Option<Content>,
38    /// The content of the document.
39    pub contents: Option<Vec<Content>>,
40    /// The custom JavaScript to be added in the `script` tag.
41    pub script: Option<String>,
42    /// The custom CSS to be added in the `style` tag.
43    pub style: Option<String>,
44    /// The custom style and script links.
45    pub links: Option<Vec<Link>>,
46    /// Custom header content.
47    pub header: Option<Content>,
48    /// Custom footer content.
49    pub footer: Option<Content>,
50}
51
52/// Link represents a link we can insert into the head of the generated document.
53#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
54pub struct Link {
55    /// Link type.
56    pub link_type: Option<LinkType>,
57    /// The link source.
58    pub src: Option<String>,
59    /// Optional integrity for SRI.
60    pub integrity: Option<String>,
61    /// Optional crossorigin for SRI.
62    pub crossorigin: Option<String>,
63}
64
65/// Link Type enum.
66#[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                    // fix dir entries
217                    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                            // get the base files
234                            if let Some(mut root_dir_contents) = init_dir_contents(root, &pathbuf) {
235                                dir_contents.append(&mut root_dir_contents);
236                            }
237
238                            // do subdirs
239                            let mut sub_dir_contents = init_dir_sections(&pathbuf)?;
240                            dir_contents.append(&mut sub_dir_contents);
241
242                            // add into the overall
243                            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
298/// Build the content data from a root directory path and optional initial value.
299pub 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        // empty
372        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![]); // initialized to empty
378        assert_eq!(data, expected);
379
380        // with subdirs
381        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        // with header and footer
391        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        // with pre-seeded data
401        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        // just single index
414        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}