224 lines
6.3 KiB
Rust
224 lines
6.3 KiB
Rust
mod article;
|
|
mod note;
|
|
pub mod syndication;
|
|
|
|
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<D: Content> {
|
|
// id: i64,
|
|
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::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<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 {
|
|
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,
|
|
})
|
|
}
|
|
}
|