use crate::config::{Config, OutputConfig};
use crate::core_types::FileInfo;
use anyhow::Result;
use log::debug;
use std::io::Write;
pub mod dry_run;
pub mod file_block;
pub mod formatter;
pub mod header;
pub mod summary;
pub mod writer;
impl From<&Config> for OutputConfig {
fn from(config: &Config) -> Self {
config.output
}
}
pub trait OutputFormatter: Send + Sync {
fn format(&self, files: &[FileInfo], opts: &OutputConfig, writer: &mut dyn Write)
-> Result<()>;
fn format_dry_run(
&self,
files: &[FileInfo],
opts: &OutputConfig,
writer: &mut dyn Write,
) -> Result<()>;
}
pub struct MarkdownFormatter;
impl OutputFormatter for MarkdownFormatter {
fn format(
&self,
files: &[FileInfo],
opts: &OutputConfig,
writer: &mut dyn Write,
) -> Result<()> {
debug!("Starting Markdown output generation...");
if files.is_empty() {
return Ok(());
}
header::write_global_header(writer)?;
let all_files_iter = files.iter();
let mut first_block = true;
for file_info in all_files_iter {
if !first_block {
writeln!(writer)?;
}
file_block::write_file_block(writer, file_info, opts)?;
first_block = false;
}
if opts.summary {
if !first_block {
writeln!(writer)?;
}
let all_processed_files: Vec<&FileInfo> = files.iter().collect();
summary::write_summary(writer, &all_processed_files, opts)?;
}
debug!("Markdown output generation complete.");
writer.flush()?; Ok(())
}
fn format_dry_run(
&self,
files: &[FileInfo],
opts: &OutputConfig,
writer: &mut dyn Write,
) -> Result<()> {
let file_refs: Vec<&FileInfo> = files.iter().collect();
dry_run::write_dry_run_output(writer, &file_refs, opts)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::core_types::{FileCounts, FileInfo};
use std::path::PathBuf;
pub(crate) fn create_mock_output_config(
backticks: bool,
filename_only: bool,
line_numbers: bool,
summary: bool,
) -> OutputConfig {
OutputConfig {
backticks,
filename_only_header: filename_only,
line_numbers,
summary,
counts: false, num_ticks: 3,
}
}
pub(crate) fn create_mock_file_info(relative_path: &str, size: u64) -> FileInfo {
FileInfo {
absolute_path: PathBuf::from("/base").join(relative_path),
relative_path: PathBuf::from(relative_path),
size,
..Default::default()
}
}
#[test]
fn test_markdown_formatter_basic() -> Result<()> {
let opts = create_mock_output_config(false, false, false, false);
let content_b = "Content B";
let content_a = "Content A";
let mut file1 = create_mock_file_info("b.txt", 10);
file1.processed_content = Some(content_b.to_string());
let mut file2 = create_mock_file_info("a.rs", 20);
file2.processed_content = Some(content_a.to_string());
let formatter = MarkdownFormatter;
let normal_files = vec![file1, file2]; let mut output = Vec::new();
formatter.format(&normal_files, &opts, &mut output)?;
let output_str = String::from_utf8(output)?;
assert!(output_str.starts_with("## File: b.txt"));
assert!(output_str.contains("```\n\n## File: a.rs"));
assert!(!output_str.ends_with("\n\n"));
assert!(output_str.ends_with("\n"));
Ok(())
}
#[test]
fn test_markdown_formatter_with_last_files() -> Result<()> {
let opts = create_mock_output_config(false, false, false, false);
let content_c = "Content C";
let content_a = "Content A";
let content_last1 = "Last 1";
let content_last0 = "Last 0";
let mut file1 = create_mock_file_info("c.txt", 10);
file1.processed_content = Some(content_c.to_string());
let mut file2 = create_mock_file_info("a.rs", 20);
file2.processed_content = Some(content_a.to_string());
let mut last1 = create_mock_file_info("last1.md", 30); last1.processed_content = Some(content_last1.to_string());
last1.is_process_last = true;
last1.process_last_order = Some(0);
let mut last0 = create_mock_file_info("last0.toml", 40); last0.processed_content = Some(content_last0.to_string());
last0.is_process_last = true;
last0.process_last_order = Some(1);
let mut normal_files = vec![file1, file2]; let mut last_files = vec![last1, last0];
normal_files.sort_by(|a, b| a.relative_path.cmp(&b.relative_path)); last_files.sort_by_key(|f| f.process_last_order);
let all_files: Vec<FileInfo> = normal_files.into_iter().chain(last_files).collect();
let formatter = MarkdownFormatter;
let mut output = Vec::new();
formatter.format(&all_files, &opts, &mut output)?;
let output_str = String::from_utf8(output)?;
assert!(output_str.starts_with("## File: a.rs"));
assert!(output_str.contains("```\n\n## File: c.txt"));
assert!(output_str.contains("```\n\n## File: last1.md"));
assert!(output_str.contains("```\n\n## File: last0.toml"));
let pos_a = output_str.find("## File: a.rs").unwrap();
let pos_c = output_str.find("## File: c.txt").unwrap();
let pos_last1 = output_str.find("## File: last1.md").unwrap();
let pos_last0 = output_str.find("## File: last0.toml").unwrap();
assert!(pos_a < pos_c); assert!(pos_c < pos_last1); assert!(pos_last1 < pos_last0);
Ok(())
}
#[test]
fn test_markdown_formatter_with_summary() -> Result<()> {
let mut opts = create_mock_output_config(false, false, false, true); opts.counts = true; let content_b = "Content B";
let content_a = "Content A";
let mut file1 = create_mock_file_info("b.txt", 10);
file1.processed_content = Some(content_b.to_string());
file1.counts = Some(FileCounts {
lines: 1,
characters: 9,
words: 2,
});
let mut file2 = create_mock_file_info("a.rs", 20);
file2.processed_content = Some(content_a.to_string());
file2.counts = Some(FileCounts {
lines: 1,
characters: 9,
words: 2,
});
let normal_files = vec![file1, file2];
let formatter = MarkdownFormatter;
let mut output = Vec::new();
formatter.format(&normal_files, &opts, &mut output)?;
let output_str = String::from_utf8(output)?;
assert!(output_str.starts_with("## File: b.txt"));
assert!(output_str.contains("```\n\n## File: a.rs"));
assert!(output_str.contains("```\n\n---\nProcessed Files: (2)\n")); assert!(output_str.contains("- a.rs (L:1 C:9 W:2)\n")); assert!(output_str.contains("- b.txt (L:1 C:9 W:2)\n"));
assert!(output_str.ends_with("- b.txt (L:1 C:9 W:2)\n"));
Ok(())
}
#[test]
fn test_markdown_formatter_no_files() -> Result<()> {
let opts = create_mock_output_config(false, false, false, false);
let files = vec![];
let formatter = MarkdownFormatter;
let mut output = Vec::new();
formatter.format(&files, &opts, &mut output)?;
let output_str = String::from_utf8(output)?;
assert_eq!(output_str, "");
Ok(())
}
#[test]
fn test_markdown_formatter_file_with_no_content() -> Result<()> {
let opts = create_mock_output_config(false, false, false, false);
let file = create_mock_file_info("empty.txt", 0); let files = vec![file];
let formatter = MarkdownFormatter;
let mut output = Vec::new();
formatter.format(&files, &opts, &mut output)?;
let output_str = String::from_utf8(output)?;
let expected = "## File: empty.txt\n```txt\n// Content not available\n```\n";
assert_eq!(output_str, expected);
Ok(())
}
#[test]
fn test_markdown_formatter_file_with_only_newline() -> Result<()> {
let opts = create_mock_output_config(false, false, false, false);
let mut file = create_mock_file_info("newline.txt", 1);
file.processed_content = Some("\n".to_string());
let files = vec![file];
let formatter = MarkdownFormatter;
let mut output = Vec::new();
formatter.format(&files, &opts, &mut output)?;
let output_str = String::from_utf8(output)?;
let expected = "## File: newline.txt\n```txt\n\n```\n";
assert_eq!(output_str, expected);
Ok(())
}
#[test]
fn test_markdown_formatter_dry_run_no_files() -> Result<()> {
let opts = create_mock_output_config(false, false, false, false);
let files = vec![];
let formatter = MarkdownFormatter;
let mut output = Vec::new();
formatter.format_dry_run(&files, &opts, &mut output)?;
let output_str = String::from_utf8(output)?;
let expected = "\n--- Dry Run: Files that would be processed ---\n--- End Dry Run ---\n";
assert_eq!(output_str, expected);
Ok(())
}
}