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))
}
}
|