diff options
Diffstat (limited to '')
-rw-r--r-- | src/article.rs | 99 |
1 files changed, 99 insertions, 0 deletions
diff --git a/src/article.rs b/src/article.rs new file mode 100644 index 0000000..057c49c --- /dev/null +++ b/src/article.rs @@ -0,0 +1,99 @@ +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<Self::Article>; + + async fn get_articles() -> Result<Vec<Self::Article>> + where + <Self as Article>::Metadata: DeserializeOwned, + <Self as Article>::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<Self::Article> + where + <Self as Article>::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<Path> + Send + Sync, + ) -> Result<(String, Self::Metadata, String)> + where + <Self as Article>::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)) + } +} |