1
0
Fork 0
website/src/post.rs

151 lines
4.2 KiB
Rust
Raw Normal View History

2023-03-25 12:23:11 +01:00
use std::path::Path;
use cached::proc_macro::cached;
use color_eyre::eyre::Result;
use glob::glob;
use lazy_static::lazy_static;
use pulldown_cmark::{html, Options, Parser};
use regex::Regex;
use serde_derive::{Deserialize, Serialize};
use tokio::fs;
use tracing::log::*;
use crate::State;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct FrontMatter {
pub title: Option<String>,
}
#[derive(Serialize, Clone, Debug)]
pub struct Post {
pub content: String,
pub slug: String,
pub absolute_path: String,
pub frontmatter: Option<FrontMatter>,
}
pub fn load_all() -> Result<Vec<Post>> {
Ok(glob("posts/**/*.md")?
.map(|p| {
let path = p.unwrap();
info!("found page: {}", path.display());
let filename = path.file_name().unwrap();
let slug = if filename.eq_ignore_ascii_case("index.md") {
path.parent().unwrap().to_string_lossy()
} else {
path.to_string_lossy()
}
.trim_start_matches("posts")
.trim_start_matches(std::path::MAIN_SEPARATOR)
.trim_end_matches(".md")
.trim_end_matches(std::path::MAIN_SEPARATOR)
.replace('\\', "/");
info!("slug: {slug}");
let raw = std::fs::read_to_string(&path).unwrap();
let (frontmatter, content) = parse_frontmatter(raw);
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
});
Post {
absolute_path: format!("/posts/{slug}/"),
slug,
content: content.unwrap_or_default(),
frontmatter,
}
})
.collect())
}
#[cached(time = 60, key = "String", convert = r#"{ String::from(path) }"#)]
pub async fn load_post(path: &str) -> Option<Post> {
let path = path
.trim_end_matches('/')
.trim_end_matches(".md")
.trim_end_matches(".html");
info!("loading post: {path}");
let path = if path.starts_with("posts/") {
Path::new(path).to_owned()
} else {
Path::new("posts").join(path)
};
let content = if let Ok(content) = fs::read_to_string(path.with_extension("md")).await {
content
} else if let Ok(content) = fs::read_to_string(path.join("index.md")).await {
content
} else {
return None;
};
let (frontmatter, content) = parse_frontmatter(content);
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
});
Some(Post {
absolute_path: format!("/{}/", path.to_string_lossy()),
slug: path.to_string_lossy().into(),
content: content.unwrap_or_default(),
frontmatter,
})
}
fn parse_frontmatter(src: String) -> (Option<FrontMatter>, Option<String>) {
lazy_static! {
static ref FRONTMATTER_REGEX: Regex = regex::Regex::new(
r"^[\s]*\+{3}(\r?\n(?s).*?(?-s))\+{3}[\s]*(?:$|(?:\r?\n((?s).*(?-s))$))"
)
.unwrap();
};
if let Some(fm) = FRONTMATTER_REGEX.captures(&src) {
(
fm.get(1)
.and_then(|m| toml::from_str(m.as_str()).expect("invalid toml")),
fm.get(2).map(|m| m.as_str().to_owned()),
)
} else {
(None, Some(src))
}
}
#[derive(Serialize)]
struct Postcontext {
content: String,
}
#[cached(time = 60, key = "String", convert = r"{ post.absolute_path.clone() }")]
pub async fn render_post(state: &State, post: &Post) -> Option<String> {
info!("rendering post: {}", post.absolute_path);
let ctx = tera::Context::from_serialize(Postcontext {
content: post.content.clone(),
})
.ok()?;
let res = match state.tera.render("post.html", &ctx) {
Ok(res) => res,
Err(e) => {
error!("Failed rendering post: {}", e);
return None;
}
};
Some(res)
}