summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/fenced_md_generator.rs178
-rw-r--r--src/generator/mod.rs383
-rw-r--r--src/lib.rs3
-rw-r--r--src/md_generator.rs340
-rw-r--r--src/run.rs30
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`````"));
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 4815653..1970f70 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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]"));
- }
-}
diff --git a/src/run.rs b/src/run.rs
index 50b076b..c28d7f3 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -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)
}
}