mod gemini; mod gopher; mod raw; use crate::template::{TemplateContext, TemplateValue}; use std::collections::HashMap; use std::fs::read_dir; use std::io::Result; use std::path::{Path, PathBuf}; use time::{format_description::FormatItem, macros::format_description, OffsetDateTime}; const DATE_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]"); struct ArchiveEntry { id: String, slug: String, } type Archiver = fn(&Path, &Path, &Path, &TemplateContext) -> Result<()>; impl ArchiveEntry { pub fn to_template_context(archive_entries: &[ArchiveEntry]) -> TemplateContext { let mut context = HashMap::new(); let archive_entries_collection = archive_entries .iter() .map(ArchiveEntry::to_template_value) .collect(); context.insert( "archive_length".to_string(), TemplateValue::Unsigned(archive_entries.len().try_into().unwrap()), ); context.insert( "posts".to_string(), TemplateValue::Collection(archive_entries_collection), ); context } pub fn to_template_value(&self) -> TemplateContext { let mut context = HashMap::new(); context.insert("id".to_string(), TemplateValue::String(self.id.clone())); context.insert("slug".to_string(), TemplateValue::String(self.slug.clone())); if let Some(title) = self.title() { context.insert("title".to_string(), TemplateValue::String(title)); } context } fn title(&self) -> Option { let date = OffsetDateTime::from_unix_timestamp_nanos( (self.id.parse::().ok()? * 1_000_000).into(), ) .ok()?; let short_date = date.format(&DATE_FORMAT).ok()?; let title = self.slug.replace('-', " "); Some(format!("{short_date} {title}")) } } fn read_archive(archive_directory: &PathBuf) -> Vec { let mut archive_entries = Vec::new(); if let Ok(entries) = read_dir(archive_directory) { for entry in entries.filter_map(Result::ok) { let entry_path = entry.path(); let post_id = entry.file_name(); if let Ok(entry_type) = entry.file_type() { if entry_type.is_dir() { if let Ok(candidates) = read_dir(&entry_path) { for candidate in candidates.filter_map(Result::ok) { let candidate_path = candidate.path(); match candidate_path.extension() { Some(extension) => { if extension == "gmi" { if let Some(slug) = candidate_path.file_stem() { if let (Some(post_id), Some(slug)) = (post_id.to_str(), slug.to_str()) { archive_entries.push(ArchiveEntry { id: post_id.to_string(), slug: slug.to_string(), }); } } } } _ => continue, } } } } } } } archive_entries.sort_by(|a, b| b.id.cmp(&a.id)); archive_entries } pub fn archive( archive_directory: &PathBuf, template_directory: &Path, output_directory: &Path, ) -> Result<()> { let archivers = available_archivers(); let archive_entries = read_archive(archive_directory); let context = ArchiveEntry::to_template_context(&archive_entries); for archiver in archivers { archiver( archive_directory, template_directory, output_directory, &context, )?; } Ok(()) } fn available_archivers() -> Vec { vec![raw::archive, gemini::archive, gopher::archive] }