weaver_lib/
lib.rs

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
17/// Weaver is the library that powers weaving, as in Hugo Weaving. It does nothing but compile
18/// templates and markdown files to their static counterparts.
19/// There is zero requirement for a config file at all, defaults are used- however specifying
20/// content locations can vary from user to user so afford them the opportunity to do so.
21pub 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
51// Helper function to normalize line endings in a byte vector
52pub fn normalize_line_endings(bytes: &[u8]) -> String {
53    let s = str::from_utf8(bytes).expect("Invalid UTF-8 in WritableFile content");
54    // Replace all CRLF (\r\n) with LF (\n)
55    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                    // Assuming route_from_path is correct and returns String
126                    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), // Panics on glob iteration error
164            }
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)))), // Panics on file read/parse errors
181                Err(e) => panic!("{:?}", e), // Panics on glob iteration error
182            }
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        // Ensure parent directories exist
192        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    // The main build orchestration function
215    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; // Await all rendering tasks
302
303        // Process the results of all rendering tasks
304        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}