use std::fmt::Write as _;
use super::diff::{ChangeKind, ContentDiff, DiffSection};
const GREEN: &str = "\x1b[32m";
const RED: &str = "\x1b[31m";
const YELLOW: &str = "\x1b[33m";
const DIM: &str = "\x1b[2m";
const RESET: &str = "\x1b[0m";
pub fn format_diff_terminal(diff: &ContentDiff) -> String {
let mut out = String::with_capacity(512);
push_header_terminal(&mut out, diff);
if diff.unchanged || diff.sections.is_empty() {
let _ = writeln!(out, "{DIM}No changes detected.{RESET}");
return out;
}
for section in &diff.sections {
push_section_terminal(&mut out, section);
}
push_summary_terminal(&mut out, diff);
out
}
pub fn format_diff_markdown(diff: &ContentDiff) -> String {
let mut out = String::with_capacity(512);
push_header_markdown(&mut out, diff);
if diff.unchanged || diff.sections.is_empty() {
out.push_str("No changes detected.\n");
return out;
}
for section in &diff.sections {
push_section_markdown(&mut out, section);
}
push_summary_markdown(&mut out, diff);
out
}
fn push_header_terminal(out: &mut String, diff: &ContentDiff) {
let _ = writeln!(
out,
"{DIM}--- {url} (old: {old}){RESET}",
url = diff.url,
old = diff.old_timestamp,
);
let _ = writeln!(
out,
"{DIM}+++ {url} (new: {new}){RESET}",
url = diff.url,
new = diff.new_timestamp,
);
}
fn push_section_terminal(out: &mut String, section: &DiffSection) {
for ctx in §ion.context {
let _ = writeln!(out, "{DIM} {ctx}{RESET}");
}
match section.kind {
ChangeKind::Added => {
let text = section.new_text.as_deref().unwrap_or("");
let _ = writeln!(out, "{GREEN}+ {text}{RESET}");
}
ChangeKind::Removed => {
let text = section.old_text.as_deref().unwrap_or("");
let _ = writeln!(out, "{RED}- {text}{RESET}");
}
ChangeKind::Modified => {
let old = section.old_text.as_deref().unwrap_or("");
let new = section.new_text.as_deref().unwrap_or("");
let _ = writeln!(out, "{RED}- {old}{RESET}");
let _ = writeln!(out, "{GREEN}+ {new}{RESET}");
}
}
}
fn push_summary_terminal(out: &mut String, diff: &ContentDiff) {
let _ = writeln!(
out,
"\n{YELLOW}Summary: {summary}{RESET}",
summary = diff.summary()
);
}
fn push_header_markdown(out: &mut String, diff: &ContentDiff) {
let _ = write!(
out,
"**Content diff**: `{url}` \nOld: `{old}` -> New: `{new}`\n\n",
url = diff.url,
old = diff.old_timestamp,
new = diff.new_timestamp,
);
}
fn push_section_markdown(out: &mut String, section: &DiffSection) {
if let Some(ctx) = section.context.first() {
let _ = write!(out, "> {ctx}\n\n");
}
match section.kind {
ChangeKind::Added => {
let text = section.new_text.as_deref().unwrap_or("");
let _ = write!(out, "**added**: {text}\n\n");
}
ChangeKind::Removed => {
let text = section.old_text.as_deref().unwrap_or("");
let _ = write!(out, "**removed**: {text}\n\n");
}
ChangeKind::Modified => {
let old = section.old_text.as_deref().unwrap_or("");
let new = section.new_text.as_deref().unwrap_or("");
let _ = write!(out, "**modified**: \n- old: {old} \n+ new: {new}\n\n");
}
}
}
fn push_summary_markdown(out: &mut String, diff: &ContentDiff) {
let _ = write!(out, "---\n**{}**\n", diff.summary());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::content::diff::{ContentSnapshot, compute_diff};
use std::time::SystemTime;
fn snap(text: &str) -> ContentSnapshot {
ContentSnapshot::new("https://example.com", text, SystemTime::UNIX_EPOCH)
}
fn make_diff_with_change() -> ContentDiff {
let old = snap("Intro.\n\nOld body.\n\nConclusion.");
let new = snap("Intro.\n\nNew body.\n\nConclusion.\n\nExtra section.");
compute_diff(&old, &new)
}
#[test]
fn terminal_format_unchanged_says_no_changes() {
let s = snap("Same.");
let diff = compute_diff(&s, &s.clone());
let out = format_diff_terminal(&diff);
assert!(out.contains("No changes"), "got: {out}");
}
#[test]
fn terminal_format_added_shows_plus_prefix() {
let old = snap("A.");
let new = snap("A.\n\nB.");
let diff = compute_diff(&old, &new);
let out = format_diff_terminal(&diff);
assert!(out.contains('+'), "expected '+' in: {out}");
}
#[test]
fn terminal_format_removed_shows_minus_prefix() {
let old = snap("A.\n\nB.");
let new = snap("A.");
let diff = compute_diff(&old, &new);
let out = format_diff_terminal(&diff);
assert!(out.contains('-'), "expected '-' in: {out}");
}
#[test]
fn terminal_format_includes_url_in_header() {
let diff = make_diff_with_change();
let out = format_diff_terminal(&diff);
assert!(out.contains("example.com"), "URL missing in: {out}");
}
#[test]
fn terminal_format_includes_summary_line() {
let diff = make_diff_with_change();
let out = format_diff_terminal(&diff);
assert!(out.contains("Summary:"), "missing Summary in: {out}");
}
#[test]
fn terminal_format_modified_shows_both_old_and_new() {
let old = snap("Intro.\n\nOld paragraph.\n\nEnd.");
let new = snap("Intro.\n\nNew paragraph.\n\nEnd.");
let diff = compute_diff(&old, &new);
let out = format_diff_terminal(&diff);
let has_change = out.contains("Old paragraph")
|| out.contains("New paragraph")
|| out.contains('-')
|| out.contains('+');
assert!(has_change, "expected change markers in: {out}");
}
#[test]
fn markdown_format_unchanged_says_no_changes() {
let s = snap("Same.");
let diff = compute_diff(&s, &s.clone());
let out = format_diff_markdown(&diff);
assert!(out.contains("No changes"), "got: {out}");
assert!(!out.contains('\x1b'), "ANSI in markdown: {out}");
}
#[test]
fn markdown_format_added_uses_added_label() {
let old = snap("A.");
let new = snap("A.\n\nB.");
let diff = compute_diff(&old, &new);
let out = format_diff_markdown(&diff);
assert!(out.contains("added"), "expected 'added' in: {out}");
}
#[test]
fn markdown_format_removed_uses_removed_label() {
let old = snap("A.\n\nB.");
let new = snap("A.");
let diff = compute_diff(&old, &new);
let out = format_diff_markdown(&diff);
assert!(out.contains("removed"), "expected 'removed' in: {out}");
}
#[test]
fn markdown_format_no_ansi_escape_codes() {
let diff = make_diff_with_change();
let out = format_diff_markdown(&diff);
assert!(
!out.contains('\x1b'),
"ANSI escape found in markdown output"
);
}
#[test]
fn markdown_format_includes_summary_separator() {
let diff = make_diff_with_change();
let out = format_diff_markdown(&diff);
assert!(out.contains("---"), "missing separator in: {out}");
}
#[test]
fn markdown_format_includes_url() {
let diff = make_diff_with_change();
let out = format_diff_markdown(&diff);
assert!(out.contains("example.com"), "URL missing in: {out}");
}
}