static_page_builder/
data.rs

1//! A collection of metadata used during site generation.
2
3use chrono::{Datelike, Utc};
4use maud::{html, Markup, PreEscaped, Render};
5use std::fs;
6
7/// Represents a logo.
8pub struct LogoLink<'a> {
9    /// The link the logo will resolve to when clicked.
10    pub url: &'a str,
11    /// The url of the logo picture.
12    pub logo: &'a str,
13    /// Alternative text if the logo cannot be loaded.
14    pub alt_text: &'a str,
15    /// Text accompanying the logo.
16    pub text: &'a str,
17}
18impl<'a> Render for LogoLink<'a> {
19    fn render(&self) -> Markup {
20        html! {
21            a.link-anchor href=(self.url) {
22                img.link-logo src=(self.logo) alt=(self.alt_text); (self.text)
23            }
24        }
25    }
26}
27
28/// Data used during site generation for things like css, scripts, contact info and menus. Most are
29/// for meta tags.
30pub struct PageMetaData<'a> {
31    /// Language of the website.
32    pub lang: &'a str,
33    /// Encoding of the website.
34    pub charset: &'a str,
35    /// Scripts to include in the website.
36    pub scripts: &'a [Script<'a>],
37    /// CSS to include in the website.
38    pub css: &'a [Css<'a>],
39    /// The title of the website.
40    pub title: &'a str,
41    /// The description of the website.
42    pub description: &'a str,
43    /// The copyright data of the website.
44    pub copyright: Copyright<'a>,
45    /// The favicon
46    pub favicons: &'a [Favicon<'a>],
47    /// The menu of the website.
48    pub menu: Option<&'a Menu<'a>>,
49    /// The points of contact for the owner of the website.
50    pub contact: Option<&'a Contact<'a>>,
51    /// The logo of the website.
52    pub logo: Option<&'a Logo<'a>>,
53    /// The theme color of the website. Affects mobile address name bars.
54    pub theme_color: &'a str,
55}
56impl<'a> Default for PageMetaData<'a> {
57    fn default() -> Self {
58        Self {
59            lang: "en-US",
60            charset: "UTF-8",
61            scripts: &[],
62            css: &[],
63            title: "Benjamin Xu",
64            description: "Benjamin Xu's personal site.",
65            copyright: Copyright {
66                name: &Name {
67                    first: "Benjamin",
68                    middle: Some("Peiyan"),
69                    last: "Xu",
70                    nicknames: &[],
71                },
72                icon: "©",
73                rights_clause: "All rights reserved",
74            },
75            favicons: &[],
76            menu: None,
77            contact: None,
78            logo: None,
79            theme_color: "#00003f",
80        }
81    }
82}
83
84pub struct Favicon<'a> {
85    pub link: &'a str,
86    pub media_type: Option<&'a str>,
87    pub sizes: Option<&'a str>
88}
89impl<'a> Render for Favicon<'a> {
90    fn render(&self) -> Markup {
91        match (self.media_type, self.sizes) {
92            (Some(mt), Some(sz)) => {
93                html! { link rel="icon" href={(self.link)} type={(mt)} sizes={(sz)} {} }
94            },
95            (None, Some(sz)) => {
96                html! { link rel="icon" href={(self.link)} sizes={(sz)} {} }
97            },
98            (Some(mt), None) => {
99                html! { link rel="icon" href={(self.link)} type={(mt)} {} }
100            },
101            (None, None) => {
102                html! { link rel="icon" href={(self.link)} {} }
103            },
104        }
105    }
106}
107
108/// Information regarding the logo. (This is very simple).
109pub struct Logo<'a> {
110    /// The url to the actual image.
111    pub src: &'a str,
112    pub href: Option<&'a str>,
113}
114impl<'a> Render for Logo<'a> {
115    fn render(&self) -> Markup {
116        html! {
117            div.logo {
118                @match self.href {
119                    Some(link) => a.logo-wrapper href=(link) {
120                        img.logo-img src=(self.src);
121                    },
122                    None => img.logo-img src=(self.src);,
123                }
124            }
125        }
126    }
127}
128
129/// Information regarding the `<script>` tags to include.
130pub enum Script<'a> {
131    /// Represents a script externally linked (in the `public/js` directory).
132    External(&'a str),
133    /// Represents a script copy and pasted into the website.
134    Embedded(&'a str),
135}
136impl<'a> Render for Script<'a> {
137    fn render(&self) -> Markup {
138        match self {
139            Script::External(src) => html! { script defer?[true] src={ (src) } {} },
140            Script::Embedded(src) => html! { script { (PreEscaped(src)) } },
141        }
142    }
143}
144impl<'a> Script<'a> {
145    /// A script for hooking in the WASM loading script
146    pub fn wasm_bindgen_loader(js_path: &str, wasm_path: &str, name: &str) -> (String, String) {
147        let glue = format!("/{js_path}/{name}.js");
148        let load = format!(
149            "\
150             document.addEventListener(\
151                \"DOMContentLoaded\",\
152                function(){{\
153                    var mod = wasm_bindgen(\"/{wasm_path}/{name}_bg.wasm\")\
154                        .catch(function(e) {{\
155                            console.log(\"Promise received from wasm load.\");\
156                            console.log(e);\
157                            e.catch(function(e) {{\
158                                console.log(e);\
159                            }});
160                        }});\
161                    if (mod.load_listeners) {{\
162                        var listeners = mod.load_listeners();\
163                    }}\
164                }}\
165             );\
166            ",
167        );
168        (glue, load)
169    }
170}
171
172/// Information regarding the `<style>` tags to include.
173pub enum Css<'a> {
174    /// Above the fold CSS. This get linked in from the resources directory, `/public`.
175    Critical { src: &'a str },
176    /// Under the fold CSS. This get linked in from the resources directory, `/public`.
177    NonCritical { src: &'a str },
178}
179impl<'a> Render for Css<'a> {
180    fn render(&self) -> Markup {
181        match self {
182            Css::NonCritical { src } => html! { link rel="stylesheet" href={
183                (src)
184            }{} },
185            Css::Critical { src } => {
186                let style = fs::read_to_string(src)
187                    .unwrap_or_else(|e| panic!("{:?} is missing ({:?})", src, e));
188                html! { style { (PreEscaped(style)) } }
189            }
190        }
191    }
192}
193
194/// A email address.
195pub struct Email<'a> {
196    /// The username portion of the email.
197    pub user: &'a str,
198    /// The domain portion of the email.
199    pub domain: &'a str,
200}
201impl<'a> Render for Email<'a> {
202    fn render(&self) -> Markup {
203        html! {
204            (self.user)"@"(self.domain)
205        }
206    }
207}
208
209/// A phone number. This is an enum for globalization.
210pub enum PhoneNumber<'a> {
211    /// A phone number in the US.
212    US {
213        /// The area code.
214        area_code: u16,
215        /// The prefix (the three numbers after the area code).
216        prefix: u16,
217        /// The line number (the four numbers after the area code).
218        line_number: u16,
219        /// A link to the icon for this number. (Work, Mobile, etc.)
220        icon: &'a str,
221    },
222}
223impl<'a> Render for PhoneNumber<'a> {
224    fn render(&self) -> Markup {
225        match self {
226            PhoneNumber::US {
227                icon,
228                area_code,
229                prefix,
230                line_number,
231            } => html! {
232                (icon)": ("(area_code)") "(prefix)"-"(line_number)
233            },
234        }
235    }
236}
237
238/// A contact card. Comprised of emails and phone numbers.
239pub struct Contact<'a> {
240    /// Emails for this contact.
241    pub email: &'a [Email<'a>],
242    /// Phone numbers for this contact.
243    pub phone: &'a [PhoneNumber<'a>],
244}
245impl<'a> Render for Contact<'a> {
246    fn render(&self) -> Markup {
247        html! {
248            @for email in self.email {
249                p.contact-email { "Email: " (email) }
250            }
251            @for phone in self.phone {
252                p.contact-phone-number { "Phone: " (phone) }
253            }
254        }
255    }
256}
257
258/// A struct representing names.
259pub struct Name<'a> {
260    /// First name.
261    pub first: &'a str,
262    /// Middle name.
263    pub middle: Option<&'a str>,
264    /// Last name.
265    pub last: &'a str,
266    /// A list of nicknames.
267    pub nicknames: &'a [&'a str],
268}
269impl<'a> Render for Name<'a> {
270    fn render(&self) -> Markup {
271        html! {
272            (self.first) " " @if let Some(middle) = self.middle {
273                @if let Some(initial) = middle.chars().next() {
274                    (initial) ". " 
275                }
276            } (self.last)
277        }
278    }
279}
280
281/// Copyright data.
282pub struct Copyright<'a> {
283    /// Person copyrighting the website.
284    pub name: &'a Name<'a>,
285    /// The copyright icon to be used.
286    pub icon: &'a str,
287    /// What rights to grant/refuse.
288    pub rights_clause: &'a str,
289}
290impl<'a> Render for Copyright<'a> {
291    fn render(&self) -> Markup {
292        let year = Utc::now().year();
293        let start_year = year - 1;
294        let end_year = year + 1;
295        html! {
296            p.copyright { (self.icon) " " (start_year) "-" (end_year) " " (self.name) ". " (self.rights_clause) "." }
297        }
298    }
299}
300
301/// An entry in the menu.
302pub struct MenuItem<'a> {
303    /// Text to display.
304    pub text: &'a str,
305    /// Where the entry links to, if it links to one.
306    pub link: Option<&'a str>,
307    /// A child menu, if one exists.
308    pub children: Option<&'a Menu<'a>>,
309}
310impl<'a> MenuItem<'a> {
311    /// Render a link to [`Markup`] if present.
312    fn render_possible_link(link: Option<&str>, text: &str) -> Markup {
313        html! {
314            @if let Some(link) = link {
315                a href=(link) { (text) }
316            } @else {
317                (text)
318            }
319        }
320    }
321}
322impl<'a> Render for MenuItem<'a> {
323    fn render(&self) -> Markup {
324        html! {
325            li {
326                (MenuItem::render_possible_link(self.link, self.text))
327                @if let Some(children) = self.children {
328                    (children)
329                }
330            }
331        }
332    }
333}
334
335/// A newtype for a list of [`MenuItem`](crate::data::MenuItem)s.
336pub struct Menu<'a>(pub &'a [MenuItem<'a>]);
337impl<'a> Render for Menu<'a> {
338    fn render(&self) -> Markup {
339        html! {
340            nav.menu {
341                ul {
342                    @for item in self.0.iter() {
343                        (item)
344                    }
345                }
346            }
347        }
348    }
349}
350impl<'a> Menu<'a> {
351    pub fn into_string(self) -> String {
352        self.render().into_string()
353    }
354}