aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/article.rs99
-rw-r--r--src/blog.rs146
-rw-r--r--src/main.rs16
-rw-r--r--src/poetry.rs48
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!()
+ }
+}