aboutsummaryrefslogtreecommitdiffstats
path: root/src/article.rs
blob: 057c49ca848f5f12486958f0460e29c2b7b6b268 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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))
    }
}