use crate::parser::Document;
use crate::site::SiteConfig;
use chrono::{DateTime, Utc};
use std::fs::File;
use std::io::Write;
use std::path::Path;
pub fn generate_feed(
documents: &[Document],
site_config: &SiteConfig,
output_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut posts: Vec<&Document> = documents
.iter()
.filter(|doc| is_post_for_feed(doc))
.collect();
posts.sort_by(|a, b| {
b.front_matter
.date
.unwrap_or(DateTime::<Utc>::MIN_UTC)
.cmp(&a.front_matter.date.unwrap_or(DateTime::<Utc>::MIN_UTC))
});
posts.truncate(20);
let feed_content = generate_atom_feed(&posts, site_config)?;
let feed_path = output_dir.join("feed.xml");
let mut file = File::create(&feed_path)?;
file.write_all(feed_content.as_bytes())?;
Ok(())
}
fn generate_atom_feed(
posts: &[&Document],
site_config: &SiteConfig,
) -> Result<String, Box<dyn std::error::Error>> {
let mut feed = String::new();
feed.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
feed.push_str("<feed xmlns=\"http://www.w3.org/2005/Atom\"");
if let Some(ref base_url) = site_config.base_url {
feed.push_str(&format!(" xml:base=\"{}\"", escape_xml_url(base_url)));
}
feed.push_str(">\n");
feed.push_str(&format!(
" <title>{}</title>\n",
escape_xml(&site_config.get_site_title())
));
if let Some(ref base_url) = site_config.base_url {
feed.push_str(&format!(
" <link href=\"{}/feed.xml\" rel=\"self\" />\n",
escape_xml_url(base_url)
));
feed.push_str(&format!(
" <link href=\"{}\" />\n",
escape_xml_url(base_url)
));
feed.push_str(&format!(" <id>{}</id>\n", escape_xml_url(base_url)));
}
let updated = posts
.first()
.and_then(|post| post.front_matter.date)
.unwrap_or_else(Utc::now);
feed.push_str(&format!(" <updated>{}</updated>\n", updated.to_rfc3339()));
feed.push_str(" <generator uri=\"https://github.com/mcaserta/krik\">Krik</generator>\n");
for post in posts {
feed.push_str(&generate_feed_entry(post, site_config)?);
}
feed.push_str("</feed>\n");
Ok(feed)
}
fn generate_feed_entry(
post: &Document,
site_config: &SiteConfig,
) -> Result<String, Box<dyn std::error::Error>> {
let mut entry = String::new();
entry.push_str(" <entry>\n");
if let Some(ref title) = post.front_matter.title {
entry.push_str(&format!(" <title>{}</title>\n", escape_xml(title)));
}
let post_url = generate_post_url(post, site_config);
entry.push_str(&format!(
" <link href=\"{}\" />\n",
escape_xml_url(&post_url)
));
entry.push_str(&format!(" <id>{}</id>\n", escape_xml_url(&post_url)));
if let Some(date) = post.front_matter.date {
entry.push_str(&format!(" <updated>{}</updated>\n", date.to_rfc3339()));
entry.push_str(&format!(
" <published>{}</published>\n",
date.to_rfc3339()
));
}
entry.push_str(" <content type=\"html\"><![CDATA[\n");
entry.push_str(&post.content);
entry.push_str("\n ]]></content>\n");
if let Some(ref tags) = post.front_matter.tags {
for tag in tags {
entry.push_str(&format!(" <category term=\"{}\" />\n", escape_xml(tag)));
}
}
entry.push_str(" </entry>\n");
Ok(entry)
}
fn generate_post_url(post: &Document, site_config: &SiteConfig) -> String {
let mut path = std::path::PathBuf::from(&post.file_path);
path.set_extension("html");
if let Some(ref base_url) = site_config.base_url {
format!(
"{}/{}",
base_url.trim_end_matches('/'),
path.to_string_lossy()
)
} else {
path.to_string_lossy().to_string()
}
}
fn is_post_for_feed(document: &Document) -> bool {
(document
.front_matter
.extra
.get("layout")
.and_then(|v| v.as_str())
== Some("post")
|| document.file_path.starts_with("posts/"))
&& document.language == "en"
}
fn escape_xml(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn escape_xml_url(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}