coverage:
cargo tarpaulin
+format:
+ cargo fmt && cargo clippy --fix
+
+lint:
+ cargo fmt -- --check && cargo clippy
+
release: rpm tar deb
@$(eval filename := $(app_name)-$(target)-$(channel))
$(MAKE) -e profile=release -e architectures='$(mac_architectures)' -e channel=$(tag) package
endif
-ci:
+ci: lint coverage
ifeq ($(GIT_REF),refs/heads/main)
$(MAKE) -e profile=release -e channel=unstable package
else ifneq (,$(findstring refs/tags/,$(GIT_REF)))
$(MAKE) -e profile=release -e channel=$(subst refs/tags/,,$(GIT_REF)) package
endif
-.PHONY: default build $(architectures) rpm package prepare set_rust ci release test coverage
+.PHONY: default build $(architectures) rpm package prepare set_rust ci release test coverage format
use std::path::PathBuf;
pub fn find_files(directory_path: &PathBuf) -> Vec<File> {
- return find_files_recursively(directory_path, directory_path);
+ find_files_recursively(directory_path, directory_path)
}
fn find_files_recursively(root_path: &PathBuf, directory_path: &PathBuf) -> Vec<File> {
let mut result: Vec<File> = vec![];
let file_handler = FileHandler::default();
- let entries = read_dir(&directory_path).unwrap();
+ let entries = read_dir(directory_path).unwrap();
for entry in entries {
let path = entry.unwrap().path();
- let relative_path = path.strip_prefix(&root_path).unwrap();
+ let relative_path = path.strip_prefix(root_path).unwrap();
if relative_path.starts_with(".git") || relative_path.starts_with(".gitignore") {
continue;
}
if path.is_dir() {
- result.append(&mut find_files_recursively(&root_path, &path))
+ result.append(&mut find_files_recursively(root_path, &path))
} else {
let file_type = file_handler.identify(&path);
- result.push(File {
- path: path,
- file_type: file_type,
- });
+ result.push(File { path, file_type });
}
}
- return result;
+ result
}
-
#[cfg(test)]
mod tests {
use std::collections::HashSet;
- use std::path::PathBuf;
use std::fs::create_dir_all;
+ use std::path::PathBuf;
use super::*;
- use crate::file_handler::FileType;
use crate::file_handler::File;
+ use crate::file_handler::FileType;
use test_utilities::*;
fn get_paths(root_directory: &PathBuf, files: &Vec<File>) -> HashSet<String> {
files
.iter()
- .map( |file|
+ .map(|file| {
file.path
.strip_prefix(root_directory)
.unwrap()
.to_string_lossy()
.to_string()
- )
+ })
.collect()
}
#[test]
fn finds_all_files() {
let test_dir = setup_test_dir();
- create_dir_all(&test_dir.join("nested"))
- .expect("Could not create nested test directory");
- create_dir_all(&test_dir.join("assets"))
- .expect("Could not create assets test directory");
+ create_dir_all(&test_dir.join("nested")).expect("Could not create nested test directory");
+ create_dir_all(&test_dir.join("assets")).expect("Could not create assets test directory");
create_test_file(&test_dir.join("test1.gmi"), "");
create_test_file(&test_dir.join("_layout.html"), "");
create_test_file(&test_dir.join("nested/nested.gmi"), "");
#[test]
fn identifies_correct_file_types() {
let test_dir = setup_test_dir();
- create_dir_all(&test_dir.join("nested"))
- .expect("Could not create nested test directory");
- create_dir_all(&test_dir.join("assets"))
- .expect("Could not create assets test directory");
+ create_dir_all(&test_dir.join("nested")).expect("Could not create nested test directory");
+ create_dir_all(&test_dir.join("assets")).expect("Could not create assets test directory");
create_test_file(&test_dir.join("_layout.html"), "");
create_test_file(&test_dir.join("nested/nested.gmi"), "");
create_test_file(&test_dir.join("assets/style.css"), "");
} else {
assert_eq!(file.file_type, FileType::File)
}
- },
+ }
_ => assert_eq!(file.file_type, FileType::File),
}
}
#[test]
fn ignores_git_directory() {
let test_dir = setup_test_dir();
- create_dir_all(&test_dir.join("nested"))
- .expect("Could not create nested test directory");
- create_dir_all(&test_dir.join("assets"))
- .expect("Could not create assets test directory");
- create_dir_all(&test_dir.join(".git"))
- .expect("Could not create git test directory");
+ create_dir_all(&test_dir.join("nested")).expect("Could not create nested test directory");
+ create_dir_all(&test_dir.join("assets")).expect("Could not create assets test directory");
+ create_dir_all(&test_dir.join(".git")).expect("Could not create git test directory");
create_test_file(&test_dir.join("_layout.html"), "");
create_test_file(&test_dir.join("nested/nested.gmi"), "");
create_test_file(&test_dir.join("assets/style.css"), "");
pub struct Strategy {}
-use std::path::PathBuf;
use std::fs::{copy, create_dir_all};
+use std::path::Path;
-use crate::file_handler::{File, FileType, FileHandlerStrategy};
+use crate::file_handler::{File, FileHandlerStrategy, FileType};
impl Strategy {
- fn handle(&self, source: &PathBuf, destination: &PathBuf, file: &File) {
- let relative_path = file.path.strip_prefix(&source).unwrap();
+ fn handle(&self, source: &Path, destination: &Path, file: &File) {
+ let relative_path = file.path.strip_prefix(source).unwrap();
let complete_destination = destination.join(relative_path);
let destination_parent = complete_destination.parent().unwrap();
create_dir_all(destination_parent).unwrap();
}
impl FileHandlerStrategy for Strategy {
- fn is(&self, path: &PathBuf) -> bool {
+ fn is(&self, path: &Path) -> bool {
!path.is_dir()
}
}
fn can_handle(&self, file_type: &FileType) -> bool {
- match file_type {
- FileType::File => true,
- _ => false,
- }
+ matches!(file_type, FileType::File)
}
- fn handle_html(&self, source: &PathBuf, destination: &PathBuf, file: &File, _l: &str) {
- return self.handle(source, destination, file);
+ fn handle_html(&self, source: &Path, destination: &Path, file: &File, _l: &str) {
+ self.handle(source, destination, file)
}
- fn handle_gemini(&self, source: &PathBuf, destination: &PathBuf, file: &File) {
- return self.handle(source, destination, file);
+ fn handle_gemini(&self, source: &Path, destination: &Path, file: &File) {
+ self.handle(source, destination, file)
}
}
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");
+ create_dir_all(&source_dir).expect("Could not create source test directory");
+ create_dir_all(&output_dir).expect("Could not create output test directory");
create_test_file(&source_dir.join("image.png"), "A fish playing the banjo");
let strategy = Strategy {};
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");
- create_dir_all(&source_dir.join("nested"))
- .expect("Could not create source test directory");
- create_test_file(&source_dir.join("nested/style.css"), "* { margin: 0; padding: 0 }");
+ create_dir_all(&source_dir).expect("Could not create source test directory");
+ create_dir_all(&output_dir).expect("Could not create output test directory");
+ create_dir_all(&source_dir.join("nested")).expect("Could not create source test directory");
+ create_test_file(
+ &source_dir.join("nested/style.css"),
+ "* { margin: 0; padding: 0 }",
+ );
let strategy = Strategy {};
let file = File {
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");
+ create_dir_all(&source_dir).expect("Could not create source test directory");
+ create_dir_all(&output_dir).expect("Could not create output test directory");
create_test_file(&source_dir.join("image.png"), "A fish playing the banjo");
let strategy = Strategy {};
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");
+ create_dir_all(&source_dir).expect("Could not create source test directory");
+ create_dir_all(&output_dir).expect("Could not create output test directory");
create_test_file(&source_dir.join("image.png"), "A fish playing the banjo");
let strategy = Strategy {};
pub struct Strategy {}
-use std::path::PathBuf;
-use std::io::Write;
use std::fs::{create_dir_all, read_to_string, File as IOFile};
+use std::io::Write;
+use std::path::Path;
-use crate::file_handler::{File, FileType, FileHandlerStrategy};
+use crate::file_handler::{File, FileHandlerStrategy, FileType};
use crate::gemini_parser::parse;
use crate::html_renderer::render_html;
}
impl FileHandlerStrategy for Strategy {
- fn is(&self, path: &PathBuf) -> bool {
+ fn is(&self, path: &Path) -> bool {
if let Some(extension) = path.extension() {
- return !path.is_dir() && extension == "gmi"
+ return !path.is_dir() && extension == "gmi";
}
false
}
}
fn can_handle(&self, file_type: &FileType) -> bool {
- match file_type {
- FileType::Gemini => true,
- _ => false,
- }
+ matches!(file_type, FileType::Gemini)
}
- fn handle_html(&self, source: &PathBuf, destination: &PathBuf, file: &File, layout: &str) {
+ fn handle_html(&self, source: &Path, destination: &Path, file: &File, layout: &str) {
let gemini_contents = read_to_string(&file.path).unwrap();
// Front matter extraction
let mut description = "";
if let Some(slice) = lines.get(..2) {
for line in slice.iter() {
- if self.is_title(&line) {
- title = self.get_title(&line).trim();
- lines_found = lines_found + 1;
+ if self.is_title(line) {
+ title = self.get_title(line).trim();
+ lines_found += 1;
continue;
}
- if self.is_description(&line) {
- description = self.get_description(&line).trim();
- lines_found = lines_found + 1;
+ if self.is_description(line) {
+ description = self.get_description(line).trim();
+ lines_found += 1;
continue;
}
}
.replace("{{ description }}", description)
.replace("{{ content }}", &content_html[..]);
-
- let relative_path = file.path.strip_prefix(&source).unwrap();
+ let relative_path = file.path.strip_prefix(source).unwrap();
let mut complete_destination = destination.join(relative_path);
complete_destination.set_extension("html");
let destination_parent = complete_destination.parent().unwrap();
create_dir_all(destination_parent).unwrap();
let mut destination_file = IOFile::create(&complete_destination).unwrap();
- destination_file.write_all(generated_html.as_bytes()).unwrap();
+ destination_file
+ .write_all(generated_html.as_bytes())
+ .unwrap();
}
- fn handle_gemini(&self, source: &PathBuf, destination: &PathBuf, file: &File) {
+ fn handle_gemini(&self, source: &Path, destination: &Path, file: &File) {
let gemini_contents = read_to_string(&file.path).unwrap();
// Front matter extraction
let mut lines_found = 0;
if let Some(slice) = lines.get(..2) {
for line in slice.iter() {
- if self.is_title(&line) {
- lines_found = lines_found + 1;
+ if self.is_title(line) {
+ lines_found += 1;
continue;
}
- if self.is_description(&line) {
- lines_found = lines_found + 1;
+ if self.is_description(line) {
+ lines_found += 1;
continue;
}
}
let gemini_source = lines[lines_found..].join("\n");
- let relative_path = file.path.strip_prefix(&source).unwrap();
+ let relative_path = file.path.strip_prefix(source).unwrap();
let complete_destination = destination.join(relative_path);
let destination_parent = complete_destination.parent().unwrap();
create_dir_all(destination_parent).unwrap();
let mut destination_file = IOFile::create(&complete_destination).unwrap();
- destination_file.write_all(gemini_source.as_bytes()).unwrap();
+ destination_file
+ .write_all(gemini_source.as_bytes())
+ .unwrap();
}
}
#[cfg(test)]
mod tests {
+ use std::fs::create_dir_all;
+
use super::*;
- use std::fs;
- fn setup() -> Strategy {
- Strategy {}
- }
+ use test_utilities::*;
- fn fixtures_dir() -> PathBuf {
- PathBuf::from("tests/fixtures")
+ #[test]
+ fn detects_title() {
+ let strategy = Strategy {};
+ assert!(strategy.is_title("--- title: Hello!"));
}
- fn fixture_path(filename: &str) -> PathBuf {
- fixtures_dir().join(filename)
+ #[test]
+ fn does_not_detect_other_keys_as_title() {
+ let strategy = Strategy {};
+ assert!(!strategy.is_title("--- description: Hello!"));
}
- fn read_fixture(filename: &str) -> String {
- fs::read_to_string(fixture_path(filename))
- .unwrap_or_else(|_| panic!("Failed to read fixture file: {}", filename))
+ #[test]
+ fn detects_description() {
+ let strategy = Strategy {};
+ assert!(strategy.is_description("--- description: What is this?"));
}
- mod front_matter_tests {
- use super::*;
-
- #[test]
- fn detects_title() {
- let strategy = setup();
- let content = read_fixture("test1.gmi");
- let first_line = content.lines().next().unwrap();
- assert!(strategy.is_title(first_line));
- }
-
- #[test]
- fn detects_description() {
- let strategy = setup();
- let content = read_fixture("test1.gmi");
- let second_line = content.lines().nth(1).unwrap();
- assert!(strategy.is_description(second_line));
- }
-
- #[test]
- fn extracts_title() {
- let strategy = setup();
- let content = read_fixture("test1.gmi");
- let first_line = content.lines().next().unwrap();
- assert_eq!(strategy.get_title(first_line).trim(), "Test Title");
- }
-
- #[test]
- fn extracts_description() {
- let strategy = setup();
- let content = read_fixture("test1.gmi");
- let second_line = content.lines().nth(1).unwrap();
- assert_eq!(strategy.get_description(second_line).trim(), "Test Description");
- }
+ #[test]
+ fn does_not_detect_other_keys_as_description() {
+ let strategy = Strategy {};
+ assert!(!strategy.is_description("--- title: What is this?"));
}
- mod file_type_tests {
- use super::*;
-
- #[test]
- fn identifies_gemini_files() {
- let strategy = setup();
- assert!(strategy.is(&fixture_path("test1.gmi")));
- assert!(!strategy.is(&fixture_path("_layout.html")));
- assert!(!strategy.is(&fixtures_dir()));
- }
-
- #[test]
- fn identifies_file_type() {
- let strategy = setup();
- assert!(matches!(strategy.identify(), FileType::Gemini));
- }
+ #[test]
+ fn extracts_title() {
+ let strategy = Strategy {};
+ assert_eq!(strategy.get_title("--- title: Hello!").trim(), "Hello!");
+ }
- #[test]
- fn handles_correct_file_type() {
- let strategy = setup();
- assert!(strategy.can_handle(&FileType::Gemini));
- assert!(!strategy.can_handle(&FileType::Layout));
- assert!(!strategy.can_handle(&FileType::File));
- assert!(!strategy.can_handle(&FileType::Unknown));
- }
+ #[test]
+ fn extracts_description() {
+ let strategy = Strategy {};
+ assert_eq!(
+ strategy
+ .get_description("--- description: What is this?")
+ .trim(),
+ "What is this?"
+ );
}
- mod file_handling_tests {
- use super::*;
-
- #[test]
- fn handles_html_generation() {
- let strategy = setup();
- let source = fixtures_dir();
- let output = fixture_path("output");
- let layout = read_fixture("_layout.html");
-
- let file = File {
- path: fixture_path("test1.gmi"),
- file_type: FileType::Gemini,
- };
-
- strategy.handle_html(
- &source,
- &output,
- &file,
- &layout,
- );
-
- let generated_path = output.join("test1.html");
- assert!(generated_path.exists());
-
- let content = fs::read_to_string(generated_path.clone()).unwrap();
- assert!(content.contains("Test Title"));
- assert!(content.contains("<h1>"));
-
- // Cleanup
- let _ = fs::remove_file(generated_path);
- }
+ #[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 handles_gemini_generation() {
- let strategy = setup();
- let source = fixtures_dir();
- let output = fixture_path("output");
+ #[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));
+ }
- let file = File {
- path: fixture_path("test1.gmi"),
- file_type: FileType::Gemini,
- };
+ #[test]
+ fn identifies_gemini_type() {
+ let strategy = Strategy {};
+ assert!(matches!(strategy.identify(), FileType::Gemini));
+ }
- strategy.handle_gemini(
- &source,
- &output,
- &file,
- );
+ #[test]
+ fn handles_gemini_type() {
+ let strategy = Strategy {};
+ assert!(strategy.can_handle(&FileType::Gemini));
+ }
- let generated_path = output.join("test1.gmi");
- assert!(generated_path.exists());
+ #[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));
+ }
- let content = fs::read_to_string(&generated_path).unwrap();
- assert!(content.contains("# Heading"));
- assert!(!content.contains("Test Title")); // Front matter should be removed
+ #[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 = "\
+<html>
+<head>
+<title>{{ title }}</title>
+<meta name=\"description\" content=\"{{ description }}\">
+</head>
+<body>{{ content }}</body>
+</html>
+";
+ create_test_file(
+ &source_dir.join("test.gmi"),
+ "\
+--- title: Page Is Cool!
+--- description: My Description
+# Test
+Hello world
+",
+ );
+
+ let strategy = Strategy {};
+ let file = File {
+ path: source_dir.join("test.gmi"),
+ file_type: FileType::Gemini,
+ };
+
+ strategy.handle_html(&source_dir, &output_dir, &file, &layout);
+
+ let html_output = output_dir.join("test.html");
+ assert!(html_output.exists());
+ assert_file_contents(
+ &html_output,
+ "\
+<html>
+<head>
+<title>Page Is Cool!</title>
+<meta name=\"description\" content=\"My Description\">
+</head>
+<body><section class=\"h1\">
+<h1> Test</h1>
+<p>Hello world</p>
+</section>
+</body>
+</html>
+",
+ );
+ }
- // Cleanup
- let _ = fs::remove_file(generated_path);
- }
+ #[test]
+ fn handles_gemini_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");
+ create_test_file(
+ &source_dir.join("test.gmi"),
+ "\
+--- title: Page Is Cool!
+--- description: My Description
+# Test
+Hello world
+",
+ );
+
+ let strategy = Strategy {};
+ let file = File {
+ path: source_dir.join("test.gmi"),
+ file_type: FileType::Gemini,
+ };
+
+ strategy.handle_gemini(&source_dir, &output_dir, &file);
+
+ let gemini_output = output_dir.join("test.gmi");
+ assert!(gemini_output.exists());
+ assert_file_contents(
+ &gemini_output,
+ "\
+# Test
+Hello world
+",
+ );
+ }
- #[test]
- fn handles_nested_structure() {
- let strategy = setup();
- let source = fixtures_dir();
- let output = fixture_path("output");
- let layout = read_fixture("_layout.html");
-
- let file = File {
- path: fixture_path("nested/nested.gmi"),
- file_type: FileType::Gemini,
- };
-
- strategy.handle_html(
- &source,
- &output,
- &file,
- &layout,
- );
-
- let generated_path = output.join("nested").join("nested.html");
- assert!(generated_path.exists());
-
- // Cleanup
- let _ = fs::remove_file(generated_path);
- let _ = fs::remove_dir(output.join("nested"));
- }
+ #[test]
+ fn handles_nested_structure() {
+ 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.join("nested")).expect("Could not create source test directory");
+ create_dir_all(&output_dir).expect("Could not create output test directory");
+ let layout = "\
+<html>
+<head>
+<title>{{ title }}</title>
+<meta name=\"description\" content=\"{{ description }}\">
+</head>
+<body>{{ content }}</body>
+</html>
+";
+ create_test_file(
+ &source_dir.join("nested/test.gmi"),
+ "\
+--- title: Page Is Cool!
+--- description: My Description
+# Test
+Hello world
+",
+ );
+
+ let strategy = Strategy {};
+ let file = File {
+ path: source_dir.join("nested/test.gmi"),
+ file_type: FileType::Gemini,
+ };
+
+ strategy.handle_html(&source_dir, &output_dir, &file, &layout);
+
+ let html_output = output_dir.join("nested/test.html");
+ assert!(html_output.exists());
+ assert_file_contents(
+ &html_output,
+ "\
+<html>
+<head>
+<title>Page Is Cool!</title>
+<meta name=\"description\" content=\"My Description\">
+</head>
+<body><section class=\"h1\">
+<h1> Test</h1>
+<p>Hello world</p>
+</section>
+</body>
+</html>
+",
+ );
}
}
pub struct Strategy {}
-use std::path::PathBuf;
+use std::path::Path;
-use crate::file_handler::{File, FileType, FileHandlerStrategy};
+use crate::file_handler::{File, FileHandlerStrategy, FileType};
impl FileHandlerStrategy for Strategy {
- fn is(&self, path: &PathBuf) -> bool {
- return !path.is_dir() && path.ends_with("_layout.html");
+ fn is(&self, path: &Path) -> bool {
+ !path.is_dir() && path.ends_with("_layout.html")
}
fn identify(&self) -> FileType {
}
fn can_handle(&self, file_type: &FileType) -> bool {
- match file_type {
- FileType::Layout => true,
- _ => false,
- }
+ matches!(file_type, FileType::Layout)
}
// We don't implement handling for layout, as we assume there's only one
// and it got handled before.
- fn handle_html(&self, _s: &PathBuf, _d: &PathBuf, _f: &File, _l: &str) {}
- fn handle_gemini(&self, _s: &PathBuf, _d: &PathBuf, _f: &File) {}
+ fn handle_html(&self, _s: &Path, _d: &Path, _f: &File, _l: &str) {}
+ fn handle_gemini(&self, _s: &Path, _d: &Path, _f: &File) {}
}
#[cfg(test)]
mod tests {
use std::fs::create_dir_all;
+ use std::path::PathBuf;
use super::*;
fn rejects_directory_named_layout() {
let test_dir = setup_test_dir();
let layout_dir = test_dir.join("_layout");
- create_dir_all(&layout_dir)
- .expect("Could not create _layout test directory");
+ create_dir_all(&layout_dir).expect("Could not create _layout test directory");
let strategy = Strategy {};
assert!(!strategy.is(&layout_dir));
}
&PathBuf::from("source"),
&PathBuf::from("dest"),
&file,
- "layout content"
+ "layout content",
);
}
};
// Should not panic
- strategy.handle_gemini(
- &PathBuf::from("source"),
- &PathBuf::from("dest"),
- &file
- );
+ strategy.handle_gemini(&PathBuf::from("source"), &PathBuf::from("dest"), &file);
}
}
use file_strategies::gemini::Strategy as GeminiStrategy;
use file_strategies::layout::Strategy as LayoutStrategy;
-use std::path::PathBuf;
use std::fs::read_to_string;
+use std::path::{Path, PathBuf};
pub struct FileHandler {
- pub strategies: Vec<Box<dyn FileHandlerStrategy>>,
- pub layout: Option<String>
+ pub strategies: Vec<Box<dyn FileHandlerStrategy>>,
+ pub layout: Option<String>,
}
impl Default for FileHandler {
fn default() -> FileHandler {
FileHandler {
strategies: vec![
- Box::new(GeminiStrategy{}),
- Box::new(LayoutStrategy{}),
- Box::new(FileStrategy{}),
+ Box::new(GeminiStrategy {}),
+ Box::new(LayoutStrategy {}),
+ Box::new(FileStrategy {}),
],
- layout: None
+ layout: None,
}
}
}
impl FileHandler {
- pub fn identify(&self, path: &PathBuf) -> FileType {
+ pub fn identify(&self, path: &Path) -> FileType {
for strategy in self.strategies.iter() {
- if strategy.is(&path) {
+ if strategy.is(path) {
return strategy.identify();
}
}
pub fn get_layout_or_panic(&mut self, files: &[File]) -> Result<(), &str> {
for file in files {
- match file.file_type {
- FileType::Layout => {
- let layout_text = read_to_string(&file.path).unwrap();
- self.layout = Some(layout_text);
- return Ok(());
- },
- _ => {}
+ if file.file_type == FileType::Layout {
+ let layout_text = read_to_string(&file.path).unwrap();
+ self.layout = Some(layout_text);
+ return Ok(());
}
}
Err("No layout found. Please ensure there's a _layout.html file at the root")
}
- pub fn handle_all(&self, source: &PathBuf, html_destination: &PathBuf, gemini_destination: &PathBuf, files: &[File]) {
+ pub fn handle_all(
+ &self,
+ source: &Path,
+ html_destination: &Path,
+ gemini_destination: &Path,
+ files: &[File],
+ ) {
files.iter().for_each(|file| {
self.handle(source, html_destination, gemini_destination, file);
});
}
- pub fn handle(&self, source: &PathBuf, html_destination: &PathBuf, gemini_destination: &PathBuf, file: &File) {
- if let Some(strategy) = self.strategies
+ pub fn handle(
+ &self,
+ source: &Path,
+ html_destination: &Path,
+ gemini_destination: &Path,
+ file: &File,
+ ) {
+ if let Some(strategy) = self
+ .strategies
.iter()
- .find(|s| s.can_handle(&file.file_type))
+ .find(|s| s.can_handle(&file.file_type))
{
- let layout = self.layout.as_ref()
- .expect("Layout should be initialized before handling files");
- strategy.handle_html(source, html_destination, file, layout);
- strategy.handle_gemini(source, gemini_destination, file);
- return;
+ let layout = self
+ .layout
+ .as_ref()
+ .expect("Layout should be initialized before handling files");
+ strategy.handle_html(source, html_destination, file, layout);
+ strategy.handle_gemini(source, gemini_destination, file);
}
}
}
pub trait FileHandlerStrategy {
- fn is(&self, path: &PathBuf) -> bool;
+ fn is(&self, path: &Path) -> bool;
fn identify(&self) -> FileType;
fn can_handle(&self, file_type: &FileType) -> bool;
- fn handle_html(&self, source: &PathBuf, destination: &PathBuf, file: &File, layout: &str);
- fn handle_gemini(&self, source: &PathBuf, destination: &PathBuf, file: &File);
+ fn handle_html(&self, source: &Path, destination: &Path, file: &File, layout: &str);
+ fn handle_gemini(&self, source: &Path, destination: &Path, file: &File);
}
#[derive(Debug, Clone, PartialEq)]
pub file_type: FileType,
}
-
#[cfg(test)]
mod tests {
+ use std::fs::create_dir_all;
use std::path::PathBuf;
use super::*;
let mut handler = FileHandler::default();
let files = vec![
create_test_internal_file("test.gmi", FileType::Gemini),
- create_test_internal_file(&layout_path.to_str().expect("Could not encode layout"), FileType::Layout),
+ create_test_internal_file(
+ &layout_path.to_str().expect("Could not encode layout"),
+ FileType::Layout,
+ ),
create_test_internal_file("regular.html", FileType::File),
];
}
impl FileHandlerStrategy for MockStrategy {
- fn is(&self, _path: &PathBuf) -> bool {
+ fn is(&self, _path: &Path) -> bool {
self.is_match
}
&self.file_type == file_type
}
- fn handle_html(&self, _source: &PathBuf, _destination: &PathBuf, _file: &File, _layout: &str) {}
- fn handle_gemini(&self, _source: &PathBuf, _destination: &PathBuf, _file: &File) {}
+ fn handle_html(&self, _source: &Path, _destination: &Path, _file: &File, _layout: &str) {}
+ fn handle_gemini(&self, _source: &Path, _destination: &Path, _file: &File) {}
}
#[test]
// Should not panic with empty vector
handler.handle_all(
&PathBuf::from("source"),
- &PathBuf::from("tests/fixtures/output"),
- &PathBuf::from("tests/fixtures/output"),
- &files
+ &PathBuf::from("output_html"),
+ &PathBuf::from("output_gemini"),
+ &files,
);
}
let mut handler = FileHandler::default();
handler.layout = Some("test layout".to_string());
- let file = create_test_internal_file("tests/fixtures/test1.gmi", FileType::Gemini);
+ let test_dir = setup_test_dir();
+ create_dir_all(&test_dir.join("output_html"))
+ .expect("Could not create output html test directory");
+ create_dir_all(&test_dir.join("output_gemini"))
+ .expect("Could not create output gemini test directory");
+ let test_path = test_dir.join("test.gmi");
+ create_test_file(&test_path, "");
+ let file = create_test_internal_file(
+ &test_path
+ .to_str()
+ .expect("Could not encode gemini test file"),
+ FileType::Gemini,
+ );
// Should not panic with valid layout
handler.handle(
- &PathBuf::from(""),
- &PathBuf::from("tests/fixtures/output"),
- &PathBuf::from("tests/fixtures/output"),
- &file
+ &test_dir,
+ &test_dir.join("output_html"),
+ &test_dir.join("output_gemini"),
+ &file,
);
}
handler.handle(
&PathBuf::from("source"),
- &PathBuf::from("tests/fixtures/output"),
- &PathBuf::from("tests/fixtures/output"),
- &file
+ &PathBuf::from("output_html"),
+ &PathBuf::from("output_gemini"),
+ &file,
);
}
let test_dir = setup_test_dir();
let layout_path = test_dir.join("_layout.html");
create_test_file(&layout_path, "");
+ create_test_file(&test_dir.join("test1.gmi"), "");
+ create_test_file(&test_dir.join("test2.gmi"), "");
+ create_test_file(&test_dir.join("test3.gmi"), "");
+ create_dir_all(&test_dir.join("output_html"))
+ .expect("Could not create output html test directory");
+ create_dir_all(&test_dir.join("output_gemini"))
+ .expect("Could not create output gemini test directory");
let mut handler = FileHandler::default();
let files = vec![
- create_test_internal_file("tests/fixtures/test1.gmi", FileType::Gemini),
- create_test_internal_file(&layout_path.to_str().expect("Could not encode layout"), FileType::Layout),
- create_test_internal_file("tests/fixtures/test2.gmi", FileType::Gemini),
- create_test_internal_file("tests/fixtures/test3.gmi", FileType::Gemini),
+ create_test_internal_file(
+ &test_dir
+ .join("test1.gmi")
+ .to_str()
+ .expect("Could not encode test1"),
+ FileType::Gemini,
+ ),
+ create_test_internal_file(
+ &layout_path.to_str().expect("Could not encode layout"),
+ FileType::Layout,
+ ),
+ create_test_internal_file(
+ &test_dir
+ .join("test2.gmi")
+ .to_str()
+ .expect("Could not encode test2"),
+ FileType::Gemini,
+ ),
+ create_test_internal_file(
+ &test_dir
+ .join("test3.gmi")
+ .to_str()
+ .expect("Could not encode test3"),
+ FileType::Gemini,
+ ),
];
let _ = handler.get_layout_or_panic(&files[1..]);
// Test with slice
handler.handle_all(
- &PathBuf::from(""),
- &PathBuf::from("tests/fixtures/output"),
- &PathBuf::from("tests/fixtures/output"),
- &files[1..] // Test with slice of last three elements
+ &test_dir,
+ &test_dir.join("output_html"),
+ &test_dir.join("output_gemini"),
+ &files[1..], // Test with slice of last three elements
);
}
}
Heading(u8, String),
Link(String, String),
Quote(String),
- ListItem(String)
+ ListItem(String),
}
/// Parses gemtext source code into a vector of GeminiLine elements.
-///
+///
/// # Arguments
/// * `source` - A string slice that contains the gemtext
-///
+///
/// # Returns
/// A `Vec<GeminiLine>` containing the rendered HTML.
pub fn parse(source: &str) -> Vec<GeminiLine> {
- source.lines()
- .fold(
- (Vec::new(), false),
- |(mut lines, is_preformatted), line| {
- let parsed = if is_preformatted {
- parse_preformatted_line(line)
- } else {
- parse_line(line)
- };
-
- let new_is_preformatted = match parsed {
- GeminiLine::PreformattedToggle(x, _) => x,
- _ => is_preformatted
- };
-
- lines.push(parsed);
- (lines, new_is_preformatted)
- }
- )
+ source
+ .lines()
+ .fold((Vec::new(), false), |(mut lines, is_preformatted), line| {
+ let parsed = if is_preformatted {
+ parse_preformatted_line(line)
+ } else {
+ parse_line(line)
+ };
+
+ let new_is_preformatted = match parsed {
+ GeminiLine::PreformattedToggle(x, _) => x,
+ _ => is_preformatted,
+ };
+
+ lines.push(parsed);
+ (lines, new_is_preformatted)
+ })
.0
}
s if s.starts_with("=>") => {
let content = s[2..].trim();
match content.split_once(char::is_whitespace) {
- Some((url, text)) => GeminiLine::Link(url.trim().to_string(), text.trim().to_string()),
+ Some((url, text)) => {
+ GeminiLine::Link(url.trim().to_string(), text.trim().to_string())
+ }
None => GeminiLine::Link(content.trim().to_string(), String::new()),
}
- },
+ }
s if s.starts_with("* ") => GeminiLine::ListItem(s[2..].to_string()),
s if s.starts_with(">") => GeminiLine::Quote(s[1..].to_string()),
s if s.starts_with("```") => GeminiLine::PreformattedToggle(true, s[3..].to_string()),
#[test]
fn test_headings() {
- assert_eq!(parse_line("### Heading"), GeminiLine::Heading(3, " Heading".to_string()));
- assert_eq!(parse_line("## Heading"), GeminiLine::Heading(2, " Heading".to_string()));
- assert_eq!(parse_line("# Heading"), GeminiLine::Heading(1, " Heading".to_string()));
+ assert_eq!(
+ parse_line("### Heading"),
+ GeminiLine::Heading(3, " Heading".to_string())
+ );
+ assert_eq!(
+ parse_line("## Heading"),
+ GeminiLine::Heading(2, " Heading".to_string())
+ );
+ assert_eq!(
+ parse_line("# Heading"),
+ GeminiLine::Heading(1, " Heading".to_string())
+ );
assert_eq!(parse_line("###"), GeminiLine::Heading(3, "".to_string()));
- assert_eq!(parse_line("#####"), GeminiLine::Heading(3, "##".to_string()));
+ assert_eq!(
+ parse_line("#####"),
+ GeminiLine::Heading(3, "##".to_string())
+ );
assert_eq!(parse_line("# "), GeminiLine::Heading(1, " ".to_string()));
- assert_eq!(parse_preformatted_line("### Heading"), GeminiLine::Text("### Heading".to_string(), true));
- assert_eq!(parse_preformatted_line("## Heading"), GeminiLine::Text("## Heading".to_string(), true));
- assert_eq!(parse_preformatted_line("# Heading"), GeminiLine::Text("# Heading".to_string(), true));
+ assert_eq!(
+ parse_preformatted_line("### Heading"),
+ GeminiLine::Text("### Heading".to_string(), true)
+ );
+ assert_eq!(
+ parse_preformatted_line("## Heading"),
+ GeminiLine::Text("## Heading".to_string(), true)
+ );
+ assert_eq!(
+ parse_preformatted_line("# Heading"),
+ GeminiLine::Text("# Heading".to_string(), true)
+ );
}
#[test]
assert_eq!(parse_line("* "), GeminiLine::ListItem("".to_string()));
assert_eq!(parse_line("*"), GeminiLine::Text("*".to_string(), false));
- assert_eq!(parse_line("*WithText"), GeminiLine::Text("*WithText".to_string(), false));
- assert_eq!(parse_line("* Multiple spaces"), GeminiLine::ListItem(" Multiple spaces".to_string()));
+ assert_eq!(
+ parse_line("*WithText"),
+ GeminiLine::Text("*WithText".to_string(), false)
+ );
+ assert_eq!(
+ parse_line("* Multiple spaces"),
+ GeminiLine::ListItem(" Multiple spaces".to_string())
+ );
}
#[test]
assert_eq!(parse_line(">"), GeminiLine::Quote("".to_string()));
assert_eq!(parse_line("> "), GeminiLine::Quote(" ".to_string()));
- assert_eq!(parse_line(">>Nested"), GeminiLine::Quote(">Nested".to_string()));
+ assert_eq!(
+ parse_line(">>Nested"),
+ GeminiLine::Quote(">Nested".to_string())
+ );
}
#[test]
GeminiLine::PreformattedToggle(true, "alt-text".to_string())
);
- assert_eq!(parse_line("```"), GeminiLine::PreformattedToggle(true, "".to_string()));
- assert_eq!(parse_line("``` "), GeminiLine::PreformattedToggle(true, " ".to_string()));
- assert_eq!(parse_line("````"), GeminiLine::PreformattedToggle(true, "`".to_string()));
+ assert_eq!(
+ parse_line("```"),
+ GeminiLine::PreformattedToggle(true, "".to_string())
+ );
+ assert_eq!(
+ parse_line("``` "),
+ GeminiLine::PreformattedToggle(true, " ".to_string())
+ );
+ assert_eq!(
+ parse_line("````"),
+ GeminiLine::PreformattedToggle(true, "`".to_string())
+ );
assert_eq!(
parse_preformatted_line("```alt-text"),
parse_preformatted_line("```"),
GeminiLine::PreformattedToggle(false, "".to_string())
);
-
}
#[test]
#[test]
fn test_malformed_input() {
- assert_eq!(parse_line("= >Not a link"), GeminiLine::Text("= >Not a link".to_string(), false));
- assert_eq!(parse_line("``Not preformatted"), GeminiLine::Text("``Not preformatted".to_string(), false));
- assert_eq!(parse_line("** Not a list"), GeminiLine::Text("** Not a list".to_string(), false));
+ assert_eq!(
+ parse_line("= >Not a link"),
+ GeminiLine::Text("= >Not a link".to_string(), false)
+ );
+ assert_eq!(
+ parse_line("``Not preformatted"),
+ GeminiLine::Text("``Not preformatted".to_string(), false)
+ );
+ assert_eq!(
+ parse_line("** Not a list"),
+ GeminiLine::Text("** Not a list".to_string(), false)
+ );
}
#[test]
>Quote
```trailing alt";
let result = parse(input);
- assert_eq!(result, vec![
- GeminiLine::Heading(1, " Heading 1".to_string()),
- GeminiLine::Heading(2, " Heading 2".to_string()),
- GeminiLine::Heading(3, " Heading 3".to_string()),
- GeminiLine::Text("Regular text".to_string(), false),
- GeminiLine::Link("https://example.com".to_string(), "Link text".to_string()),
- GeminiLine::ListItem("List item".to_string()),
- GeminiLine::Quote("Quote".to_string()),
- GeminiLine::PreformattedToggle(true, "alt".to_string()),
- GeminiLine::Text("code".to_string(), true),
- GeminiLine::Text("# Heading 1".to_string(), true),
- GeminiLine::Text("## Heading 2".to_string(), true),
- GeminiLine::Text("### Heading 3".to_string(), true),
- GeminiLine::Text("=> https://example.com Link text".to_string(), true),
- GeminiLine::Text("* List item".to_string(), true),
- GeminiLine::Text(">Quote".to_string(), true),
- GeminiLine::PreformattedToggle(false, "".to_string()),
- ]);
+ assert_eq!(
+ result,
+ vec![
+ GeminiLine::Heading(1, " Heading 1".to_string()),
+ GeminiLine::Heading(2, " Heading 2".to_string()),
+ GeminiLine::Heading(3, " Heading 3".to_string()),
+ GeminiLine::Text("Regular text".to_string(), false),
+ GeminiLine::Link("https://example.com".to_string(), "Link text".to_string()),
+ GeminiLine::ListItem("List item".to_string()),
+ GeminiLine::Quote("Quote".to_string()),
+ GeminiLine::PreformattedToggle(true, "alt".to_string()),
+ GeminiLine::Text("code".to_string(), true),
+ GeminiLine::Text("# Heading 1".to_string(), true),
+ GeminiLine::Text("## Heading 2".to_string(), true),
+ GeminiLine::Text("### Heading 3".to_string(), true),
+ GeminiLine::Text("=> https://example.com Link text".to_string(), true),
+ GeminiLine::Text("* List item".to_string(), true),
+ GeminiLine::Text(">Quote".to_string(), true),
+ GeminiLine::PreformattedToggle(false, "".to_string()),
+ ]
+ );
}
}
use crate::gemini_parser::GeminiLine;
/// Renders HTML from a vector of GeminiLine elements.
-///
+///
/// # Arguments
/// * `lines` - Vector of GeminiLine elements to render
-///
+///
/// # Returns
/// A String containing the rendered HTML.
pub fn render_html(lines: Vec<GeminiLine>) -> String {
fn line_preamble(
line: &GeminiLine,
last_line: Option<&GeminiLine>,
- heading_stack: &mut Vec<u8>
+ heading_stack: &mut Vec<u8>,
) -> String {
let mut html = String::new();
if let Some(last_line) = last_line {
match last_line {
- GeminiLine::ListItem(_) => {
- match line {
- GeminiLine::ListItem(_) => {},
- _ => html.push_str("</ul>\n"),
- }
+ GeminiLine::ListItem(_) => match line {
+ GeminiLine::ListItem(_) => {}
+ _ => html.push_str("</ul>\n"),
},
- GeminiLine::Quote(_) => {
- match line {
- GeminiLine::Quote(_) => {},
- _ => html.push_str("</blockquote>\n"),
- }
+ GeminiLine::Quote(_) => match line {
+ GeminiLine::Quote(_) => {}
+ _ => html.push_str("</blockquote>\n"),
},
_ => {}
}
}
heading_stack.push(*level);
html.push_str(&format!("<section class=\"h{}\">\n", level));
+ }
+ GeminiLine::ListItem(_) => match last_line {
+ Some(GeminiLine::ListItem(_)) => {}
+ _ => html.push_str("<ul>\n"),
},
- GeminiLine::ListItem(_) => {
- match last_line {
- Some(GeminiLine::ListItem(_)) => {},
- _ => html.push_str("<ul>\n"),
- }
- },
- GeminiLine::Quote(_) => {
- match last_line {
- Some(GeminiLine::Quote(_)) => {},
- _ => html.push_str("<blockquote>\n"),
- }
+ GeminiLine::Quote(_) => match last_line {
+ Some(GeminiLine::Quote(_)) => {}
+ _ => html.push_str("<blockquote>\n"),
},
_ => {}
}
GeminiLine::Link(url, text) => {
let display = if text.is_empty() { url } else { text };
format!("<p class=\"a\"><a href=\"{}\">{}</a></p>", url, display)
- },
+ }
GeminiLine::Heading(level, content) => format!("<h{}>{}</h{}>", level, content, level),
GeminiLine::ListItem(content) => format!("<li>{}</li>", content),
- GeminiLine::PreformattedToggle(true, alt_text) => { format!("<pre aria-label=\"{}\">", alt_text) }
- GeminiLine::PreformattedToggle(false, _) => { "</pre>".to_string() }
- GeminiLine::Text(content, true) | GeminiLine::Quote(content) => format!("{}", content)
+ GeminiLine::PreformattedToggle(true, alt_text) => {
+ format!("<pre aria-label=\"{}\">", alt_text)
+ }
+ GeminiLine::PreformattedToggle(false, _) => "</pre>".to_string(),
+ GeminiLine::Text(content, true) | GeminiLine::Quote(content) => content.to_string(),
}
}
#[test]
fn test_simple_text() {
- let input = vec![
- GeminiLine::Text("Hello world".to_string(), false),
- ];
- assert_eq!(
- render_html(input),
- "<p>Hello world</p>\n"
- );
+ let input = vec![GeminiLine::Text("Hello world".to_string(), false)];
+ assert_eq!(render_html(input), "<p>Hello world</p>\n");
}
#[test]
-mod gemini_parser;
-mod html_renderer;
mod file_finder;
mod file_handler;
+mod gemini_parser;
+mod html_renderer;
-use std::io::Result;
-use std::process::exit;
use std::env::current_dir;
use std::fs::{create_dir_all, remove_dir_all};
+use std::io::Result;
+use std::process::exit;
use crate::file_finder::find_files;
use crate::file_handler::FileHandler;
// Step 2. Load the layout
let mut file_handler = FileHandler::default();
match file_handler.get_layout_or_panic(&files) {
- Ok(_) => {},
+ Ok(_) => {}
Err(error) => {
eprintln!("{}", error);
exit(1);
}
// Step 3. Prepare the target priority
- match remove_dir_all(&html_destination) {
- _ => {}
- };
- match remove_dir_all(&gemini_destination) {
- _ => {}
- };
-
- create_dir_all(&html_destination)
- .expect("Could not create HTML directory.");
- create_dir_all(&gemini_destination)
- .expect("Could not create Gemini directory.");
+ let _ = remove_dir_all(&html_destination);
+ let _ = remove_dir_all(&gemini_destination);
+
+ create_dir_all(&html_destination).expect("Could not create HTML directory.");
+ create_dir_all(&gemini_destination).expect("Could not create Gemini directory.");
// Step 4. Process all files
file_handler.handle_all(&source, &html_destination, &gemini_destination, &files);
let html_dir = parent.join(format!("{}_html", dir_name));
let gemini_dir = parent.join(format!("{}_gemini", dir_name));
- let _cleanup = TestDir {
- paths: vec![test_dir.clone(), html_dir.clone(), gemini_dir.clone()]
+ let _cleanup = TestDir {
+ paths: vec![test_dir.clone(), html_dir.clone(), gemini_dir.clone()],
};
// Create test input files
- create_test_file(&test_dir.join("_layout.html"), "\
+ create_test_file(
+ &test_dir.join("_layout.html"),
+ "\
<html>
<head><title>{{ title }}</title></head>
<body>{{ content }}</body>
</html>
-");
- create_test_file(&test_dir.join("test.gmi"), "\
+",
+ );
+ create_test_file(
+ &test_dir.join("test.gmi"),
+ "\
--- title: Page Is Cool!
# Test
Hello world
-");
+",
+ );
create_test_file(&test_dir.join("test.png"), "A picture of a cute cat");
// Run the program from the test directory
assert!(gemini_dir.exists());
let html_output = html_dir.join("test.html");
- assert_file_contents(&html_output, "\
+ assert_file_contents(
+ &html_output,
+ "\
<html>
<head><title>Page Is Cool!</title></head>
<body><section class=\"h1\">
</section>
</body>
</html>
-");
+",
+ );
let html_asset_output = html_dir.join("test.png");
assert_file_contents(&html_asset_output, "A picture of a cute cat");
let gemini_output = gemini_dir.join("test.gmi");
- assert_file_contents(&gemini_output, "\
+ assert_file_contents(
+ &gemini_output,
+ "\
# Test
Hello world
-");
+",
+ );
let gemini_asset_output = gemini_dir.join("test.png");
assert!(gemini_asset_output.exists());
let html_dir = parent.join(format!("{}_html", dir_name));
let gemini_dir = parent.join(format!("{}_gemini", dir_name));
- let _cleanup = TestDir {
- paths: vec![test_dir.clone(), html_dir.clone(), gemini_dir.clone()]
+ let _cleanup = TestDir {
+ paths: vec![test_dir.clone(), html_dir.clone(), gemini_dir.clone()],
};
// Create test input files
- create_test_file(&test_dir.join("test.gmi"), "\
+ create_test_file(
+ &test_dir.join("test.gmi"),
+ "\
--- title: Page Is Cool!
# Test
Hello world
-");
+",
+ );
create_test_file(&test_dir.join("test.png"), "A picture of a cute cat");
// Run the program from the test directory
+++ /dev/null
-<html>
-<head><title>{{ title }}</title></head>
-<body>
-{{ content }}
-</body>
-</html>
+++ /dev/null
---- title: Test Title
---- description: Test Description
-# Heading
-Some content here