summaryrefslogtreecommitdiff
path: root/src/md_generator.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/md_generator.rs')
-rw-r--r--src/md_generator.rs340
1 files changed, 0 insertions, 340 deletions
diff --git a/src/md_generator.rs b/src/md_generator.rs
deleted file mode 100644
index 7740247..0000000
--- a/src/md_generator.rs
+++ /dev/null
@@ -1,340 +0,0 @@
-//! Generate Markdown from path lists.
-//!
-//! This module normalizes paths, skips duplicates, and renders selected files into Markdown.
-
-use std::{collections::HashSet, io::Write, path::Path};
-
-use crate::{
- logger::Logger,
- normalizer::Normalizer,
- renderer::{RenderOptions, Renderer},
- util::path_display::display_path,
-};
-
-const DEFAULT_PROJECT_NAME: &str = "Project Outline";
-
-/// Generates Markdown for `paths`.
-///
-/// Relative paths are resolved against `origin_base`. File headings use paths made relative to
-/// `root`.
-///
-/// Duplicate files are skipped after normalization, while preserving the first occurrence order.
-///
-/// Rendering behavior such as file-size limits and placeholder output is controlled by
-/// `render_options`.
-pub fn generate_markdown_from_paths<W: Write>(
- paths: &[&Path],
- output: W,
- render_options: RenderOptions,
- root: &Path,
- origin_base: &Path,
- project_title: Option<&str>,
- logger: Logger,
-) -> Result<(), Box<dyn std::error::Error>> {
- let normalizer = Normalizer::new(root, origin_base)?;
-
- let mut renderer = Renderer::new(output, render_options).with_logger(logger);
- let project_title = project_title.unwrap_or_else(|| derive_project_title(root));
- renderer.render_header(project_title)?;
-
- let mut seen_paths = HashSet::new();
- for path in paths {
- let normalized_path = normalizer.normalize(path)?;
- if !seen_paths.insert(normalized_path.clone()) {
- logger.warn(format!(
- "skipping duplicate file: {}",
- display_path(&normalized_path.root_relative)
- ));
- continue;
- }
- renderer.render_path(&normalized_path)?;
- }
- Ok(())
-}
-
-fn derive_project_title(root: &Path) -> &str {
- if let Some(os_str_name) = root.file_name()
- && let Some(name) = os_str_name.to_str()
- {
- name
- } else {
- DEFAULT_PROJECT_NAME
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::ffi::OsStr;
- use std::fs;
- use std::io::Write;
- use std::os::unix::ffi::OsStrExt;
- use std::path::Path;
-
- use crate::logger::Logger;
- use crate::renderer::RenderOptions;
- use tempfile::tempdir;
-
- use super::{DEFAULT_PROJECT_NAME, derive_project_title, generate_markdown_from_paths};
-
- fn generate_with_defaults<W: Write>(
- paths: &[&Path],
- output: W,
- root: &Path,
- origin_base: &Path,
- project_title: Option<&str>,
- ) -> Result<(), Box<dyn std::error::Error>> {
- let logger = Logger::default();
- let render_options = RenderOptions::default();
- generate_markdown_from_paths(
- paths,
- output,
- render_options,
- root,
- origin_base,
- project_title,
- logger,
- )
- }
-
- #[test]
- fn empty_path_list_produces_header_with_specified_project_title() {
- let temp_dir = tempdir().unwrap();
- let paths = [];
- let mut output = Vec::new();
- let root = temp_dir.path();
- let origin_base = temp_dir.path();
-
- generate_with_defaults(&paths, &mut output, root, origin_base, Some("Project name"))
- .unwrap();
-
- assert_eq!(String::from_utf8(output).unwrap(), "# Project name\n");
- }
-
- #[test]
- fn renders_single_file_from_path_list() {
- let temp_dir = tempdir().unwrap();
- let origin_base = temp_dir.path();
- let paths = [Path::new("test_main.rs")];
- let mut output = Vec::new();
- let root = temp_dir.path();
-
- fs::write(origin_base.join("test_main.rs"), "fn main() {}").unwrap();
-
- generate_with_defaults(&paths, &mut output, root, origin_base, None).unwrap();
-
- let output_str = String::from_utf8(output).unwrap();
-
- assert!(output_str.contains("## File: test_main.rs"));
- assert!(output_str.contains("fn main() {}"));
- }
-
- #[test]
- fn renders_multiple_files_in_order() {
- let temp_dir = tempdir().unwrap();
- let origin_base = temp_dir.path();
- let paths = [Path::new("a.rs"), Path::new("b.rs")];
- let mut output = Vec::new();
- let root = temp_dir.path();
-
- fs::write(origin_base.join("a.rs"), "A").unwrap();
- fs::write(origin_base.join("b.rs"), "B").unwrap();
-
- generate_with_defaults(&paths, &mut output, root, origin_base, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
-
- let a_pos = output.find("a.rs").unwrap();
- let b_pos = output.find("b.rs").unwrap();
-
- assert!(a_pos < b_pos);
- }
-
- #[test]
- fn normalizes_paths_before_rendering() {
- let temp_dir = tempdir().unwrap();
- let origin_base = temp_dir.path();
- let paths = [Path::new("test/./main.rs")];
- let mut output = Vec::new();
- let root = temp_dir.path();
-
- let write_dir = temp_dir.path().join("test");
- fs::create_dir_all(&write_dir).unwrap();
- fs::write(write_dir.join("main.rs"), "fn main() {}").unwrap();
-
- generate_with_defaults(&paths, &mut output, root, origin_base, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
-
- assert!(output.contains("## File: test/main.rs"));
- }
-
- #[test]
- fn resolves_paths_from_origin_but_outputs_root_relative_paths() {
- let temp_dir = tempdir().unwrap();
- let origin_base = temp_dir.path().join("sandbox/src");
- let paths = [Path::new("main.rs")];
- let mut output = Vec::new();
- let root = temp_dir.path().join("project");
-
- fs::create_dir_all(&origin_base).unwrap();
- fs::write(origin_base.join("main.rs"), "fn main() {}").unwrap();
-
- generate_with_defaults(&paths, &mut output, &root, &origin_base, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
-
- // Must contain file content → proves correct reading
- assert!(output.contains("fn main() {}"));
-
- // Must contain normalized path → proves normalization applied
- assert!(output.contains("sandbox/src/main.rs"));
- }
-
- #[test]
- fn absolute_paths_are_not_resolved_against_origin() {
- let temp_dir1 = tempdir().unwrap();
- let temp_dir2 = tempdir().unwrap();
- let origin_base = temp_dir2.path();
- let filepath = temp_dir1.path().join("test_main.rs");
- let paths = [filepath.as_ref()];
- let mut output = Vec::new();
- let root = temp_dir2.path();
- fs::write(&filepath, "fn main() {}").unwrap();
-
- generate_with_defaults(&paths, &mut output, root, origin_base, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
-
- // Must contain file content → proves correct reading
- assert!(output.contains("fn main() {}"));
- }
-
- #[test]
- fn duplicate_files_in_sequence_are_skipped() {
- let temp_dir = tempdir().unwrap();
- let origin = temp_dir.path();
- let root = temp_dir.path();
-
- fs::write(origin.join("a.rs"), "A").unwrap();
-
- let paths = [Path::new("a.rs"), Path::new("a.rs")];
- let mut output = Vec::new();
-
- generate_with_defaults(&paths, &mut output, root, origin, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
-
- assert_eq!(output.matches("## File: a.rs").count(), 1);
- }
-
- #[test]
- fn duplicate_files_are_skipped_with_preserved_display_order_even_if_not_adjacent() {
- let temp_dir = tempdir().unwrap();
- let origin = temp_dir.path();
- let root = temp_dir.path();
-
- fs::write(origin.join("a.rs"), "A").unwrap();
- fs::write(origin.join("b.rs"), "B").unwrap();
-
- let paths = [Path::new("a.rs"), Path::new("b.rs"), Path::new("a.rs")];
- let mut output = Vec::new();
-
- generate_with_defaults(&paths, &mut output, root, origin, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
- assert_eq!(output.matches("## File: a.rs").count(), 1);
- assert_eq!(output.matches("## File: b.rs").count(), 1);
- let a_pos = output.find("a.rs").unwrap();
- let b_pos = output.find("b.rs").unwrap();
- assert!(a_pos < b_pos);
- }
-
- #[test]
- fn lexically_equivalent_paths_are_detected_as_duplicates() {
- let temp_dir = tempdir().unwrap();
- let origin = temp_dir.path();
- let root = temp_dir.path();
-
- fs::create_dir_all(origin.join("bla")).unwrap();
- fs::write(origin.join("a.rs"), "A").unwrap();
- fs::write(origin.join("b.rs"), "B").unwrap();
-
- let paths = [
- Path::new("a.rs"),
- Path::new("b.rs"),
- Path::new("bla/../a.rs"),
- ];
- let mut output = Vec::new();
-
- generate_with_defaults(&paths, &mut output, root, origin, None).unwrap();
-
- let output = String::from_utf8(output).unwrap();
- assert_eq!(output.matches("## File: a.rs").count(), 1);
- }
-
- #[test]
- fn project_title_is_derived_from_root_by_default_even_if_directory_does_not_exist() {
- let temp_dir = tempdir().unwrap();
- let origin_base = temp_dir.path();
- let paths = [];
- let mut output = Vec::new();
- let root = temp_dir.path().join("repo2markdown");
-
- generate_with_defaults(&paths, &mut output, &root, origin_base, None).unwrap();
-
- let output_str = String::from_utf8(output).unwrap();
-
- assert_eq!(output_str, "# repo2markdown\n");
- }
-
- #[test]
- fn project_title_fallsback_to_default_if_root_is_filesystem_root() {
- assert_eq!(derive_project_title(Path::new("/")), DEFAULT_PROJECT_NAME);
- }
-
- #[test]
- fn project_title_fallsback_if_root_ending_is_not_utf8() {
- let root = Path::new(OsStr::from_bytes(b"/root/fd\xC3"));
- assert_eq!(derive_project_title(root), DEFAULT_PROJECT_NAME);
- }
-
- #[test]
- fn deriving_project_title_from_root_ignores_trailing_slash() {
- let root = Path::new("/root/repo2markdown/");
- assert_eq!(derive_project_title(root), "repo2markdown");
- }
-
- #[test]
- fn generator_uses_render_options_for_large_file_placeholders() {
- let temp_dir = tempdir().unwrap();
- let origin_base = temp_dir.path();
- let root = temp_dir.path();
-
- let file_path = origin_base.join("big.txt");
- fs::write(&file_path, "A".repeat(10)).unwrap();
-
- let paths = [Path::new("big.txt")];
- let mut output = Vec::new();
-
- let config = RenderOptions {
- placeholder_for_binary_files: false,
- placeholder_for_large_files: true,
- max_file_size: 5,
- };
-
- generate_markdown_from_paths(
- &paths,
- &mut output,
- config,
- root,
- origin_base,
- None,
- Logger::default(),
- )
- .unwrap();
-
- let output = String::from_utf8(output).unwrap();
- assert!(output.contains("## File: big.txt"));
- assert!(output.contains("[FILE TOO LARGE]"));
- }
-}