use mdbook_rss_feed::{build_feed, FeedOptions};
use serde_json::Value;
use std::fs;
use std::io::{self, Read, Write};
use std::path::PathBuf;
fn handle_mdbook_hooks(args: &[String]) -> bool {
if args.get(1).map(String::as_str) == Some("--version")
|| args.get(1).map(String::as_str) == Some("-V")
{
println!("mdbook-rss-feed {}", env!("CARGO_PKG_VERSION"));
return true;
}
if args.get(1).map(String::as_str) == Some("supports") {
println!("true");
return true;
}
false
}
#[allow(clippy::struct_excessive_bools)]
struct FeedConfig {
src_dir: PathBuf,
site_url: String,
title: String,
description: String,
full_preview: bool,
paginated: bool,
max_items: usize,
#[cfg_attr(not(feature = "json-feed"), allow(dead_code))]
json_enabled: bool,
#[cfg_attr(not(feature = "atom"), allow(dead_code))]
atom_enabled: bool,
}
impl FeedConfig {
fn from_json(context: &Value) -> Self {
let root = context
.pointer("/root")
.and_then(|v| v.as_str())
.unwrap_or(".");
Self {
src_dir: PathBuf::from(root).join("src"),
site_url: context
.pointer("/config/output/html/site-url")
.and_then(|v| v.as_str())
.unwrap_or("https://example.com/")
.to_string(),
title: context
.pointer("/config/book/title")
.and_then(|v| v.as_str())
.unwrap_or("My mdBook")
.to_string(),
description: context
.pointer("/config/book/description")
.and_then(|v| v.as_str())
.unwrap_or("Description")
.to_string(),
full_preview: context
.pointer("/config/preprocessor/rss-feed/full-preview")
.and_then(Value::as_bool)
.unwrap_or(false),
paginated: context
.pointer("/config/preprocessor/rss-feed/paginated")
.and_then(Value::as_bool)
.unwrap_or(false),
max_items: context
.pointer("/config/preprocessor/rss-feed/max-items")
.and_then(Value::as_u64)
.map_or(0, |n| usize::try_from(n).unwrap_or(usize::MAX)),
json_enabled: context
.pointer("/config/preprocessor/rss-feed/json-feed")
.and_then(Value::as_bool)
.unwrap_or(false),
atom_enabled: context
.pointer("/config/preprocessor/rss-feed/atom")
.and_then(Value::as_bool)
.unwrap_or(false),
}
}
fn feed_options(&self) -> FeedOptions<'_> {
FeedOptions {
title: &self.title,
site_url: &self.site_url,
description: &self.description,
full_preview: self.full_preview,
max_items: self.max_items,
paginated: self.paginated,
}
}
}
fn write_rss_pages(config: &FeedConfig, pages: &[mdbook_rss_feed::FeedPage]) -> io::Result<()> {
for page in pages {
let rss_path = config.src_dir.join(&page.filename);
let rss_content = page.channel.to_string();
eprintln!(
"Writing RSS page {} ({} bytes)",
rss_path.display(),
rss_content.len()
);
fs::write(&rss_path, &rss_content)?;
}
Ok(())
}
#[cfg(feature = "json-feed")]
fn write_json_pages(
config: &FeedConfig,
pages: &[mdbook_rss_feed::FeedPage],
) -> Result<(), Box<dyn std::error::Error>> {
use mdbook_rss_feed::rss_to_json_feed;
if !config.json_enabled {
return Ok(());
}
for (page_idx, page) in pages.iter().enumerate() {
let suffix = if page_idx == 0 {
String::new()
} else {
(page_idx + 1).to_string()
};
let self_url = format!("{}/feed{}.json", config.site_url, suffix);
let json_feed = rss_to_json_feed(&page.channel, Some(&self_url), None);
let json_path = config.src_dir.join(if page_idx == 0 {
"feed.json".to_string()
} else {
format!("feed{}.json", page_idx + 1)
});
fs::write(&json_path, serde_json::to_vec_pretty(&json_feed)?)?;
}
Ok(())
}
#[cfg(not(feature = "json-feed"))]
fn write_json_pages(
_config: &FeedConfig,
_pages: &[mdbook_rss_feed::FeedPage],
) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[cfg(feature = "atom")]
fn write_atom_pages(config: &FeedConfig, pages: &[mdbook_rss_feed::FeedPage]) -> io::Result<()> {
use mdbook_rss_feed::rss_to_atom;
if !config.atom_enabled {
return Ok(());
}
for (page_idx, page) in pages.iter().enumerate() {
let atom_feed = rss_to_atom(&page.channel);
let atom_path = config.src_dir.join(if page_idx == 0 {
"atom.xml".to_string()
} else {
format!("atom{}.xml", page_idx + 1)
});
fs::write(&atom_path, atom_feed.to_string())?;
}
Ok(())
}
#[cfg(not(feature = "atom"))]
fn write_atom_pages(_config: &FeedConfig, _pages: &[mdbook_rss_feed::FeedPage]) -> io::Result<()> {
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
if handle_mdbook_hooks(&args) {
return Ok(());
}
let mut input = String::new();
io::stdin().read_to_string(&mut input)?;
let input_array: Vec<Value> = serde_json::from_str(&input)?;
let [_context, _book] = input_array.as_slice() else {
return Err("expected mdBook to send a [context, book] pair on stdin".into());
};
let config = FeedConfig::from_json(&input_array[0]);
let book = &input_array[1];
let result = build_feed(&config.src_dir, &config.feed_options())?;
write_rss_pages(&config, &result.pages)?;
write_json_pages(&config, &result.pages)?;
write_atom_pages(&config, &result.pages)?;
io::stderr().flush()?;
println!("{}", serde_json::to_string(book)?);
Ok(())
}