pub struct Strategy {} use std::fs::{File as IOFile, create_dir_all, read_to_string}; use std::io::{Error, ErrorKind, Result, Write}; use std::path::Path; use crate::file_handler::{File, FileType, Strategy as FileHandlerStrategy}; use gema_texto::{gemini_parser::parse, html_renderer::render_html}; impl Strategy { fn is_title(line: &str) -> bool { line.starts_with("--- title:") } fn is_description(line: &str) -> bool { line.starts_with("--- description:") } fn get_title(line: &str) -> &str { match line.split_once("--- title:") { Some(title_line) => title_line.1, None => "", } } fn get_description(line: &str) -> &str { match line.split_once("--- description:") { Some(description_line) => description_line.1, None => "", } } } impl FileHandlerStrategy for Strategy { fn is(&self, path: &Path) -> bool { if let Some(extension) = path.extension() { return !path.is_dir() && extension == "gmi"; } false } fn identify(&self) -> FileType { FileType::Gemini } fn can_handle(&self, file_type: &FileType) -> bool { matches!(file_type, FileType::Gemini) } fn handle_html( &self, source: &Path, destination: &Path, file: &File, layout: &str, ) -> Result<()> { let gemini_contents = read_to_string(&file.path)?; // Front matter extraction let lines: Vec<&str> = gemini_contents.split('\n').collect(); let mut lines_found = 0; let mut title = ""; let mut description = ""; if let Some(slice) = lines.get(..2) { for line in slice { if Strategy::is_title(line) { title = Strategy::get_title(line).trim(); lines_found += 1; continue; } if Strategy::is_description(line) { description = Strategy::get_description(line).trim(); lines_found += 1; } } } let gemini_lines = lines.get(lines_found..).unwrap_or(&[]); let gemini_source = gemini_lines.join("\n"); let content_html = render_html(&parse(&gemini_source[..])); let generated_html = layout .replace("{{ title }}", title) .replace("{{ description }}", description) .replace("{{ content }}", &content_html[..]); let relative_path = file.path.strip_prefix(source).map_err(|_| { Error::new( ErrorKind::InvalidData, "Path was not part of source directory.", ) })?; let mut complete_destination = destination.join(relative_path); complete_destination.set_extension("html"); match complete_destination.parent() { Some(destination_parent) => { create_dir_all(destination_parent)?; let mut destination_file = IOFile::create(&complete_destination)?; destination_file.write_all(generated_html.as_bytes())?; Ok(()) } None => Err(Error::new( ErrorKind::InvalidData, "Destination parent was not readable.", )), } } fn handle_gemini(&self, source: &Path, destination: &Path, file: &File) -> Result<()> { let gemini_contents = read_to_string(&file.path)?; // Front matter extraction let lines: Vec<&str> = gemini_contents.split('\n').collect(); let mut lines_found = 0; if let Some(slice) = lines.get(..2) { for line in slice { if Strategy::is_title(line) { lines_found += 1; continue; } if Strategy::is_description(line) { lines_found += 1; } } } let gemini_lines = lines.get(lines_found..).unwrap_or(&[]); let gemini_source = gemini_lines.join("\n"); let relative_path = file.path.strip_prefix(source).map_err(|_| { Error::new( ErrorKind::InvalidData, "Path was not part of source directory.", ) })?; let complete_destination = destination.join(relative_path); match complete_destination.parent() { Some(destination_parent) => { create_dir_all(destination_parent)?; let mut destination_file = IOFile::create(&complete_destination)?; destination_file.write_all(gemini_source.as_bytes())?; Ok(()) } None => Err(Error::new( ErrorKind::InvalidData, "Destination parent was not readable.", )), } } } #[cfg(test)] mod tests { use std::fs::create_dir_all; use super::*; use test_utilities::*; #[test] fn detects_title() { assert!(Strategy::is_title("--- title: Hello!")); } #[test] fn does_not_detect_other_keys_as_title() { assert!(!Strategy::is_title("--- description: Hello!")); } #[test] fn detects_description() { assert!(Strategy::is_description("--- description: What is this?")); } #[test] fn does_not_detect_other_keys_as_description() { assert!(!Strategy::is_description("--- title: What is this?")); } #[test] fn extracts_title() { assert_eq!(Strategy::get_title("--- title: Hello!").trim(), "Hello!"); } #[test] fn returns_empty_string_on_bad_title_extraction() { assert_eq!(Strategy::get_title("--- mitle: Hello!").trim(), ""); } #[test] fn extracts_description() { assert_eq!( Strategy::get_description("--- description: What is this?").trim(), "What is this?" ); } #[test] fn returns_empty_string_on_bad_description_extraction() { assert_eq!( Strategy::get_description("--- mescription: NOOO!").trim(), "" ); } #[test] fn identifies_gemini_file() { let test_dir = setup_test_dir(); create_test_file(&test_dir.join("test.gmi"), ""); let strategy = Strategy {}; assert!(strategy.is(&test_dir.join("test.gmi"))); } #[test] fn rejects_non_gemini_file() { let test_dir = setup_test_dir(); create_test_file(&test_dir.join("_layout.html"), ""); create_test_file(&test_dir.join("image.png"), ""); let strategy = Strategy {}; assert!(!strategy.is(&test_dir.join("_layout.html"))); assert!(!strategy.is(&test_dir.join("image.png"))); assert!(!strategy.is(&test_dir)); } #[test] fn identifies_gemini_type() { let strategy = Strategy {}; assert!(matches!(strategy.identify(), FileType::Gemini)); } #[test] fn handles_gemini_type() { let strategy = Strategy {}; assert!(strategy.can_handle(&FileType::Gemini)); } #[test] fn rejects_non_gemini_types() { let strategy = Strategy {}; assert!(!strategy.can_handle(&FileType::Layout)); assert!(!strategy.can_handle(&FileType::File)); assert!(!strategy.can_handle(&FileType::Unknown)); } #[test] fn handles_html_generation() { let test_dir = setup_test_dir(); let source_dir = test_dir.join("source"); let output_dir = test_dir.join("output"); create_dir_all(&source_dir).expect("Could not create source test directory"); create_dir_all(&output_dir).expect("Could not create output test directory"); let layout = "\
Hello world
Hello world