summaryrefslogblamecommitdiff
path: root/src/blog/post.rs
blob: 3c276902acbed33ea9c230e7aaaed64cd4fc0346 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13


                                              
                








                                        
                     


































                                                                                                                          
             








                                                   












                                                               















































































































                                                                                
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>,
    pub link: String,
}

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,
        link,
        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));

    let mut result: Vec<Post> = futures::future::join_all(futs)
        .await
        .into_iter()
        .map(Result::unwrap)
        .collect();

    if result.len() == 0 {
        Err(eyre!("No posts found"))
    } else {
        result.sort();
        result.reverse();
        Ok(result)
    } 
}

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))
        }
    }
}