use crate::archiver::archive; use crate::configuration::Configuration; use crate::constants::METADATA_FILENAME; use crate::generator::generate; use crate::metadata::Metadata; use crate::post::Post; use gema_texto::{gemini_parser::parse, html_renderer::render_html}; use std::fs::{File, create_dir_all, read_dir, remove_dir_all}; use std::io::{Read, Result}; use std::path::Path; pub struct Generate; impl Generate { pub fn new() -> Self { Generate } fn read_posts(posts_directory: &Path, max_posts: u8) -> Vec { let mut posts = Vec::new(); for i in 0..max_posts { let post_path = posts_directory.join(i.to_string()); if let Some(post) = Generate::read_post(&post_path, i) { posts.push(post); } } posts } fn find_blog_content(post_path: &Path) -> Option { let entries = read_dir(post_path).ok()?; for entry in entries.filter_map(Result::ok) { let entry_path = entry.path(); if let Some(extension) = entry_path.extension() { if extension == "gmi" { let mut file = File::open(entry_path).ok()?; let mut contents = String::new(); file.read_to_string(&mut contents).ok()?; return Some(contents); } } } None } fn read_post(post_directory: &Path, index: u8) -> Option { let metadata_path = post_directory.join(METADATA_FILENAME); let metadata = Metadata::read_or_create(&metadata_path); let raw = Generate::find_blog_content(post_directory)?; let html = render_html(&parse(&raw)); Some(Post { metadata, index, html, raw, }) } } impl super::Command for Generate { fn before_dependencies(&self) -> Vec> { vec![] } fn execute(&self, _: Option<&String>, configuration: &Configuration, _: &str) -> Result<()> { let _ = remove_dir_all(&configuration.blog_output_directory); create_dir_all(&configuration.blog_output_directory)?; let posts = Generate::read_posts(&configuration.posts_directory, configuration.max_posts); generate( &configuration.static_directory, &configuration.templates_directory, &configuration.blog_output_directory, &posts, )?; let _ = remove_dir_all(&configuration.archive_output_directory); create_dir_all(&configuration.archive_output_directory)?; archive( &configuration.archive_directory, &configuration.templates_directory, &configuration.archive_output_directory, )?; Ok(()) } fn after_dependencies(&self) -> Vec> { vec![] } fn command(&self) -> &'static str { "generate" } fn help(&self) -> &'static str { "\t\t\t\tGenerates the blog assets" } } #[cfg(test)] mod tests { use std::fs::{File, create_dir_all}; use std::io::Write; use super::*; use crate::command::Command; use crate::configuration::Configuration; use test_utilities::*; #[test] fn test_generate_command() { let generate = Generate::new(); let test_dir = setup_test_dir(); let static_dir = test_dir.join("static"); let template_dir = test_dir.join("templates"); let blog_output_dir = test_dir.join("blog_output"); let archive_output_dir = test_dir.join("archive_output"); let posts_dir = test_dir.join("posts"); let archive_dir = test_dir.join("archive"); create_dir_all(&static_dir).expect("Could not create static directory"); create_dir_all(&template_dir).expect("Could not create template directory"); create_dir_all(&blog_output_dir).expect("Could not create blog output directory"); create_dir_all(&archive_output_dir).expect("Could not create archive output directory"); create_dir_all(&posts_dir).expect("Could not create output directory"); create_test_file(&static_dir.join("file.txt"), "wow"); create_test_file( &template_dir.join("index.html"), "\ {{~ posts:post }} {{= post.html }} {{~}} ", ); create_test_file( &template_dir.join("feed.xml"), "\ {{~ posts:post }} {{= post.id }} {{~}} ", ); create_test_file( &template_dir.join("index.txt"), "\ {{~ posts:post }} {{= post.raw }} {{~}} ", ); // Create the posts create_dir_all(posts_dir.join("0")).expect("Could not create post 0 directory"); create_test_file( &posts_dir.join("0/metadata.json"), "{ \"id\": \"1736035200000\", \"created_on\": 1736035200000 }", ); create_test_file(&posts_dir.join("0/ignored"), "IGNORED"); create_test_file(&posts_dir.join("0/the-latest.gmi"), "The latest is here."); create_dir_all(posts_dir.join("1")).expect("Could not create post 1 directory"); create_test_file( &posts_dir.join("1/metadata.json"), "{ \"id\": \"1736045200000\", \"created_on\": 1736045200000 }", ); create_test_file(&posts_dir.join("1/the-latest.gmi"), "Often forgotten."); create_dir_all(posts_dir.join("2")).expect("Could not create post 2 directory"); create_test_file( &posts_dir.join("2/metadata.json"), "{ \"id\": \"1736045200009\", \"created_on\": 1736045200009 }", ); create_dir_all(posts_dir.join("3")).expect("Could not create post 3 directory"); let invalid_bytes = vec![0xFF, 0xFF, 0xFF, 0xFF]; create_test_file( &posts_dir.join("3/metadata.json"), "{ \"id\": \"1736045200099\", \"created_on\": 1736045200099 }", ); let mut bad_file = File::create(posts_dir.join("3/bad-file.gmi")).expect("Could not create bad file."); bad_file .write_all(&invalid_bytes) .expect("Could not write bad bytes to bad file."); let mut configuration = Configuration::new().unwrap(); configuration.archive_directory = archive_dir.clone(); configuration.static_directory = static_dir.clone(); configuration.templates_directory = template_dir.clone(); configuration.blog_output_directory = blog_output_dir.clone(); configuration.archive_output_directory = archive_output_dir.clone(); configuration.posts_directory = posts_dir.clone(); configuration.max_posts = 4; generate .execute(None, &configuration, "generate") .expect("Could not generate"); assert_file_contents(&blog_output_dir.join("file.txt"), "wow"); assert_file_contents( &blog_output_dir.join("index.html"), "\

The latest is here.

Often forgotten.

", ); assert_file_contents( &blog_output_dir.join("feed.xml"), "\ 1736035200000 1736045200000 ", ); cleanup_test_dir(&test_dir); } #[test] fn test_generate_command_archive() { let generate = Generate::new(); let test_dir = setup_test_dir(); let static_dir = test_dir.join("static"); let template_dir = test_dir.join("templates"); let blog_output_dir = test_dir.join("blog_output"); let archive_output_dir = test_dir.join("archive_output"); let posts_dir = test_dir.join("posts"); let archive_dir = test_dir.join("archive"); create_dir_all(&static_dir).expect("Could not create static directory"); create_dir_all(&template_dir).expect("Could not create template directory"); create_dir_all(&blog_output_dir).expect("Could not create blog output directory"); create_dir_all(&archive_output_dir).expect("Could not create archive output directory"); create_dir_all(&posts_dir).expect("Could not create output directory"); create_test_file(&static_dir.join("file.txt"), "wow"); // Create the archive posts create_dir_all(archive_dir.join("1736035200000")) .expect("Could not create post 0 directory"); create_test_file( &archive_dir.join("1736035200000/metadata.json"), "{ \"id\": \"1736035200000\", \"created_on\": 1736035200000 }", ); create_test_file( &archive_dir.join("1736035200000/the-latest.gmi"), "The latest is here.", ); create_dir_all(archive_dir.join("1736045200000")) .expect("Could not create post 1 archive directory"); create_test_file( &archive_dir.join("1736045200000/metadata.json"), "{ \"id\": \"1736045200000\", \"created_on\": 1736045200000 }", ); let mut configuration = Configuration::new().unwrap(); configuration.archive_directory = archive_dir.clone(); configuration.static_directory = static_dir.clone(); configuration.templates_directory = template_dir.clone(); configuration.blog_output_directory = blog_output_dir.clone(); configuration.archive_output_directory = archive_output_dir.clone(); configuration.posts_directory = posts_dir.clone(); configuration.max_posts = 4; generate .execute(None, &configuration, "generate") .expect("Could not generate"); assert_file_contains( &archive_output_dir.join("index.gmi"), "\n=> ./1736035200000/the-latest.gmi 2025-01-05 the latest", ); assert_file_contains( &archive_output_dir.join("index.gph"), "\n[0|2025-01-05 the latest|./1736035200000/the-latest.gmi|server|port]", ); assert_file_contents( &archive_output_dir.join("1736035200000/the-latest.gmi"), "The latest is here.", ); cleanup_test_dir(&test_dir); } #[test] fn generate_before_dependencies() { let generate = Generate::new(); let dependencies = generate.before_dependencies(); assert_eq!(dependencies.len(), 0); } #[test] fn generate_after_dependencies() { let generate = Generate::new(); let dependencies = generate.after_dependencies(); assert_eq!(dependencies.len(), 0); } // These two tests feel pointless but I'm doing it for the coverage :p #[test] fn generate_command_output() { let generate = Generate::new(); generate.command(); } #[test] fn generate_help_output() { let generate = Generate::new(); generate.help(); } }