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