diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/fenced_md_generator.rs | 178 | ||||
| -rw-r--r-- | src/generator/mod.rs | 383 | ||||
| -rw-r--r-- | src/lib.rs | 3 | ||||
| -rw-r--r-- | src/md_generator.rs | 340 | ||||
| -rw-r--r-- | src/run.rs | 30 |
5 files changed, 394 insertions, 540 deletions
diff --git a/src/fenced_md_generator.rs b/src/fenced_md_generator.rs deleted file mode 100644 index bf44e3a..0000000 --- a/src/fenced_md_generator.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Generate Markdown and wrap the entire result in an outer Markdown fence. -//! -//! This module is useful when the generated project Markdown needs to be embedded inside another -//! document or a prompt. - -use std::{io::Write, path::Path}; - -use crate::{ - logger::Logger, md_generator::generate_markdown_from_paths, renderer::RenderOptions, - util::fence::generate_outer_backticks, -}; - -/// Generates Markdown from `paths` and wraps the full output in a fence. -/// -/// The inner Markdown is produced by [`generate_markdown_from_paths`]. The outer fence length is -/// chosen so that any backtick runs inside the generated Markdown cannot close the outer fence. -/// -/// `render_options` controls file-size limits and placeholder behavior. `root` controls displayed -/// relative paths, while `origin_base` controls how relative input paths are resolved. -pub fn generate_fenced_markdown<W: Write>( - paths: &[&Path], - mut output: W, - render_options: RenderOptions, - root: &Path, - origin_base: &Path, - project_title: Option<&str>, - logger: Logger, -) -> Result<(), Box<dyn std::error::Error>> { - let mut md_output = Vec::new(); - generate_markdown_from_paths( - paths, - &mut md_output, - render_options, - root, - origin_base, - project_title, - logger, - )?; - let fence = generate_outer_backticks(&md_output); - writeln!(output, "{}markdown", fence)?; - output.write_all(&md_output)?; - writeln!(output, "{}", fence)?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use std::fs; - use std::io::Write; - use std::path::Path; - use tempfile::tempdir; - - use crate::logger::Logger; - use crate::renderer::RenderOptions; - - use super::generate_fenced_markdown; - - fn generate_fenced_with_defaults<W: Write>( - paths: &[&Path], - output: W, - root: &std::path::Path, - origin_base: &std::path::Path, - project_title: Option<&str>, - ) -> Result<(), Box<dyn std::error::Error>> { - let logger = Logger::default(); - let render_options = RenderOptions::default(); - generate_fenced_markdown( - paths, - output, - render_options, - root, - origin_base, - project_title, - logger, - ) - } - - #[test] - fn empty_path_list_produces_wrapped_header() { - let temp_dir = tempdir().unwrap(); - let paths = []; - let mut output = Vec::new(); - let root = temp_dir.path(); - let origin_base = temp_dir.path(); - - generate_fenced_with_defaults(&paths, &mut output, root, origin_base, Some("Project name")) - .unwrap(); - - assert_eq!( - String::from_utf8(output).unwrap(), - "```markdown\n# Project name\n```\n" - ); - } - - #[test] - fn single_file_is_wrapped() { - 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_fenced_with_defaults(&paths, &mut output, root, origin_base, None).unwrap(); - - let output_str = String::from_utf8(output).unwrap(); - // Must contain outer fence - assert!(output_str.starts_with("````markdown\n")); - assert!(output_str.ends_with("\n````\n")); - // Must contain file content - assert!(output_str.contains("## File: test_main.rs")); - assert!(output_str.contains("fn main() {}")); - } - - #[test] - fn multiple_files_are_wrapped_in_one_outer_fence() { - 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_fenced_with_defaults(&paths, &mut output, root, origin_base, None).unwrap(); - - let output_str = String::from_utf8(output).unwrap(); - // Each inner file must have its own language‑specific fence - assert!(output_str.contains("```rust\nA\n```")); - assert!(output_str.contains("```rust\nB\n```")); - // Only one outer fence at start and end - assert_eq!(output_str.matches("````markdown").count(), 1); - assert_eq!(output_str.matches("\n````\n").count(), 1); - } - - #[test] - fn inner_file_with_four_backticks_causes_outer_fence_of_six_backticks() { - let temp_dir = tempdir().unwrap(); - let origin_base = temp_dir.path(); - let paths = [Path::new("backticks.rs")]; - let mut output = Vec::new(); - let root = temp_dir.path(); - - let content = "````"; // 4 backticks - fs::write(origin_base.join("backticks.rs"), content).unwrap(); - - generate_fenced_with_defaults(&paths, &mut output, root, origin_base, None).unwrap(); - - let output_str = String::from_utf8(output).unwrap(); - // Inner fence should be 5 backticks because inner content contains 4 backticks - assert!(output_str.contains("`````rust\n````\n`````")); - // Outer fence must then be 6 backticks - assert!(output_str.starts_with("``````markdown\n")); - assert!(output_str.ends_with("\n``````\n")); - } - - #[test] - fn project_title_with_three_backticks_causes_outer_fence_of_four_backticks() { - let temp_dir = tempdir().unwrap(); - let paths = []; - let mut output = Vec::new(); - let root = temp_dir.path(); - let origin_base = temp_dir.path(); - - let project_title = "Project ``` name"; - generate_fenced_with_defaults(&paths, &mut output, root, origin_base, Some(project_title)) - .unwrap(); - - let output_str = String::from_utf8(output).unwrap(); - // Outer fence must be 4 backticks because the header contains 3 backticks - assert!(output_str.starts_with("````markdown\n")); - assert!(output_str.ends_with("\n````\n")); - // The header line itself should appear unchanged - assert!(output_str.contains("# Project ``` name")); - } -} diff --git a/src/generator/mod.rs b/src/generator/mod.rs new file mode 100644 index 0000000..5c0b84a --- /dev/null +++ b/src/generator/mod.rs @@ -0,0 +1,383 @@ +use std::{collections::HashSet, io::Write, path::Path}; + +use crate::{ + logger::Logger, + normalizer::Normalizer, + renderer::{RenderOptions, Renderer}, + util::{fence::generate_outer_backticks, path_display::display_path}, +}; + +const DEFAULT_PROJECT_NAME: &str = "Project Outline"; + +pub struct MarkdownGenerator<'a> { + logger: Logger, + origin_base: &'a Path, + paths: &'a [&'a Path], + project_title: Option<&'a str>, + render_options: RenderOptions, + root: &'a Path, +} + +impl<'a> MarkdownGenerator<'a> { + pub fn new(paths: &'a [&'a Path]) -> Self { + MarkdownGenerator { + logger: Logger::default(), + origin_base: Path::new("."), + paths, + project_title: None, + render_options: RenderOptions::default(), + root: Path::new("."), + } + } + + pub fn with_root(mut self, root: &'a Path) -> Self { + self.root = root; + self + } + + pub fn with_origin(mut self, origin: &'a Path) -> Self { + self.origin_base = origin; + self + } + + pub fn with_project_title(mut self, project_title: Option<&'a str>) -> Self { + self.project_title = project_title; + self + } + + pub fn with_logger(mut self, logger: Logger) -> Self { + self.logger = logger; + self + } + + pub fn with_render_options(mut self, render_options: RenderOptions) -> Self { + self.render_options = render_options; + self + } + + fn get_or_derive_project_title(&self) -> &str { + if let Some(project_title) = &self.project_title { + project_title + } else if let Some(os_str_name) = self.root.file_name() + && let Some(name) = os_str_name.to_str() + { + name + } else { + DEFAULT_PROJECT_NAME + } + } + + pub fn generate<W: Write>(self, output: W) -> Result<(), Box<dyn std::error::Error>> { + let normalizer = Normalizer::new(self.root, self.origin_base)?; + let mut renderer = Renderer::new(output, self.render_options).with_logger(self.logger); + renderer.render_header(self.get_or_derive_project_title())?; + let mut seen_paths = HashSet::new(); + for path in self.paths { + let normalized_path = normalizer.normalize(path)?; + if !seen_paths.insert(normalized_path.clone()) { + self.logger.warn(format!( + "skipping duplicate file: {}", + display_path(&normalized_path.root_relative) + )); + continue; + } + renderer.render_path(&normalized_path)?; + } + Ok(()) + } + + pub fn generate_fenced<W: Write>( + self, + mut output: W, + ) -> Result<(), Box<dyn std::error::Error>> { + let mut md_output = Vec::new(); + self.generate(&mut md_output)?; + + let fence = generate_outer_backticks(&md_output); + writeln!(output, "{}markdown", fence)?; + output.write_all(&md_output)?; + writeln!(output, "{}", fence)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::{fs, path::Path}; + + use tempfile::tempdir; + + use crate::{logger::Logger, renderer::RenderOptions}; + + use super::MarkdownGenerator; + + fn generate_to_string( + paths: &[&Path], + root: &Path, + origin: &Path, + project_title: Option<&str>, + ) -> Result<String, Box<dyn std::error::Error>> { + let mut output = Vec::new(); + + MarkdownGenerator::new(paths) + .with_root(root) + .with_origin(origin) + .with_project_title(project_title) + .generate(&mut output)?; + + Ok(String::from_utf8(output).unwrap()) + } + + fn generate_fenced_to_string( + paths: &[&Path], + root: &Path, + origin: &Path, + project_title: Option<&str>, + ) -> Result<String, Box<dyn std::error::Error>> { + let mut output = Vec::new(); + + MarkdownGenerator::new(paths) + .with_root(root) + .with_origin(origin) + .with_project_title(project_title) + .generate_fenced(&mut output)?; + + Ok(String::from_utf8(output).unwrap()) + } + + #[test] + fn empty_path_list_renders_header_with_specified_project_title() { + let temp_dir = tempdir().unwrap(); + let paths = []; + + let output = generate_to_string( + &paths, + temp_dir.path(), + temp_dir.path(), + Some("Project name"), + ) + .unwrap(); + + assert_eq!(output, "# Project name\n"); + } + + #[test] + fn project_title_is_derived_from_root_when_not_specified() { + let temp_dir = tempdir().unwrap(); + let paths = []; + let root = temp_dir.path().join("repo2markdown"); + + let output = generate_to_string(&paths, &root, temp_dir.path(), None).unwrap(); + + assert_eq!(output, "# repo2markdown\n"); + } + + #[test] + fn renders_single_file() { + let temp_dir = tempdir().unwrap(); + fs::write(temp_dir.path().join("main.rs"), "fn main() {}").unwrap(); + + let paths = [Path::new("main.rs")]; + + let output = generate_to_string( + &paths, + temp_dir.path(), + temp_dir.path(), + Some("Project name"), + ) + .unwrap(); + + assert!(output.contains("# Project name\n")); + assert!(output.contains("## File: main.rs")); + assert!(output.contains("```rust\nfn main() {}\n```")); + } + + #[test] + fn renders_multiple_files_in_input_order() { + let temp_dir = tempdir().unwrap(); + + fs::write(temp_dir.path().join("a.rs"), "A").unwrap(); + fs::write(temp_dir.path().join("b.rs"), "B").unwrap(); + + let paths = [Path::new("a.rs"), Path::new("b.rs")]; + + let output = generate_to_string( + &paths, + temp_dir.path(), + temp_dir.path(), + Some("Project name"), + ) + .unwrap(); + + let a_pos = output.find("## File: a.rs").unwrap(); + let b_pos = output.find("## File: b.rs").unwrap(); + + assert!(a_pos < b_pos); + assert!(output.contains("```rust\nA\n```")); + assert!(output.contains("```rust\nB\n```")); + } + + #[test] + fn resolves_relative_paths_from_origin() { + let temp_dir = tempdir().unwrap(); + let origin = temp_dir.path().join("src"); + + fs::create_dir_all(&origin).unwrap(); + fs::write(origin.join("main.rs"), "fn main() {}").unwrap(); + + let paths = [Path::new("main.rs")]; + + let output = + generate_to_string(&paths, temp_dir.path(), &origin, Some("Project name")).unwrap(); + + assert!(output.contains("## File: src/main.rs")); + assert!(output.contains("fn main() {}")); + } + + #[test] + fn displays_paths_relative_to_root() { + let temp_dir = tempdir().unwrap(); + let root = temp_dir.path().join("project"); + let origin = root.join("src"); + + fs::create_dir_all(&origin).unwrap(); + fs::write(origin.join("main.rs"), "fn main() {}").unwrap(); + + let paths = [Path::new("main.rs")]; + + let output = generate_to_string(&paths, &root, &origin, Some("Project name")).unwrap(); + + assert!(output.contains("## File: src/main.rs")); + assert!(output.contains("fn main() {}")); + } + + #[test] + fn skips_duplicate_paths() { + let temp_dir = tempdir().unwrap(); + + fs::write(temp_dir.path().join("a.rs"), "A").unwrap(); + + let paths = [Path::new("a.rs"), Path::new("a.rs")]; + + let output = generate_to_string( + &paths, + temp_dir.path(), + temp_dir.path(), + Some("Project name"), + ) + .unwrap(); + + assert_eq!(output.matches("## File: a.rs").count(), 1); + } + + #[test] + fn skips_lexically_equivalent_duplicate_paths() { + let temp_dir = tempdir().unwrap(); + + fs::create_dir_all(temp_dir.path().join("src")).unwrap(); + fs::write(temp_dir.path().join("a.rs"), "A").unwrap(); + + let paths = [Path::new("a.rs"), Path::new("src/../a.rs")]; + + let output = generate_to_string( + &paths, + temp_dir.path(), + temp_dir.path(), + Some("Project name"), + ) + .unwrap(); + + assert_eq!(output.matches("## File: a.rs").count(), 1); + } + + #[test] + fn uses_render_options_for_large_file_placeholders() { + let temp_dir = tempdir().unwrap(); + + fs::write(temp_dir.path().join("big.txt"), "A".repeat(10)).unwrap(); + + let paths = [Path::new("big.txt")]; + + let mut output = Vec::new(); + + let render_options = RenderOptions { + max_file_size: 5, + placeholder_for_binary_files: false, + placeholder_for_large_files: true, + }; + + MarkdownGenerator::new(&paths) + .with_root(temp_dir.path()) + .with_origin(temp_dir.path()) + .with_project_title(Some("Project name")) + .with_render_options(render_options) + .with_logger(Logger::default()) + .generate(&mut output) + .unwrap(); + + let output = String::from_utf8(output).unwrap(); + + assert!(output.contains("## File: big.txt")); + assert!(output.contains("[FILE TOO LARGE]")); + } + + #[test] + fn fenced_generation_wraps_empty_markdown_output() { + let temp_dir = tempdir().unwrap(); + let paths: [&Path; 0] = []; + + let output = generate_fenced_to_string( + &paths, + temp_dir.path(), + temp_dir.path(), + Some("Project name"), + ) + .unwrap(); + + assert_eq!(output, "```markdown\n# Project name\n```\n"); + } + + #[test] + fn fenced_generation_wraps_file_output() { + let temp_dir = tempdir().unwrap(); + + fs::write(temp_dir.path().join("main.rs"), "fn main() {}").unwrap(); + + let paths = [Path::new("main.rs")]; + + let output = generate_fenced_to_string( + &paths, + temp_dir.path(), + temp_dir.path(), + Some("Project name"), + ) + .unwrap(); + + assert!(output.starts_with("````markdown\n")); + assert!(output.ends_with("\n````\n")); + assert!(output.contains("# Project name")); + assert!(output.contains("## File: main.rs")); + assert!(output.contains("```rust\nfn main() {}\n```")); + } + + #[test] + fn fenced_generation_expands_outer_fence_when_inner_markdown_contains_backticks() { + let temp_dir = tempdir().unwrap(); + + fs::write(temp_dir.path().join("backticks.rs"), "````").unwrap(); + + let paths = [Path::new("backticks.rs")]; + + let output = generate_fenced_to_string( + &paths, + temp_dir.path(), + temp_dir.path(), + Some("Project name"), + ) + .unwrap(); + + assert!(output.starts_with("``````markdown\n")); + assert!(output.ends_with("\n``````\n")); + assert!(output.contains("`````rust\n````\n`````")); + } +} @@ -10,9 +10,8 @@ mod util; pub mod fence_wrapper; -pub mod fenced_md_generator; +pub mod generator; pub mod logger; -pub mod md_generator; pub mod normalizer; pub mod path_list_editor; pub mod renderer; 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]")); - } -} @@ -7,8 +7,7 @@ use std::io::{Read, Write}; use repo2markdown::{ - fenced_md_generator::generate_fenced_markdown, logger::Logger, - md_generator::generate_markdown_from_paths, path_list::paths_from_null_separated_bytes, + generator::MarkdownGenerator, logger::Logger, path_list::paths_from_null_separated_bytes, }; use crate::cli::Cli; @@ -31,26 +30,17 @@ pub fn run<R: Read, W: Write>( input.read_to_end(&mut input_buf)?; let paths = paths_from_null_separated_bytes(&input_buf); + let md_generator = MarkdownGenerator::new(&paths) + .with_logger(logger) + .with_origin(&cli.origin) + .with_project_title(cli.name.as_deref()) + .with_render_options(cli.render_options()) + .with_root(&cli.root); + if cli.fenced { - generate_fenced_markdown( - &paths, - output, - cli.render_options(), - &cli.root, - &cli.origin, - cli.name.as_deref(), - logger, - ) + md_generator.generate_fenced(output) } else { - generate_markdown_from_paths( - &paths, - output, - cli.render_options(), - &cli.root, - &cli.origin, - cli.name.as_deref(), - logger, - ) + md_generator.generate(output) } } |
