use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; use tokio::fs::DirEntry; use tokio::task::JoinHandle; use crate::error::CustomError; use crate::intermediate::blog_post_page::BlogPostPageInput; use crate::intermediate::registry::Registry; use crate::intermediate::IntermediateContext; use crate::walk_fs::walk_fs; use crate::walk_fs::WalkAction; use crate::walk_fs::WalkFsFilterResult; use super::BlogPostPage; #[derive(Debug)] pub(crate) struct BlogPost { pub(crate) id: String, pub(crate) pages: Vec, } impl BlogPost { pub(crate) async fn load_blog_post, R: AsRef, S: AsRef>( root_dir: R, posts_dir: S, post_dir: P, ) -> Result { async fn inner( _root_dir: &Path, posts_dir: &Path, post_dir: &Path, ) -> Result { let post_id = post_dir.strip_prefix(posts_dir)?.as_os_str(); let org_files = { let mut ret = Vec::new(); let org_files_iter = get_org_files(post_dir).await?; for entry in org_files_iter { ret.push(entry.await??); } ret }; let parsed_org_files = { let mut ret = Vec::new(); for (path, contents) in org_files.iter() { let parsed = organic::parser::parse_file(contents.as_str(), Some(path)) .map_err(|_| CustomError::Static("Failed to parse org-mode document."))?; ret.push((path, contents, parsed)); } ret }; let pages = { let mut ret = Vec::new(); for (real_path, _contents, parsed_document) in parsed_org_files.iter() { let mut registry = Registry::new(); // Assign IDs to the targets organic::types::AstNode::from(parsed_document) .iter_all_ast_nodes() .for_each(|node| { if let organic::types::AstNode::Target(target) = node { registry.get_target(target.value); } }); let registry = Arc::new(Mutex::new(registry)); let intermediate_context = IntermediateContext::new(registry)?; let relative_to_post_dir_path = real_path.strip_prefix(post_dir)?; ret.push( BlogPostPage::new( intermediate_context, BlogPostPageInput::new(relative_to_post_dir_path, parsed_document), ) .await?, ); } ret }; Ok(BlogPost { id: post_id.to_string_lossy().into_owned(), pages, }) } inner(root_dir.as_ref(), posts_dir.as_ref(), post_dir.as_ref()).await } /// Get the date for a blog post. /// /// The date is set by the "#+date" export setting. This will /// first attempt to read the date from an index.org if such a /// file exists. If that file does not exist or that file does not /// contain a date export setting, then this will iterate through /// all the pages under the blog post looking for any page that /// contains a date export setting. It will return the first date /// found. pub(crate) fn get_date(&self) -> Option<&str> { let index_page_date = self .get_index_page() .and_then(|index_page| index_page.date.as_deref()); if index_page_date.is_some() { return index_page_date; } self.pages .iter() .filter_map(|page| page.date.as_deref()) .next() } /// Get the blog post page for index.org pub(crate) fn get_index_page(&self) -> Option<&BlogPostPage> { self.pages .iter() .find(|page| page.path == Path::new("index.org")) } } async fn read_file(path: PathBuf) -> std::io::Result<(PathBuf, String)> { let contents = tokio::fs::read_to_string(&path).await?; Ok((path, contents)) } pub(crate) async fn get_org_files>( root_dir: P, ) -> Result>>, CustomError> { let org_files = walk_fs(root_dir, filter_to_org_files).await?; let org_files = org_files .into_iter() .map(|entry| entry.path()) .map(|path| tokio::spawn(read_file(path))); Ok(org_files) } async fn filter_to_org_files(entry: &DirEntry) -> WalkFsFilterResult { let file_type = entry.file_type().await?; if file_type.is_dir() { return Ok(WalkAction::Recurse); } if file_type.is_file() { if entry .path() .extension() .map(|ext| ext.eq_ignore_ascii_case("org")) .unwrap_or(false) { return Ok(WalkAction::HaltAndCapture); } return Ok(WalkAction::Halt); } unreachable!("Unhandled file type."); }