From 830f45423fe04046b5b201927729b17a5819f694 Mon Sep 17 00:00:00 2001 From: _ <_@_> Date: Thu, 11 Sep 2025 01:27:08 +0000 Subject: [PATCH] extract output module --- src/main.rs | 233 ++------------------------------------------------ src/output.rs | 222 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+), 224 deletions(-) create mode 100644 src/output.rs diff --git a/src/main.rs b/src/main.rs index 368500e..0189611 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,13 @@ use chrono::DateTime; use clap::Parser as _; -use std::{collections::BTreeSet, io::Write as _, time::Duration}; +use std::time::Duration; use prelude::*; #[cfg(test)] mod tests; +mod output; mod prelude; mod wac_campfire; mod wac_common_ninja; @@ -30,31 +31,12 @@ struct CalendarUi { short_name: String, } -#[derive(Deserialize)] -struct ConfigOutput { - /// Used as the OpenGraph description in meta tags - description: String, - - hide_summaries: BTreeSet, - - /// Hide all these UIDs from the final output - hide_uids: BTreeSet, - - /// Timezone to use for output (e.g. "Antarctica/South_Pole") - /// - /// - timezone: chrono_tz::Tz, - - /// Used as the page title and OpenGraph title in meta tags - title: String, -} - #[derive(Deserialize)] struct Config { campfires: Vec, common_ninjas: Vec, icals: Vec, - output: ConfigOutput, + output: output::Config, } impl Config { @@ -152,22 +134,6 @@ struct EventInstance { url: Option, } -impl EventInstance { - fn filter(&self, config_output: &ConfigOutput) -> bool { - if let Some(uid) = &self.uid - && config_output.hide_uids.contains(uid) - { - return false; - } - if let Some(summary) = &self.summary - && config_output.hide_summaries.contains(summary) - { - return false; - } - true - } -} - #[derive(Default)] struct Data { campfires: Vec, @@ -197,7 +163,7 @@ fn read_data_from_disk(config: &Config) -> Result { fn process_data<'a>( data: &'a Data, - config_output: &'a ConfigOutput, + config_output: &'a output::Config, now: DateTime, ) -> Result> { let params = Parameters::new(now)?; @@ -208,7 +174,7 @@ fn process_data<'a>( for ev in campfire .event_instances(¶ms)? .into_iter() - .filter(|x| x.filter(config_output)) + .filter(|x| config_output.filter(x)) { instances.push(ev); } @@ -218,7 +184,7 @@ fn process_data<'a>( for ev in common_ninja .event_instances(¶ms)? .into_iter() - .filter(|x| x.filter(config_output)) + .filter(|x| config_output.filter(x)) { instances.push(ev); } @@ -228,7 +194,7 @@ fn process_data<'a>( for ev in ical .event_instances(¶ms)? .into_iter() - .filter(|x| x.filter(config_output)) + .filter(|x| config_output.filter(x)) { instances.push(ev); } @@ -238,187 +204,6 @@ fn process_data<'a>( Ok(instances) } -fn output_html( - config: &ConfigOutput, - instances: &[EventInstance], - now: DateTime, -) -> Result<()> { - let today = now.date_naive(); - let mut last_month_printed: Option = None; - let mut last_date_printed = None; - let mut html_list = vec![]; - let mut day_list = vec![]; - for ei in instances { - let date = ei.dtstart.date_naive(); - let past = date < today; - let month = date.format("%B").to_string(); - match last_month_printed { - Some(ref x) if *x == month => {} - None | Some(_) => { - // FIXME: De-dupe - if !day_list.is_empty() { - html_list.push(maud::html! { - ul { @for entry in day_list { - (entry) - } } - }); - day_list = vec![]; - } - - html_list.push(maud::html! { - h2 { (month) } - }); - last_month_printed = Some(month); - } - } - if last_date_printed != Some(date) { - // FIXME: De-dupe - if !day_list.is_empty() { - html_list.push(maud::html! { - ul { @for entry in day_list { - (entry) - } } - }); - day_list = vec![]; - } - if past { - html_list.push(maud::html! { - p class="past"{ s { (date.format("%-d %A")) } } - }); - } else { - let date_s = date.format("%-d %A"); - let id = date.format("%F"); - html_list.push(maud::html! { - h3 id=(id) { (date_s) " " a href=(format!("#{id}")) title=(format!("Permalink to {date_s}")) {"🔗"} } - hr{} - }); - } - - last_date_printed = Some(date); - } - let time = ei.dtstart.time(); - let time = time - .map(|t| t.format("%l:%M %P").to_string()) - .unwrap_or_else(|| "No time listed".to_string()); - - let summary = ei - .summary - .as_deref() - .unwrap_or("Data error BXH45NAR - No summary in event"); - let summary = if let Some(url) = &ei.url { - maud::html! {a href=(url) {(summary)}} - } else { - maud::html! {(summary)} - }; - - if past { - day_list.push(maud::html! { - li class="past" { (time) " - " (summary) } - }); - } else { - let calendar_link = if let Some(html_url) = &ei.calendar_ui.html_url { - maud::html! { a href=(html_url) { (ei.calendar_ui.short_name) } } - } else { - maud::html! { (ei.calendar_ui.short_name)} - }; - - // This is where the main stuff happens - - tracing::debug!(uid = ei.uid, summary = ei.summary); - day_list.push(maud::html! { - li { details { - summary { (time) " - " (summary) } - ul { - li { (calendar_link) " calendar" } - @if let Some(location) = &ei.location { - li { "Location: " (location) } - } - } - } - } }); - } - } - // FIXME: De-dupe - if !day_list.is_empty() { - html_list.push(maud::html! { - ul { @for entry in day_list { - (entry) - } } - }); - } - - std::fs::create_dir_all("output")?; - { - let temp_path = "output/calendars.html.tmp"; - let final_path = "output/calendars.html"; - let mut f = std::fs::File::create(temp_path)?; - - f.write_all("".as_bytes())?; - std::fs::rename(temp_path, final_path)?; - } - - { - let temp_path = "output/index.html.tmp"; - let final_path = "output/index.html"; - let mut f = std::fs::File::create(temp_path)?; - let css = r#" - - "#; - - let description = &config.description; - let title = &config.title; - - let s = maud::html! { - (maud::PreEscaped("")) - html lang="en" { - head { - meta http-equiv="Content-Type" content="text/html; charset=utf-8" {} - meta name="viewport" content="width=device-width, initial-scale=1" {} - (maud::PreEscaped(css)) - - meta property="og:locale" content="en" {} - meta property="og:type" content="website" {} - - meta name="description" content=(description) {} - meta property="description" content=(description) {} - meta property="og:description" content=(description) {} - - title { (title) } - met property="og:title" content=(title) {} - } - body { - h1 { (title) } - img src="hero.webp" width="700" height="233" {} - p { "Written at: " (now.format("%F %T")) } - @for entry in html_list { - (entry) - } - } - } - } - .into_string(); - - f.write_all(s.as_bytes())?; - std::fs::rename(temp_path, final_path)?; - } - - Ok(()) -} - static APP_USER_AGENT: &str = concat!( env!("CARGO_PKG_NAME"), "_Z7FSRRA7/", @@ -453,7 +238,7 @@ async fn do_everything(cli: &CliAuto) -> Result<()> { let data = read_data_from_disk(&config)?; let instances = process_data(&data, &config.output, now)?; - output_html(&config.output, &instances, now)?; + output::write_html(&config.output, &instances, now)?; Ok(()) } @@ -491,7 +276,7 @@ fn main_debug_output(cli: CliDebugOutput) -> Result<()> { let tz = &config.output.timezone; let now = Utc::now().with_timezone(tz); let instances = process_data(&data, &config.output, now).context("Failed to process data")?; - output_html(&config.output, &instances, now).context("Failed to output HTML")?; + output::write_html(&config.output, &instances, now).context("Failed to output HTML")?; Ok(()) } diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..49498c0 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,222 @@ +//! The code that writes HTML for web browsers to read + +use chrono::DateTime; +use std::{collections::BTreeSet, io::Write as _}; + +use crate::{EventInstance, prelude::*}; + +#[derive(Deserialize)] +pub(crate) struct Config { + /// Used as the OpenGraph description in meta tags + description: String, + + hide_summaries: BTreeSet, + + /// Hide all these UIDs from the final output + hide_uids: BTreeSet, + + /// Timezone to use for output (e.g. "Antarctica/South_Pole") + /// + /// + pub(crate) timezone: chrono_tz::Tz, + + /// Used as the page title and OpenGraph title in meta tags + title: String, +} + +impl Config { + pub(crate) fn filter(&self, instance: &EventInstance) -> bool { + if let Some(uid) = &instance.uid + && self.hide_uids.contains(uid) + { + return false; + } + if let Some(summary) = &instance.summary + && self.hide_summaries.contains(summary) + { + return false; + } + true + } +} + +pub(crate) fn write_html( + config: &Config, + instances: &[EventInstance], + now: DateTime, +) -> Result<()> { + let today = now.date_naive(); + let mut last_month_printed: Option = None; + let mut last_date_printed = None; + let mut html_list = vec![]; + let mut day_list = vec![]; + for ei in instances { + let date = ei.dtstart.date_naive(); + let past = date < today; + let month = date.format("%B").to_string(); + match last_month_printed { + Some(ref x) if *x == month => {} + None | Some(_) => { + // FIXME: De-dupe + if !day_list.is_empty() { + html_list.push(maud::html! { + ul { @for entry in day_list { + (entry) + } } + }); + day_list = vec![]; + } + + html_list.push(maud::html! { + h2 { (month) } + }); + last_month_printed = Some(month); + } + } + if last_date_printed != Some(date) { + // FIXME: De-dupe + if !day_list.is_empty() { + html_list.push(maud::html! { + ul { @for entry in day_list { + (entry) + } } + }); + day_list = vec![]; + } + if past { + html_list.push(maud::html! { + p class="past"{ s { (date.format("%-d %A")) } } + }); + } else { + let date_s = date.format("%-d %A"); + let id = date.format("%F"); + html_list.push(maud::html! { + h3 id=(id) { (date_s) " " a href=(format!("#{id}")) title=(format!("Permalink to {date_s}")) {"🔗"} } + hr{} + }); + } + + last_date_printed = Some(date); + } + let time = ei.dtstart.time(); + let time = time + .map(|t| t.format("%l:%M %P").to_string()) + .unwrap_or_else(|| "No time listed".to_string()); + + let summary = ei + .summary + .as_deref() + .unwrap_or("Data error BXH45NAR - No summary in event"); + let summary = if let Some(url) = &ei.url { + maud::html! {a href=(url) {(summary)}} + } else { + maud::html! {(summary)} + }; + + if past { + day_list.push(maud::html! { + li class="past" { (time) " - " (summary) } + }); + } else { + let calendar_link = if let Some(html_url) = &ei.calendar_ui.html_url { + maud::html! { a href=(html_url) { (ei.calendar_ui.short_name) } } + } else { + maud::html! { (ei.calendar_ui.short_name)} + }; + + // This is where the main stuff happens + + tracing::debug!(uid = ei.uid, summary = ei.summary); + day_list.push(maud::html! { + li { details { + summary { (time) " - " (summary) } + ul { + li { (calendar_link) " calendar" } + @if let Some(location) = &ei.location { + li { "Location: " (location) } + } + } + } + } }); + } + } + // FIXME: De-dupe + if !day_list.is_empty() { + html_list.push(maud::html! { + ul { @for entry in day_list { + (entry) + } } + }); + } + + std::fs::create_dir_all("output")?; + { + let temp_path = "output/calendars.html.tmp"; + let final_path = "output/calendars.html"; + let mut f = std::fs::File::create(temp_path)?; + + f.write_all("".as_bytes())?; + std::fs::rename(temp_path, final_path)?; + } + + { + let temp_path = "output/index.html.tmp"; + let final_path = "output/index.html"; + let mut f = std::fs::File::create(temp_path)?; + let css = r#" + + "#; + + let description = &config.description; + let title = &config.title; + + let s = maud::html! { + (maud::PreEscaped("")) + html lang="en" { + head { + meta http-equiv="Content-Type" content="text/html; charset=utf-8" {} + meta name="viewport" content="width=device-width, initial-scale=1" {} + (maud::PreEscaped(css)) + + meta property="og:locale" content="en" {} + meta property="og:type" content="website" {} + + meta name="description" content=(description) {} + meta property="description" content=(description) {} + meta property="og:description" content=(description) {} + + title { (title) } + met property="og:title" content=(title) {} + } + body { + h1 { (title) } + img src="hero.webp" width="700" height="233" {} + p { "Written at: " (now.format("%F %T")) } + @for entry in html_list { + (entry) + } + } + } + } + .into_string(); + + f.write_all(s.as_bytes())?; + std::fs::rename(temp_path, final_path)?; + } + + Ok(()) +}