aboutsummaryrefslogtreecommitdiffstats
path: root/src/article.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/article.rs')
-rw-r--r--src/article.rs99
1 files changed, 99 insertions, 0 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))
+ }
+}