use super::util::from_frontmatter; use super::util::output_rendered_template; use super::util::templates::filters; use super::util::templates::TemplateCommon; use super::{content_path, markdown}; use askama::Template; use chrono::NaiveDate; use once_cell::sync::Lazy; use pulldown_cmark::{html, Event}; use regex::Regex; use serde::Deserialize; use std::fs; use std::io::Read; use std::path::PathBuf; pub fn generate() { let mut shows = fs::read_dir(content_path("tv")) .unwrap() .map(|entry| { let entry = entry.unwrap(); if !entry.file_type().unwrap().is_file() { return None; } let path = entry.path(); let mut f = fs::File::open(&path).unwrap(); let mut buf = String::new(); f.read_to_string(&mut buf).unwrap(); Show::new(path, &buf).ok() }) .flatten() .collect::>(); shows.sort_by(|a, b| a.last_updated().cmp(b.last_updated()).reverse()); for show in shows.iter() { output_rendered_template( &ShowTemplate { show }, format!("/tv/{}/index.html", show.slug), ) .unwrap(); } output_rendered_template(&TvTemplate { shows: &shows }, "/tv/index.html").unwrap(); } #[derive(Template)] #[template(path = "show.html")] struct ShowTemplate<'a> { show: &'a Show, } impl<'a> TemplateCommon for ShowTemplate<'a> {} impl<'a> ShowTemplate<'a> { fn permalink(&self) -> String { format!("/tv/{}/", self.show.slug) } } #[derive(Template)] #[template(path = "tv.html")] struct TvTemplate<'a> { shows: &'a [Show], } impl<'a> TemplateCommon for TvTemplate<'a> {} impl<'a> TvTemplate<'a> { fn permalink(&self) -> &'static str { "/tv/" } } struct Show { pub metadata: ShowMetadata, pub slug: String, pub episodes: Vec, } impl Show { fn new(path: PathBuf, contents: &str) -> anyhow::Result { let (metadata, rest_contents) = match from_frontmatter::<'_, ShowMetadata>(contents) { Ok(res) => res, Err(e) => return Err(e), }; let slug = path.file_stem().unwrap().to_str().unwrap().to_owned(); let episodes = parse_episodes(rest_contents); Ok(Self { metadata, slug, episodes, }) } fn last_updated(&self) -> &NaiveDate { &self.episodes.iter().max_by_key(|ep| ep.date).unwrap().date } } fn parse_episodes(contents: &str) -> Vec { Episodes { iter: markdown::parse(contents), buf: vec![], cur_episode_meta: None, finished: false, } .collect() } #[derive(Debug, Deserialize)] struct ShowMetadata { pub title: String, } #[derive(Debug)] struct Episode { title: String, date: NaiveDate, content: String, } struct Episodes<'a, I: Iterator>> { iter: I, buf: Vec>, cur_episode_meta: Option<(String, NaiveDate)>, finished: bool, } impl<'a, I: Iterator>> Iterator for Episodes<'a, I> { type Item = Episode; fn next(&mut self) -> Option { loop { match self.iter.next() { Some(Event::Html(ref s)) if is_ep_comment(s) => { if self.cur_episode_meta.is_some() { let ep = self.create_episode(); self.cur_episode_meta = Some(parse_meta(s)); return Some(ep); } else { self.cur_episode_meta = Some(parse_meta(s)); } } Some(e) => { self.buf.push(e); } None => { if self.finished { return None; } else { self.finished = true; return Some(self.create_episode()); } } } } } } impl<'a, I: Iterator>> Episodes<'a, I> { fn create_episode(&mut self) -> Episode { let meta = self.cur_episode_meta.take().unwrap(); let mut content = String::new(); let events = std::mem::take(&mut self.buf); html::push_html(&mut content, events.into_iter()); Episode { title: meta.0, date: meta.1, content, } } } static EPISODE_SEPARATOR: Lazy = Lazy::new(|| Regex::new(r#"^\s*\s*$"#).unwrap()); fn is_ep_comment(s: impl AsRef) -> bool { EPISODE_SEPARATOR.is_match(s.as_ref()) } fn parse_meta(s: impl AsRef) -> (String, NaiveDate) { let captures = EPISODE_SEPARATOR.captures(s.as_ref()).unwrap(); let title = &captures[1]; let date_str = &captures[2]; ( title.into(), NaiveDate::parse_from_str(date_str, "%Y-%m-%d").expect("episode entry date"), ) }