v6/src/generator/tv.rs

193 lines
4.9 KiB
Rust

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::<Vec<_>>();
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<Episode>,
}
impl Show {
fn new(path: PathBuf, contents: &str) -> anyhow::Result<Self> {
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<Episode> {
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<Item = Event<'a>>> {
iter: I,
buf: Vec<Event<'a>>,
cur_episode_meta: Option<(String, NaiveDate)>,
finished: bool,
}
impl<'a, I: Iterator<Item = Event<'a>>> Iterator for Episodes<'a, I> {
type Item = Episode;
fn next(&mut self) -> Option<Self::Item> {
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<Item = Event<'a>>> 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<Regex> =
Lazy::new(|| Regex::new(r#"^\s*<!-- episode "(.+)" (.*) -->\s*$"#).unwrap());
fn is_ep_comment(s: impl AsRef<str>) -> bool {
EPISODE_SEPARATOR.is_match(s.as_ref())
}
fn parse_meta(s: impl AsRef<str>) -> (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"),
)
}