1use config::{TemplateLang, WeaverConfig};
2use document::Document;
3use futures::future::join_all;
4use glob::glob;
5use liquid::model::KString;
6use owo_colors::OwoColorize;
7use partial::Partial;
8use renderers::{
9 ContentRenderer, MarkdownRenderer, WritableFile,
10 globals::{LiquidGlobals, LiquidGlobalsPage},
11};
12use routes::route_from_path;
13use std::{collections::HashMap, error::Error, fmt::Display, path::PathBuf, sync::Arc};
14use template::Template;
15use tokio::sync::Mutex;
16
17pub mod config;
22pub mod document;
23pub mod filters;
24pub mod partial;
25pub mod renderers;
26pub mod routes;
27pub mod template;
28
29use std::fs;
30use std::path::Path;
31
32fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<WritableFile, BuildError> {
33 fs::create_dir_all(&dst).unwrap();
34 for entry in fs::read_dir(src).unwrap() {
35 let entry = entry.unwrap();
36 let ty = entry.file_type().unwrap();
37 if ty.is_dir() {
38 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
39 } else {
40 fs::copy(entry.path(), dst.as_ref().join(entry.file_name())).unwrap();
41 }
42 }
43 Ok(WritableFile {
44 contents: "".into(),
45 path: "".into(),
46 emit: true,
47 })
48}
49
50pub fn normalize_line_endings(bytes: &[u8]) -> String {
52 let s = str::from_utf8(bytes).expect("Invalid UTF-8 in WritableFile content");
53 s.replace("\r\n", "\n")
55}
56
57#[derive(Debug)]
58pub enum BuildError {
59 Err(String),
60 IoError(String),
61 GlobError(String),
62 DocumentError(String),
63 TemplateError(String),
64 RouteError(String),
65 RenderError(String),
66 JoinError(String),
67}
68
69impl Error for BuildError {}
70
71impl Display for BuildError {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match self {
74 BuildError::Err(msg) => write!(f, "Generic Build Error: {}", msg),
75 BuildError::IoError(msg) => write!(f, "I/O Error: {}", msg),
76 BuildError::GlobError(msg) => write!(f, "Glob Error: {}", msg),
77 BuildError::DocumentError(msg) => write!(f, "Document Error: {}", msg),
78 BuildError::TemplateError(msg) => write!(f, "Template Error: {}", msg),
79 BuildError::RouteError(msg) => write!(f, "Route Error: {}", msg),
80 BuildError::RenderError(msg) => write!(f, "Render Error: {}", msg),
81 BuildError::JoinError(msg) => write!(f, "Task Join Error: {}", msg),
82 }
83 }
84}
85
86impl From<tokio::task::JoinError> for BuildError {
87 fn from(err: tokio::task::JoinError) -> Self {
88 BuildError::JoinError(err.to_string())
89 }
90}
91
92pub struct Weaver {
93 pub config: Arc<WeaverConfig>,
94 pub tags: Vec<String>,
95 pub routes: Vec<String>,
96 pub templates: Vec<Arc<Mutex<Template>>>,
97 pub documents: Vec<Arc<Mutex<Document>>>,
98 pub partials: Vec<Partial>,
99 all_documents_by_route: HashMap<KString, Arc<Mutex<Document>>>,
100}
101
102impl Weaver {
103 pub fn new(base_path: PathBuf) -> Self {
104 Self {
105 config: Arc::new(WeaverConfig::new(base_path)),
106 tags: vec![],
107 routes: vec![],
108 templates: vec![],
109 partials: vec![],
110 documents: vec![],
111 all_documents_by_route: HashMap::new(),
112 }
113 }
114
115 pub fn scan_content(&mut self) -> &mut Self {
116 for entry in glob(format!("{}/**/*.md", self.config.content_dir).as_str())
117 .expect("Failed to read glob pattern")
118 {
119 match entry {
120 Ok(path) => {
121 let mut doc = Document::new_from_path(path.clone());
122
123 self.tags.append(&mut doc.metadata.tags);
124 let route = route_from_path(self.config.content_dir.clone().into(), path);
126 self.routes.push(route.clone());
127
128 let doc_arc_mutex = Arc::new(Mutex::new(doc));
129 self.documents.push(Arc::clone(&doc_arc_mutex));
130
131 self.all_documents_by_route
132 .insert(KString::from(route), doc_arc_mutex);
133 }
134 Err(e) => panic!("{:?}", e),
135 }
136 }
137
138 self
139 }
140
141 pub fn scan_partials(&mut self) -> &mut Self {
142 let extension = match self.config.templating_language {
143 TemplateLang::Liquid => ".liquid",
144 };
145 println!(
146 "Searching for {} templates in {}",
147 &extension, &self.config.partials_dir
148 );
149 for entry in glob(format!("{}/**/*{}", self.config.partials_dir, extension).as_str())
150 .expect("Failed to read glob pattern")
151 {
152 match entry {
153 Ok(pathbuf) => {
154 println!(
155 "Found partial {}, registering {}",
156 pathbuf.display(),
157 pathbuf.file_name().unwrap().to_string_lossy()
158 );
159 let partial = Partial::new_from_path(pathbuf);
160 self.partials.push(partial);
161 }
162 Err(e) => panic!("{:?}", e), }
164 }
165
166 self
167 }
168
169 pub fn scan_templates(&mut self) -> &mut Self {
170 let extension = match self.config.templating_language {
171 TemplateLang::Liquid => ".liquid",
172 };
173 for entry in glob(format!("{}/**/*{}", self.config.template_dir, extension).as_str())
174 .expect("Failed to read glob pattern")
175 {
176 match entry {
177 Ok(pathbuf) => self
178 .templates
179 .push(Arc::new(Mutex::new(Template::new_from_path(pathbuf)))), Err(e) => panic!("{:?}", e), }
182 }
183
184 self
185 }
186
187 async fn write_result_to_system(&self, target: WritableFile) -> Result<(), BuildError> {
188 let full_output_path = target.path.clone();
189
190 if let Some(parent) = full_output_path.parent() {
192 tokio::fs::create_dir_all(parent).await.map_err(|e| {
193 BuildError::IoError(format!(
194 "Failed to create parent directories for {:?}: {}",
195 full_output_path, e
196 ))
197 })?;
198 }
199
200 println!("Writing {}", full_output_path.display().green());
201 tokio::fs::write(&full_output_path, target.contents)
202 .await
203 .map_err(|e| {
204 BuildError::IoError(format!(
205 "Failed to write file {:?}: {}",
206 full_output_path, e
207 ))
208 })?;
209
210 Ok(())
211 }
212
213 pub async fn build(&self) -> Result<(), BuildError> {
215 let mut all_liquid_pages_map: HashMap<KString, LiquidGlobalsPage> = HashMap::new();
216 let mut convert_tasks = vec![];
217
218 for document_arc_mutex in self.documents.iter() {
219 let doc_arc_mutex_clone = Arc::clone(document_arc_mutex);
220 let config_arc = Arc::clone(&self.config);
221
222 convert_tasks.push(tokio::spawn(async move {
223 let doc_guard = doc_arc_mutex_clone.lock().await;
224 let route = route_from_path(
225 config_arc.content_dir.clone().into(),
226 doc_guard.at_path.clone().into(),
227 );
228 let liquid_page = LiquidGlobalsPage::from(&*doc_guard);
229
230 (KString::from(route), liquid_page)
231 }));
232 }
233
234 let converted_pages: Vec<Result<(KString, LiquidGlobalsPage), tokio::task::JoinError>> =
235 join_all(convert_tasks).await;
236
237 for result in converted_pages {
238 let (route, liquid_page) = result.map_err(|e| BuildError::JoinError(e.to_string()))?;
239 all_liquid_pages_map.insert(route, liquid_page);
240 }
241
242 let all_liquid_pages_map_arc = Arc::new(all_liquid_pages_map);
243
244 let templates_arc = Arc::new(self.templates.clone());
245 let config_arc = Arc::clone(&self.config);
246 let partials_arc = Arc::new(self.partials.clone());
247
248 let mut tasks = vec![];
249
250 for document_arc_mutex in &self.documents {
251 let document_arc = Arc::clone(document_arc_mutex);
252
253 let all_liquid_pages_map_clone = Arc::clone(&all_liquid_pages_map_arc);
254 let mut globals =
255 LiquidGlobals::new(Arc::clone(&document_arc), &all_liquid_pages_map_clone).await;
256
257 let templates = Arc::clone(&templates_arc);
258 let config = Arc::clone(&config_arc);
259 let partials = Arc::clone(&partials_arc);
260
261 let doc_task = tokio::spawn(async move {
262 let md_renderer =
263 MarkdownRenderer::new(document_arc, templates, config, partials.to_vec());
264
265 md_renderer.render(&mut globals, partials.to_vec()).await
266 });
267
268 tasks.push(doc_task);
269 }
270
271 let public_copy_task = tokio::spawn(async move {
272 let config = Arc::clone(&config_arc);
273 let folder_name = config
274 .public_dir
275 .clone()
276 .split('/')
277 .next_back()
278 .unwrap()
279 .to_string();
280 let target = format!("{}/{}", config.build_dir.clone(), folder_name);
281
282 if fs::exists(&config.public_dir)
283 .expect("failed to check if there was a public directory")
284 {
285 println!("Copying {} to {}", config.public_dir.clone(), &target);
286
287 copy_dir_all(config.public_dir.clone(), target)
288 } else {
289 Ok(WritableFile {
290 contents: "".into(),
291 path: "".into(),
292 emit: false,
293 })
294 }
295 });
296
297 tasks.push(public_copy_task);
298
299 let render_results: Vec<Result<Result<WritableFile, BuildError>, tokio::task::JoinError>> =
300 join_all(tasks).await; for join_result in render_results {
304 match join_result {
305 Ok(render_result) => match render_result {
306 Ok(writable_file) => {
307 if writable_file.path.as_os_str() != "" && writable_file.emit {
308 self.write_result_to_system(writable_file).await?;
309 }
310 }
311 Err(render_error) => {
312 eprintln!("Rendering error: {}", render_error.red());
313 return Err(render_error);
314 }
315 },
316 Err(join_error) => {
317 eprintln!("Task join error: {}", join_error.red());
318 return Err(BuildError::JoinError(join_error.to_string()));
319 }
320 }
321 }
322
323 Ok(())
324 }
325}