use std::{cmp::Ordering, path::PathBuf};
use glob::glob;
use color_eyre::eyre::{Result, Context, eyre};
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<FixedOffset>,
}
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<Ordering> {
Some(self.date.cmp(&other.date))
}
}
async fn read_post(dir: &str, fname: PathBuf) -> Result<Post> {
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<FixedOffset> = DateTime::<Utc>::from_utc(NaiveDateTime::new(date, NaiveTime::from_hms(0,0,0)), Utc)
.with_timezone(&Utc)
.into();
Ok(Post {
front_matter,
body_html,
date
} )
}
pub async fn load(dir: &str) -> Result<Vec<Post>> {
let futs = glob(&format!("{}/*.md", dir))?
.filter_map(Result::ok)
.map(|fname| read_post(dir, fname));
Ok(Vec::new())
}
mod frontmatter {
use serde::{Serialize, Deserialize};
use color_eyre::eyre::Result;
#[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)]
pub struct Data {
pub title: String,
pub date: String,
pub series: Option<Vec<String>>,
pub tags: Option<Vec<String>>,
}
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");
}
},
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))
}
}
}