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