use std::path::PathBuf;
use similar::TextDiff;
pub struct FormatEntry {
pub path: PathBuf,
pub current: String,
pub canonical: String,
}
pub struct FmtChange {
pub path: PathBuf,
pub diff: String,
pub canonical: String,
}
#[derive(Default)]
pub struct FmtReport {
pub changes: Vec<FmtChange>,
pub unchanged: usize,
}
impl FmtReport {
pub fn has_changes(&self) -> bool {
!self.changes.is_empty()
}
}
pub fn fmt(entries: Vec<FormatEntry>) -> FmtReport {
let mut report = FmtReport::default();
for entry in entries {
if entry.current == entry.canonical {
report.unchanged += 1;
continue;
}
let cur_fm = extract_frontmatter(&entry.current).unwrap_or(entry.current.as_str());
let can_fm = extract_frontmatter(&entry.canonical).unwrap_or(entry.canonical.as_str());
let diff = TextDiff::from_lines(cur_fm, can_fm)
.unified_diff()
.header("current", "canonical")
.to_string();
report.changes.push(FmtChange {
path: entry.path,
diff,
canonical: entry.canonical,
});
}
report
}
fn extract_frontmatter(source: &str) -> Option<&str> {
let after_open = source.strip_prefix("---\n")?;
let end = after_open.find("\n---")?;
Some(&after_open[..end])
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(path: &str, current: &str, canonical: &str) -> FormatEntry {
FormatEntry {
path: PathBuf::from(path),
current: current.to_string(),
canonical: canonical.to_string(),
}
}
#[test]
fn identical_entries_are_counted_as_unchanged() {
let report = fmt(vec![entry(
"a.md",
"---\nid: X\n---\n\nbody\n",
"---\nid: X\n---\n\nbody\n",
)]);
assert_eq!(report.unchanged, 1);
assert!(report.changes.is_empty());
assert!(!report.has_changes());
}
#[test]
fn differing_entries_produce_a_change_with_canonical_payload() {
let current = "---\nid: X\npriority: high\n---\n\nbody\n";
let canonical = "---\nid: X\n---\n\nbody\n";
let report = fmt(vec![entry("a.md", current, canonical)]);
assert_eq!(report.unchanged, 0);
assert_eq!(report.changes.len(), 1);
let change = &report.changes[0];
assert_eq!(change.path, PathBuf::from("a.md"));
assert_eq!(change.canonical, canonical);
assert!(
change.diff.contains("-priority: high"),
"diff should show removed line, got:\n{}",
change.diff
);
}
#[test]
fn diff_excludes_body_only_changes_when_frontmatter_matches() {
let current = "---\nid: X\n---\n\nold body\n";
let canonical = "---\nid: X\n---\n\nnew body\n";
let report = fmt(vec![entry("a.md", current, canonical)]);
assert_eq!(report.changes.len(), 1);
let change = &report.changes[0];
assert!(
change.diff.is_empty() || !change.diff.contains("body"),
"frontmatter diff should not mention body, got:\n{}",
change.diff
);
}
#[test]
fn missing_frontmatter_falls_back_to_full_content_for_diff() {
let report = fmt(vec![entry("a.md", "raw text\n", "raw text canonical\n")]);
assert_eq!(report.changes.len(), 1);
assert!(!report.changes[0].diff.is_empty());
}
#[test]
fn mixed_entries_split_correctly() {
let report = fmt(vec![
entry(
"same.md",
"---\nid: A\n---\n\nbody\n",
"---\nid: A\n---\n\nbody\n",
),
entry(
"diff.md",
"---\nid: B\nx: y\n---\n\nbody\n",
"---\nid: B\n---\n\nbody\n",
),
]);
assert_eq!(report.unchanged, 1);
assert_eq!(report.changes.len(), 1);
assert_eq!(report.changes[0].path, PathBuf::from("diff.md"));
}
}