1use document::Document;
2use futures::future::join_all;
3use glob::glob;
4use liquid::model::KString;
5use renderers::{
6 ContentRenderer, MarkdownRenderer, WritableFile,
7 globals::{LiquidGlobals, LiquidGlobalsPage},
8};
9use routes::route_from_path;
10use std::{collections::HashMap, error::Error, fmt::Display, path::PathBuf, sync::Arc};
11use template::Template;
12use tokio::sync::Mutex;
13
14use config::{TemplateLang, WeaverConfig};
15
16pub mod config;
21pub mod document;
22pub mod filters;
23pub mod renderers;
24pub mod routes;
25pub mod template;
26
27#[derive(Debug)]
28pub enum BuildError {
29 Err(String),
30 IoError(String),
31 GlobError(String),
32 DocumentError(String),
33 TemplateError(String),
34 RouteError(String),
35 RenderError(String),
36 JoinError(String),
37}
38
39impl Error for BuildError {}
40
41impl Display for BuildError {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 BuildError::Err(msg) => write!(f, "Generic Build Error: {}", msg),
45 BuildError::IoError(msg) => write!(f, "I/O Error: {}", msg),
46 BuildError::GlobError(msg) => write!(f, "Glob Error: {}", msg),
47 BuildError::DocumentError(msg) => write!(f, "Document Error: {}", msg),
48 BuildError::TemplateError(msg) => write!(f, "Template Error: {}", msg),
49 BuildError::RouteError(msg) => write!(f, "Route Error: {}", msg),
50 BuildError::RenderError(msg) => write!(f, "Render Error: {}", msg),
51 BuildError::JoinError(msg) => write!(f, "Task Join Error: {}", msg),
52 }
53 }
54}
55
56impl From<tokio::task::JoinError> for BuildError {
57 fn from(err: tokio::task::JoinError) -> Self {
58 BuildError::JoinError(err.to_string())
59 }
60}
61
62pub struct Weaver {
63 pub config: Arc<WeaverConfig>,
64 pub tags: Vec<String>,
65 pub routes: Vec<String>,
66 pub templates: Vec<Arc<Mutex<Template>>>,
67 pub documents: Vec<Arc<Mutex<Document>>>,
68 all_documents_by_route: HashMap<KString, Arc<Mutex<Document>>>,
69}
70
71impl Weaver {
72 pub fn new(base_path: PathBuf) -> Self {
73 Self {
74 config: Arc::new(WeaverConfig::new_from_path(base_path)),
75 tags: vec![],
76 routes: vec![],
77 templates: vec![],
78 documents: vec![],
79 all_documents_by_route: HashMap::new(),
80 }
81 }
82
83 pub fn scan_content(&mut self) -> &mut Self {
84 for entry in glob(format!("{}/**/*.md", self.config.content_dir).as_str())
85 .expect("Failed to read glob pattern")
86 {
87 match entry {
88 Ok(path) => {
89 let mut doc = Document::new_from_path(path.clone());
90
91 self.tags.append(&mut doc.metadata.tags);
92 let route = route_from_path(self.config.content_dir.clone().into(), path);
94 self.routes.push(route.clone());
95
96 let doc_arc_mutex = Arc::new(Mutex::new(doc));
97 self.documents.push(Arc::clone(&doc_arc_mutex));
98
99 self.all_documents_by_route
100 .insert(KString::from(route), doc_arc_mutex);
101 }
102 Err(e) => panic!("{:?}", e),
103 }
104 }
105
106 self
107 }
108
109 pub fn scan_templates(&mut self) -> &mut Self {
110 let extension = match self.config.templating_language {
111 TemplateLang::Liquid => ".liquid",
112 };
113 for entry in glob(format!("{}/**/*{}", self.config.template_dir, extension).as_str())
114 .expect("Failed to read glob pattern")
115 {
116 match entry {
117 Ok(pathbuf) => self
118 .templates
119 .push(Arc::new(Mutex::new(Template::new_from_path(pathbuf)))), Err(e) => panic!("{:?}", e), }
122 }
123
124 self
125 }
126
127 async fn write_result_to_system(&self, target: WritableFile) -> Result<(), BuildError> {
128 let full_output_path = target.path.clone();
129
130 if let Some(parent) = full_output_path.parent() {
132 tokio::fs::create_dir_all(parent).await.map_err(|e| {
133 BuildError::IoError(format!(
134 "Failed to create parent directories for {:?}: {}",
135 full_output_path, e
136 ))
137 })?;
138 }
139
140 tokio::fs::write(&full_output_path, target.contents)
141 .await
142 .map_err(|e| {
143 BuildError::IoError(format!(
144 "Failed to write file {:?}: {}",
145 full_output_path, e
146 ))
147 })?;
148
149 Ok(())
150 }
151
152 pub async fn build(&self) -> Result<(), BuildError> {
154 let mut all_liquid_pages_map: HashMap<KString, LiquidGlobalsPage> = HashMap::new();
155 let mut convert_tasks = vec![];
156
157 for document_arc_mutex in self.documents.iter() {
158 let doc_arc_mutex_clone = Arc::clone(document_arc_mutex);
159 let config_arc = Arc::clone(&self.config);
160
161 convert_tasks.push(tokio::spawn(async move {
162 let doc_guard = doc_arc_mutex_clone.lock().await;
163 let route = route_from_path(
164 config_arc.content_dir.clone().into(),
165 doc_guard.at_path.clone().into(),
166 );
167 let liquid_page = LiquidGlobalsPage::from(&*doc_guard);
168
169 (KString::from(route), liquid_page)
170 }));
171 }
172
173 let converted_pages: Vec<Result<(KString, LiquidGlobalsPage), tokio::task::JoinError>> =
174 join_all(convert_tasks).await;
175
176 for result in converted_pages {
177 let (route, liquid_page) = result.map_err(|e| BuildError::JoinError(e.to_string()))?;
178 all_liquid_pages_map.insert(route, liquid_page);
179 }
180
181 let all_liquid_pages_map_arc = Arc::new(all_liquid_pages_map);
182
183 let templates_arc = Arc::new(self.templates.clone());
184 let config_arc = Arc::clone(&self.config);
185
186 let mut tasks = vec![];
187
188 for document_arc_mutex in &self.documents {
189 let document_arc = Arc::clone(document_arc_mutex);
190
191 let all_liquid_pages_map_clone = Arc::clone(&all_liquid_pages_map_arc);
192 let mut globals =
193 LiquidGlobals::new(Arc::clone(&document_arc), &all_liquid_pages_map_clone).await;
194
195 let templates = Arc::clone(&templates_arc);
196 let config = Arc::clone(&config_arc);
197
198 let doc_task = tokio::spawn(async move {
199 let md_renderer = MarkdownRenderer::new(document_arc, templates, config);
200
201 md_renderer.render(&mut globals).await
202 });
203
204 tasks.push(doc_task);
205 }
206
207 let render_results: Vec<Result<Result<WritableFile, BuildError>, tokio::task::JoinError>> =
208 join_all(tasks).await; for join_result in render_results {
212 match join_result {
213 Ok(render_result) => match render_result {
214 Ok(writable_file) => {
215 self.write_result_to_system(writable_file).await?;
216 }
217 Err(render_error) => {
218 eprintln!("Rendering error: {}", render_error);
219 return Err(render_error);
220 }
221 },
222 Err(join_error) => {
223 eprintln!("Task join error: {}", join_error);
224 return Err(BuildError::JoinError(join_error.to_string()));
225 }
226 }
227 }
228
229 Ok(())
230 }
231}