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}