aboutsummaryrefslogtreecommitdiffstats
path: root/src/posts
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/posts.rs62
-rw-r--r--src/posts/article.rs0
-rw-r--r--src/posts/mod.rs228
-rw-r--r--src/posts/syndication.rs96
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
-}