mkhtmllib/
lib.rs

1#![crate_name = "mkhtmllib"]
2//! # mkhtmllib
3//! This is supposed to go along the official Terminal Wrapper, but it is actually just supposed to have any Wrapper,
4//!
5//! Makes HTML files from `header.html` and `footer.html` and `pages`,
6//!
7//! Used to be a simple bash script that I used to build simple sites years ago, then I lost control over myself..
8//!
9//! mkhtml works in a simple way, it builds HTML files using a simple pattern:
10//! - {header.html}
11//! - {pages/\*}
12//! - {footer.html}
13//!
14//! Built files will be named after their name in `pages_dir`
15//!
16//! Copies {static/\*} into {build/static/}.
17
18extern crate fs_extra;
19extern crate walkdir;
20
21use fs_extra::dir::{copy, CopyOptions};
22use std::env::current_dir;
23use std::fs::{create_dir, remove_dir_all, File};
24use std::io::{Read, Write};
25use std::path::PathBuf;
26use walkdir::WalkDir;
27use Error::{CopyFailed, ReadFailed, RemoveFailed, WriteFailed};
28
29
30/// # mkhtml main function
31/// Takes a config as input, does not return anything,
32///
33/// Builds files using configuration given in `Config`,
34///
35/// Adds a simple HTML comment watermark at the top of every HTML file built with it,
36///
37/// Uses `walkdir` to go through every folders and sub folders than might be in `pages_dir`,
38///
39/// If `CARGO_PKG_VERSION` environment variable is "dry", files will be deleted after run,
40///
41/// Returns `()` on success, `mkhtmllib::Error` on errors.
42pub fn mkhtml(config: Config) -> Result<(), Error> {
43    // if build dir already exists, delete
44    // Report error
45    if config.clone().get_build_dir().is_dir() {
46        let rm = remove_dir_all(config.clone().get_build_dir());
47
48        if rm.is_err() {
49            return Err(RemoveFailed);
50        }
51    }
52
53    // for every paths we need
54    for d in config.clone().iter() {
55        // check if it exists, if not, create directory
56        chk_dir(d)?;
57    }
58
59    // list files in pages_dir
60    let files = WalkDir::new(config.clone().get_pages_dir()).follow_links(true);
61
62    for file in files {
63        let f = file.unwrap();
64
65        // for every directory in pages_dir (recursive)
66        if f.path().is_dir() {
67            // create path strings and build final_path from path in pages_dir
68            let base_path = f.path().as_os_str().to_os_string().into_string().unwrap();
69
70            let from = config.clone().get_pages_dir().canonicalize().unwrap().into_os_string().into_string().unwrap();
71            let to = config.clone().get_pages_dir().canonicalize().unwrap().into_os_string().into_string().unwrap();
72
73            let final_path = str::replace(&base_path, &from, &to);
74
75            // create path to check in build_dir & checks it
76            chk_dir(PathBuf::from(final_path))?;
77        } else {
78            // for every files in pages_dir (recursive)
79            let watermark_str =
80                "<!-- Built with mkhtml 3 (https://github.com/jusdepatate/mkhtml) -->".to_string();
81
82            // create path strings and build final_path from path in pages_dir
83            let base_path = f.path().as_os_str().to_os_string().into_string().unwrap();
84            let from = config.clone().get_pages_dir().canonicalize().unwrap().into_os_string().into_string().unwrap();
85            let to = config.clone().get_build_dir().canonicalize().unwrap().into_os_string().into_string().unwrap();
86            let final_path = str::replace(&base_path, &from, &to);
87
88            // read header and footer files
89            let header = read_file(config.clone().get_parts_dir().join("header.html"))?;
90            let footer = read_file(config.clone().get_parts_dir().join("footer.html"))?;
91
92            // create file body using watermark, header, page content and footer
93            let file_body = watermark_str + "\n" +
94                &*header + "\n" +
95                &*read_file(PathBuf::from(base_path))? + "\n" +
96                &*footer;
97
98            // write content to file
99            write_file(PathBuf::from(final_path.clone()), file_body)?;
100        };
101    };
102
103    // Copying `static_dir` into `build_dir`.
104    let copy = copy(config.clone().get_static_dir(), config.clone().get_build_dir(), &CopyOptions::new());
105    if copy.is_err() {
106        return Err(CopyFailed)
107    }
108
109    const VERSION: &str = env!("CARGO_PKG_VERSION");
110    if VERSION == "dry" {
111        remove_dir_all(config.get_build_dir()).unwrap();
112    }
113
114    Ok(())
115}
116
117/// # Write File
118/// Takes a path: `String` and content: `String` as argument,
119///
120/// Does not return anything,
121///
122/// Simply writes to path given as argument,
123///
124/// Returns `()` on success, `mkhtmllib::Error` on errors.
125fn write_file(path: PathBuf, content: String) -> Result<(), Error> {
126    // try to create file
127    let create = File::create(&path);
128    if create.is_err() {
129        return Err(WriteFailed)
130    }
131    let mut file = create.unwrap();
132
133    // try to write to file, panic on error
134    let write = file.write_all(content.as_bytes());
135
136    if write.is_err() {
137        return Err(WriteFailed)
138    }
139    Ok(())
140}
141
142/// # Read File
143/// Takes a path: `PathBuf` as argument,
144///
145/// Returns file content in a `String`,
146///
147/// Simply reads a path given as argument,
148///
149/// Returns `()` on success, `mkhtmllib::Error` on errors.
150fn read_file(path: PathBuf) -> Result<String, Error> {
151    // try to open file, panic on error
152    let open = File::open(path);
153    if open.is_err() {
154        return Err(ReadFailed)
155    }
156
157    let mut file = open.unwrap();
158
159    // try to read file, panic on error
160    let mut content = "".to_string();
161    return match file.read_to_string(&mut content) {
162        Ok(_) => Ok(content),
163        Err(_) => Err(ReadFailed),
164    }
165}
166
167/// # Check/Create directory
168/// Takes a directory path as argument,
169///
170/// Check that it exists, if not, creates given path as a folder,
171///
172/// Returns `()` on success, `mkhtmllib::Error` on errors.
173fn chk_dir(path: PathBuf) -> Result<(), Error>{
174    // if path doesn't exist
175    if !path.is_dir() {
176        // create path, panic on error
177        let create = create_dir(path);
178        if create.is_err() {
179            return Err(WriteFailed);
180        }
181    }
182    Ok(())
183}
184
185/// # mkhtml lib errors
186#[derive(Debug)]
187pub enum Error {
188    WriteFailed,
189    RemoveFailed,
190    CopyFailed,
191    ReadFailed,
192}
193
194/// # Config struct
195/// Holds basic configuration of mkhtml, including all required paths for it to work,
196///
197/// Has `get_*` and `set_*`,
198///
199/// Clone-able.
200#[derive(Clone)]
201pub struct Config {
202    pages_dir: PathBuf,
203    parts_dir: PathBuf,
204    static_dir: PathBuf,
205    build_dir: PathBuf,
206}
207
208impl Config {
209    /// Returns a sample `Config`.
210    pub fn new() -> Config {
211        // get cwd and turn it into a string
212        let cwd = current_dir().unwrap();
213
214        // Build default configuration
215        Config {
216            pages_dir: cwd.join("pages"),
217            parts_dir: cwd.join("parts"),
218            static_dir: cwd.join("static"),
219            build_dir: cwd.join("builds"),
220        }
221    }
222
223    /// Returns config in a `[PathBuf; 4]`
224    pub fn iter(self) -> [PathBuf; 4] {
225        return [self.pages_dir, self.parts_dir, self.static_dir, self.build_dir]
226    }
227
228    pub fn get_pages_dir(self) -> PathBuf { return self.pages_dir }
229    pub fn get_parts_dir(self) -> PathBuf { return self.parts_dir }
230    pub fn get_static_dir(self) -> PathBuf { return self.static_dir }
231    pub fn get_build_dir(self) -> PathBuf { return self.build_dir }
232
233    pub fn set_pages_dir(&mut self, path: PathBuf) { self.pages_dir = path }
234    pub fn set_parts_dir(&mut self, path: PathBuf) { self.parts_dir = path }
235    pub fn set_static_dir(&mut self, path: PathBuf) { self.static_dir = path }
236    pub fn set_build_dir(&mut self, path: PathBuf) { self.build_dir = path }
237}
238
239#[cfg(test)]
240mod tests {
241    use {chk_dir, read_file, write_file};
242    use std::env::current_dir;
243    use std::fs::remove_file;
244    use std::path::PathBuf;
245    use walkdir::WalkDir;
246
247    #[test]
248    fn test_write_file() {
249        // tries to write a file that's named after the current dir +"a" with test
250        write_file(current_dir().unwrap().join("a"), "test".to_string()).unwrap();
251        remove_file(current_dir().unwrap().join("a")).unwrap();
252    }
253
254    #[test]
255    #[should_panic]
256    fn test_write_file_panic() {
257        // tries to write to "/" (either not a file or permission denied)
258        write_file(PathBuf::from("/"), "test".to_string()).unwrap();
259    }
260
261    #[test]
262    fn test_read_file() {
263        // find the first file in the parent directory and read it
264        for file in WalkDir::new("..").into_iter().filter_map(|file| file.ok()) {
265            if file.metadata().unwrap().is_file() {
266                read_file(PathBuf::from(file.path())).unwrap();
267                return
268            }
269        }
270    }
271
272    #[test]
273    #[should_panic]
274    fn test_read_file_panic() {
275        // tries to read "/" (is either not a file or doesnt exist)
276        read_file(PathBuf::from("/")).unwrap();
277    }
278
279    #[test]
280    fn test_chk_dir() {
281        // checks that the current directory exists
282        chk_dir(current_dir().unwrap()).unwrap();
283    }
284
285    #[test]
286    #[should_panic]
287    fn test_chk_dir_panic() {
288        // check that this stupidly named folder exists
289        chk_dir(PathBuf::from("/b3VpCg==/")).unwrap();
290    }
291}