use std::path::Path; use markdown::{mdast::Node, CompileOptions, Constructs, Options, ParseOptions}; use serde::de::DeserializeOwned; use tokio::{fs, io::AsyncReadExt}; use crate::{error::BlossomError, posts::Post, Result}; #[async_trait::async_trait] pub trait Article { type Metadata; type Article; /// the directory of the articles fn directory() -> &'static str; fn new(file_name: String, metadata: Self::Metadata, content: String) -> Result; async fn get_articles() -> Result> where ::Metadata: DeserializeOwned, ::Article: Post + Send, { let mut articles = Vec::new(); let mut articles_dir = fs::read_dir(Self::directory()).await?; while let Some(entry) = articles_dir.next_entry().await? { let (file_name, metadata, content) = Self::from_path(entry.path()).await?; let article = Self::new(file_name, metadata, content)?; articles.push(article); } articles.sort_by_key(|article| article.published_at().clone()); articles.reverse(); Ok(articles) } async fn get_article(file_name: &str) -> Result where ::Metadata: DeserializeOwned, { let path = Path::new(Self::directory()) .join(Path::new(file_name)) .with_extension("md"); println!("{:?}", path); let (file_name, metadata, content) = Self::from_path(path).await?; let article = Self::new(file_name, metadata, content)?; Ok(article) } /// returns the article id, the metadata, and the content from the file path async fn from_path( path: impl AsRef + Send + Sync, ) -> Result<(String, Self::Metadata, String)> where ::Metadata: DeserializeOwned, { let mut file = fs::File::open(&path).await?; let mut buf = String::new(); file.read_to_string(&mut buf).await?; let parse_options = ParseOptions { constructs: Constructs { frontmatter: true, ..Constructs::gfm() }, ..ParseOptions::default() }; let tree = markdown::to_mdast(&buf, &parse_options).unwrap(); let children = tree.children(); let metadata; if let Some(children) = children { if let Some(toml) = children.into_iter().find_map(|el| match el { Node::Toml(toml) => Some(&toml.value), _ => None, }) { metadata = toml } else { return Err(BlossomError::NoMetadata); } } else { return Err(BlossomError::NoMetadata); }; let metadata = toml::from_str(metadata)?; let file_name = path .as_ref() .file_prefix() .ok_or(BlossomError::NotAFile)? .to_string_lossy() .to_string(); // TODO: cache render only when needed let options = Options { parse: parse_options, compile: CompileOptions { gfm_task_list_item_checkable: true, allow_dangerous_html: true, ..CompileOptions::default() }, }; let content = markdown::to_html_with_options(&buf, &options).unwrap(); Ok((file_name, metadata, content)) } }