pinch/
lib.rs

1use std::collections::HashMap;
2
3use crate::plugins::{Context, PluginDefinition, PluginRole};
4use serde::Deserialize;
5use std::cmp::Ordering;
6use toml::Value;
7use walkdir::{DirEntry, WalkDir};
8
9pub mod plugins;
10pub mod utils;
11
12#[derive(Debug, Deserialize, Clone)]
13pub struct InputFile {
14    pub filename: String,
15    pub path: String,
16    pub is_directory: bool,
17    pub extension: String,
18}
19
20impl InputFile {
21    pub fn create_directory(&self, output_filename: String) -> String {
22        utils::create_directory(
23            output_filename
24                .as_str()
25                .replace(&self.filename.to_string(), ""),
26        );
27        output_filename
28    }
29
30    pub fn replace_extensions(&self, new_value: &str) -> Self {
31        let new_filepath = self.replace_path_extension(new_value);
32
33        InputFile {
34            filename: self.replace_filename_extension(new_value),
35            path: new_filepath.to_string(),
36            is_directory: self.is_directory,
37            extension: utils::file_extension(new_filepath.as_str()),
38        }
39    }
40
41    fn from(dir_entry: DirEntry) -> Self {
42        let filename = dir_entry.file_name().to_str().unwrap().to_string();
43        InputFile {
44            filename: filename.to_string(),
45            extension: utils::file_extension(filename.as_str()),
46            path: dir_entry.path().to_str().unwrap().to_string(),
47            is_directory: dir_entry.file_type().is_dir(),
48        }
49    }
50
51    pub fn replace_filename_extension(&self, new_value: &str) -> String {
52        self.filename.replace(self.extension.as_str(), new_value)
53    }
54
55    pub fn replace_path_extension(&self, new_value: &str) -> String {
56        self.path.replace(self.extension.as_str(), new_value)
57    }
58
59    pub fn read_contents(&self) -> String {
60        utils::read_file(self.path.to_string())
61    }
62
63    pub fn is_in_directory(&self, directory: String, root: String) -> bool {
64        self.path.replace(&(root + "/"), "").starts_with(&directory)
65    }
66
67    pub fn is_extension(&self, extension: &str) -> bool {
68        self.extension.eq(extension)
69    }
70}
71
72#[derive(Debug, Deserialize)]
73pub struct Config {
74    pub name: Option<String>,
75    pub description: Option<String>,
76    pub assets_directory_name: Option<String>,
77    pub data_directory_name: Option<String>,
78    pub output_directory_name: Option<String>,
79    pub root_directory_path: Option<String>,
80    pub config_filename: Option<String>,
81    pub files: Option<Vec<InputFile>>,
82    pub plugin_options: Option<HashMap<String, Value>>,
83}
84
85impl Config {
86    pub fn sorted_plugins(&self) {}
87
88    pub fn from_file(file_path: &str) -> Self {
89        let contents = utils::read_file(file_path.to_string());
90        let mut config: Config = toml::from_str(&contents).unwrap();
91        config.config_filename = Some("inapinch.toml".to_string()); // TODO: pull from `file_path`
92        config.root_directory_path =
93            Some(file_path.replace(&("/".to_owned() + config.config_filename().as_str()), ""));
94        config.files = Some(config.find_files());
95        config
96    }
97
98    fn find_files(&self) -> Vec<InputFile> {
99        let mut files = Vec::new();
100        for entry in WalkDir::new(self.root_directory_path()) {
101            let input_file = InputFile::from(entry.unwrap());
102            if !input_file.is_directory
103                && input_file.filename.ne(&self.config_filename())
104                && !input_file
105                    .is_in_directory(self.output_directory_name(), self.root_directory_path())
106            {
107                files.push(input_file);
108            }
109        }
110        files
111    }
112
113    fn root_directory_path(&self) -> String {
114        self.root_directory_path.as_ref().unwrap().to_string()
115    }
116
117    fn config_filename(&self) -> String {
118        utils::to_string(self.config_filename.as_ref(), "inapinch.toml")
119    }
120
121    fn output_filename(&self, input_file: &InputFile) -> String {
122        let file_root = self.root_directory_path();
123        input_file.path.as_str().replace(
124            file_root.to_string().as_str(),
125            &*(file_root + "/" + self.output_directory_name().as_str()),
126        )
127    }
128
129    fn assets_directory_name(&self) -> String {
130        utils::to_string(self.assets_directory_name.as_ref(), "assets")
131    }
132
133    fn data_directory_name(&self) -> String {
134        utils::to_string(self.data_directory_name.as_ref(), "data")
135    }
136
137    fn output_directory_name(&self) -> String {
138        utils::to_string(self.output_directory_name.as_ref(), "dist")
139    }
140}
141
142pub struct Pinch {
143    pub config: Config,
144    pub plugins: HashMap<String, PluginDefinition>,
145    pub context: Context,
146}
147
148impl Pinch {
149    pub fn from_config(config: Config) -> Self {
150        Pinch {
151            config,
152            plugins: HashMap::new(),
153            context: HashMap::new(),
154        }
155    }
156
157    pub fn from_file(file_path: &str) -> Self {
158        Pinch::from_config(Config::from_file(file_path))
159    }
160
161    pub fn register_file(&mut self, input_file: InputFile) {
162        let mut files = self.config.files.clone().unwrap();
163        files.push(input_file);
164        self.config.files = Some(files);
165    }
166
167    pub fn register_plugin(&mut self, plugin: PluginDefinition) {
168        self.plugins.insert(plugin.name.to_string(), plugin);
169    }
170
171    pub fn remove_plugin(&mut self, name: String) -> Option<PluginDefinition> {
172        self.plugins.remove(name.as_str())
173    }
174
175    pub fn build_with_defaults(&mut self) {
176        if self.plugins.is_empty() {
177            self.plugins = HashMap::new();
178            self.register_plugin(plugins::assets::plugin());
179            self.register_plugin(plugins::data::plugin());
180            self.register_plugin(plugins::handlebars::plugin());
181            self.register_plugin(plugins::markdown::plugin());
182        }
183        self.build()
184    }
185
186    pub fn build(&mut self) {
187        self.pre_process();
188        self.process_files();
189        self.post_process();
190    }
191
192    fn pre_process(&mut self) {
193        if self.config.files.is_none() {
194            panic!("No files configured")
195        }
196
197        utils::create_directory(
198            self.config.root_directory_path() + "/" + self.config.output_directory_name().as_str(),
199        );
200
201        let mut plugins: Vec<&PluginDefinition> = self.plugins.values().collect();
202        plugins.sort_by(|a, b| match a.role {
203            PluginRole::LoadContext => match b.role {
204                PluginRole::LoadContext => Ordering::Equal,
205                PluginRole::Prep => Ordering::Greater,
206                _ => Ordering::Less,
207            },
208            PluginRole::Prep => match b.role {
209                PluginRole::LoadContext => Ordering::Less,
210                PluginRole::Prep => Ordering::Equal,
211                _ => Ordering::Less,
212            },
213            PluginRole::Transform => match b.role {
214                PluginRole::LoadContext => Ordering::Greater,
215                PluginRole::Prep => Ordering::Greater,
216                PluginRole::Transform => Ordering::Equal,
217                PluginRole::Custom => Ordering::Less,
218            },
219            PluginRole::Custom => match b.role {
220                PluginRole::Custom => Ordering::Equal,
221                _ => Ordering::Greater,
222            },
223        });
224        let mut all_new_files: Vec<InputFile> = vec![];
225        for plugin in plugins {
226            if plugin.pre_process.is_some() {
227                let (context, new_files) =
228                    plugin.pre_process.unwrap()(&self.config, self.context.to_owned());
229
230                self.context = context;
231                if new_files.is_some() {
232                    for new_file in new_files.unwrap() {
233                        all_new_files.push(new_file);
234                    }
235                }
236            }
237        }
238
239        for new_file in all_new_files {
240            self.register_file(new_file);
241        }
242    }
243
244    fn process_files(&self) {
245        for file in self.config.files.as_ref().unwrap() {
246            for (_name, plugin) in self.plugins.iter() {
247                if plugin.applies.is_some() && plugin.applies.unwrap()(file) {
248                    let apply_plugin = plugin.process.expect("`process` is required");
249                    let output_contents =
250                        apply_plugin(file.read_contents(), self.context.to_owned(), &self.config);
251                    let output_filename = self.config.output_filename(file);
252                    utils::create_directory(
253                        output_filename
254                            .as_str()
255                            .replace(&file.filename.to_string(), ""),
256                    );
257
258                    let create_output_filename = plugin.output_filename.expect("`output_filename` isn't set. This field is required for `PluginLifecycle::Process` plugins.");
259                    utils::create_file(
260                        create_output_filename(file, output_filename),
261                        output_contents,
262                    );
263                }
264            }
265        }
266    }
267
268    fn post_process(&self) {
269        for (_name, plugin) in self.plugins.iter() {
270            if plugin.post_process.is_some() {
271                plugin.post_process.unwrap()(&self.config, self.context.clone());
272            }
273        }
274    }
275}
276
277#[cfg(test)]
278mod test {
279    use super::*;
280
281    #[test]
282    fn read_options_from_file() {
283        let config = Config::from_file("example_apps/basic/inapinch.toml");
284        assert_eq!(config.name, Some("basic".to_string()));
285        assert_eq!(config.description, None);
286        assert_eq!(config.files.unwrap().len(), 1); // index.md, exclude inapinch.toml
287    }
288
289    #[test]
290    fn build_basic_site() {
291        let mut pinch = Pinch::from_file("example_apps/basic/inapinch.toml");
292        pinch.build_with_defaults();
293        assert_eq!(pinch.plugins.len(), 4); // markdown, data, handlebars, assets
294
295        let index_contents =
296            utils::read_file("example_apps/basic/dist/pages/index.html".to_string());
297        assert_eq!(index_contents.trim(), "<h1>pinch</h1>");
298        // utils::remove_directories("example_apps/basic/dist".to_string());
299    }
300
301    #[test]
302    fn build_complex_site() {
303        Pinch::from_file("example_apps/complex/inapinch.toml").build_with_defaults();
304
305        let index_contents =
306            utils::read_file("example_apps/complex/dist/pages/index.html".to_string());
307        assert_eq!(
308            index_contents.trim(),
309            "<h1>Hello Jeff</h1>\n<p>Would you like to subscribe to cat facts?</p>"
310        );
311        // utils::remove_directories("example_apps/complex/dist".to_string());
312    }
313}