summaryrefslogtreecommitdiff
path: root/src/renderer.rs
diff options
context:
space:
mode:
authorA Farzat <a@farzat.xyz>2026-06-22 19:56:40 +0300
committerA Farzat <a@farzat.xyz>2026-06-22 19:56:40 +0300
commit1293ea92bdbf3eb2086ec117e355714b6ee54ae9 (patch)
tree9de76726abd8c4de07904c7ed0284599211a53db /src/renderer.rs
parent711df9de70e8a61a441b7ba7200ae006258ce978 (diff)
downloadrepo2markdown-1293ea92bdbf3eb2086ec117e355714b6ee54ae9.tar.gz
repo2markdown-1293ea92bdbf3eb2086ec117e355714b6ee54ae9.zip
Make Logger's output destination customizableHEADmaster
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.rs229
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(), "");
+ }
}