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)
| 184 | /// (via [`html_escape`]); the embedded report JSON is `<`/`>`/`&`-escaped so it |
| 185 | /// cannot break out of its `<script>` container. |
| 186 | pub fn render_html(report: &TriageReport) -> String { |
| 187 | let verdict_class = match report.verdict { |
| 188 | Verdict::Blocked => "blocked", |
| 189 | Verdict::Stale => "stale", |
| 190 | Verdict::Ready => "ready", |
| 191 | }; |
| 192 | let verdict_label = match report.verdict { |
| 193 | Verdict::Blocked => "BLOCKED", |
| 194 | Verdict::Stale => "STALE", |
| 195 | Verdict::Ready => "READY", |
| 196 | }; |
| 197 | let merkle_short: String = report.inputs.view_merkle.chars().take(8).collect(); |
| 198 | |
| 199 | let mut h = String::new(); |
| 200 | h.push_str("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n"); |
| 201 | h.push_str("<meta charset=\"utf-8\">\n"); |
| 202 | h.push_str("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"); |
| 203 | h.push_str(&format!( |
| 204 | "<title>Triage {} \u{2192} {}</title>\n", |
| 205 | html_escape(&report.inputs.feature), |
| 206 | html_escape(&report.inputs.target) |
| 207 | )); |
| 208 | h.push_str("<style>\n"); |
| 209 | h.push_str(HTML_STYLE); |
| 210 | h.push_str("\n</style>\n</head>\n<body>\n"); |
| 211 | |
| 212 | // Verdict banner. |
| 213 | h.push_str(&format!("<header class=\"banner {verdict_class}\">\n")); |
| 214 | h.push_str(&format!( |
| 215 | "<div class=\"verdict\">{}</div>\n", |
| 216 | html_escape(verdict_label) |
| 217 | )); |
| 218 | h.push_str(&format!( |
| 219 | "<div class=\"route\">{} \u{2192} {}</div>\n", |
| 220 | html_escape(&report.inputs.feature), |
| 221 | html_escape(&report.inputs.target) |
| 222 | )); |
| 223 | h.push_str(&format!( |
| 224 | "<div class=\"merkle\">merkle {}</div>\n", |
| 225 | html_escape(&merkle_short) |
| 226 | )); |
| 227 | let s = &report.summary; |
| 228 | h.push_str(&format!( |
| 229 | "<div class=\"counts\">{} changes \u{00b7} {} files \u{00b7} criteria {} met / {} unmet \u{00b7} findings {} block / {} warn / {} info</div>\n", |
| 230 | s.changes, s.files, s.criteria_met, s.criteria_unmet, s.findings_block, s.findings_warn, s.findings_info |
| 231 | )); |
| 232 | h.push_str(&format!( |
| 233 | "<div class=\"ref\">{}</div>\n", |
| 234 | html_escape(&report.reference) |
| 235 | )); |
| 236 | h.push_str("</header>\n"); |
| 237 | |
| 238 | // Findings. |
| 239 | h.push_str("<section id=\"findings\">\n<h2>Findings</h2>\n"); |
| 240 | h.push_str("<div class=\"filters\">\n"); |
| 241 | for (label, key) in [ |
| 242 | ("All", "all"), |
| 243 | ("Block", "block"), |