Render the report as ONE self-contained HTML document. No external URLs, CDNs, or scripts: all CSS is inline and the only JavaScript is a tiny vanilla filter function. Every interpolated string is HTML-escaped (via [`html_escape`]); the embedded report JSON is `<`/`>`/`&`-escaped so it cannot break out of its ` ` container.
(report: &TriageReport)
| 294 | /// (via [`html_escape`]); the embedded report JSON is `<`/`>`/`&`-escaped so it |
| 295 | /// cannot break out of its `<script>` container. |
| 296 | pub fn render_html(report: &TriageReport) -> String { |
| 297 | let verdict_class = match report.verdict { |
| 298 | Verdict::Blocked => "blocked", |
| 299 | Verdict::Stale => "stale", |
| 300 | Verdict::Ready => "ready", |
| 301 | }; |
| 302 | let verdict_label = match report.verdict { |
| 303 | Verdict::Blocked => "BLOCKED", |
| 304 | Verdict::Stale => "STALE", |
| 305 | Verdict::Ready => "READY", |
| 306 | }; |
| 307 | let merkle_short: String = report.inputs.view_merkle.chars().take(8).collect(); |
| 308 | |
| 309 | let mut h = String::new(); |
| 310 | h.push_str("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"); |
| 311 | h.push_str("<meta charset=\"utf-8\">\n"); |
| 312 | h.push_str("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"); |
| 313 | h.push_str(&format!( |
| 314 | "<title>Triage {} \u{2192} {}</title>\n", |
| 315 | html_escape(&report.inputs.feature), |
| 316 | html_escape(&report.inputs.target) |
| 317 | )); |
| 318 | h.push_str("<style>\n"); |
| 319 | h.push_str(HTML_STYLE); |
| 320 | h.push_str("\n</style>\n</head>\n<body>\n"); |
| 321 | |
| 322 | // Verdict banner. |
| 323 | h.push_str(&format!("<header class=\"banner {verdict_class}\">\n")); |
| 324 | h.push_str(&format!( |
| 325 | "<div class=\"verdict\">{}</div>\n", |
| 326 | html_escape(verdict_label) |
| 327 | )); |
| 328 | h.push_str(&format!( |
| 329 | "<div class=\"route\">{} \u{2192} {}</div>\n", |
| 330 | html_escape(&report.inputs.feature), |
| 331 | html_escape(&report.inputs.target) |
| 332 | )); |
| 333 | h.push_str(&format!( |
| 334 | "<div class=\"merkle\">merkle {}</div>\n", |
| 335 | html_escape(&merkle_short) |
| 336 | )); |
| 337 | let s = &report.summary; |
| 338 | h.push_str(&format!( |
| 339 | "<div class=\"counts\">{} changes \u{00b7} {} files \u{00b7} criteria {} met / {} unmet \u{00b7} findings {} block / {} warn / {} info</div>\n", |
| 340 | s.changes, s.files, s.criteria_met, s.criteria_unmet, s.findings_block, s.findings_warn, s.findings_info |
| 341 | )); |
| 342 | h.push_str(&format!( |
| 343 | "<div class=\"ref\">{}</div>\n", |
| 344 | html_escape(&report.reference) |
| 345 | )); |
| 346 | h.push_str("</header>\n"); |
| 347 | |
| 348 | // Findings. |
| 349 | h.push_str("<section id=\"findings\">\n<h2>Findings</h2>\n"); |
| 350 | h.push_str("<div class=\"filters\">\n"); |
| 351 | for (label, key) in [ |
| 352 | ("All", "all"), |
| 353 | ("Block", "block"), |