diff options
| author | A Farzat <a@farzat.xyz> | 2026-06-22 19:56:40 +0300 |
|---|---|---|
| committer | A Farzat <a@farzat.xyz> | 2026-06-22 19:56:40 +0300 |
| commit | 1293ea92bdbf3eb2086ec117e355714b6ee54ae9 (patch) | |
| tree | 9de76726abd8c4de07904c7ed0284599211a53db /src/renderer.rs | |
| parent | 711df9de70e8a61a441b7ba7200ae006258ce978 (diff) | |
| download | repo2markdown-1293ea92bdbf3eb2086ec117e355714b6ee54ae9.tar.gz repo2markdown-1293ea92bdbf3eb2086ec117e355714b6ee54ae9.zip | |
This aids in testing and might allow logging to a file at a later point.
Diffstat (limited to 'src/renderer.rs')
| -rw-r--r-- | src/renderer.rs | 229 |
1 files changed, 173 insertions, 56 deletions
diff --git a/src/renderer.rs b/src/renderer.rs index 1db1215..e89482d 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -22,26 +22,21 @@ pub const DEFAULT_MAX_FILE_SIZE: u64 = 1_000_000; /// Renders files and project metadata as Markdown. #[derive(Debug)] -pub struct Renderer<W: Write> { +pub struct Renderer<'a, W: Write, LW: Write> { output: W, + logger: &'a Logger<LW>, max_file_size: u64, - logger: Logger, placeholder_for_binary_files: bool, placeholder_for_large_files: bool, } -impl<W: Write> Renderer<W> { - /// Creates a renderer with default [`RenderOptions`]. - pub fn new_with_defaults(output: W) -> Self { - Self::new(output, RenderOptions::default()) - } - +impl<'a, W: Write, LW: Write> Renderer<'a, W, LW> { /// Creates a renderer with explicit rendering options. - pub fn new(output: W, config: RenderOptions) -> Self { + pub fn new(output: W, logger: &'a Logger<LW>, config: RenderOptions) -> Self { Self { output, + logger, max_file_size: config.max_file_size, - logger: Logger::default(), placeholder_for_binary_files: config.placeholder_for_binary_files, placeholder_for_large_files: config.placeholder_for_large_files, } @@ -53,12 +48,6 @@ impl<W: Write> Renderer<W> { self } - /// Sets the logger used for warnings and informational messages. - pub fn with_logger(mut self, logger: Logger) -> Self { - self.logger = logger; - self - } - /// Controls whether skipped binary files are represented by placeholders. pub fn with_binary_file_placeholder(mut self, placeholder_for_binary_files: bool) -> Self { self.placeholder_for_binary_files = placeholder_for_binary_files; @@ -198,21 +187,33 @@ fn human_readable_size(bytes: u64) -> String { mod tests { use std::{ ffi::OsStr, - io::Cursor, + io::{Cursor, Write}, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, }; use tempfile::tempdir; - use crate::normalizer::NormalizedPath; + use crate::{ + logger::{Logger, Verbosity}, + normalizer::NormalizedPath, + util::test_support::{logger_with_shared_log, quiet_logger}, + }; + + use super::{RenderOptions, Renderer, human_readable_size}; - use super::{Renderer, human_readable_size}; + fn renderer_for_tests<'a, W: Write, LW: Write>( + output: W, + logger: &'a Logger<LW>, + ) -> Renderer<'a, W, LW> { + Renderer::new(output, logger, RenderOptions::default()) + } #[test] fn renderer_writes_header() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); renderer.render_header("Project name").unwrap(); @@ -222,193 +223,205 @@ mod tests { #[test] fn renderer_renders_single_rust_file_with_language_fence() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); let input = Cursor::new("fn main() {}"); renderer.render_file(Path::new("main.rs"), input).unwrap(); - let expected = "\n## File: main.rs\n```rust\nfn main() {}\n```\n"; + let expected = "\n## File: main.rs\n```rust\nfn main() {}\n```\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } #[test] fn binary_files_are_not_included_in_output_at_all_by_default() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); let input = Cursor::new(&[0x00, 0x01, 0x02, 0xc3]); renderer.render_file(Path::new("image.png"), input).unwrap(); - let expected = ""; - assert_eq!(String::from_utf8(output).unwrap(), expected); + assert_eq!(String::from_utf8(output).unwrap(), ""); } #[test] fn renderer_can_place_a_placeholder_for_binary_files() { let mut output = Vec::new(); + let logger = quiet_logger(); let mut renderer = - Renderer::new_with_defaults(&mut output).with_binary_file_placeholder(true); + renderer_for_tests(&mut output, &logger).with_binary_file_placeholder(true); let input = Cursor::new(&[0x00, 0x01, 0x02, 0xc3]); renderer.render_file(Path::new("image.png"), input).unwrap(); - let expected = "\n## File: image.png\n[BINARY FILE]\n"; + let expected = "\n## File: image.png\n[BINARY FILE]\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } #[test] fn filename_with_linebreaks_and_invalid_chars_handled_properly() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); let input = Cursor::new("fn main() {}"); let filename = Path::new(OsStr::from_bytes(b"jap\xE3\x81\x82dir/some\nma\xc3in.rs")); renderer.render_file(filename, input).unwrap(); - let expected = "\n## File: japあdir/some\\nma\\xC3in.rs\n```rust\nfn main() {}\n```\n"; + let expected = "\n## File: japあdir/some\\nma\\xC3in.rs\n```rust\nfn main() {}\n```\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } #[test] fn file_with_backticks_is_handled_safely() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); let input = Cursor::new("fn main() { println!(\"``` inside\"); }"); renderer .render_file(Path::new("example.rs"), input) .unwrap(); - let expected = "\n## File: example.rs\n````rust\n\ - fn main() { println!(\"``` inside\"); }\n````\n"; + let expected = + "\n## File: example.rs\n````rust\nfn main() { println!(\"``` inside\"); }\n````\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } #[test] fn unknown_filetype_has_no_language_fence() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); let input = Cursor::new("hello world"); renderer.render_file(Path::new("readme"), input).unwrap(); - let expected = "\n## File: readme\n```\nhello world\n```\n"; + let expected = "\n## File: readme\n```\nhello world\n```\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } #[test] fn txt_file_has_no_language_fence() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); let input = Cursor::new("hello world"); renderer.render_file(Path::new("hello.txt"), input).unwrap(); - let expected = "\n## File: hello.txt\n```\nhello world\n```\n"; + let expected = "\n## File: hello.txt\n```\nhello world\n```\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } #[test] fn python_file_has_python_language_fence() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); let input = Cursor::new("print('hello')"); renderer.render_file(Path::new("main.py"), input).unwrap(); - let expected = "\n## File: main.py\n```python\nprint('hello')\n```\n"; + let expected = "\n## File: main.py\n```python\nprint('hello')\n```\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } #[test] fn json_file_has_json_language_fence() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); let input = Cursor::new("{\"key\":\"value\"}"); renderer.render_file(Path::new("main.json"), input).unwrap(); - let expected = "\n## File: main.json\n```json\n{\"key\":\"value\"}\n```\n"; + let expected = "\n## File: main.json\n```json\n{\"key\":\"value\"}\n```\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } #[test] fn uppercase_extension_is_still_detected() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); let input = Cursor::new("fn main() {}"); renderer.render_file(Path::new("MAIN.RS"), input).unwrap(); - let expected = "\n## File: MAIN.RS\n```rust\nfn main() {}\n```\n"; + let expected = "\n## File: MAIN.RS\n```rust\nfn main() {}\n```\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } #[test] fn bash_file_detected_from_shebang() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); let input = Cursor::new("#! /bin/bash\necho hello"); renderer.render_file(Path::new("hello"), input).unwrap(); - let expected = "\n## File: hello\n```bash\n#! /bin/bash\necho hello\n```\n"; + let expected = "\n## File: hello\n```bash\n#! /bin/bash\necho hello\n```\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } #[test] fn python_file_detected_from_shebang() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output); + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger); let input = Cursor::new("#! /bin/python3\nprint('hello')"); renderer.render_file(Path::new("hello"), input).unwrap(); - let expected = "\n## File: hello\n```python\n#! /bin/python3\nprint('hello')\n```\n"; + let expected = "\n## File: hello\n```python\n#! /bin/python3\nprint('hello')\n```\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } #[test] fn large_files_are_not_included_in_output_at_all_by_default() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output).with_max_file_size(5); // smaller than file + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger).with_max_file_size(5); let temp_dir = tempdir().unwrap(); let file_path = temp_dir.path().join("big.txt"); - let content = "A".repeat(10); // 10 bytes -> bigger than the limit - std::fs::write(&file_path, &content).unwrap(); + std::fs::write(&file_path, "A".repeat(10)).unwrap(); + let normalized_path = NormalizedPath { root_relative: PathBuf::from("big.txt"), absolute: file_path, }; renderer.render_path(&normalized_path).unwrap(); - let expected = ""; - assert_eq!(String::from_utf8(output).unwrap(), expected); + assert_eq!(String::from_utf8(output).unwrap(), ""); } #[test] fn renderer_can_place_a_placeholder_for_large_files() { let mut output = Vec::new(); - let mut renderer = Renderer::new_with_defaults(&mut output) + let logger = quiet_logger(); + let mut renderer = renderer_for_tests(&mut output, &logger) .with_large_file_placeholder(true) - .with_max_file_size(5); // smaller than file + .with_max_file_size(5); let temp_dir = tempdir().unwrap(); let file_path = temp_dir.path().join("big.txt"); - let content = "A".repeat(10); // 10 bytes -> bigger than the limit - std::fs::write(&file_path, &content).unwrap(); + std::fs::write(&file_path, "A".repeat(10)).unwrap(); + let normalized_path = NormalizedPath { root_relative: PathBuf::from("big.txt"), absolute: file_path, }; renderer.render_path(&normalized_path).unwrap(); - let expected = "\n## File: big.txt\n[FILE TOO LARGE]\n"; + let expected = "\n## File: big.txt\n[FILE TOO LARGE]\n"; assert_eq!(String::from_utf8(output).unwrap(), expected); } @@ -419,4 +432,108 @@ mod tests { assert_eq!(human_readable_size(1_048_576), "1.0 MiB"); assert_eq!(human_readable_size(5_242_880), "5.0 MiB"); } + + #[test] + fn verbose_verbosity_logs_rendered_text_files() { + let mut output = Vec::new(); + let (logger, logs) = logger_with_shared_log(Verbosity::Verbose); + let mut renderer = renderer_for_tests(&mut output, &logger); + + renderer + .render_file(Path::new("main.rs"), Cursor::new("fn main() {}")) + .unwrap(); + + assert_eq!(logs.contents(), "Info: rendering file: main.rs\n"); + } + + #[test] + fn normal_verbosity_does_not_log_rendered_text_files() { + let mut output = Vec::new(); + let (logger, logs) = logger_with_shared_log(Verbosity::Normal); + let mut renderer = renderer_for_tests(&mut output, &logger); + + renderer + .render_file(Path::new("main.rs"), Cursor::new("fn main() {}")) + .unwrap(); + + assert_eq!(logs.contents(), ""); + } + + #[test] + fn normal_verbosity_logs_skipped_binary_files() { + let mut output = Vec::new(); + let (logger, logs) = logger_with_shared_log(Verbosity::Normal); + let mut renderer = renderer_for_tests(&mut output, &logger); + + renderer + .render_file( + Path::new("image.png"), + Cursor::new(&[0x00, 0x01, 0x02, 0xc3]), + ) + .unwrap(); + + assert_eq!( + logs.contents(), + "Warning: skipping binary file: image.png\n" + ); + } + + #[test] + fn quiet_verbosity_suppresses_skipped_binary_file_warning() { + let mut output = Vec::new(); + let (logger, logs) = logger_with_shared_log(Verbosity::Quiet); + let mut renderer = renderer_for_tests(&mut output, &logger); + + renderer + .render_file( + Path::new("image.png"), + Cursor::new(&[0x00, 0x01, 0x02, 0xc3]), + ) + .unwrap(); + + assert_eq!(logs.contents(), ""); + } + + #[test] + fn normal_verbosity_logs_skipped_large_files() { + let mut output = Vec::new(); + let (logger, logs) = logger_with_shared_log(Verbosity::Normal); + let mut renderer = renderer_for_tests(&mut output, &logger).with_max_file_size(5); + + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("big.txt"); + std::fs::write(&file_path, "A".repeat(10)).unwrap(); + + let normalized_path = NormalizedPath { + root_relative: PathBuf::from("big.txt"), + absolute: file_path, + }; + + renderer.render_path(&normalized_path).unwrap(); + + assert_eq!( + logs.contents(), + "Warning: skipping large file: big.txt (10 B > limit 5 B)\n" + ); + } + + #[test] + fn quiet_verbosity_suppresses_skipped_large_file_warning() { + let mut output = Vec::new(); + let (logger, logs) = logger_with_shared_log(Verbosity::Quiet); + let mut renderer = renderer_for_tests(&mut output, &logger).with_max_file_size(5); + + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("big.txt"); + std::fs::write(&file_path, "A".repeat(10)).unwrap(); + + let normalized_path = NormalizedPath { + root_relative: PathBuf::from("big.txt"), + absolute: file_path, + }; + + renderer.render_path(&normalized_path).unwrap(); + + assert_eq!(logs.contents(), ""); + } } |
