walker_common/report/
mod.rs1mod stats;
4mod summary;
5
6pub use stats::*;
7pub use summary::*;
8
9use std::fmt::Display;
10use std::io::Write;
11use time::macros::format_description;
12
13const BOOTSTRAP_VERSION: &str = "5.3.7";
14const BOOTSTRAP_CSS_SRI: &str =
15 "sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr";
16const BOOTSTRAP_JS_SRI: &str =
17 "sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q";
18
19#[derive(Clone, Debug, Default)]
21pub struct ReportOptions {
22 pub bootstrap: Bootstrap,
23}
24
25#[derive(Clone, Default, Debug)]
27pub enum Bootstrap {
28 #[default]
30 Default,
31 Custom {
33 location: String,
37 js_location: Option<String>,
39 css_integrity: Option<String>,
41 js_integrity: Option<String>,
43 },
44}
45
46impl Bootstrap {
47 pub fn css_location(&self) -> String {
48 match self {
49 Self::Default => format!(
50 "https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/css/bootstrap.min.css"
51 ),
52 Self::Custom {
53 location,
54 js_location,
55 ..
56 } => match js_location {
57 Some(_) => location.clone(),
58 None => format!("{location}/css/bootstrap.min.css"),
59 },
60 }
61 }
62
63 pub fn css_integrity(&self) -> Option<String> {
64 match self {
65 Self::Default => Some(BOOTSTRAP_CSS_SRI.into()),
66 Self::Custom { css_integrity, .. } => css_integrity.clone(),
67 }
68 }
69
70 pub fn js_location(&self) -> String {
71 match self {
72 Self::Default => format!(
73 "https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/js/bootstrap.bundle.min.js"
74 ),
75 Self::Custom {
76 location,
77 js_location,
78 ..
79 } => match js_location {
80 Some(js_location) => js_location.clone(),
81 None => format!("{location}/js/bootstrap.bundle.min.js"),
82 },
83 }
84 }
85
86 pub fn js_integrity(&self) -> Option<String> {
87 match self {
88 Self::Default => Some(BOOTSTRAP_JS_SRI.into()),
89 Self::Custom { js_integrity, .. } => js_integrity.clone(),
90 }
91 }
92}
93
94pub fn render(
95 mut write: impl Write,
96 title: impl Display,
97 report: impl Display,
98 options: &ReportOptions,
99) -> anyhow::Result<()> {
100 writeln!(
101 write,
102 r#"<!doctype html>
103<html lang="en">
104 <head>
105 <meta charset="utf-8">
106 <meta name="viewport" content="width=device-width, initial-scale=1">
107 <title>{title}</title>
108 <link href="{css}" rel="stylesheet" {css_integrity} crossorigin="anonymous">
109 </head>
110 <body>
111 <div class="container-fluid">
112 <h1>
113 {title} <span class="badge bg-secondary">{date}</span>
114 </h1>
115 {report}
116 </div>
117 <script src="{js}" {js_integrity} crossorigin="anonymous"></script>
118 </body>
119</html>
120"#,
121 date = time::OffsetDateTime::now_local()
122 .unwrap_or_else(|_| time::OffsetDateTime::now_utc())
123 .format(&format_description!(
124 "[year]-[month padding:zero]-[day padding:zero] [hour repr:24]:[minute padding:zero]:[second padding:zero] [offset_hour sign:mandatory]:[offset_minute]"
125 ))
126 .unwrap_or_else(|_| "Unknown".to_string()),
127 css = html_escape::encode_quoted_attribute(&options.bootstrap.css_location()),
128 js = html_escape::encode_quoted_attribute(&options.bootstrap.js_location()),
129 css_integrity = options
130 .bootstrap
131 .css_integrity()
132 .map(|sri| format!(
133 r#"integrity="{sri}""#,
134 sri = html_escape::encode_quoted_attribute(&sri)
135 ))
136 .unwrap_or_default(),
137 js_integrity = options
138 .bootstrap
139 .js_integrity()
140 .map(|sri| format!(
141 r#"integrity="{sri}""#,
142 sri = html_escape::encode_quoted_attribute(&sri)
143 ))
144 .unwrap_or_default(),
145 title = html_escape::encode_text(&title.to_string()),
146 )?;
147
148 Ok(())
149}