diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..004ebd7 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,3 @@ +{ + "MD025": false +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1b905b9..da63f7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,13 +68,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.67" +version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ea188f25f0255d8f92797797c97ebf5631fa88178beb1a46fdf5622c9a00e4" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.6", + "syn 2.0.10", ] [[package]] @@ -355,9 +355,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" dependencies = [ "libc", ] @@ -405,7 +405,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.6", + "syn 2.0.10", ] [[package]] @@ -422,7 +422,7 @@ checksum = "631569015d0d8d54e6c241733f944042623ab6df7bc3be7466874b05fcdb1c5f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.6", + "syn 2.0.10", ] [[package]] @@ -817,9 +817,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", @@ -1265,9 +1265,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.2" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cce168fea28d3e05f158bda4576cf0c844d5045bc2cc3620fa0292ed5bb5814c" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", @@ -1282,9 +1282,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" [[package]] name = "rustversion" @@ -1336,7 +1336,7 @@ checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.6", + "syn 2.0.10", ] [[package]] @@ -1359,6 +1359,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1459,9 +1468,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.6" +version = "2.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece519cfaf36269ea69d16c363fa1d59ceba8296bbfbfc003c3176d01f2816ee" +checksum = "5aad1363ed6d37b84299588d62d3a7d95b5a5c2d9aad5c85609fda12afaa1f40" dependencies = [ "proc-macro2", "quote", @@ -1523,7 +1532,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.6", + "syn 2.0.10", ] [[package]] @@ -1580,6 +1589,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -1920,12 +1963,15 @@ dependencies = [ "color-eyre", "glob", "hyper", + "lazy_static", "pulldown-cmark", + "regex", "serde", "serde_derive", "serde_json", "tera", "tokio", + "toml", "tower", "tower-http", "tracing", @@ -2038,6 +2084,15 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index cb45af0..b86776f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,17 @@ axum = { version = "0.6.12", features = ["http2"] } cached = "0.42.0" color-eyre = "0.6.1" glob = "0.3.0" +#grass = { version = "0.12.3", features = ["random"] } # not really needed yet hyper = { version = "0.14.19", features = ["full"] } +lazy_static = "1.4.0" pulldown-cmark = "0.9.2" +regex = "1.7.2" serde = "1.0.144" serde_derive = "1.0.144" serde_json = "1.0.85" tera = "1.17.0" tokio = { version = "1.19.2", features = ["full"] } +toml = "0.7.3" tower = { version = "0.4.12", features = ["full"] } tower-http = { version = "0.4.0", features = ["full"] } tracing = "0.1.35" diff --git a/posts/foldertest/index.md b/posts/foldertest/index.md index 752c225..44ea513 100644 --- a/posts/foldertest/index.md +++ b/posts/foldertest/index.md @@ -1,3 +1,7 @@ ++++ +title="TOML metadata test" ++++ + # Testing post as index within folder hope it works yay diff --git a/posts/foldertest/nestedpost.md b/posts/foldertest/nestedpost.md new file mode 100644 index 0000000..abdb7f7 --- /dev/null +++ b/posts/foldertest/nestedpost.md @@ -0,0 +1,6 @@ ++++ +title='nested post test does not work' ++++ +# yet again a nested post test + +will it work this time, at least with the slug?? diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 92f21f7..0362aec 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,4 +1,3 @@ -use serde_derive::Serialize; use std::sync::Arc; use tracing::log::*; @@ -17,11 +16,6 @@ pub enum Error { pub type Result>> = std::result::Result; -#[derive(Serialize)] -struct PageContext { - content: String, -} - pub async fn index(Extension(state): Extension>) -> Result { let ctx = tera::Context::new(); let res = state.tera.render("index.html", &ctx).map_err(|e| { diff --git a/src/handlers/posts.rs b/src/handlers/posts.rs index ac80c3e..371ceab 100644 --- a/src/handlers/posts.rs +++ b/src/handlers/posts.rs @@ -1,43 +1,23 @@ use std::sync::Arc; use axum::{extract::Path, response::Html, Extension}; -use cached::proc_macro::cached; -use pulldown_cmark::{html, Options, Parser}; use tracing::log::*; use super::{Error, Result}; -use crate::{handlers::PageContext, State}; +use crate::{post::render_post, State}; pub async fn view(Path(path): Path, Extension(state): Extension>) -> Result { - let post = path.trim_end_matches('/'); - info!("Requested post: {}", post); + info!("Requested post: {}", path); + let post = state + .posts + .iter() + .find(|p| p.slug.eq_ignore_ascii_case(&path)) + .ok_or(Error::NotFound)?; + //let post = load_post(&path).await.ok_or(Error::NotFound)?; let res = render_post(&state, post).await.ok_or(Error::NotFound)?; Ok(Html(res.into())) } -#[cached(time = 60, key = "String", convert = r"{ path.to_owned() }")] -async fn render_post(state: &State, path: &str) -> Option { - info!("Rendering post..."); - let post = state.posts.iter().find(|p| p.slug == path)?; - - let options = Options::all(); - let parser = Parser::new_ext(&post.content, options); - - let mut out = String::new(); - html::push_html(&mut out, parser); - - let ctx = tera::Context::from_serialize(PageContext { content: out }).ok()?; - - let res = match state.tera.render("post.html", &ctx) { - Ok(res) => res, - Err(e) => { - error!("Failed rendering post: {}", e); - return None; - } - }; - Some(res) -} - pub async fn index(Extension(state): Extension>) -> Result { let mut ctx = tera::Context::new(); ctx.insert("posts", &state.posts); diff --git a/src/main.rs b/src/main.rs index 2f597c4..b34892f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,63 +1,30 @@ -use std::sync::Arc; +use std::{sync::Arc, time}; -use axum::{ - routing::get, - Extension, Router, -}; +use axum::{routing::get, Extension, Router}; use color_eyre::eyre::Result; -use glob::glob; -use serde_derive::Serialize; +use post::Post; use tera::Tera; use tower_http::trace::TraceLayer; use tracing::log::*; mod handlers; +mod post; pub struct State { posts: Vec, tera: Tera, } -#[derive(Serialize)] -pub struct Post { - pub name: String, - pub slug: String, - pub content: String, -} - #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; tracing_subscriber::fmt::init(); info!("Starting server..."); + let ts = time::Instant::now(); + let tera = Tera::new("templates/**/*")?; - let posts = glob("posts/**/*.md")? - .map(|p| { - let path = p.unwrap(); - info!("found page: {}", path.display()); - - let filename = path.file_name().unwrap().to_string_lossy(); - - let (filename, _) = filename.rsplit_once('.').unwrap(); - - let slug = if filename.eq_ignore_ascii_case("index") { - path.parent() - .unwrap() - .file_name() - .unwrap() - .to_string_lossy() - .into_owned() - } else { - filename.to_owned() - }; - Post { - name: slug.clone(), - slug, - content: std::fs::read_to_string(&path).unwrap(), - } - }) - .collect(); + let posts = post::load_all()?; let state = Arc::new(State { tera, posts }); @@ -67,13 +34,19 @@ async fn main() -> Result<()> { let app = Router::new() .route("/", get(handlers::index)) - .nest("/posts", Router::new() - .route("/", get(handlers::posts::index)) - .route("/:route/", get(handlers::posts::view)) - .fallback_service(tower_http::services::ServeDir::new("./posts"))) + .nest( + "/posts", + Router::new() + .route("/", get(handlers::posts::index)) + .route("/:route/", get(handlers::posts::view)) + .fallback_service(tower_http::services::ServeDir::new("./posts")), + ) .nest_service("/static", tower_http::services::ServeDir::new("./static")) .layer(middleware); + let duration = time::Instant::now() - ts; + + info!("loaded server in {}ms", duration.as_secs_f64() * 1000.0); info!("Now listening at http://localhost:8180"); axum::Server::bind(&"0.0.0.0:8180".parse().unwrap()) diff --git a/src/post.rs b/src/post.rs new file mode 100644 index 0000000..776866d --- /dev/null +++ b/src/post.rs @@ -0,0 +1,150 @@ +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, +} + +#[derive(Serialize, Clone, Debug)] +pub struct Post { + pub content: String, + pub slug: String, + pub absolute_path: String, + pub frontmatter: Option, +} + +pub fn load_all() -> Result> { + 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 { + 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, Option) { + 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 { + 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) +} diff --git a/templates/postsindex.html b/templates/postsindex.html index c39843c..a86009a 100644 --- a/templates/postsindex.html +++ b/templates/postsindex.html @@ -5,7 +5,7 @@

i occasionally write some stuff i guess

{% endblock main %} \ No newline at end of file