1
0
Fork 0
website/src/post.rs

165 lines
4.5 KiB
Rust
Raw Normal View History

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-06-18 11:34:08 +02:00
use crate::{AppState, WebsiteError, markdown};
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>,
2023-04-03 23:33:25 +02:00
pub updated: Option<toml::value::Datetime>,
2023-03-25 21:38:16 +01:00
pub draft: Option<bool>,
pub aliases: Option<Vec<String>>,
pub tags: Option<Vec<String>>,
2023-03-25 12:23:11 +01: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>>,
2023-04-03 23:33:25 +02:00
pub updated: Option<DateTime<FixedOffset>>,
2023-03-25 21:38:16 +01:00
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-04-03 23:33:25 +02:00
updated: fm
.updated
.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| {
2023-06-18 11:34:08 +02:00
markdown::render_markdown_to_html(&c)
}).transpose()?;
2023-03-25 12:23:11 +01:00
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
}
2023-04-02 15:26:20 +02:00
#[instrument(skip(src))]
2023-03-25 16:14:53 +01:00
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-04-10 00:46:58 +02:00
#[instrument(skip(state, post))]
pub async fn render_post(state: &AppState, 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-04-10 00:46:58 +02:00
ctx.insert("base_url", &state.base_url);
2023-03-25 16:14:53 +01:00
2023-04-10 00:46:58 +02:00
state.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;
2023-04-10 00:46:58 +02:00
use crate::AppState;
2023-03-25 21:38:16 +01:00
#[tokio::test]
2023-03-26 00:30:21 +01:00
async fn render_all_posts() {
2023-04-10 00:46:58 +02:00
let state = AppState {
base_url: "localhost:8180".into(),
posts: super::load_all().await.unwrap(),
tera: Tera::new("templates/**/*").unwrap(),
..Default::default()
};
for post in state.posts.values() {
super::render_post(&state, post).await.unwrap();
2023-03-25 21:38:16 +01:00
}
}
}