1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! Common functionality for creating the reports

use std::fmt::Display;
use std::io::Write;
use time::macros::format_description;

const BOOTSTRAP_VERSION: &str = "5.3.3";
const BOOTSTRAP_CSS_SRI: &str =
    "sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH";
const BOOTSTRAP_JS_SRI: &str =
    "sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy";

/// Options for rendering reports.
#[derive(Clone, Debug, Default)]
pub struct ReportOptions {
    pub bootstrap: Bootstrap,
}

/// Options for the imported bootstrap resources.
#[derive(Clone, Default, Debug)]
pub enum Bootstrap {
    /// Use a default version served from a CDN
    #[default]
    Default,
    /// Use a custom version
    Custom {
        /// The location to Bootstrap.
        ///
        /// The is considered the base URL, unless `js_location` is present as well. In which case it's considered a full URL.
        location: String,
        /// The specific location of the bootstrap JS file.
        js_location: Option<String>,
        /// An optional SRI value for the CSS resource
        css_integrity: Option<String>,
        /// An optional SRI value for the JS resource
        js_integrity: Option<String>,
    },
}

impl Bootstrap {
    pub fn css_location(&self) -> String {
        match self {
            Self::Default => format!("https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/css/bootstrap.min.css"),
            Self::Custom {location, js_location, ..} => match js_location {
                Some(_) => location.clone(),
                None => format!("{location}/css/bootstrap.min.css" ),
            },
        }
    }

    pub fn css_integrity(&self) -> Option<String> {
        match self {
            Self::Default => Some(BOOTSTRAP_CSS_SRI.into()),
            Self::Custom { css_integrity, .. } => css_integrity.clone(),
        }
    }

    pub fn js_location(&self) -> String {
        match self {
            Self::Default => format!("https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/js/bootstrap.bundle.min.js"),
            Self::Custom {location, js_location, ..} => {
                match js_location {
                    Some(js_location) => js_location.clone(),
                    None => format!("{location}/js/bootstrap.bundle.min.js")
                }
            }
        }
    }

    pub fn js_integrity(&self) -> Option<String> {
        match self {
            Self::Default => Some(BOOTSTRAP_JS_SRI.into()),
            Self::Custom { js_integrity, .. } => js_integrity.clone(),
        }
    }
}

pub fn render(
    mut write: impl Write,
    title: impl Display,
    report: impl Display,
    options: &ReportOptions,
) -> anyhow::Result<()> {
    writeln!(
        write,
        r#"<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{title}</title>
    <link href="{css}" rel="stylesheet" {css_integrity} crossorigin="anonymous">
  </head>
  <body>
    <div class="container-fluid">
      <h1>
        {title} <span class="badge bg-secondary">{date}</span>
      </h1>
      {report}
    </div>
    <script src="{js}" {js_integrity} crossorigin="anonymous"></script>
  </body>
</html>
"#,
        date = time::OffsetDateTime::now_local()
            .unwrap_or_else(|_| time::OffsetDateTime::now_utc())
            .format(&format_description!(
                "[year]-[month padding:zero]-[day padding:zero] [hour repr:24]:[minute padding:zero]:[second padding:zero] [offset_hour sign:mandatory]:[offset_minute]"
            ))
            .unwrap_or_else(|_| "Unknown".to_string()),
        css = html_escape::encode_quoted_attribute(&options.bootstrap.css_location()),
        js = html_escape::encode_quoted_attribute(&options.bootstrap.js_location()),
        css_integrity = options
            .bootstrap
            .css_integrity()
            .map(|sri| format!(
                r#"integrity="{sri}""#,
                sri = html_escape::encode_quoted_attribute(&sri)
            ))
            .unwrap_or_default(),
        js_integrity = options
            .bootstrap
            .css_integrity()
            .map(|sri| format!(
                r#"integrity="{sri}""#,
                sri = html_escape::encode_quoted_attribute(&sri)
            ))
            .unwrap_or_default(),
        title = html_escape::encode_text(&title.to_string()),
    )?;

    Ok(())
}