mod article; mod note; use std::collections::HashSet; use async_trait::async_trait; use chrono::{DateTime, Utc}; use markdown::{mdast::Node, Constructs, Options, ParseOptions}; use rocket::http::Status; use serde::{ser::SerializeStruct, Deserialize, Serialize}; use tokio::fs; use tokio::io::AsyncReadExt; use crate::{error::BlossomError, Result}; #[derive(Serialize, Debug)] enum PostType { Article, Note, } #[derive(Debug)] pub struct Post { // id: i64, subject: Option, created_at: DateTime, updated_at: Option>, tags: Vec, post_type: PostType, render: Option, data: D, } impl Post { pub async fn render(&mut self) -> Result<()> { self.render = Some(self.data.render().await?); Ok(()) } } impl Serialize for Post { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { let mut state = serializer.serialize_struct("Post", 7)?; state.serialize_field("subject", &self.subject)?; state.serialize_field("created_at", &self.created_at.to_string())?; state.serialize_field( "updated_at", &self.updated_at.and_then(|time| Some(time.to_string())), )?; state.serialize_field("tags", &self.tags)?; state.serialize_field("post_type", &self.post_type)?; state.serialize_field("render", &self.render)?; state.serialize_field("data", &self.data)?; state.end() } } pub async fn get_blogposts() -> Result>> { let mut blogposts: Vec> = Vec::new(); let mut articles_dir = fs::read_dir("./articles").await?; while let Some(file) = articles_dir.next_entry().await? { let name = file.file_name(); let name = name.to_str().unwrap_or_default()[..name.len() - 3].to_owned(); let blogpost: Article = Article { path: file.path().to_str().unwrap_or_default().to_owned(), name, }; let blogpost = Post::try_from(blogpost).await.unwrap(); blogposts.push(blogpost); } blogposts.sort_by_key(|post| post.created_at); blogposts.reverse(); Ok(blogposts) } pub async fn get_blogpost(post_name: &str) -> Result> { let mut articles_dir = fs::read_dir("./articles").await?; while let Some(file) = articles_dir.next_entry().await? { let name = file.file_name(); let name = &name.to_str().unwrap_or_default()[..name.len() - 3]; if name == post_name { let blogpost = Article { path: file.path().to_str().unwrap_or_default().to_owned(), name: name.to_owned(), }; let blogpost = Post::try_from(blogpost).await?; return Ok(blogpost); } } Err(BlossomError::NotFound(Status::new(404))) } pub fn get_tags(posts: &Vec>) -> Vec<&String> { let mut tags = posts .into_iter() .fold(HashSet::new(), |mut acc, post| { let tags = &post.tags; for tag in tags { acc.insert(tag); } acc }) .into_iter() .collect::>(); tags.sort(); tags } pub fn filter_by_tags<'p, D: Content>( posts: Vec>, filter_tags: &HashSet, ) -> Vec> { posts .into_iter() .filter(|post| { for tag in &post.tags { match filter_tags.contains(tag) { true => return true, false => continue, } } false }) .collect() } #[async_trait] pub trait Content { async fn render(&self) -> Result; } #[derive(Serialize, Debug)] pub struct Article { path: String, name: String, } impl Article { async fn tree(&self) -> Result { let mut file = fs::File::open(&self.path).await?; let mut buf = String::new(); file.read_to_string(&mut buf).await?; Ok(markdown::to_mdast( &buf, &ParseOptions { constructs: Constructs { frontmatter: true, ..Constructs::default() }, ..ParseOptions::default() }, ) .unwrap()) } async fn frontmatter(&self) -> Result { let tree = self.tree().await?; let children = tree.children(); if let Some(children) = children { if let Some(toml) = children.into_iter().find_map(|el| match el { Node::Toml(toml) => Some(toml.value.to_owned()), _ => None, }) { return Ok(toml); }; } Err(BlossomError::NoMetadata(Status::new(500))) } } #[async_trait] impl Content for Article { async fn render(&self) -> Result { let mut file = fs::File::open(&self.path).await?; let mut buf = String::new(); file.read_to_string(&mut buf).await?; let options = Options { parse: ParseOptions { constructs: Constructs { frontmatter: true, ..Constructs::default() }, ..ParseOptions::default() }, ..Options::default() }; Ok(markdown::to_html_with_options(&buf, &options).unwrap()) } } #[derive(Deserialize)] struct ArticleMetadata { title: String, created_at: String, updated_at: Option, tags: Vec, } impl Post
{ async fn try_from(article: Article) -> Result> { let metadata = article.frontmatter().await?; let metadata: ArticleMetadata = toml::from_str(&metadata)?; let updated_at = if let Some(updated_at) = metadata.updated_at { Some(updated_at.parse::>()?) } else { None }; Ok(Post { subject: Some(metadata.title), created_at: metadata.created_at.parse::>()?, updated_at, tags: metadata.tags, post_type: PostType::Article, render: None, data: article, }) } }