use std::path::Path;
use rss::{Channel, ChannelBuilder, Guid, Item, ItemBuilder};
use crate::article::collect_articles;
use crate::error::Result;
use crate::preview::render_preview;
pub struct FeedPage {
pub filename: String,
pub channel: Channel,
}
pub struct BuildResult {
pub pages: Vec<FeedPage>,
}
#[derive(Debug, Clone)]
pub struct FeedOptions<'a> {
pub title: &'a str,
pub site_url: &'a str,
pub description: &'a str,
pub full_preview: bool,
pub max_items: usize,
pub paginated: bool,
}
fn article_link(base_url: &str, article_path: &str) -> String {
let html_path = article_path
.replace('\\', "/")
.replace(".md", ".html")
.replace("/README.html", "/index.html");
format!("{base_url}/{html_path}")
}
fn build_channel(title: &str, base_url: &str, description: &str, items: &[Item]) -> Channel {
ChannelBuilder::default()
.title(title)
.link(format!("{base_url}/"))
.description(description)
.items(items.to_vec())
.generator(Some(format!(
"mdbook-rss-feed {}",
env!("CARGO_PKG_VERSION")
)))
.build()
}
fn paginate(items: &[Item], opts: &FeedOptions<'_>, base_url: &str) -> Vec<FeedPage> {
let mut pages = Vec::new();
let should_paginate = opts.paginated && opts.max_items > 0 && items.len() > opts.max_items;
if !should_paginate {
let channel = build_channel(opts.title, base_url, opts.description, items);
pages.push(FeedPage {
filename: "rss.xml".to_string(),
channel,
});
return pages;
}
let total_pages = items.len().div_ceil(opts.max_items);
for page_idx in 0..total_pages {
let start = page_idx * opts.max_items;
let end = (start + opts.max_items).min(items.len());
let slice = &items[start..end];
let filename = if page_idx == 0 {
"rss.xml".to_string()
} else {
format!("rss{}.xml", page_idx + 1)
};
let channel = build_channel(opts.title, base_url, opts.description, slice);
pages.push(FeedPage { filename, channel });
}
pages
}
pub fn build_feed(src_dir: &Path, opts: &FeedOptions<'_>) -> Result<BuildResult> {
let articles = collect_articles(src_dir)?;
let base_url = opts.site_url.trim_end_matches('/');
let items: Vec<Item> = articles
.into_iter()
.map(|article| {
let link = article_link(base_url, &article.path);
let preview = render_preview(
&article.content,
article.fm.description.as_deref(),
opts.full_preview,
);
let mut item = ItemBuilder::default();
item.title(Some(article.fm.title.clone()));
item.link(Some(link.clone()));
item.description(Some(preview));
item.guid(Some(Guid {
value: link,
permalink: true,
}));
if let Some(date) = article.fm.date {
item.pub_date(Some(date.to_rfc2822()));
}
if let Some(author) = article.fm.author {
item.author(Some(author));
}
item.build()
})
.collect();
Ok(BuildResult {
pages: paginate(&items, opts, base_url),
})
}