diff options
Diffstat (limited to 'src/blog.rs')
-rw-r--r-- | src/blog.rs | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/src/blog.rs b/src/blog.rs new file mode 100644 index 0000000..22cee05 --- /dev/null +++ b/src/blog.rs @@ -0,0 +1,186 @@ +use std::path::Path; + +use markdown::{mdast::Node, CompileOptions, Constructs, Options, ParseOptions}; +use serde::Deserialize; +use tokio::{fs, io::AsyncReadExt}; + +use chrono::{DateTime, Utc}; + +use crate::{ + error::BlossomError, + posts::{Post, PostType}, + Result, +}; + +static ARTICLES_DIR: &str = "./articles"; + +#[derive(Clone)] +pub struct Blogpost { + file_name: String, + title: String, + published_at: DateTime<Utc>, + updated_at: Option<DateTime<Utc>>, + tags: Vec<String>, + content: String, +} + +#[derive(Deserialize)] +pub struct BlogpostMetadata { + title: String, + published_at: String, + updated_at: Option<String>, + tags: Vec<String>, +} + +impl Post for Blogpost { + fn id(&self) -> &str { + &self.file_name + } + + fn subject(&self) -> Option<&str> { + Some(&self.title) + } + + fn published_at(&self) -> &DateTime<Utc> { + &self.published_at + } + + fn updated_at(&self) -> &DateTime<Utc> { + if let Some(updated_at) = &self.updated_at { + updated_at + } else { + &self.published_at + } + } + + fn tags(&self) -> &Vec<String> { + &self.tags + } + + fn lang(&self) -> &str { + "en" + } + + fn post_type(&self) -> PostType { + PostType::Article + } + + fn content(&self) -> &str { + &self.content + } +} + +impl Blogpost { + pub fn file_name(&self) -> &str { + &self.file_name + } + + async fn from_path(path: impl AsRef<Path>) -> Result<Self> { + 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 metadata: BlogpostMetadata = markdown::to_mdast(&buf, &parse_options) + .unwrap() + .try_into()?; + let file_name = path + .as_ref() + .file_name() + .ok_or(BlossomError::NotAFile)? + .to_string_lossy(); + let file_name = file_name[..file_name.len() - 3].to_owned(); + let updated_at = if let Some(updated_at) = metadata.updated_at { + Some(updated_at.parse::<DateTime<Utc>>()?) + } else { + None + }; + // 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(Self { + file_name, + title: metadata.title, + published_at: metadata.published_at.parse::<DateTime<Utc>>()?, + updated_at, + tags: metadata.tags, + content, + }) + } + + // async fn render(&mut self) -> Result<String> { + // let path = Path::new(ARTICLES_DIR) + // .join(&self.file_name) + // .with_extension("md"); + // // TODO: remove unwraps when file read failure + // let mut file = fs::File::open(&path).await.unwrap(); + // let mut buf = String::new(); + // file.read_to_string(&mut buf).await.unwrap(); + // let parse_options = ParseOptions { + // constructs: Constructs { + // frontmatter: true, + // ..Constructs::gfm() + // }, + // ..ParseOptions::default() + // }; + // 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(content) + // } +} + +impl TryFrom<Node> for BlogpostMetadata { + type Error = BlossomError; + + fn try_from(tree: Node) -> std::prelude::v1::Result<Self, Self::Error> { + 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), + _ => None, + }) { + return Ok(toml::from_str(toml)?); + }; + } + Err(BlossomError::NoMetadata) + } +} + +pub async fn get_blogposts() -> Result<Vec<Blogpost>> { + let mut blogposts: Vec<Blogpost> = Vec::new(); + let mut articles_dir = fs::read_dir(ARTICLES_DIR).await?; + while let Some(entry) = articles_dir.next_entry().await? { + let blogpost = Blogpost::from_path(entry.path()).await?; + blogposts.push(blogpost); + } + blogposts.sort_by_key(|post| post.published_at); + blogposts.reverse(); + Ok(blogposts) +} + +pub async fn get_blogpost(file_name: &str) -> Result<Blogpost> { + let path = Path::new(ARTICLES_DIR) + .join(Path::new(file_name)) + .with_extension("md"); + println!("{:?}", path); + Blogpost::from_path(path).await +} |