use std::{cmp::Ordering, fs}; use anyhow::{bail, Context}; use orgize::{elements::Keyword, export::HtmlHandler, Element}; use time::{macros::format_description, Date}; use crate::{ date::parse_org_date, load::Loadable, parser::{parse, CustomHtmlHandler, Parser}, util::slugify, }; #[derive(Eq)] pub struct Post { pub title: String, pub date: Option<Date>, pub index: u32, pub content: String, pub url: String, } impl Post { pub fn html(&self) -> anyhow::Result<String> { let title = &self.title; let content = &self.content; let date = &self.date_f()?; let mut out = format!(r#"<div class="header-container"><h1>{title}</h1>"#); if let Some(date) = date { out.push_str(r#"<div class="header-date">"#); out.push_str(date); out.push_str("</div>"); } out.push_str("</div>"); out.push_str(content); Ok(out) } pub fn link(&self) -> anyhow::Result<String> { let link = match self.date_f()? { Some(date_f) => format!( r#"<tr><td><a href="{}">{}</a></td><td>[{}]</td></tr>"#, self.url, self.title, date_f ), None => format!( r#"<tr><td><a href="{}">{}</a></td><td></td></tr>"#, self.url, self.title ), }; Ok(link) } pub fn link_short(&self) -> String { format!( r#"<a href="{}" class="page-link">{}</a>"#, self.url, self.title ) } fn date_f(&self) -> anyhow::Result<Option<String>> { let date_f = match self.date { Some(date) => date.format(format_description!("[day] [month repr:short] [year]"))?, None => return Ok(None), }; Ok(Some(date_f)) } } impl Loadable for Post { fn load(path: &std::path::Path, slug_prefix: &str) -> anyhow::Result<Self> { let file_name = path .file_stem() .context("Could not get file name")? .to_str() .context("Could not convert filename into string")? .to_owned(); let url = slugify(&format!("/{slug_prefix}/{file_name}")); parse(PostParser::new(url)?, &fs::read_to_string(path)?) } } impl Ord for Post { fn cmp(&self, other: &Self) -> Ordering { let mut o = self.index.cmp(&other.index); if o.is_eq() { o = self.date.cmp(&other.date).reverse(); } o } } impl PartialOrd for Post { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } } impl PartialEq for Post { fn eq(&self, other: &Self) -> bool { self.index == other.index && self.date == other.date } } struct PostParser { title: Option<String>, date: Option<Date>, index: Option<u32>, url: String, content: Vec<u8>, handler: CustomHtmlHandler, } impl PostParser { fn new(url: String) -> anyhow::Result<Self> { Ok(Self { title: None, date: None, index: None, url, content: vec![], handler: CustomHtmlHandler::new()?, }) } } impl Parser<Post> for PostParser { fn start(&mut self, e: &Element) -> anyhow::Result<()> { match e { Element::Keyword(Keyword { key, value, .. }) if key == "TITLE" => { if self.title.is_some() { bail!("Post has more than one title"); } self.title = Some(value.to_string()); } Element::Keyword(Keyword { key, value, .. }) if key == "DATE" => { if self.date.is_some() { bail!("Post has more than one date"); } self.date = Some(parse_org_date(value)?); } Element::Keyword(Keyword { key, value, .. }) if key == "INDEX" => { if self.index.is_some() { bail!("Post has more than one index"); } self.index = Some(value.parse()?); } _ => { self.handler.start(&mut self.content, e)?; } } Ok(()) } fn end(&mut self, e: &Element) -> anyhow::Result<()> { self.handler.end(&mut self.content, e)?; Ok(()) } fn render(self) -> anyhow::Result<Post> { let title = self.title.context("Could not find post title")?; let date = self.date; let index = self.index.unwrap_or(u32::MAX); let content = String::from_utf8(self.content)?; let url = self.url; Ok(Post { title, date, index, content, url, }) } }