From 8bfd102f79277f4783c9cbd9a83bd39d5ce73eb5 Mon Sep 17 00:00:00 2001 From: _ <_@_> Date: Thu, 11 Sep 2025 05:26:28 +0000 Subject: [PATCH] got something showable for Facebook feeds --- src/main.rs | 10 +- src/output.rs | 297 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 296 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index 12688de..0719bac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -340,15 +340,7 @@ fn main_debug_rss(cli: CliDebugRss) -> Result<()> { } } - items.sort_by_key(|item| item.date); - - for item in items.iter().rev() { - println!("{}", item.channel_title); - println!("{}", item.inner.title.as_ref().unwrap()); - println!("{}", item.date.to_rfc3339()); - println!("{}", item.inner.link.as_ref().unwrap()); - println!(); - } + items.sort_by_key(|item| std::cmp::Reverse(item.date)); std::fs::create_dir_all("output")?; output::atomic_write( diff --git a/src/output.rs b/src/output.rs index 61fb772..6608b4e 100644 --- a/src/output.rs +++ b/src/output.rs @@ -118,8 +118,96 @@ fn calendars_page(upstreams: &[crate::CalendarUi], now: DateTime) .into_string() } -pub(crate) fn feed_page(_feed_items: &[crate::FeedItem], _now: DateTime) -> String { - todo!() +pub(crate) fn feed_page(feed_items: &[crate::FeedItem], now: DateTime) -> String { + let mut feed_items = feed_items.iter(); + let mut machine = date_machine::StateMachine::default(); + let mut html_list = vec![]; + + loop { + match machine.pull() { + date_machine::Output::Data(date_machine::DataChunk { month, date, items }) => { + if let Some(month) = month { + html_list.push(maud::html! { h2 { (month) } }); + } + html_list.push(maud::html! { + (day_link(date)) + hr{} + ul { + @for item in items { + li { + (item) + } + } + } + }); + } + date_machine::Output::EndOfInput => break, + date_machine::Output::NeedInput => {} + } + + if let Some(item) = feed_items.next() { + let desc = item + .inner + .description + .as_ref() + .map(|desc| match desc.get(0..100) { + None => desc.to_string(), + Some(short_desc) => format!("{short_desc} ..."), + }); + + machine.push( + item.date.date_naive(), + maud::html! { + @if let Some(link) = &item.inner.link { + a href = (link) { (item.inner.title.as_deref().unwrap_or_default()) } + } @else { + (item.inner.title.as_deref().unwrap_or_default()) + } + ul { + @if let Some(desc) = desc { + li { (desc) } + } + li { (item.channel_title) } + } + }, + ); + } else { + machine.flush(); + } + } + + let description = "A feed merged from several input feeds"; + let title = "Feed"; + + 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) } + p { "Written at: " (now.format("%F %T")) } + p { a href = "calendars.html" { "Upstream calendars" } } + @for entry in html_list { + (entry) + } + } + } + } + .into_string() } fn index_page( @@ -280,3 +368,208 @@ pub(crate) fn write_html( Ok(()) } + +/// Takes in a stream of items with dates, and formats them out categorized by month and date. +mod date_machine { + use chrono::NaiveDate; + use std::{cmp::PartialEq, collections::VecDeque, fmt}; + + pub(crate) struct DataChunk { + pub(crate) month: Option, + pub(crate) date: NaiveDate, + pub(crate) items: Vec, + } + + // Debug only if T: Debug + impl fmt::Debug for DataChunk { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DataChunk") + .field("month", &self.month) + .field("date", &self.date) + .field("items", &self.items) + .finish() + } + } + + // PartialEq only if T: PartialEq + impl PartialEq for DataChunk { + fn eq(&self, other: &Self) -> bool { + self.month == other.month && self.date == other.date && self.items == other.items + } + } + + pub(crate) enum Output { + Data(DataChunk), + EndOfInput, + NeedInput, + } + + // Debug only if T: Debug + impl fmt::Debug for Output { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Output::Data(chunk) => f.debug_tuple("Data").field(chunk).finish(), + Output::EndOfInput => f.write_str("EndOfInput"), + Output::NeedInput => f.write_str("NeedInput"), + } + } + } + + // PartialEq only if T: PartialEq + impl PartialEq for Output { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Output::Data(a), Output::Data(b)) => a == b, + (Output::EndOfInput, Output::EndOfInput) => true, + (Output::NeedInput, Output::NeedInput) => true, + _ => false, + } + } + } + + #[derive(Default)] + pub(crate) struct StateMachine { + data: VecDeque>, + flushed: bool, + last_month_printed: Option, + } + + impl StateMachine { + pub(crate) fn pull(&mut self) -> Output { + if self.data.len() >= 2 + && let Some(chunk) = self.data.pop_front() + { + if chunk.month.is_some() && self.last_month_printed != chunk.month { + self.last_month_printed = chunk.month.clone(); + } + return Output::Data(chunk); + } + if self.flushed { + if let Some(chunk) = self.data.pop_front() { + return Output::Data(chunk); + } + return Output::EndOfInput; + } + Output::NeedInput + } + + pub(crate) fn push(&mut self, date: NaiveDate, item: T) { + let month = Some(date.format("%B").to_string()); + let month = if month == self.last_month_printed { + None + } else { + month + }; + if let Some(chunk) = self.data.front_mut() { + if chunk.date == date { + chunk.items.push(item); + } else if chunk.month == month { + self.data.push_back(DataChunk { + month: None, + date, + items: vec![item], + }); + } else { + self.data.push_back(DataChunk { + month, + date, + items: vec![item], + }); + } + } else { + self.data.push_back(DataChunk { + month, + date, + items: vec![item], + }); + } + } + + pub(crate) fn flush(&mut self) { + self.flushed = true; + } + } +} + +#[cfg(test)] +mod tests { + use chrono::NaiveDate; + + #[test] + fn date_state_machine() { + use super::date_machine::*; + + let mut machine: StateMachine<&str> = StateMachine::default(); + let mut items = vec![ + (2025, 09, 10, "alpha"), + (2025, 09, 10, "bravo"), + (2025, 09, 11, "charlie"), + (2025, 09, 12, "delta"), + (2025, 09, 13, "echo"), + (2025, 10, 1, "foxtrot"), + (2025, 10, 2, "golf"), + (2025, 10, 3, "hotel"), + ] + .into_iter(); + let mut output = vec![]; + + loop { + match machine.pull() { + Output::Data(chunk) => { + output.push(chunk); + continue; + } + Output::EndOfInput => break, + Output::NeedInput => {} + } + + if let Some((y, m, d, item)) = items.next() { + let date = NaiveDate::from_ymd_opt(y, m, d).unwrap(); + machine.push(date, item); + } else { + machine.flush(); + } + } + + let expected = vec![ + DataChunk { + month: Some("September".to_string()), + date: NaiveDate::from_ymd_opt(2025, 09, 10).unwrap(), + items: vec!["alpha", "bravo"], + }, + DataChunk { + month: None, + date: NaiveDate::from_ymd_opt(2025, 09, 11).unwrap(), + items: vec!["charlie"], + }, + DataChunk { + month: None, + date: NaiveDate::from_ymd_opt(2025, 09, 12).unwrap(), + items: vec!["delta"], + }, + DataChunk { + month: None, + date: NaiveDate::from_ymd_opt(2025, 09, 13).unwrap(), + items: vec!["echo"], + }, + DataChunk { + month: Some("October".to_string()), + date: NaiveDate::from_ymd_opt(2025, 10, 1).unwrap(), + items: vec!["foxtrot"], + }, + DataChunk { + month: None, + date: NaiveDate::from_ymd_opt(2025, 10, 2).unwrap(), + items: vec!["golf"], + }, + DataChunk { + month: None, + date: NaiveDate::from_ymd_opt(2025, 10, 3).unwrap(), + items: vec!["hotel"], + }, + ]; + assert_eq!(output, expected, "{output:#?} != {expected:#?}"); + + assert_eq!("abc".get(0..100).unwrap_or("abc"), "abc"); + } +}