diff options
Diffstat (limited to '')
-rw-r--r-- | src/posts.rs | 62 | ||||
-rw-r--r-- | src/posts/article.rs | 0 | ||||
-rw-r--r-- | src/posts/mod.rs | 228 | ||||
-rw-r--r-- | src/posts/syndication.rs | 96 |
4 files changed, 62 insertions, 324 deletions
diff --git a/src/posts.rs b/src/posts.rs new file mode 100644 index 0000000..415f743 --- /dev/null +++ b/src/posts.rs @@ -0,0 +1,62 @@ +use std::collections::HashSet; + +use chrono::{DateTime, Utc}; +use serde::Serialize; + +#[derive(Serialize, Debug)] +pub enum PostType { + Article, + Note, +} + +pub trait Post { + fn id(&self) -> &str; + fn subject(&self) -> Option<&str>; + fn published_at(&self) -> &DateTime<Utc>; + fn updated_at(&self) -> &DateTime<Utc>; + fn tags(&self) -> &Vec<String>; + fn lang(&self) -> &str; + fn post_type(&self) -> PostType; + fn content(&self) -> &str; + + fn link(&self) -> String { + "https://en.blos.sm/posts/".to_owned() + self.id() + } + + fn get_tags<'a>(posts: &'a Vec<Self>) -> Vec<&'a String> + where + Self: Sized, + { + 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::<Vec<_>>(); + tags.sort(); + tags + } + + fn filter_by_tags(posts: Vec<Self>, filter_tags: &HashSet<String>) -> Vec<Self> + where + Self: Sized, + { + posts + .into_iter() + .filter(|post| { + for tag in post.tags() { + match filter_tags.contains(tag) { + true => return true, + false => continue, + } + } + false + }) + .collect() + } +} diff --git a/src/posts/article.rs b/src/posts/article.rs deleted file mode 100644 index e69de29..0000000 --- a/src/posts/article.rs +++ /dev/null diff --git a/src/posts/mod.rs b/src/posts/mod.rs deleted file mode 100644 index c75dd53..0000000 --- a/src/posts/mod.rs +++ /dev/null @@ -1,228 +0,0 @@ -mod article; -mod note; -pub mod syndication; - -use std::collections::HashSet; - -use async_trait::async_trait; -use chrono::{DateTime, Utc}; -use markdown::{mdast::Node, CompileOptions, 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<D: Content> { - id: String, - subject: Option<String>, - created_at: DateTime<Utc>, - updated_at: Option<DateTime<Utc>>, - tags: Vec<String>, - post_type: PostType, - render: Option<String>, - data: D, -} - -impl<D: Content> Post<D> { - pub async fn render(&mut self) -> Result<()> { - self.render = Some(self.data.render().await?); - Ok(()) - } -} - -impl<D: Content + Serialize> Serialize for Post<D> { - fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> - 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<Vec<Post<Article>>> { - let mut blogposts: Vec<Post<Article>> = 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<Post<Article>> { - 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<D: Content>(posts: &Vec<Post<D>>) -> 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::<Vec<_>>(); - tags.sort(); - tags -} - -pub fn filter_by_tags<'p, D: Content>( - posts: Vec<Post<D>>, - filter_tags: &HashSet<String>, -) -> Vec<Post<D>> { - 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<String>; -} - -#[derive(Serialize, Debug)] -pub struct Article { - path: String, - name: String, -} - -impl Article { - async fn tree(&self) -> Result<Node> { - 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<String> { - 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<String> { - 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::gfm() - }, - ..ParseOptions::default() - }, - compile: CompileOptions { - gfm_task_list_item_checkable: true, - allow_dangerous_html: true, - ..CompileOptions::default() - }, - }; - Ok(markdown::to_html_with_options(&buf, &options).unwrap()) - } -} - -#[derive(Deserialize)] -struct ArticleMetadata { - title: String, - created_at: String, - updated_at: Option<String>, - tags: Vec<String>, -} - -impl Post<Article> { - async fn try_from(article: Article) -> Result<Post<Article>> { - 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::<DateTime<Utc>>()?) - } else { - None - }; - - Ok(Post { - id: article.name.to_owned(), - subject: Some(metadata.title), - created_at: metadata.created_at.parse::<DateTime<Utc>>()?, - updated_at, - tags: metadata.tags, - post_type: PostType::Article, - render: None, - data: article, - }) - } -} diff --git a/src/posts/syndication.rs b/src/posts/syndication.rs deleted file mode 100644 index f6f0b17..0000000 --- a/src/posts/syndication.rs +++ /dev/null @@ -1,96 +0,0 @@ -use atom_syndication::{Category, Content, Entry, Feed, Generator, Link, Person, Text, TextType}; - -use super::{Article, Post}; - -pub async fn atom(posts: Vec<Post<Article>>) -> Feed { - let me = Person { - name: "cel".into(), - email: Some("cel@blos.sm".into()), - uri: Some("https://blos.sm".into()), - }; - let mut authors = Vec::new(); - authors.push(me); - let link = Link { - href: "https://blos.sm/feed".into(), - rel: "self".into(), - hreflang: Some("en".into()), - mime_type: Some("application/atom+xml".into()), - title: Some("atom feed".into()), - length: None, - }; - let mut links = Vec::new(); - links.push(link); - let mut feed = Feed { - title: Text { - value: "cel's site".into(), - base: None, - lang: Some("en".into()), - r#type: TextType::Text, - }, - id: "https://blos.sm".into(), - updated: posts[0].created_at.into(), - authors: authors.clone(), - categories: Vec::new(), - contributors: authors.clone(), - generator: Some(Generator { - value: "blos.sm".into(), - uri: Some("https://bunny.garden/cel/blos.sm".into()), - version: None, - }), - icon: Some("/icon.png".into()), - links: links.clone(), - logo: Some("/logo.png".into()), - rights: None, - subtitle: None, - entries: Vec::new(), - base: Some("https://blos.sm".into()), - lang: Some("en".into()), - ..Default::default() - }; - for mut post in posts { - post.render().await.unwrap_or_default(); - let mut id = String::from("https://blos.sm/blog/"); - id.push_str(&post.data.name); - let categories = post - .tags - .into_iter() - .map(|tag| Category { - term: tag.clone(), - scheme: None, - label: Some(tag.clone()), - }) - .collect(); - let entry = Entry { - title: Text { - value: post.subject.unwrap_or_default(), - base: None, - lang: Some("en".into()), - r#type: TextType::Text, - }, - id: id.clone(), - updated: if let Some(updated_at) = post.updated_at { - updated_at.into() - } else { - post.created_at.into() - }, - authors: authors.clone(), - categories, - contributors: authors.clone(), - links: links.clone(), - published: Some(post.created_at.into()), - rights: None, - source: None, - summary: None, - content: Some(Content { - base: None, - lang: Some("en".into()), - value: post.render, - src: Some(id), - content_type: Some("html".to_string()), - }), - ..Default::default() - }; - feed.entries.push(entry); - } - feed -} |