use color_eyre::eyre::{eyre, Context, Result}; use glob::glob; use std::{cmp::Ordering, path::PathBuf}; use tokio::fs; use chrono::prelude::*; #[derive(Eq, PartialEq, Debug, Clone)] pub struct Post { pub front_matter: frontmatter::Data, pub body_html: String, pub date: DateTime, pub link: String, pub author: String, pub draft: bool, } 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)) } } async fn read_post(dir: &str, fname: PathBuf) -> Result { let body = fs::read_to_string(fname.clone()) .await .wrap_err_with(|| format!("couldn't read {:?}", fname))?; let (front_matter, content_offset) = frontmatter::Data::parse(body.clone().as_str()) .wrap_err_with(|| format!("can't parse frontmatter of {:?}", fname))?; let body = &body[content_offset..]; let date = NaiveDate::parse_from_str(&front_matter.clone().date, "%Y-%m-%d") .map_err(|why| eyre!("error parsing date in {:?}: {}", fname, why))?; let link = format!("{}/{}", dir, fname.file_stem().unwrap().to_str().unwrap()); let body_html = crate::internal::markdown::render(&body) .wrap_err_with(|| format!("can't parse markdown for {:?}", fname))?; let date: DateTime = DateTime::::from_utc(NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)), Utc) .with_timezone(&Utc) .into(); let author = &front_matter .clone() .author .unwrap_or("Cara Salter".to_string()); let draft = &front_matter.clone().draft.unwrap_or(false); Ok(Post { front_matter, body_html, link, date, author: author.clone(), draft: draft.clone(), }) } pub async fn load(dir: &str) -> Result> { let futs = glob(&format!("{}/*.md", dir))? .filter_map(Result::ok) .map(|fname| read_post(dir, fname)); let mut result: Vec = futures::future::join_all(futs) .await .into_iter() .map(Result::unwrap) .filter(|p| !p.draft) .collect(); info!("Loaded {:?} posts", result.len()); if result.len() == 0 { Err(eyre!("No posts found")) } else { result.sort(); result.reverse(); Ok(result) } } mod frontmatter { use color_eyre::eyre::Result; use serde::{Deserialize, Serialize}; #[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] pub struct Data { pub title: String, pub date: String, pub series: Option>, pub tags: Option>, pub author: Option, pub draft: Option, } enum ParseState { Searching, ReadingFM { buf: String, line_start: bool }, SkipNewLine { end: bool }, ReadingMark { count: usize, end: bool }, } #[derive(Debug, thiserror::Error)] enum Error { #[error("EOF while parsing")] EOF, #[error("Error parsing YAML: {0:?}")] Yaml(#[from] serde_yaml::Error), } impl Data { pub fn parse(input: &str) -> Result<(Data, usize)> { let mut state = ParseState::Searching; let mut payload = None; let offset; let mut chars = input.char_indices(); 'parse: loop { let (i, ch) = match chars.next() { Some(x) => x, None => return Err(Error::EOF)?, }; match &mut state { ParseState::Searching => match ch { '-' => { state = ParseState::ReadingMark { count: 1, end: false, }; } '\n' | '\t' | ' ' => {} _ => { panic!("Start of frontmatter not found!"); } }, ParseState::ReadingMark { count, end } => match ch { '-' => { *count += 1; if *count == 3 { state = ParseState::SkipNewLine { end: *end }; } } _ => { panic!("Malformed frontmatter: {:?}", input); } }, ParseState::SkipNewLine { end } => match ch { '\n' => { if *end { offset = i + 1; break 'parse; } else { state = ParseState::ReadingFM { buf: String::new(), line_start: true, }; } } _ => { panic!("Expected newline, got {:?}", ch); } }, ParseState::ReadingFM { buf, line_start } => match ch { '-' if *line_start => { let mut state_tmp = ParseState::ReadingMark { count: 1, end: true, }; std::mem::swap(&mut state, &mut state_tmp); if let ParseState::ReadingFM { buf, .. } = state_tmp { payload = Some(buf); } else { unreachable!(); } } ch => { buf.push(ch); *line_start = ch == '\n'; } }, } } let payload = payload.unwrap(); let fm = serde_yaml::from_str(&payload)?; Ok((fm, offset)) } } }