1use std::fmt::Write as FmtWrite;
7
8use tokmd_types::{DiffRow, DiffTotals};
9
10fn format_delta(delta: i64) -> String {
11 if delta > 0 {
12 format!("+{}", delta)
13 } else {
14 delta.to_string()
15 }
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum DiffColorMode {
20 Off,
21 Ansi,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct DiffRenderOptions {
26 pub compact: bool,
27 pub color: DiffColorMode,
28}
29
30impl Default for DiffRenderOptions {
31 fn default() -> Self {
32 Self {
33 compact: false,
34 color: DiffColorMode::Off,
35 }
36 }
37}
38
39fn format_delta_colored(delta: i64, mode: DiffColorMode) -> String {
40 let raw = format_delta(delta);
41 if mode == DiffColorMode::Off {
42 return raw;
43 }
44 if delta > 0 {
45 format!("\x1b[32m{}\x1b[0m", raw)
46 } else if delta < 0 {
47 format!("\x1b[31m{}\x1b[0m", raw)
48 } else {
49 format!("\x1b[33m{}\x1b[0m", raw)
50 }
51}
52
53fn format_pct_delta_colored(delta_pct: f64, mode: DiffColorMode) -> String {
54 let raw = format!("{:+.1}%", delta_pct);
55 if mode == DiffColorMode::Off {
56 return raw;
57 }
58 if delta_pct > 0.0 {
59 format!("\x1b[32m{}\x1b[0m", raw)
60 } else if delta_pct < 0.0 {
61 format!("\x1b[31m{}\x1b[0m", raw)
62 } else {
63 format!("\x1b[33m{}\x1b[0m", raw)
64 }
65}
66
67fn percent_change(old: usize, new: usize) -> f64 {
68 if old > 0 {
69 ((new as f64 - old as f64) / old as f64) * 100.0
70 } else if new > 0 {
71 100.0
72 } else {
73 0.0
74 }
75}
76
77pub fn render_diff_md_with_options(
79 from_source: &str,
80 to_source: &str,
81 rows: &[DiffRow],
82 totals: &DiffTotals,
83 options: DiffRenderOptions,
84) -> String {
85 let mut s = String::with_capacity((rows.len() + 20) * 80);
87
88 let _ = writeln!(s, "## Diff: {} → {}", from_source, to_source);
89 s.push('\n');
90
91 let languages_added = rows
92 .iter()
93 .filter(|r| r.old_code == 0 && r.new_code > 0)
94 .count();
95 let languages_removed = rows
96 .iter()
97 .filter(|r| r.old_code > 0 && r.new_code == 0)
98 .count();
99 let languages_modified = rows
100 .len()
101 .saturating_sub(languages_added + languages_removed);
102
103 if options.compact {
104 s.push_str("### Summary\n\n");
105 s.push_str("|Metric|Value|\n");
106 s.push_str("|---|---:|\n");
107 let _ = writeln!(s, "|From LOC|{}|", totals.old_code);
108 let _ = writeln!(s, "|To LOC|{}|", totals.new_code);
109 let _ = writeln!(
110 s,
111 "|Delta LOC|{}|",
112 format_delta_colored(totals.delta_code, options.color)
113 );
114 let _ = writeln!(
115 s,
116 "|LOC Change|{}|",
117 format_pct_delta_colored(
118 percent_change(totals.old_code, totals.new_code),
119 options.color
120 )
121 );
122 let _ = writeln!(
123 s,
124 "|Delta Lines|{}|",
125 format_delta_colored(totals.delta_lines, options.color)
126 );
127 let _ = writeln!(
128 s,
129 "|Delta Files|{}|",
130 format_delta_colored(totals.delta_files, options.color)
131 );
132 let _ = writeln!(
133 s,
134 "|Delta Bytes|{}|",
135 format_delta_colored(totals.delta_bytes, options.color)
136 );
137 let _ = writeln!(
138 s,
139 "|Delta Tokens|{}|",
140 format_delta_colored(totals.delta_tokens, options.color)
141 );
142 let _ = writeln!(s, "|Languages changed|{}|", rows.len());
143 let _ = writeln!(s, "|Languages added|{}|", languages_added);
144 let _ = writeln!(s, "|Languages removed|{}|", languages_removed);
145 let _ = writeln!(s, "|Languages modified|{}|", languages_modified);
146 return s;
147 }
148
149 s.push_str("### Summary\n\n");
151 s.push_str("|Metric|From|To|Delta|Change|\n");
152 s.push_str("|---|---:|---:|---:|---:|\n");
153
154 let _ = writeln!(
155 s,
156 "|LOC|{}|{}|{}|{}|",
157 totals.old_code,
158 totals.new_code,
159 format_delta_colored(totals.delta_code, options.color),
160 format_pct_delta_colored(
161 percent_change(totals.old_code, totals.new_code),
162 options.color
163 )
164 );
165 let _ = writeln!(
166 s,
167 "|Lines|{}|{}|{}|{}|",
168 totals.old_lines,
169 totals.new_lines,
170 format_delta_colored(totals.delta_lines, options.color),
171 format_pct_delta_colored(
172 percent_change(totals.old_lines, totals.new_lines),
173 options.color
174 )
175 );
176 let _ = writeln!(
177 s,
178 "|Files|{}|{}|{}|{}|",
179 totals.old_files,
180 totals.new_files,
181 format_delta_colored(totals.delta_files, options.color),
182 format_pct_delta_colored(
183 percent_change(totals.old_files, totals.new_files),
184 options.color
185 )
186 );
187 let _ = writeln!(
188 s,
189 "|Bytes|{}|{}|{}|{}|",
190 totals.old_bytes,
191 totals.new_bytes,
192 format_delta_colored(totals.delta_bytes, options.color),
193 format_pct_delta_colored(
194 percent_change(totals.old_bytes, totals.new_bytes),
195 options.color
196 )
197 );
198 let _ = writeln!(
199 s,
200 "|Tokens|{}|{}|{}|{}|",
201 totals.old_tokens,
202 totals.new_tokens,
203 format_delta_colored(totals.delta_tokens, options.color),
204 format_pct_delta_colored(
205 percent_change(totals.old_tokens, totals.new_tokens),
206 options.color
207 )
208 );
209 s.push('\n');
210
211 s.push_str("### Language Movement\n\n");
212 s.push_str("|Type|Count|\n");
213 s.push_str("|---|---:|\n");
214 let _ = writeln!(s, "|Changed|{}|", rows.len());
215 let _ = writeln!(s, "|Added|{}|", languages_added);
216 let _ = writeln!(s, "|Removed|{}|", languages_removed);
217 let _ = writeln!(s, "|Modified|{}|", languages_modified);
218 s.push('\n');
219
220 s.push_str("### Language Breakdown\n\n");
222 s.push_str("|Language|Old LOC|New LOC|Delta|\n");
223 s.push_str("|---|---:|---:|---:|\n");
224
225 for row in rows {
226 let _ = writeln!(
227 s,
228 "|{}|{}|{}|{}|",
229 row.lang,
230 row.old_code,
231 row.new_code,
232 format_delta_colored(row.delta_code, options.color)
233 );
234 }
235
236 let _ = writeln!(
237 s,
238 "|**Total**|{}|{}|{}|",
239 totals.old_code,
240 totals.new_code,
241 format_delta_colored(totals.delta_code, options.color)
242 );
243
244 s
245}
246
247pub fn render_diff_md(
249 from_source: &str,
250 to_source: &str,
251 rows: &[DiffRow],
252 totals: &DiffTotals,
253) -> String {
254 render_diff_md_with_options(
255 from_source,
256 to_source,
257 rows,
258 totals,
259 DiffRenderOptions::default(),
260 )
261}
262
263#[cfg(test)]
264mod tests {
265 use super::format_delta;
266
267 #[test]
268 fn test_format_delta() {
269 assert_eq!(format_delta(5), "+5");
271 assert_eq!(format_delta(0), "0");
272 assert_eq!(format_delta(-3), "-3");
273 }
274}