@@ -6,25 +6,115 @@ use askama::Template;
66
77use crate :: analyzer:: stats:: { AnalysisResult , LanguageSummary , Summary } ;
88use crate :: error:: Result ;
9+ use crate :: insight:: Grade ;
910
1011use super :: format:: { OutputFormat , OutputOptions , Report } ;
1112
12- /// HTML report template.
13+ // ── Analysis (existing) ──────────────────────────────────
14+
1315#[ derive( Template ) ]
1416#[ template( path = "report.html" ) ]
15- struct HtmlReport < ' a > {
17+ struct AnalysisHtmlReport < ' a > {
1618 title : & ' a str ,
1719 generated_at : String ,
1820 summary : & ' a Summary ,
1921 by_language : Vec < ( & ' a str , & ' a LanguageSummary ) > ,
2022 elapsed_secs : f64 ,
2123}
2224
23- /// HTML output formatter.
25+ // ── Health ───────────────────────────────────────────────
26+
27+ struct HtmlDimensionScore {
28+ dimension : String ,
29+ score_display : u32 ,
30+ grade : Grade ,
31+ }
32+
33+ struct HtmlDirectoryHealth {
34+ path : String ,
35+ score_display : u32 ,
36+ grade : Grade ,
37+ file_count : usize ,
38+ }
39+
40+ struct HtmlFileHealth {
41+ path : String ,
42+ score_display : u32 ,
43+ grade : Grade ,
44+ top_issue : String ,
45+ }
46+
47+ #[ derive( Template ) ]
48+ #[ template( path = "health.html" ) ]
49+ struct HealthHtmlReport {
50+ generated_at : String ,
51+ model : String ,
52+ grade : String ,
53+ score : u32 ,
54+ dimensions : Vec < HtmlDimensionScore > ,
55+ by_directory : Vec < HtmlDirectoryHealth > ,
56+ worst_files : Vec < HtmlFileHealth > ,
57+ }
58+
59+ // ── Hotspot ──────────────────────────────────────────────
60+
61+ struct HtmlFileHotspot {
62+ path : String ,
63+ language : String ,
64+ commits : usize ,
65+ lines_added : usize ,
66+ lines_deleted : usize ,
67+ cyclomatic : usize ,
68+ score_display : String ,
69+ score_pct : u32 ,
70+ risk : String ,
71+ }
72+
73+ #[ derive( Template ) ]
74+ #[ template( path = "hotspot.html" ) ]
75+ struct HotspotHtmlReport {
76+ generated_at : String ,
77+ since : String ,
78+ total_commits : usize ,
79+ files : Vec < HtmlFileHotspot > ,
80+ }
81+
82+ // ── Trend ────────────────────────────────────────────────
83+
84+ struct HtmlTrendMetric {
85+ label : String ,
86+ from_value : usize ,
87+ to_value : usize ,
88+ signed_delta : i64 ,
89+ delta_display : String ,
90+ percent_display : String ,
91+ }
92+
93+ struct HtmlLanguageTrend {
94+ language : String ,
95+ status : String ,
96+ code_from : usize ,
97+ code_to : usize ,
98+ code_delta : i64 ,
99+ code_delta_display : String ,
100+ }
101+
102+ #[ derive( Template ) ]
103+ #[ template( path = "trend.html" ) ]
104+ struct TrendHtmlReport {
105+ from_date : String ,
106+ from_label : String ,
107+ to_date : String ,
108+ to_label : String ,
109+ metrics : Vec < HtmlTrendMetric > ,
110+ by_language : Vec < HtmlLanguageTrend > ,
111+ }
112+
113+ // ── OutputFormat impl ────────────────────────────────────
114+
24115pub struct HtmlOutput ;
25116
26117impl HtmlOutput {
27- /// Create a new HTML output formatter.
28118 pub fn new ( ) -> Self {
29119 Self
30120 }
@@ -53,35 +143,14 @@ impl OutputFormat for HtmlOutput {
53143 ) -> Result < ( ) > {
54144 match report {
55145 Report :: Analysis ( result) => self . write_analysis ( result, options, writer) ,
56- Report :: Health ( report) => self . write_json_html ( "Code Health Report" , report, writer) ,
57- Report :: Hotspot ( report) => self . write_json_html ( "Hotspot Analysis" , report, writer) ,
58- Report :: Trend ( report) => self . write_json_html ( "Trend Report" , report, writer) ,
146+ Report :: Health ( report) => self . write_health ( report, writer) ,
147+ Report :: Hotspot ( report) => self . write_hotspot ( report, writer) ,
148+ Report :: Trend ( report) => self . write_trend ( report, writer) ,
59149 }
60150 }
61151}
62152
63153impl HtmlOutput {
64- fn write_json_html < T : serde:: Serialize > (
65- & self ,
66- title : & str ,
67- data : & T ,
68- writer : & mut dyn Write ,
69- ) -> Result < ( ) > {
70- let json = serde_json:: to_string_pretty ( data) ?;
71- writeln ! ( writer, "<!DOCTYPE html>" ) ?;
72- writeln ! ( writer, "<html><head><meta charset=\" utf-8\" >" ) ?;
73- writeln ! ( writer, "<title>Codelens - {title}</title>" ) ?;
74- writeln ! (
75- writer,
76- "<style>body{{font-family:monospace;margin:2em;}}pre{{background:#f5f5f5;padding:1em;overflow:auto;}}</style>"
77- ) ?;
78- writeln ! ( writer, "</head><body>" ) ?;
79- writeln ! ( writer, "<h1>{title}</h1>" ) ?;
80- writeln ! ( writer, "<pre>{json}</pre>" ) ?;
81- writeln ! ( writer, "</body></html>" ) ?;
82- Ok ( ( ) )
83- }
84-
85154 fn write_analysis (
86155 & self ,
87156 result : & AnalysisResult ,
@@ -99,7 +168,7 @@ impl HtmlOutput {
99168 by_language. truncate ( n) ;
100169 }
101170
102- let report = HtmlReport {
171+ let report = AnalysisHtmlReport {
103172 title : "Codelens - Code Statistics Report" ,
104173 generated_at : chrono:: Local :: now ( ) . format ( "%Y-%m-%d %H:%M:%S" ) . to_string ( ) ,
105174 summary : & result. summary ,
@@ -110,4 +179,141 @@ impl HtmlOutput {
110179 write ! ( writer, "{}" , report. render( ) ?) ?;
111180 Ok ( ( ) )
112181 }
182+
183+ fn write_health (
184+ & self ,
185+ report : & crate :: insight:: health:: HealthReport ,
186+ writer : & mut dyn Write ,
187+ ) -> Result < ( ) > {
188+ let html = HealthHtmlReport {
189+ generated_at : chrono:: Local :: now ( ) . format ( "%Y-%m-%d %H:%M:%S" ) . to_string ( ) ,
190+ model : report. model . clone ( ) ,
191+ grade : report. grade . to_string ( ) ,
192+ score : report. score as u32 ,
193+ dimensions : report
194+ . dimensions
195+ . iter ( )
196+ . map ( |d| HtmlDimensionScore {
197+ dimension : d. dimension . to_string ( ) ,
198+ score_display : d. score as u32 ,
199+ grade : d. grade ,
200+ } )
201+ . collect ( ) ,
202+ by_directory : report
203+ . by_directory
204+ . iter ( )
205+ . map ( |d| HtmlDirectoryHealth {
206+ path : d. path . display ( ) . to_string ( ) ,
207+ score_display : d. score as u32 ,
208+ grade : d. grade ,
209+ file_count : d. file_count ,
210+ } )
211+ . collect ( ) ,
212+ worst_files : report
213+ . worst_files
214+ . iter ( )
215+ . map ( |f| HtmlFileHealth {
216+ path : f. path . display ( ) . to_string ( ) ,
217+ score_display : f. score as u32 ,
218+ grade : f. grade ,
219+ top_issue : f. top_issue . to_string ( ) ,
220+ } )
221+ . collect ( ) ,
222+ } ;
223+ write ! ( writer, "{}" , html. render( ) ?) ?;
224+ Ok ( ( ) )
225+ }
226+
227+ fn write_hotspot (
228+ & self ,
229+ report : & crate :: insight:: hotspot:: HotspotReport ,
230+ writer : & mut dyn Write ,
231+ ) -> Result < ( ) > {
232+ let html = HotspotHtmlReport {
233+ generated_at : chrono:: Local :: now ( ) . format ( "%Y-%m-%d %H:%M:%S" ) . to_string ( ) ,
234+ since : report. since . clone ( ) ,
235+ total_commits : report. total_commits ,
236+ files : report
237+ . files
238+ . iter ( )
239+ . map ( |h| HtmlFileHotspot {
240+ path : h. path . display ( ) . to_string ( ) ,
241+ language : h. language . clone ( ) ,
242+ commits : h. churn . commits ,
243+ lines_added : h. churn . lines_added ,
244+ lines_deleted : h. churn . lines_deleted ,
245+ cyclomatic : h. complexity . cyclomatic ,
246+ score_display : format ! ( "{:.2}" , h. hotspot_score) ,
247+ score_pct : ( h. hotspot_score * 100.0 ) as u32 ,
248+ risk : h. risk . to_string ( ) ,
249+ } )
250+ . collect ( ) ,
251+ } ;
252+ write ! ( writer, "{}" , html. render( ) ?) ?;
253+ Ok ( ( ) )
254+ }
255+
256+ fn write_trend (
257+ & self ,
258+ report : & crate :: insight:: trend:: TrendReport ,
259+ writer : & mut dyn Write ,
260+ ) -> Result < ( ) > {
261+ let d = & report. delta ;
262+ let make_metric =
263+ |label : & str , dv : & crate :: insight:: DeltaValue < usize > | -> HtmlTrendMetric {
264+ let signed = dv. signed_delta ( ) ;
265+ HtmlTrendMetric {
266+ label : label. to_string ( ) ,
267+ from_value : dv. from ,
268+ to_value : dv. to ,
269+ signed_delta : signed,
270+ delta_display : if signed >= 0 {
271+ format ! ( "+{signed}" )
272+ } else {
273+ format ! ( "{signed}" )
274+ } ,
275+ percent_display : format ! ( "{:+.1}%" , dv. percent) ,
276+ }
277+ } ;
278+
279+ let metrics = vec ! [
280+ make_metric( "Files" , & d. files) ,
281+ make_metric( "Code" , & d. code) ,
282+ make_metric( "Comments" , & d. comment) ,
283+ make_metric( "Blank" , & d. blank) ,
284+ make_metric( "Complexity" , & d. complexity) ,
285+ make_metric( "Functions" , & d. functions) ,
286+ ] ;
287+
288+ let by_language: Vec < HtmlLanguageTrend > = report
289+ . by_language
290+ . iter ( )
291+ . map ( |lt| {
292+ let signed = lt. code . signed_delta ( ) ;
293+ HtmlLanguageTrend {
294+ language : lt. language . clone ( ) ,
295+ status : lt. status . to_string ( ) ,
296+ code_from : lt. code . from ,
297+ code_to : lt. code . to ,
298+ code_delta : signed,
299+ code_delta_display : if signed >= 0 {
300+ format ! ( "+{signed}" )
301+ } else {
302+ format ! ( "{signed}" )
303+ } ,
304+ }
305+ } )
306+ . collect ( ) ;
307+
308+ let html = TrendHtmlReport {
309+ from_date : report. from . timestamp . format ( "%Y-%m-%d" ) . to_string ( ) ,
310+ from_label : report. from . label . as_deref ( ) . unwrap_or ( "" ) . to_string ( ) ,
311+ to_date : report. to . timestamp . format ( "%Y-%m-%d" ) . to_string ( ) ,
312+ to_label : report. to . label . as_deref ( ) . unwrap_or ( "" ) . to_string ( ) ,
313+ metrics,
314+ by_language,
315+ } ;
316+ write ! ( writer, "{}" , html. render( ) ?) ?;
317+ Ok ( ( ) )
318+ }
113319}
0 commit comments