1use chrono::{Datelike, Utc};
4use maud::{html, Markup, PreEscaped, Render};
5use std::fs;
6
7pub struct LogoLink<'a> {
9 pub url: &'a str,
11 pub logo: &'a str,
13 pub alt_text: &'a str,
15 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
28pub struct PageMetaData<'a> {
31 pub lang: &'a str,
33 pub charset: &'a str,
35 pub scripts: &'a [Script<'a>],
37 pub css: &'a [Css<'a>],
39 pub title: &'a str,
41 pub description: &'a str,
43 pub copyright: Copyright<'a>,
45 pub favicons: &'a [Favicon<'a>],
47 pub menu: Option<&'a Menu<'a>>,
49 pub contact: Option<&'a Contact<'a>>,
51 pub logo: Option<&'a Logo<'a>>,
53 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
108pub struct Logo<'a> {
110 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
129pub enum Script<'a> {
131 External(&'a str),
133 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 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
172pub enum Css<'a> {
174 Critical { src: &'a str },
176 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
194pub struct Email<'a> {
196 pub user: &'a str,
198 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
209pub enum PhoneNumber<'a> {
211 US {
213 area_code: u16,
215 prefix: u16,
217 line_number: u16,
219 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
238pub struct Contact<'a> {
240 pub email: &'a [Email<'a>],
242 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
258pub struct Name<'a> {
260 pub first: &'a str,
262 pub middle: Option<&'a str>,
264 pub last: &'a str,
266 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
281pub struct Copyright<'a> {
283 pub name: &'a Name<'a>,
285 pub icon: &'a str,
287 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
301pub struct MenuItem<'a> {
303 pub text: &'a str,
305 pub link: Option<&'a str>,
307 pub children: Option<&'a Menu<'a>>,
309}
310impl<'a> MenuItem<'a> {
311 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
335pub 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}