got something showable for Facebook feeds
This commit is contained in:
parent
b320605f26
commit
8bfd102f79
2 changed files with 296 additions and 11 deletions
10
src/main.rs
10
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(
|
||||
|
|
297
src/output.rs
297
src/output.rs
|
@ -118,8 +118,96 @@ fn calendars_page(upstreams: &[crate::CalendarUi], now: DateTime<chrono_tz::Tz>)
|
|||
.into_string()
|
||||
}
|
||||
|
||||
pub(crate) fn feed_page(_feed_items: &[crate::FeedItem], _now: DateTime<chrono_tz::Tz>) -> String {
|
||||
todo!()
|
||||
pub(crate) fn feed_page(feed_items: &[crate::FeedItem], now: DateTime<chrono_tz::Tz>) -> 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("<!DOCTYPE html>"))
|
||||
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<T> {
|
||||
pub(crate) month: Option<String>,
|
||||
pub(crate) date: NaiveDate,
|
||||
pub(crate) items: Vec<T>,
|
||||
}
|
||||
|
||||
// Debug only if T: Debug
|
||||
impl<T: fmt::Debug> fmt::Debug for DataChunk<T> {
|
||||
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<T: PartialEq> PartialEq for DataChunk<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.month == other.month && self.date == other.date && self.items == other.items
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum Output<T> {
|
||||
Data(DataChunk<T>),
|
||||
EndOfInput,
|
||||
NeedInput,
|
||||
}
|
||||
|
||||
// Debug only if T: Debug
|
||||
impl<T: fmt::Debug> fmt::Debug for Output<T> {
|
||||
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<T: PartialEq> PartialEq for Output<T> {
|
||||
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<T> {
|
||||
data: VecDeque<DataChunk<T>>,
|
||||
flushed: bool,
|
||||
last_month_printed: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> StateMachine<T> {
|
||||
pub(crate) fn pull(&mut self) -> Output<T> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue