aboutsummaryrefslogtreecommitdiffstats
path: root/src/blog.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/blog.rs186
1 files changed, 186 insertions, 0 deletions
diff --git a/src/blog.rs b/src/blog.rs
new file mode 100644
index 0000000..22cee05
--- /dev/null
+++ b/src/blog.rs
@@ -0,0 +1,186 @@
+use std::path::Path;
+
+use markdown::{mdast::Node, CompileOptions, Constructs, Options, ParseOptions};
+use serde::Deserialize;
+use tokio::{fs, io::AsyncReadExt};
+
+use chrono::{DateTime, Utc};
+
+use crate::{
+ error::BlossomError,
+ posts::{Post, PostType},
+ Result,
+};
+
+static ARTICLES_DIR: &str = "./articles";
+
+#[derive(Clone)]
+pub struct Blogpost {
+ file_name: String,
+ title: String,
+ published_at: DateTime<Utc>,
+ updated_at: Option<DateTime<Utc>>,
+ tags: Vec<String>,
+ content: String,
+}
+
+#[derive(Deserialize)]
+pub struct BlogpostMetadata {
+ title: String,
+ published_at: String,
+ updated_at: Option<String>,
+ tags: Vec<String>,
+}
+
+impl Post for Blogpost {
+ fn id(&self) -> &str {
+ &self.file_name
+ }
+
+ fn subject(&self) -> Option<&str> {
+ Some(&self.title)
+ }
+
+ fn published_at(&self) -> &DateTime<Utc> {
+ &self.published_at
+ }
+
+ fn updated_at(&self) -> &DateTime<Utc> {
+ if let Some(updated_at) = &self.updated_at {
+ updated_at
+ } else {
+ &self.published_at
+ }
+ }
+
+ fn tags(&self) -> &Vec<String> {
+ &self.tags
+ }
+
+ fn lang(&self) -> &str {
+ "en"
+ }
+
+ fn post_type(&self) -> PostType {
+ PostType::Article
+ }
+
+ fn content(&self) -> &str {
+ &self.content
+ }
+}
+
+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
+}