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 syntect::{
15 highlighting::ThemeSet,
16 html::{ClassStyle, css_for_theme_with_class_style},
17};
18use tasks::{
19 WeaverTask, atom_feed_task::AtomFeedTask, public_copy_task::PublicCopyTask,
20 sitemap_task::SiteMapTask, well_known_copy_task::WellKnownCopyTask,
21};
22use template::Template;
23use tokio::{sync::Mutex, task::JoinHandle};
24
25pub mod config;
30pub mod document;
31pub mod document_toc;
32pub mod filters;
33pub mod partial;
34pub mod renderers;
35pub mod routes;
36pub mod slugify;
37pub mod tasks;
38pub mod template;
39
40pub fn normalize_line_endings(bytes: &[u8]) -> String {
42 let s = str::from_utf8(bytes).expect("Invalid UTF-8 in WritableFile content");
43 s.replace("\r\n", "\n")
45}
46
47#[derive(Debug)]
48pub enum BuildError {
49 Err(String),
50 IoError(String),
51 GlobError(String),
52 DocumentError(String),
53 TemplateError(String),
54 RouteError(String),
55 RenderError(String),
56 JoinError(String),
57}
58
59impl Error for BuildError {}
60
61impl Display for BuildError {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 BuildError::Err(msg) => write!(f, "Generic Build Error: {}", msg),
65 BuildError::IoError(msg) => write!(f, "I/O Error: {}", msg),
66 BuildError::GlobError(msg) => write!(f, "Glob Error: {}", msg),
67 BuildError::DocumentError(msg) => write!(f, "Document Error: {}", msg),
68 BuildError::TemplateError(msg) => write!(f, "Template Error: {}", msg),
69 BuildError::RouteError(msg) => write!(f, "Route Error: {}", msg),
70 BuildError::RenderError(msg) => write!(f, "Render Error: {}", msg),
71 BuildError::JoinError(msg) => write!(f, "Task Join Error: {}", msg),
72 }
73 }
74}
75
76impl From<tokio::task::JoinError> for BuildError {
77 fn from(err: tokio::task::JoinError) -> Self {
78 BuildError::JoinError(err.to_string())
79 }
80}
81
82pub struct Weaver {
83 pub config: Arc<WeaverConfig>,
84 pub tags: Vec<String>,
85 pub routes: Vec<String>,
86 pub templates: Vec<Arc<Mutex<Template>>>,
87 pub documents: Vec<Arc<Mutex<Document>>>,
88 pub partials: Vec<Partial>,
89 pub all_documents_by_route: HashMap<KString, Arc<Mutex<Document>>>,
90 tasks: Vec<Arc<Box<dyn WeaverTask>>>,
91}
92
93impl Weaver {
94 pub fn new(base_path: PathBuf) -> Self {
95 Self {
96 config: Arc::new(WeaverConfig::new(base_path)),
97 tags: vec![],
98 routes: vec![],
99 templates: vec![],
100 partials: vec![],
101 documents: vec![],
102 all_documents_by_route: HashMap::new(),
103 tasks: vec![
104 Arc::new(Box::new(PublicCopyTask {})),
105 Arc::new(Box::new(WellKnownCopyTask {})),
106 Arc::new(Box::new(SiteMapTask {})),
107 Arc::new(Box::new(AtomFeedTask {})),
108 ],
109 }
110 }
111
112 pub fn scan_content(&mut self) -> &mut Self {
113 for entry in glob(format!("{}/**/*.md", self.config.content_dir).as_str())
114 .expect("Failed to read glob pattern")
115 {
116 match entry {
117 Ok(path) => {
118 let mut doc = Document::new_from_path(
119 self.config.content_dir.clone().into(),
120 path.clone(),
121 );
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 fn get_css_for_theme(&self) -> String {
214 let theme_set = ThemeSet::load_defaults();
216
217 if let Some(theme) = theme_set.themes.get(&self.config.syntax_theme) {
219 css_for_theme_with_class_style(theme, ClassStyle::Spaced).unwrap()
220 } else {
221 eprintln!(
222 "Didn't find theme '{}'. Defaulting.",
223 &self.config.syntax_theme
224 );
225 css_for_theme_with_class_style(
226 theme_set.themes.get("base16-ocean.dark").unwrap(),
227 ClassStyle::Spaced,
228 )
229 .unwrap()
230 }
231 }
232 pub async fn build(&self) -> Result<(), BuildError> {
234 let mut all_liquid_pages_map: HashMap<KString, LiquidGlobalsPage> = HashMap::new();
235 let mut convert_tasks = vec![];
236 let extra_css = self.get_css_for_theme();
237
238 for document_arc_mutex in self.documents.iter() {
239 let doc_arc_mutex_clone = Arc::clone(document_arc_mutex);
240 let config_arc = Arc::clone(&self.config);
241
242 convert_tasks.push(tokio::spawn(async move {
243 let doc_guard = doc_arc_mutex_clone.lock().await;
244 let route = route_from_path(
245 config_arc.content_dir.clone().into(),
246 doc_guard.at_path.clone().into(),
247 );
248 let liquid_page = LiquidGlobalsPage::from(&*doc_guard);
249
250 (KString::from(route), liquid_page)
251 }));
252 }
253
254 let converted_pages: Vec<Result<(KString, LiquidGlobalsPage), tokio::task::JoinError>> =
255 join_all(convert_tasks).await;
256
257 for result in converted_pages {
258 let (route, liquid_page) = result.map_err(|e| BuildError::JoinError(e.to_string()))?;
259 all_liquid_pages_map.insert(route, liquid_page);
260 }
261
262 let all_liquid_pages_map_arc = Arc::new(all_liquid_pages_map);
263
264 let templates_arc = Arc::new(self.templates.clone());
265 let config_arc_copy = Arc::clone(&self.config.clone());
268 let partials_arc = Arc::new(self.partials.clone());
269
270 let mut tasks: Vec<JoinHandle<Result<Option<WritableFile>, BuildError>>> = vec![];
271
272 for document_arc_mutex in &self.documents {
276 let document_arc = Arc::clone(document_arc_mutex);
277
278 let all_liquid_pages_map_clone = Arc::clone(&all_liquid_pages_map_arc);
279 let mut globals = LiquidGlobals::new(
280 Arc::clone(&document_arc),
281 &all_liquid_pages_map_clone,
282 Arc::clone(&self.config),
283 )
284 .await;
285 globals.extra_css = extra_css.clone();
286
287 let templates = Arc::clone(&templates_arc);
288 let config = Arc::clone(&config_arc_copy);
289 let partials = Arc::clone(&partials_arc);
290
291 let doc_task = tokio::spawn(async move {
292 let md_renderer =
293 MarkdownRenderer::new(document_arc, templates, config, partials.to_vec());
294
295 md_renderer.render(&mut globals, partials.to_vec()).await
296 });
297
298 tasks.push(doc_task);
299 }
300
301 tasks.extend(self.tasks.iter().map(|t| {
302 let t = Arc::clone(t);
303 let config = Arc::clone(&config_arc_copy);
304 let content = Arc::clone(&all_liquid_pages_map_arc);
305 tokio::spawn(async move { t.run(config, &content).await })
306 }));
307
308 let render_results: Vec<
309 Result<Result<Option<WritableFile>, BuildError>, tokio::task::JoinError>,
310 > = join_all(tasks).await; for join_result in render_results {
314 match join_result {
315 Ok(render_result) => match render_result {
316 Ok(writable_file_option) => match writable_file_option {
317 Some(writable_file) => {
318 if writable_file.path.as_os_str() != "" && writable_file.emit {
319 self.write_result_to_system(writable_file).await?;
320 }
321 }
322 None => continue,
323 },
324 Err(render_error) => {
325 eprintln!("Rendering error: {}", render_error.red());
326 return Err(render_error);
327 }
328 },
329 Err(join_error) => {
330 eprintln!("Task join error: {}", join_error.red());
331 return Err(BuildError::JoinError(join_error.to_string()));
332 }
333 }
334 }
335
336 Ok(())
337 }
338}