use std::fs;
use std::path::Path;
use std::sync::Arc;
use askama::Template;
use crate::Config;
use crate::Document;
use crate::Site;
pub fn generate_html_index_pages(
config: Arc<Config>,
documents: Vec<Document>,
lucky_logo_time: bool,
) {
let allowed_group_bys = vec![GroupBy::Category, GroupBy::Module, GroupBy::Author];
for group_by in allowed_group_bys.clone() {
let document_tables = group_and_sort_documents(documents.clone(), group_by.clone());
let index_template = IndexTemplate {
site: config.site.clone(),
tables: document_tables,
group_by: group_by.clone(),
allowed_group_bys: allowed_group_bys.clone(),
lucky_logo_time,
};
let html = index_template.render().unwrap();
let key = group_by.key();
fs::write(
config.site.output_dir.join(format!("index_by_{key}.html")),
&html,
)
.unwrap();
}
fs::copy(
config.site.output_dir.join("index_by_category.html"),
config.site.output_dir.join("index.html"),
)
.unwrap();
}
pub fn generate_html_error_pages(config: Arc<Config>, lucky_logo_time: bool) {
for (error_template, filename) in [
(
ErrorTemplate {
site: config.site.clone(),
error: "Fehler 404".to_string(),
description: "Diese Datei existiert nicht mehr :/".to_string(),
lucky_logo_time,
},
Path::new("404_documents.html"),
),
(
ErrorTemplate {
site: config.site.clone(),
error: "Fehler 404".to_string(),
description: "Diese Seite existiert nicht".to_string(),
lucky_logo_time,
},
Path::new("404.html"),
),
] {
let html = error_template.render().unwrap();
fs::write(config.site.output_dir.join(filename), html).unwrap();
}
}
#[derive(Clone, PartialEq)]
enum GroupBy {
Category,
Module,
Author,
}
impl GroupBy {
fn key(&self) -> String {
String::from(match self {
GroupBy::Category => "category",
GroupBy::Module => "module",
GroupBy::Author => "author",
})
}
fn name(&self) -> String {
String::from(match self {
GroupBy::Category => "Kategorie",
GroupBy::Module => "Modul",
GroupBy::Author => "Autor",
})
}
}
#[derive(Clone, PartialEq)]
enum SortBy {
Title,
Author,
Module,
Category,
ModificationTime,
}
#[derive(Template)]
#[template(path = "index.html.j2")]
struct IndexTemplate {
site: Site,
tables: Vec<DocumentTable>,
group_by: GroupBy,
allowed_group_bys: Vec<GroupBy>,
lucky_logo_time: bool,
}
#[derive(Clone, Debug, PartialEq)]
struct DocumentTable {
title: String,
documents: Vec<Document>,
}
#[derive(Template, Clone)]
#[template(path = "error.html.j2")]
struct ErrorTemplate {
site: Site,
error: String,
description: String,
lucky_logo_time: bool,
}
fn group_and_sort_documents(documents: Vec<Document>, group_by: GroupBy) -> Vec<DocumentTable> {
let mut tables = vec![];
for document in documents {
let title = match group_by.clone() {
GroupBy::Category => document.module.category(),
GroupBy::Author => document.authors.join(", "),
GroupBy::Module => document.module.name(),
};
match tables
.clone()
.into_iter()
.position(|table: DocumentTable| table.title == title)
{
Some(index) => tables[index].documents.push(document),
None => tables.push(DocumentTable {
title,
documents: vec![document],
}),
}
}
tables.sort_by_key(|table| table.title.clone());
for table in &mut tables {
table.documents.sort_by(|a, b| {
for sort_by in [
SortBy::Category,
SortBy::Module,
SortBy::Title,
SortBy::Author,
SortBy::ModificationTime,
] {
let order: std::cmp::Ordering = match sort_by {
SortBy::Title => a.title.cmp(&b.title),
SortBy::Author => a.authors.cmp(&b.authors).reverse(),
SortBy::Module => a.module.name().cmp(&b.module.name()),
SortBy::Category => a.module.category().cmp(&b.module.category()),
SortBy::ModificationTime => {
a.modification_time.cmp(&b.modification_time).reverse()
}
};
if order == std::cmp::Ordering::Equal {
continue;
}
return order;
}
std::cmp::Ordering::Equal
})
}
tables
}
#[cfg(test)]
mod test {
use std::fs;
use std::path::PathBuf;
use assert_fs::prelude::*;
use chrono::TimeZone;
use chrono::Utc;
use parameterized::parameterized;
use url::Url;
use super::*;
use crate::config::Provider;
use crate::config::Source;
use crate::config::test::example_config;
use crate::config::test::example_licenses;
use crate::inventory::Module;
use crate::inventory::Semester1;
use crate::inventory::Semester2;
static DESCRIPTION: &str = "Typst4EI ist eine Formelsammlung­sammlung von/für EI-Studenten der TU München. Du hast eine tolle Formelsammlung und möchtest sie mit deinen Kommilitonen teilen? Du hast einen Fehler gefunden? Du willst einfach Danke sagen? Dann schreib uns per E-Mail an <a href='mailto:info@typst4ei.de'>info@typst4ei.de</a>!";
#[parameterized(group_by = {
GroupBy::Category,
GroupBy::Module,
GroupBy::Author,
})]
fn test_generate_html_index_pages(group_by: GroupBy) {
let directory = assert_fs::TempDir::new().unwrap();
let config = {
let mut config = example_config();
config.site.output_dir = directory.to_path_buf();
config.site.description = DESCRIPTION.to_string();
Arc::new(config)
};
let (documents, _, _, _) = example_documents();
generate_html_index_pages(config.clone(), documents, false);
let key = match group_by {
GroupBy::Category => "category",
GroupBy::Module => "module",
GroupBy::Author => "author",
};
let actual_filename = config.site.output_dir.join(format!("index_by_{key}.html"));
let expected_filename =
PathBuf::from("testdata/generate_html_pages").join(format!("index_by_{key}.html"));
let actual_html = fs::read_to_string(&actual_filename).unwrap();
let expected_html = fs::read_to_string(&expected_filename).unwrap();
pretty_assertions::assert_eq!(expected_html, actual_html);
directory
.child("index.html")
.assert(predicates::path::eq_file(directory.join("index.html")));
}
#[parameterized(filename = {
"404.html",
"404_documents.html",
})]
fn test_generate_html_error_pages(filename: &str) {
let directory = assert_fs::TempDir::new().unwrap();
let config = {
let mut config = example_config();
config.site.output_dir = directory.to_path_buf();
config.site.description = DESCRIPTION.to_string();
Arc::new(config)
};
generate_html_error_pages(config.clone(), false);
let actual_filename = config.site.output_dir.join(filename);
let expected_filename = PathBuf::from("testdata/generate_html_pages").join(filename);
let actual_html = fs::read_to_string(&actual_filename).unwrap();
let expected_html = fs::read_to_string(&expected_filename).unwrap();
pretty_assertions::assert_eq!(expected_html, actual_html);
}
#[ignore]
#[test]
fn test_generate_html_pages_overwrite_expected_html_files() {
let config = {
let mut config = example_config();
config.site.output_dir = PathBuf::from("testdata/generate_html_pages");
config.site.description = DESCRIPTION.to_string();
Arc::new(config)
};
let (documents, _, _, _) = example_documents();
let lucky_logo_time = false;
generate_html_index_pages(config.clone(), documents, lucky_logo_time);
generate_html_error_pages(config.clone(), lucky_logo_time);
}
#[parameterized(group_by = {
GroupBy::Category,
GroupBy::Module,
GroupBy::Author,
})]
fn test_group_and_sort_documents(group_by: GroupBy) {
let (
documents,
documents_grouped_by_category,
documents_grouped_by_module,
documents_grouped_by_authors,
) = example_documents();
let actual_documents = group_and_sort_documents(documents, group_by.clone());
let expected_documents = match group_by {
GroupBy::Category => documents_grouped_by_category,
GroupBy::Module => documents_grouped_by_module,
GroupBy::Author => documents_grouped_by_authors,
};
pretty_assertions::assert_eq!(expected_documents, actual_documents);
}
fn example_documents() -> (
Vec<Document>,
Vec<DocumentTable>,
Vec<DocumentTable>,
Vec<DocumentTable>,
) {
let alex_source = Source {
provider: Provider::Gitea,
owner: "alex".to_string(),
repo: "TUM-Formelsammlungen".to_string(),
custom_display_url: Some(Url::parse("https://gitea.mintcalc.com/").unwrap()),
custom_fetch_url: Some(Url::parse("https://gitea.localhost/").unwrap()),
hide_urls: false,
};
let max_source = Source {
provider: Provider::Codeberg,
owner: "max".to_string(),
repo: "kleine-kochbuecher-der-elektrotechnik".to_string(),
custom_display_url: None,
custom_fetch_url: None,
hide_urls: false,
};
let moritz_source = Source {
provider: Provider::Gitea,
owner: "moritz".to_string(),
repo: "tum-fs".to_string(),
custom_display_url: None,
custom_fetch_url: None,
hide_urls: true,
};
let (cc0, cc_by_sa_4_0) = example_licenses();
let alex_sem1_analysis_1 = Document {
filename: "sem1-analysis.pdf".to_string(),
title: "Analysis 1".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester1(Semester1::Analysis1),
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(1_700_000_000, 0).unwrap(),
};
let max_sem1_analysis_1 = Document {
filename: "sem1-analysis-1.pdf".to_string(),
title: "Analysis 1".to_string(),
authors: vec!["Max".to_string()],
module: Module::Semester1(Semester1::Analysis1),
license: cc_by_sa_4_0.clone(),
source: max_source.clone(),
modification_time: Utc.timestamp_opt(1_700_000_000, 0).unwrap(),
};
let alex_sem1_analysis_1_copy = Document {
filename: "sem1-analysis-1-copy.pdf".to_string(),
title: "Analysis 1".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester1(Semester1::Analysis1),
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(1_500_000_000, 0).unwrap(),
};
let alex_sem1_computertechnik = Document {
filename: "sem1-computertechnik.pdf".to_string(),
title: "Computertechnik".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester1(Semester1::ComputerTechnology),
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(1_300_000_000, 0).unwrap(),
};
let max_sem1_digitaltechnik = Document {
filename: "sem1-digitaltechnik.pdf".to_string(),
title: "Digitaltechnik".to_string(),
authors: vec!["Max".to_string()],
module: Module::Semester1(Semester1::DigitalTechnology),
license: cc_by_sa_4_0.clone(),
source: max_source.clone(),
modification_time: Utc.timestamp_opt(1_700_000_000, 0).unwrap(),
};
let moritz_sem1_digitaltechnik = Document {
filename: "sem1-digitaltechnik-2.pdf".to_string(),
title: "Digitaltechnik".to_string(),
authors: vec!["Moritz".to_string()],
module: Module::Semester1(Semester1::DigitalTechnology),
license: cc0.clone(),
source: moritz_source.clone(),
modification_time: Utc.timestamp_opt(1_700_000_000, 0).unwrap(),
};
let alex_sem1_linare_algebra = Document {
filename: "sem1-lineare-algebra.pdf".to_string(),
title: "Lineare Algebra".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester1(Semester1::LinearAlgebra),
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(1_700_000_000, 0).unwrap(),
};
let alex_sem1_schaltungstheorie = Document {
filename: "sem1-schaltungstheorie.pdf".to_string(),
title: "Schaltungstheorie".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester1(Semester1::CircuitTheory),
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(1_400_000_000, 0).unwrap(),
};
let moritz_sem1_schaltungstheorie = Document {
filename: "sem1-schaltungstheorie-2.pdf".to_string(),
title: "Schaltungstheorie".to_string(),
authors: vec!["Moritz".to_string()],
module: Module::Semester1(Semester1::CircuitTheory),
license: cc0.clone(),
source: moritz_source.clone(),
modification_time: Utc.timestamp_opt(1_300_000_000, 0).unwrap(),
};
let alex_sem1_linare_algebra_2 = Document {
filename: "sem1-lineare-algebra-2.pdf".to_string(),
title: "Lineare Algebra".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester1(Semester1::LinearAlgebra),
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(1_900_000_000, 0).unwrap(),
};
let alex_sem2_algorithmen_und_datenstrukturen = Document {
filename: "sem2-algorithmen-und-datenstrukturen.pdf".to_string(),
title: "Algorithmen und Datenstrukturen".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester2(Semester2::AlgorithmsAndDataStructures),
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(1_700_000_000, 0).unwrap(),
};
let max_sem2_algorithmen_und_datenstrukturen = Document {
filename: "sem2-algorithmen-und-datenstrukturen-2.pdf".to_string(),
title: "Algorithmen und Datenstrukturen".to_string(),
authors: vec!["Max".to_string()],
module: Module::Semester2(Semester2::AlgorithmsAndDataStructures),
license: cc_by_sa_4_0.clone(),
source: max_source.clone(),
modification_time: Utc.timestamp_opt(2_700_000_000, 0).unwrap(),
};
let alex_sem2_analysis_2 = Document {
filename: "sem2-analysis-2.pdf".to_string(),
title: "Analysis 2".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester2(Semester2::Analysis2),
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(1_500_000_000, 0).unwrap(),
};
let max_sem2_analysis_2 = Document {
filename: "sem2-analysis-2.pdf".to_string(),
title: "Analysis 2".to_string(),
authors: vec!["Max".to_string()],
module: Module::Semester2(Semester2::Analysis2),
license: cc_by_sa_4_0,
source: max_source,
modification_time: Utc.timestamp_opt(2_300_000_000, 0).unwrap(),
};
let alex_sem2_kalender = Document {
filename: "sem2-kalender.pdf".to_string(),
title: "Kalender".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Misc,
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(2_900_000_000, 0).unwrap(),
};
let alex_sem2_elektrizitaet_und_magnetismus = Document {
filename: "sem2-elektrizität-und-magnetismus.pdf".to_string(),
title: "Elektrizität und Magnetismus".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester2(Semester2::ElectrityAndMagnetism),
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(1_500_000_000, 0).unwrap(),
};
let alex_sem2_elektrizitaet_und_magnetismus_flash_cards = Document {
filename: "sem2-elektrizität-und-magnetismus-flash-cards.pdf".to_string(),
title: "Elektrizität und Magnetismus Flash Cards".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester2(Semester2::ElectrityAndMagnetism),
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(2_200_000_000, 0).unwrap(),
};
let alex_sem2_physik = Document {
filename: "sem2-physik.pdf".to_string(),
title: "Physik".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester2(Semester2::Physics),
license: cc0.clone(),
source: alex_source.clone(),
modification_time: Utc.timestamp_opt(2_500_000_000, 0).unwrap(),
};
let alex_sem2_systemtheorie = Document {
filename: "sem2-systemtheorie.pdf".to_string(),
title: "Systemtheorie".to_string(),
authors: vec!["Alex".to_string()],
module: Module::Semester2(Semester2::SystemsTheory),
license: cc0,
source: alex_source,
modification_time: Utc.timestamp_opt(2_800_000_000, 0).unwrap(),
};
let documents = vec![
alex_sem1_analysis_1.clone(),
max_sem1_analysis_1.clone(),
alex_sem1_analysis_1_copy.clone(),
alex_sem1_computertechnik.clone(),
max_sem1_digitaltechnik.clone(),
alex_sem2_analysis_2.clone(),
moritz_sem1_digitaltechnik.clone(),
alex_sem1_linare_algebra.clone(),
alex_sem1_schaltungstheorie.clone(),
moritz_sem1_schaltungstheorie.clone(),
alex_sem1_linare_algebra_2.clone(),
alex_sem2_algorithmen_und_datenstrukturen.clone(),
max_sem2_algorithmen_und_datenstrukturen.clone(),
max_sem2_analysis_2.clone(),
alex_sem2_kalender.clone(),
alex_sem2_elektrizitaet_und_magnetismus.clone(),
alex_sem2_elektrizitaet_und_magnetismus_flash_cards.clone(),
alex_sem2_physik.clone(),
alex_sem2_systemtheorie.clone(),
];
let documents_grouped_by_category = vec![
DocumentTable {
title: "Semester 1".to_string(),
documents: vec![
max_sem1_analysis_1.clone(),
alex_sem1_analysis_1.clone(),
alex_sem1_analysis_1_copy.clone(),
alex_sem1_computertechnik.clone(),
moritz_sem1_digitaltechnik.clone(),
max_sem1_digitaltechnik.clone(),
alex_sem1_linare_algebra_2.clone(),
alex_sem1_linare_algebra.clone(),
moritz_sem1_schaltungstheorie.clone(),
alex_sem1_schaltungstheorie.clone(),
],
},
DocumentTable {
title: "Semester 2".to_string(),
documents: vec![
max_sem2_algorithmen_und_datenstrukturen.clone(),
alex_sem2_algorithmen_und_datenstrukturen.clone(),
max_sem2_analysis_2.clone(),
alex_sem2_analysis_2.clone(),
alex_sem2_elektrizitaet_und_magnetismus.clone(),
alex_sem2_elektrizitaet_und_magnetismus_flash_cards.clone(),
alex_sem2_physik.clone(),
alex_sem2_systemtheorie.clone(),
],
},
DocumentTable {
title: "Verschiedenes".to_string(),
documents: vec![alex_sem2_kalender.clone()],
},
];
let documents_grouped_by_module = vec![
DocumentTable {
title: "Algorithmen und Datenstrukturen".to_string(),
documents: vec![
max_sem2_algorithmen_und_datenstrukturen.clone(),
alex_sem2_algorithmen_und_datenstrukturen.clone(),
],
},
DocumentTable {
title: "Analysis 1".to_string(),
documents: vec![
max_sem1_analysis_1.clone(),
alex_sem1_analysis_1.clone(),
alex_sem1_analysis_1_copy.clone(),
],
},
DocumentTable {
title: "Analysis 2".to_string(),
documents: vec![max_sem2_analysis_2.clone(), alex_sem2_analysis_2.clone()],
},
DocumentTable {
title: "Computertechnik".to_string(),
documents: vec![alex_sem1_computertechnik.clone()],
},
DocumentTable {
title: "Digitaltechnik".to_string(),
documents: vec![
moritz_sem1_digitaltechnik.clone(),
max_sem1_digitaltechnik.clone(),
],
},
DocumentTable {
title: "Elektrizität und Magnetismus".to_string(),
documents: vec![
alex_sem2_elektrizitaet_und_magnetismus.clone(),
alex_sem2_elektrizitaet_und_magnetismus_flash_cards.clone(),
],
},
DocumentTable {
title: "Lineare Algebra".to_string(),
documents: vec![
alex_sem1_linare_algebra_2.clone(),
alex_sem1_linare_algebra.clone(),
],
},
DocumentTable {
title: "Physik".to_string(),
documents: vec![alex_sem2_physik.clone()],
},
DocumentTable {
title: "Schaltungstheorie".to_string(),
documents: vec![
moritz_sem1_schaltungstheorie.clone(),
alex_sem1_schaltungstheorie.clone(),
],
},
DocumentTable {
title: "Systemtheorie".to_string(),
documents: vec![alex_sem2_systemtheorie.clone()],
},
DocumentTable {
title: "Verschiedenes".to_string(),
documents: vec![alex_sem2_kalender.clone()],
},
];
let documents_grouped_by_authors = vec![
DocumentTable {
title: "Alex".to_string(),
documents: vec![
alex_sem1_analysis_1.clone(),
alex_sem1_analysis_1_copy.clone(),
alex_sem1_computertechnik.clone(),
alex_sem1_linare_algebra_2.clone(),
alex_sem1_linare_algebra.clone(),
alex_sem1_schaltungstheorie.clone(),
alex_sem2_algorithmen_und_datenstrukturen.clone(),
alex_sem2_analysis_2.clone(),
alex_sem2_elektrizitaet_und_magnetismus.clone(),
alex_sem2_elektrizitaet_und_magnetismus_flash_cards.clone(),
alex_sem2_physik.clone(),
alex_sem2_systemtheorie.clone(),
alex_sem2_kalender.clone(),
],
},
DocumentTable {
title: "Max".to_string(),
documents: vec![
max_sem1_analysis_1.clone(),
max_sem1_digitaltechnik.clone(),
max_sem2_algorithmen_und_datenstrukturen.clone(),
max_sem2_analysis_2.clone(),
],
},
DocumentTable {
title: "Moritz".to_string(),
documents: vec![
moritz_sem1_digitaltechnik.clone(),
moritz_sem1_schaltungstheorie.clone(),
],
},
];
return (
documents,
documents_grouped_by_category,
documents_grouped_by_module,
documents_grouped_by_authors,
);
}
}