1
0
Fork 0
website/src/post.rs

187 lines
4.8 KiB
Rust
Raw Normal View History

2023-03-25 16:14:53 +01:00
use std::{collections::HashMap, path::Path, sync::Arc};
2023-03-25 12:23:11 +01:00
2023-03-25 16:14:53 +01:00
use axum::{response::Html, routing::get, Extension, Router};
use chrono::{DateTime, FixedOffset};
2023-03-25 12:23:11 +01:00
use glob::glob;
2023-03-25 16:14:53 +01:00
use hyper::Uri;
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::*};
use crate::{handlers, State};
2023-03-25 12:23:11 +01:00
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FrontMatter {
2023-03-25 16:14:53 +01:00
pub title: String,
pub date: DateTime<FixedOffset>,
}
#[derive(Deserialize, Debug)]
pub struct TomlFrontMatter {
pub title: String,
pub date: toml::value::Datetime,
2023-03-25 12:23:11 +01:00
}
#[derive(Serialize, Clone, Debug)]
pub struct Post {
pub content: String,
pub slug: String,
pub absolute_path: String,
2023-03-25 16:14:53 +01:00
pub frontmatter: FrontMatter,
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());
let post = load_post(&path.to_string_lossy()).await?;
res.insert(post.slug.clone(), post);
}
Ok(res)
2023-03-25 12:23:11 +01:00
}
2023-03-25 16:14:53 +01:00
#[instrument]
pub async fn load_post(path: &str) -> color_eyre::eyre::Result<Post> {
let path = path.replace('\\', "/");
let slug = path
.trim_start_matches("posts")
.trim_start_matches('/')
.trim_start_matches('\\')
.trim_end_matches(".html")
2023-03-25 12:23:11 +01:00
.trim_end_matches(".md")
2023-03-25 16:14:53 +01:00
.trim_end_matches("index")
.trim_end_matches('\\')
.trim_end_matches('/');
2023-03-25 12:23:11 +01:00
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 16:14:53 +01:00
let date = toml_date_to_chrono(tomlfm.date)?;
let frontmatter = FrontMatter {
title: tomlfm.title,
date,
};
Ok(Post {
absolute_path: format!("/posts/{}/", slug),
slug: slug.to_string(),
2023-03-25 12:23:11 +01:00
content: content.unwrap_or_default(),
frontmatter,
})
}
2023-03-25 16:14:53 +01:00
fn toml_date_to_chrono(
toml: toml::value::Datetime,
) -> color_eyre::eyre::Result<DateTime<FixedOffset>> {
Ok(DateTime::parse_from_rfc3339(&toml.to_string())?)
}
#[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
})
}
#[instrument(skip(state, post))]
async fn render_post(state: &State, post: &Post) -> Result<String, handlers::Error> {
let mut ctx = tera::Context::new();
ctx.insert("title", &post.frontmatter.title);
ctx.insert("content", &post.content);
state.tera.render("post.html", &ctx).map_err(|e| e.into())
}
#[instrument(skip(posts))]
pub fn build_router<'a, I>(posts: I) -> Router
where
I: Iterator<Item = &'a Post>,
{
let mut router = Router::new().route("/", get(index));
for post in posts {
let slug = &post.slug;
let path = format!("/{slug}/");
info!("adding post route: {path}");
router = router.route(&path, get(view));
2023-03-25 12:23:11 +01:00
}
2023-03-25 16:14:53 +01:00
router
2023-03-25 12:23:11 +01:00
}
2023-03-25 16:14:53 +01:00
#[instrument(skip(state))]
pub async fn view(
uri: Uri,
Extension(state): Extension<Arc<State>>,
) -> Result<Html<String>, handlers::Error> {
debug!("viewing post: {uri}");
let post = state
.posts
.get(uri.path().trim_matches('/'))
.ok_or(handlers::Error::NotFound)?;
let res = render_post(&state, post).await?;
Ok(Html(res))
2023-03-25 12:23:11 +01:00
}
2023-03-25 16:14:53 +01:00
#[instrument(skip(state))]
pub async fn index(
Extension(state): Extension<Arc<State>>,
) -> Result<Html<String>, handlers::Error> {
let mut ctx = tera::Context::new();
2023-03-25 12:23:11 +01:00
2023-03-25 16:14:53 +01:00
let mut posts = state.posts.values().collect::<Vec<&Post>>();
posts.sort_by_key(|p| &p.frontmatter.date);
ctx.insert("title", "Posts");
ctx.insert("posts", &posts);
let res = state.tera.render("posts_index.html", &ctx)?;
Ok(Html(res))
2023-03-25 12:23:11 +01:00
}