diff --git a/Cargo.lock b/Cargo.lock index 3cdc05e..2014c9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,17 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "chrono" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6" +dependencies = [ + "num-integer", + "num-traits", + "time", +] + [[package]] name = "clap" version = "2.33.1" @@ -524,6 +535,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "h2" version = "0.2.5" @@ -759,6 +776,12 @@ version = "0.2.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9f8082297d534141b30c8d39e9b1773713ab50fdbe4ff30f750d063b3bfd701" +[[package]] +name = "linked-hash-map" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" + [[package]] name = "log" version = "0.3.9" @@ -938,6 +961,25 @@ dependencies = [ "version_check 0.9.2", ] +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg 1.0.0", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg 1.0.0", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -1604,6 +1646,18 @@ dependencies = [ "url", ] +[[package]] +name = "serde_yaml" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3e2dd40a7cdc18ca80db804b7f461a39bb721160a85c9a1fa30134bf3c02a5" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + [[package]] name = "sha-1" version = "0.8.2" @@ -1713,6 +1767,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.0.1" @@ -2184,8 +2258,10 @@ name = "xesite" version = "2.0.0" dependencies = [ "anyhow", + "chrono", "comrak", "envy", + "glob", "log 0.4.8", "mime 0.3.16", "pretty_env_logger", @@ -2193,6 +2269,17 @@ dependencies = [ "ructe", "serde", "serde_dhall", + "serde_yaml", + "thiserror", "tokio", "warp", ] + +[[package]] +name = "yaml-rust" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index 59429a3..79d88a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,17 +9,21 @@ build = "src/build.rs" [dependencies] anyhow = "1" +chrono = "0.4" comrak = "0.8" envy = "0.4" +glob = "0.3" log = "0" mime = "0.3.0" pretty_env_logger = "0" rand = "0" ructe = "0.11" serde_dhall = "0.5.3" +serde_yaml = "0.8" serde = { version = "1", features = ["derive"] } tokio = { version = "0.2", features = ["macros"] } warp = "0.2" +thiserror = "1" [build-dependencies] ructe = { version = "0.11", features = ["warp02"] } diff --git a/src/main.rs b/src/main.rs index 57396ed..ba5a471 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use warp::{path, Filter}; pub mod app; pub mod handlers; +pub mod post; pub mod signalboost; use app::State; diff --git a/src/post/frontmatter.rs b/src/post/frontmatter.rs new file mode 100644 index 0000000..1cc8032 --- /dev/null +++ b/src/post/frontmatter.rs @@ -0,0 +1,114 @@ +/// This code was borrowed from @fasterthanlime. + +use anyhow::{Result}; +use serde::{Serialize, Deserialize}; + +#[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] +pub struct Data { + pub title: String, + pub date: String, + pub series: Option, + pub tags: Option>, + pub slides_link: Option, + pub image: Option, + pub thumb: Option, + pub show: Option, +} + +enum State { + SearchForStart, + ReadingMarker { count: usize, end: bool }, + ReadingFrontMatter { buf: String, line_start: bool }, + SkipNewline { end: bool }, +} + +#[derive(Debug, thiserror::Error)] +enum Error { + #[error("EOF while parsing frontmatter")] + EOF, + #[error("Error parsing yaml: {0:?}")] + Yaml(#[from] serde_yaml::Error), +} + +impl Data { + pub fn parse(input: &str) -> Result<(Data, usize)> { + let mut state = State::SearchForStart; + + let mut payload = None; + let offset; + + let mut chars = input.char_indices(); + 'parse: loop { + let (idx, ch) = match chars.next() { + Some(x) => x, + None => return Err(Error::EOF)?, + }; + match &mut state { + State::SearchForStart => match ch { + '-' => { + state = State::ReadingMarker { + count: 1, + end: false, + }; + } + '\n' | '\t' | ' ' => { + // ignore whitespace + } + _ => { + panic!("Start of frontmatter not found"); + } + }, + State::ReadingMarker { count, end } => match ch { + '-' => { + *count += 1; + if *count == 3 { + state = State::SkipNewline { end: *end }; + } + } + _ => { + panic!("Malformed frontmatter marker"); + } + }, + State::SkipNewline { end } => match ch { + '\n' => { + if *end { + offset = idx + 1; + break 'parse; + } else { + state = State::ReadingFrontMatter { + buf: String::new(), + line_start: true, + }; + } + } + _ => panic!("Expected newline, got {:?}",), + }, + State::ReadingFrontMatter { buf, line_start } => match ch { + '-' if *line_start => { + let mut state_temp = State::ReadingMarker { + count: 1, + end: true, + }; + std::mem::swap(&mut state, &mut state_temp); + if let State::ReadingFrontMatter { buf, .. } = state_temp { + payload = Some(buf); + } else { + unreachable!(); + } + } + ch => { + buf.push(ch); + *line_start = ch == '\n'; + } + }, + } + } + + // unwrap justification: option set in state machine, Rust can't statically analyze it + let payload = payload.unwrap(); + + let fm: Self = serde_yaml::from_str(&payload)?; + + Ok((fm, offset)) + } +} diff --git a/src/post/mod.rs b/src/post/mod.rs new file mode 100644 index 0000000..e47eed8 --- /dev/null +++ b/src/post/mod.rs @@ -0,0 +1,66 @@ +use anyhow::{anyhow, Result}; +use chrono::prelude::*; +use glob::glob; +use std::{cmp::Ordering, fs}; + +pub mod frontmatter; + +#[derive(Eq, PartialEq, Debug)] +pub struct Post { + pub front_matter: frontmatter::Data, + pub link: String, + pub body: String, + pub body_html: String, + pub date: NaiveDate, +} + +impl Ord for Post { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(&other).unwrap() + } +} + +impl PartialOrd for Post { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.date.cmp(&other.date)) + } +} + +pub fn load(dir: &str) -> Result> { + let mut result: Vec = vec![]; + + for path in glob(&format!("{}/*.markdown", dir))?.filter_map(Result::ok) { + let body = fs::read_to_string(path.clone())?; + let (fm, content_offset) = frontmatter::Data::parse(body.clone().as_str())?; + let markup = &body[content_offset..]; + let date = NaiveDate::parse_from_str(&fm.clone().date, "%Y-%m-%d")?; + + result.push(Post { + front_matter: fm, + link: format!("{}/{}", dir, path.file_stem().unwrap().to_str().unwrap()), + body: markup.to_string(), + body_html: crate::app::markdown(&markup), + date: date, + }) + } + + if result.len() == 0 { + Err(anyhow!("no posts loaded")) + } else { + result.sort(); + result.reverse(); + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + + #[test] + fn blog() -> Result<()> { + load("./blog")?; + Ok(()) + } +}