2023-03-25 22:12:49 +01:00
|
|
|
use std::{collections::HashMap, path::Path};
|
2023-03-25 12:23:11 +01:00
|
|
|
|
2023-03-25 16:14:53 +01:00
|
|
|
use chrono::{DateTime, FixedOffset};
|
2023-03-25 12:23:11 +01:00
|
|
|
use glob::glob;
|
2023-03-25 16:14:53 +01:00
|
|
|
|
2023-03-25 12:23:11 +01:00
|
|
|
use lazy_static::lazy_static;
|
|
|
|
use pulldown_cmark::{html, Options, Parser};
|
|
|
|
use regex::Regex;
|
|
|
|
use serde_derive::{Deserialize, Serialize};
|
|
|
|
use tokio::fs;
|
|
|
|
|
2023-03-25 16:14:53 +01:00
|
|
|
use tracing::{instrument, log::*};
|
|
|
|
|
2023-03-25 22:12:49 +01:00
|
|
|
use crate::WebsiteError;
|
2023-03-25 12:23:11 +01:00
|
|
|
|
2023-03-25 21:38:16 +01:00
|
|
|
#[derive(Deserialize, Debug, Default)]
|
2023-03-25 16:14:53 +01:00
|
|
|
pub struct TomlFrontMatter {
|
|
|
|
pub title: String,
|
2023-03-25 21:38:16 +01:00
|
|
|
pub date: Option<toml::value::Datetime>,
|
|
|
|
pub draft: Option<bool>,
|
|
|
|
pub aliases: Option<Vec<String>>,
|
|
|
|
pub tags: Option<Vec<String>>,
|
2023-03-25 12:23:11 +01:00
|
|
|
}
|
|
|
|
|
2023-03-26 13:05:39 +02:00
|
|
|
#[derive(Serialize, Clone, Debug, Default)]
|
2023-03-25 12:23:11 +01:00
|
|
|
pub struct Post {
|
2023-03-25 21:38:16 +01:00
|
|
|
pub title: String,
|
|
|
|
pub date: Option<DateTime<FixedOffset>>,
|
|
|
|
pub aliases: Vec<String>,
|
|
|
|
pub tags: Vec<String>,
|
2023-03-25 12:23:11 +01:00
|
|
|
pub content: String,
|
|
|
|
pub slug: String,
|
|
|
|
pub absolute_path: String,
|
2023-03-25 21:38:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Post {
|
|
|
|
pub fn new(slug: String, content: String, fm: TomlFrontMatter) -> Post {
|
|
|
|
Post {
|
|
|
|
absolute_path: format!("/posts/{}/", slug),
|
|
|
|
slug,
|
|
|
|
content,
|
|
|
|
title: fm.title,
|
2023-03-25 22:12:49 +01:00
|
|
|
date: fm
|
|
|
|
.date
|
|
|
|
.map(|d| DateTime::parse_from_rfc3339(&d.to_string()).expect("bad toml datetime")),
|
2023-03-25 21:38:16 +01:00
|
|
|
aliases: fm.aliases.unwrap_or_default(),
|
|
|
|
tags: fm.tags.unwrap_or_default(),
|
|
|
|
}
|
|
|
|
}
|
2023-03-26 12:01:59 +02:00
|
|
|
|
|
|
|
pub fn is_published(&self) -> bool {
|
|
|
|
let now = chrono::offset::Local::now();
|
|
|
|
if let Some(date) = self.date {
|
|
|
|
return date.timestamp() - now.timestamp() <= 0;
|
|
|
|
}
|
|
|
|
true
|
|
|
|
}
|
2023-03-25 12:23:11 +01:00
|
|
|
}
|
|
|
|
|
2023-03-25 16:14:53 +01:00
|
|
|
#[instrument]
|
|
|
|
pub async fn load_all() -> color_eyre::eyre::Result<HashMap<String, Post>> {
|
|
|
|
let mut res = HashMap::<String, Post>::new();
|
|
|
|
for path in glob("posts/**/*.md")? {
|
|
|
|
let path = path.unwrap();
|
|
|
|
debug!("found page: {}", path.display());
|
|
|
|
|
2023-03-25 21:38:16 +01:00
|
|
|
let path = path.to_string_lossy().replace('\\', "/");
|
|
|
|
let slug = path
|
|
|
|
.trim_start_matches("posts")
|
|
|
|
.trim_start_matches('/')
|
|
|
|
.trim_start_matches('\\')
|
|
|
|
.trim_end_matches(".html")
|
|
|
|
.trim_end_matches(".md")
|
|
|
|
.trim_end_matches("index")
|
|
|
|
.trim_end_matches('\\')
|
|
|
|
.trim_end_matches('/');
|
|
|
|
|
|
|
|
let post = load_post(slug).await?;
|
2023-03-25 16:14:53 +01:00
|
|
|
|
2023-03-25 21:38:16 +01:00
|
|
|
res.insert(slug.to_string(), post);
|
2023-03-25 16:14:53 +01:00
|
|
|
}
|
|
|
|
Ok(res)
|
2023-03-25 12:23:11 +01:00
|
|
|
}
|
|
|
|
|
2023-03-25 16:14:53 +01:00
|
|
|
#[instrument]
|
2023-03-25 21:38:16 +01:00
|
|
|
pub async fn load_post(slug: &str) -> color_eyre::eyre::Result<Post> {
|
2023-03-25 16:14:53 +01:00
|
|
|
debug!("loading post: {slug}");
|
2023-03-25 12:23:11 +01:00
|
|
|
|
2023-03-25 16:14:53 +01:00
|
|
|
let file_path = Path::new("posts").join(slug);
|
2023-03-25 12:23:11 +01:00
|
|
|
|
2023-03-25 16:14:53 +01:00
|
|
|
let content = if let Ok(content) = fs::read_to_string(file_path.with_extension("md")).await {
|
2023-03-25 12:23:11 +01:00
|
|
|
content
|
|
|
|
} else {
|
2023-03-25 16:14:53 +01:00
|
|
|
fs::read_to_string(file_path.join("index.md")).await?
|
2023-03-25 12:23:11 +01:00
|
|
|
};
|
|
|
|
|
2023-03-25 16:14:53 +01:00
|
|
|
let (tomlfm, content) = parse_frontmatter(content)?;
|
|
|
|
let tomlfm = tomlfm.expect("Missing frontmatter");
|
2023-03-25 12:23:11 +01:00
|
|
|
|
|
|
|
let content = content.map(|c| {
|
|
|
|
let options = Options::all();
|
|
|
|
let mut content_html = String::new();
|
|
|
|
let parser = Parser::new_ext(&c, options);
|
|
|
|
html::push_html(&mut content_html, parser);
|
|
|
|
content_html
|
|
|
|
});
|
|
|
|
|
2023-03-25 22:12:49 +01:00
|
|
|
Ok(Post::new(
|
|
|
|
slug.to_string(),
|
|
|
|
content.unwrap_or_default(),
|
|
|
|
tomlfm,
|
|
|
|
))
|
2023-03-25 16:14:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[instrument]
|
|
|
|
fn parse_frontmatter(
|
|
|
|
src: String,
|
|
|
|
) -> color_eyre::eyre::Result<(Option<TomlFrontMatter>, Option<String>)> {
|
2023-03-25 12:23:11 +01:00
|
|
|
lazy_static! {
|
|
|
|
static ref FRONTMATTER_REGEX: Regex = regex::Regex::new(
|
|
|
|
r"^[\s]*\+{3}(\r?\n(?s).*?(?-s))\+{3}[\s]*(?:$|(?:\r?\n((?s).*(?-s))$))"
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
};
|
|
|
|
|
2023-03-25 16:14:53 +01:00
|
|
|
Ok(if let Some(captures) = FRONTMATTER_REGEX.captures(&src) {
|
2023-03-25 12:23:11 +01:00
|
|
|
(
|
2023-03-25 16:14:53 +01:00
|
|
|
Some(toml::from_str(captures.get(1).unwrap().as_str())?),
|
|
|
|
captures.get(2).map(|m| m.as_str().to_owned()),
|
2023-03-25 12:23:11 +01:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
(None, Some(src))
|
2023-03-25 16:14:53 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-03-25 21:38:16 +01:00
|
|
|
#[instrument(skip(tera, post))]
|
2023-03-25 22:12:49 +01:00
|
|
|
pub async fn render_post(tera: &tera::Tera, post: &Post) -> Result<String, WebsiteError> {
|
2023-03-25 16:14:53 +01:00
|
|
|
let mut ctx = tera::Context::new();
|
2023-03-25 21:38:16 +01:00
|
|
|
ctx.insert("page", &post);
|
2023-03-25 16:14:53 +01:00
|
|
|
|
2023-03-25 21:38:16 +01:00
|
|
|
tera.render("post.html", &ctx).map_err(|e| e.into())
|
2023-03-25 16:14:53 +01:00
|
|
|
}
|
|
|
|
|
2023-03-25 21:38:16 +01:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use tera::Tera;
|
|
|
|
|
|
|
|
#[tokio::test]
|
2023-03-26 00:30:21 +01:00
|
|
|
async fn render_all_posts() {
|
2023-03-25 21:38:16 +01:00
|
|
|
let tera = Tera::new("templates/**/*").unwrap();
|
|
|
|
let posts = super::load_all().await.unwrap();
|
|
|
|
|
|
|
|
for (_slug, post) in posts {
|
|
|
|
super::render_post(&tera, &post).await.unwrap();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|