diff options
-rw-r--r-- | src/article.rs | 99 | ||||
-rw-r--r-- | src/blog.rs | 146 | ||||
-rw-r--r-- | src/main.rs | 16 | ||||
-rw-r--r-- | src/poetry.rs | 48 |
4 files changed, 188 insertions, 121 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)) + } +} diff --git a/src/blog.rs b/src/blog.rs index 22cee05..0e757d5 100644 --- a/src/blog.rs +++ b/src/blog.rs @@ -7,12 +7,13 @@ use tokio::{fs, io::AsyncReadExt}; use chrono::{DateTime, Utc}; use crate::{ + article::Article, error::BlossomError, posts::{Post, PostType}, Result, }; -static ARTICLES_DIR: &str = "./articles"; +static DIRECTORY: &str = "./articles"; #[derive(Clone)] pub struct Blogpost { @@ -32,6 +33,32 @@ pub struct BlogpostMetadata { tags: Vec<String>, } +impl Article for Blogpost { + type Metadata = BlogpostMetadata; + + type Article = Blogpost; + + fn directory() -> &'static str { + DIRECTORY + } + + fn new(file_name: String, metadata: Self::Metadata, content: String) -> Result<Self::Article> { + let updated_at = if let Some(updated_at) = metadata.updated_at { + Some(updated_at.parse::<DateTime<Utc>>()?) + } else { + None + }; + Ok(Blogpost { + file_name, + title: metadata.title, + published_at: metadata.published_at.parse::<DateTime<Utc>>()?, + updated_at, + tags: metadata.tags, + content, + }) + } +} + impl Post for Blogpost { fn id(&self) -> &str { &self.file_name @@ -45,12 +72,8 @@ impl Post for Blogpost { &self.published_at } - fn updated_at(&self) -> &DateTime<Utc> { - if let Some(updated_at) = &self.updated_at { - updated_at - } else { - &self.published_at - } + fn updated_at(&self) -> Option<&DateTime<Utc>> { + self.updated_at.as_ref() } fn tags(&self) -> &Vec<String> { @@ -74,113 +97,4 @@ 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 } diff --git a/src/main.rs b/src/main.rs index de9b7ad..3d52f46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,17 @@ +#![feature(path_file_prefix)] + +mod article; mod atom; mod blog; mod error; mod live; +mod poetry; mod posts; mod scrobbles; mod skweets; mod templates; mod utils; -use std::rc::Rc; use std::{collections::HashSet, time::Duration}; use poem::http::StatusCode; @@ -28,6 +31,9 @@ use error::BlossomError; use serde::Deserialize; use tracing_subscriber::FmtSubscriber; +use crate::article::Article; +use crate::blog::Blogpost; + type Result<T> = std::result::Result<T, BlossomError>; #[derive(RustEmbed)] @@ -41,7 +47,7 @@ async fn home(Data(reqwest): Data<&reqwest::Client>) -> templates::Home { live::get_live_status(reqwest), scrobbles::get_now_playing(&listenbrainz_client), // skweets::get_recents(&clients.skinnyverse), - blog::get_blogposts() + Blogpost::get_articles() ); let is_live = live.unwrap_or_default().online; let listenbrainz = listenbrainz.unwrap_or_default(); @@ -56,7 +62,7 @@ async fn home(Data(reqwest): Data<&reqwest::Client>) -> templates::Home { // #[get("/blog/<blogpost>")] #[handler] async fn blogpost(Path(blogpost): Path<String>) -> Result<templates::Blogpost> { - let blogpost = blog::get_blogpost(&blogpost).await?; + let blogpost = Blogpost::get_article(&blogpost).await?; Ok(templates::Blogpost { blogpost, filter_tags: HashSet::new(), @@ -71,7 +77,7 @@ struct FilterTags { // #[get("/blog?<filter>")] #[handler] async fn get_blog(filter_tags: Option<Query<FilterTags>>) -> Result<templates::Blog> { - let mut blogposts = blog::get_blogposts().await?; + let mut blogposts = Blogpost::get_articles().await?; let tags: Vec<String> = posts::Post::get_tags(&blogposts) .into_iter() .map(|tag| tag.to_owned()) @@ -90,7 +96,7 @@ async fn get_blog(filter_tags: Option<Query<FilterTags>>) -> Result<templates::B #[handler] async fn feed() -> Result<Response> { - let posts = blog::get_blogposts().await?; + let posts = Blogpost::get_articles().await?; // TODO: i18n let context = atom::Context { page_title: "celeste's hard drive".to_owned(), diff --git a/src/poetry.rs b/src/poetry.rs new file mode 100644 index 0000000..f5f194c --- /dev/null +++ b/src/poetry.rs @@ -0,0 +1,48 @@ +use chrono::{DateTime, Utc}; + +use crate::posts::Post; + +pub struct Poem { + file_name: String, + title: Option<String>, + created_at: DateTime<Utc>, + published_at: DateTime<Utc>, + updated_at: Option<DateTime<Utc>>, + content: String, + // TODO: localisation (get lang from file names) + lang: String, +} + +impl Post for Poem { + fn id(&self) -> &str { + &self.file_name + } + + fn subject(&self) -> Option<&str> { + self.title.as_deref() + } + + fn published_at(&self) -> &DateTime<Utc> { + &self.published_at + } + + fn updated_at(&self) -> Option<&DateTime<Utc>> { + self.updated_at.as_ref() + } + + fn tags(&self) -> &Vec<String> { + todo!() + } + + fn lang(&self) -> &str { + "en" + } + + fn post_type(&self) -> crate::posts::PostType { + todo!() + } + + fn content(&self) -> &str { + todo!() + } +} |