193 lines
4.9 KiB
Rust
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"),
|
||
|
)
|
||
|
}
|