151 lines
4.2 KiB
Rust
151 lines
4.2 KiB
Rust
|
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)
|
||
|
}
|