use anyhow::{Context, Result}; use maud::{html, Markup}; use serde::Deserialize; use toml::value::Date; use self::page::Page; mod page; #[derive(Deserialize)] struct Data { stories: Vec, } impl page::Page for Data { fn page_title() -> String { String::from("Home") } fn alternative_htmlfilename() -> Option { Some(String::from("index")) } fn render_content(&self) -> Markup { let last_story = self.stories.first(); html!( @if let Some(story) = last_story { div class="latest-story" lang=(story.html_lang_value()) { h2 lang="en" { "Latest Story" } h3 { (story.name) } p { (story.description) } ul lang="en" { li { "Language: " (story.language) } li { a href=(&story.format_pdf_path()) download { "PDF" } } } } } ) } fn content_style() -> Markup { html!( """ h2 { font-size: 2.5rem; color: #2F3C57; text-decoration: 0.1rem solid underline black; } h3 { font-size: 1.8rem; color: #2F3C57; padding: 2rem 0; } .latest-story { padding: 3rem; margin: 3rem; text-align: left; background-color: #FFD1A3; hyphens: auto; * { margin: 1rem 0; } p { font-size: 1.2rem; } } a { text-decoration: underline; color: #3D1D49; } ul { list-style: none; } """ ) } } impl page::Page for Vec { fn page_title() -> String { String::from("Stories") } fn render_content(&self) -> Markup { html!( @for story in self { (story.render_as_card()) } ) } fn prepare_content(&mut self) -> Result<()> { self.sort_by(|a, b| { let date1 = a.update_date.unwrap_or(a.upload_date); let date2 = b.update_date.unwrap_or(b.upload_date); date2.cmp(&date1) }); for story in self { std::fs::copy(&story.path, story.format_local_pdf_path())?; } Ok(()) } fn content_style() -> Markup { html!( """ h2 { font-size: 1.8rem; margin-bottom: 2rem; color: #2F3C57; } hr { margin: 1rem; } .story-card { text-align: left; font-size: 1.2rem; hyphens: auto; ul { display: flex; justify-content: start; list-style: none; flex-wrap: wrap; li { font-size: 1rem; padding: 0.8rem; margin: 0.8rem; background-color: #2F3C57; color: #ffd1a3; outline: 0.2rem solid #2F3C57; a { text-decoration: underline #E1CCEB; color: #E1CCEB; } } } hr { margin: 2rem 0; } padding: 3rem; margin: 3rem; background-color: #ffd1a3; border-bottom: 0.1rem dotted black; } """ ) } } #[derive(Deserialize, Debug)] enum StoryLanguage { German, English, } impl std::fmt::Display for StoryLanguage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } #[derive(Deserialize)] struct Story { name: String, description: String, language: StoryLanguage, upload_date: Date, update_date: Option, path: String, } impl Story { fn render_as_card(&self) -> Markup { html!(div class="story-card" lang=(self.html_lang_value()) { card { h2 { (self.name) } p { (self.description) } hr {} ul lang="en" { li { "Language: " (self.language) } li { "Uploaded on: " (self.upload_date) } @if let Some(update_date) = self.update_date { li { "Updated on: " (update_date) } } li { a href=(&self.format_pdf_path()) download { "PDF" } } } } }) } fn format_pdf_path(&self) -> String { let snake_case_name = self.name.to_lowercase().replace(" ", "_"); format!("stories/{}.pdf", snake_case_name) } fn format_local_pdf_path(&self) -> String { format!("dist/{}", self.format_pdf_path()) } fn html_lang_value(&self) -> String { match self.language { StoryLanguage::German => String::from("de"), StoryLanguage::English => String::from("en"), } } } fn main() -> Result<()> { let data_file_content = std::fs::read_to_string("data.toml").with_context(|| "Failed to read data.toml")?; let mut data: Data = toml::from_str(&data_file_content)?; data.stories.write_page()?; data.write_page()?; Ok(()) }