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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
//! A collection of metadata used during site generation.

use chrono::{Datelike, Utc};
use maud::{html, Markup, PreEscaped, Render};
use std::fs;
use typed_builder::TypedBuilder;

/// Represents a logo.
pub struct LogoLink<'a> {
    /// The link the logo will resolve to when clicked.
    pub url: &'a str,
    /// The url of the logo picture.
    pub logo: &'a str,
    /// Alternative text if the logo cannot be loaded.
    pub alt_text: &'a str,
    /// Text accompanying the logo.
    pub text: &'a str,
}
impl<'a> Render for LogoLink<'a> {
    fn render(&self) -> Markup {
        html! {
            a.link-anchor href=(self.url) {
                img.link-logo src=(self.logo) alt=(self.alt_text); (self.text)
            }
        }
    }
}

/// Data used during site generation for things like css, scripts, contact info and menus. Most are
/// for meta tags.
#[derive(TypedBuilder)]
pub struct MetaData<'a> {
    /// Language of the website.
    #[builder(default = "en-US")]
    pub lang: &'a str,
    /// Encoding of the website.
    #[builder(default = "UTF-8")]
    pub charset: &'a str,
    /// Scripts to include in the website.
    #[builder(default_code = "&[]")]
    pub scripts: &'a [Script<'a>],
    /// CSS to include in the website.
    #[builder(default=&[])]
    pub css: &'a [Css<'a>],
    /// The title of the website.
    #[builder(default = "Benjamin Xu")]
    pub title: &'a str,
    /// The description of the website.
    #[builder(default = "Benjamin Xu's personal site.")]
    pub description: &'a str,
    /// The copyright data of the website.
    #[builder(default_code = r#"Copyright {
        name: &Name {
            first: "Benjamin",
            middle: Some("Peiyan"),
            last: "Xu",
            nicknames: &[],
        },
        icon: "©",
        rights_clause: "All rights reserved",
    }"#)]
    pub copyright: Copyright<'a>,
    /// The menu of the website.
    #[builder(default)]
    pub menu: Option<&'a Menu<'a>>,
    /// The points of contact for the owner of the website.
    #[builder(default)]
    pub contact: Option<&'a Contact<'a>>,
    /// The logo of the website.
    #[builder(default)]
    pub logo: Option<&'a Logo<'a>>,
    /// The theme color of the website. Affects mobile address name bars.
    #[builder(default = "#00003f")]
    pub theme_color: &'a str,
}
impl<'a> Default for MetaData<'a> {
    fn default() -> Self {
        Self::builder().build()
    }
}
/// Information regarding the logo. (This is very simple).
pub struct Logo<'a> {
    /// The url to the actual image.
    pub src: &'a str,
    pub href: Option<&'a str>,
}
impl<'a> Render for Logo<'a> {
    fn render(&self) -> Markup {
        html! {
            div.logo {
                @match self.href {
                    Some(link) => a.logo-wrapper href=(link) {
                        img.logo-img src=(self.src);
                    },
                    None => img.logo-img src=(self.src);,
                }
            }
        }
    }
}
/// Information regarding the `<script>` tags to include.
pub enum Script<'a> {
    /// Represents a script externally linked (in the `public/js` directory).
    External(&'a str),
    /// Represents a script copy and pasted into the website.
    Embedded(&'a str),
}
impl<'a> Render for Script<'a> {
    fn render(&self) -> Markup {
        match self {
            Script::External(src) => html! { script defer?[true] src={ "/public/js/"(src) } {} },
            Script::Embedded(src) => html! { script { (PreEscaped(src)) } },
        }
    }
}
impl<'a> Script<'a> {
    /// A default script loading wasm glue for my wasm code.
    pub fn wasm_bindgen_loader(name: &str) -> (String, String) {
        let glue = format!("wasm-bindgen-glue/{}.js", name);
        let load = format!("\
             document.addEventListener(\
                \"DOMContentLoaded\",\
                function(){{\
                    var mod = wasm_bindgen(\"/public/wasm/{}_bg.wasm\").catch(function(e) {{ console.log(\"Promise received from wasm load.\"); console.log(e); e.catch(function(e) {{ console.log(e) }}) }});\
                    if (mod.load_listeners) {{\
                        var listeners = mod.load_listeners();\
                    }}\
                }}\
             );\
            ",
            name
        );
        (glue, load)
    }
}
/// Information regarding the `<style>` tags to include.
pub enum Css<'a> {
    /// Above the fold CSS. This get linked in from the resources directory, `/public`.
    Critical { src: &'a str },
    /// Under the fold CSS. This get linked in from the resources directory, `/public`.
    NonCritical { src: &'a str },
}
impl<'a> Render for Css<'a> {
    fn render(&self) -> Markup {
        match self {
            Css::NonCritical { src } => html! { link rel="stylesheet" href={
                "/public/css/"(src)".css"
            }{} },
            Css::Critical { src } => {
                let style = fs::read_to_string(format!("./public/css/{}.css", src).as_str())
                    .unwrap_or_else(|src| panic!("./public/css/{}.css is missing", src));
                html! { style { (PreEscaped(style)) } }
            }
        }
    }
}
/// A email address.
pub struct Email<'a> {
    /// The username portion of the email.
    pub user: &'a str,
    /// The domain portion of the email.
    pub domain: &'a str,
}
impl<'a> Render for Email<'a> {
    fn render(&self) -> Markup {
        html! {
            (self.user)"@"(self.domain)
        }
    }
}
/// A phone number. This is an enum for globalization.
pub enum PhoneNumber<'a> {
    /// A phone number in the US.
    US {
        /// The area code.
        area_code: u16,
        /// The prefix (the three numbers after the area code).
        prefix: u16,
        /// The line number (the four numbers after the area code).
        line_number: u16,
        /// A link to the icon for this number. (Work, Mobile, etc.)
        icon: &'a str,
    },
}
impl<'a> Render for PhoneNumber<'a> {
    fn render(&self) -> Markup {
        match self {
            PhoneNumber::US {
                icon,
                area_code,
                prefix,
                line_number,
            } => html! {
                (icon)": ("(area_code)") "(prefix)"-"(line_number)
            },
        }
    }
}
/// A contact card. Comprised of emails and phone numbers.
pub struct Contact<'a> {
    /// Emails for this contact.
    pub email: &'a [Email<'a>],
    /// Phone numbers for this contact.
    pub phone: &'a [PhoneNumber<'a>],
}
impl<'a> Render for Contact<'a> {
    fn render(&self) -> Markup {
        html! {
            @for email in self.email {
                p.contact-email { "Email: " (email) }
            }
            @for phone in self.phone {
                p.contact-phone-number { "Phone: " (phone) }
            }
        }
    }
}
/// A struct representing names.
pub struct Name<'a> {
    /// First name.
    pub first: &'a str,
    /// Middle name.
    pub middle: Option<&'a str>,
    /// Last name.
    pub last: &'a str,
    /// A list of nicknames.
    pub nicknames: &'a [&'a str],
}
impl<'a> Render for Name<'a> {
    fn render(&self) -> Markup {
        html! {
            (self.first) " " @if let Some(middle) = self.middle {
                @if let Some(initial) = middle.chars().next() {
                    (initial)
                }
            } ". " (self.last)
        }
    }
}
/// Copyright data.
pub struct Copyright<'a> {
    /// Person copyrighting the website.
    pub name: &'a Name<'a>,
    /// The copyright icon to be used.
    pub icon: &'a str,
    /// What rights to grant/refuse.
    pub rights_clause: &'a str,
}
impl<'a> Render for Copyright<'a> {
    fn render(&self) -> Markup {
        let year = Utc::now().year();
        let start_year = year - 1;
        let end_year = year + 1;
        html! {
            p.copyright { (self.icon) " " (start_year) "-" (end_year) " " (self.name) ". " (self.rights_clause) "." }
        }
    }
}
/// An entry in the menu.
pub struct MenuItem<'a> {
    /// Text to display.
    pub text: &'a str,
    /// Where the entry links to, if it links to one.
    pub link: Option<&'a str>,
    /// A child menu, if one exists.
    pub children: Option<&'a Menu<'a>>,
}
impl<'a> MenuItem<'a> {
    /// Render a link to [`Markup`] if present.
    fn render_possible_link(link: Option<&str>, text: &str) -> Markup {
        html! {
            @if let Some(link) = link {
                a href=(link) { (text) }
            } @else {
                (text)
            }
        }
    }
}
impl<'a> Render for MenuItem<'a> {
    fn render(&self) -> Markup {
        html! {
            li {
                (MenuItem::render_possible_link(self.link, self.text))
                @if let Some(children) = self.children {
                    (children)
                }
            }
        }
    }
}
/// A newtype for a list of [`MenuItem`](crate::data::MenuItem)s.
pub struct Menu<'a>(pub &'a [MenuItem<'a>]);
impl<'a> Render for Menu<'a> {
    fn render(&self) -> Markup {
        html! {
            nav.menu {
                ul {
                    @for item in self.0.iter() {
                        (item)
                    }
                }
            }
        }
    }
}
impl<'a> Render for &Menu<'a> {
    fn render(&self) -> Markup {
        (*self).render()
    }
}
impl<'a> Menu<'a> {
    pub fn into_string(self) -> String {
        self.render().into_string()
    }
}