use axum::{ Router, extract::Path, response::Html, routing::get }; use tower_http::services::ServeDir; use askama::Template; // use typst::model; // use typst_html; use std::{collections::HashMap, fs, io::{Error, Write}, process::{Command, Stdio}}; use regex::Regex; fn load_posts() -> Result, Error> { let dirs = fs::read_dir("static/posts")?; let re_slug = Regex::new(r#"#let\s+post_slug\s*=\s*"([^"]*)""#).unwrap(); let re_summary = Regex::new(r#"#let\s+post_summary\s*=\s*"([^"]*)""#).unwrap(); let re_img = Regex::new(r#"#let\s+post_preview_image\s*=\s*"([^"]*)""#).unwrap(); let re_title = Regex::new("(?m)^=+ (.*)").unwrap(); let mut posts: HashMap = HashMap::new(); for dir in dirs { let dir_path = dir.as_ref().unwrap().path(); let typst_path = dir_path.join("post.typ"); let typst = fs::read_to_string(typst_path).expect("Failed reading post"); let title = re_title.captures(&typst) .expect(format!("Post title not found in {}", dir_path.to_str().unwrap()).as_str()) .get(1).unwrap().as_str().trim().to_string(); let slug = re_slug.captures(&typst) .expect("Post slug not found") .get(1).unwrap().as_str().trim().to_string(); let summary = re_summary.captures(&typst) .expect("Post summary not found") .get(1).unwrap().as_str().trim().to_string(); let img_path = "/".to_string() + dir?.path().join( re_img.captures(&typst) .expect("Post preview image not found") .get(1).unwrap().as_str().trim().to_string() ).to_str().expect("Failed converting path to string"); posts.insert( slug.clone(), Post { slug: slug, title: title, preview_image: img_path.clone(), summary: summary, render: Html(PostTemplate { content: typst_to_html(typst), image: img_path }.render().expect("Failed rendering post")), } ); } Ok(posts) } #[derive(Template)] #[template(path = "post.html")] struct PostTemplate { image: String, content: String, } #[derive(Clone)] #[derive(Debug)] struct Post { title: String, preview_image: String, summary: String, render: Html, slug: String } #[derive(Debug)] #[derive(Template)] #[template(path = "posts.html")] struct PostsTemplate { posts: Vec } // Template for home #[derive(Template)] #[template(path = "home.html", ext = "html")] struct HomeTemplate { content: String } fn typst_to_html(typst: String) -> String { let mut child = Command::new("typst") .args(["compile", "-", "-", "--format", "html", "--features", "html" ]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .expect("Failed running typst"); let mut stdin = child.stdin.take().expect("Failed opening STDIN"); stdin.write_all(typst.as_bytes()).expect("Failed writing to STDIN"); drop(stdin); let output = child.wait_with_output().expect("Failed reading output"); String::from_utf8_lossy(&output.stdout).to_string() } // , posts: &Vec async fn get_post( Path(slug): Path, posts: HashMap ) -> Html { if posts.contains_key(&slug) { return posts[&slug].render.clone() } else { return Html("404".to_string()); } } #[tokio::main] async fn main() { let home_html = Html( HomeTemplate { content: typst_to_html(fs::read_to_string( "static/home.typ" ).expect("Couldnt read file")) }.render().expect("Failed rendering home template") ); let posts: HashMap = load_posts().expect("Failed loading posts"); let posts_html = Html( PostsTemplate{ posts: posts.values().cloned().collect::>() }.render().expect("Failed rendering posts") ); let site: Router = Router::new() .route("/", get(home_html)) .route("/posts", get(posts_html)) .route("/posts/{slug}", get(|slug| get_post(slug, posts))) .nest_service("/static", ServeDir::new("static")); let listener = tokio::net::TcpListener::bind("0.0.0.0:42069").await.unwrap(); println!("Running on http://127.0.0.1:42069"); axum::serve(listener, site).await.unwrap(); }