1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
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<String> {
let date = OffsetDateTime::from_unix_timestamp_nanos(
(self.id.parse::<u64>().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<ArchiveEntry> {
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<Archiver> {
vec![raw::archive, gemini::archive, gopher::archive]
}
|