use crate::feed_store::FeedStore; use crate::to_checked_pathbuf; use crate::Config; use anyhow::Result; use camino::Utf8Path; use feed_rs::model::Feed; use std::collections::{BTreeMap, HashMap}; use std::fs::{copy, create_dir_all, File}; use tera::{from_value, Tera}; use url::Url; pub fn build(config: &Config, feed_store: &mut FeedStore) -> Result<()> { let mut tera = if let Some(theme) = &config.theme { create_tera(&config.templates_dir.join(theme))? } else { create_tera(&config.templates_dir)? }; let out_dir = to_checked_pathbuf(&config.out_dir); let mut context = tera::Context::new(); let (feeds, entries): (HashMap, _) = feed_store.collect(config.max_entries); context.insert("config", config); context.insert("feeds", &feeds); context.insert("entries", &entries); context.insert("lang", &config.lang); context.insert("PKG_AUTHORS", env!("CARGO_PKG_AUTHORS")); context.insert("PKG_HOMEPAGE", env!("CARGO_PKG_HOMEPAGE")); context.insert("PKG_NAME", env!("CARGO_PKG_NAME")); context.insert("PKG_VERSION", env!("CARGO_PKG_VERSION")); tera.register_function("get_author", GetAuthorFunction { feeds }); tera.register_function("get_feed_config", GetFeedConfigFunction { feeds: config.feeds.iter().map(|feed| (feed.url.clone(), feed.clone())).collect() }); for name in tera.get_template_names() { debug!("Processing template {name}"); let file = File::create(format!("{out_dir}/{name}"))?; tera.render_to(name, &context, file)?; } // Copy static assets from theme, if any if let Some(theme) = &config.theme { let assets_dir = config.templates_dir.join(theme).join("assets"); if assets_dir.is_dir() { copy_assets(&assets_dir, &out_dir)?; } } Ok(()) } /// Recursively copy assets from one dir to another /// /// Symlinks are ignored. fn copy_assets(orig: &Utf8Path, dest: &Utf8Path) -> Result<()> { if orig.is_dir() { if !dest.is_dir() { create_dir_all(dest)?; } for entry in orig.read_dir_utf8()? { let entry = entry?; copy_assets(entry.path(), &dest.join(entry.file_name()))?; } } else if orig.is_file() { copy(orig, dest)?; } Ok(()) } fn create_tera(templates_dir: &Utf8Path) -> Result { let dir = to_checked_pathbuf(templates_dir); let mut tera = tera::Tera::new(&format!("{dir}/*"))?; // disable autoescape as this would corrupt urls or the entriy contents. todo check this! tera.autoescape_on(vec![]); Ok(tera) } struct GetFeedConfigFunction { feeds: BTreeMap, } impl tera::Function for GetFeedConfigFunction { fn call(&self, args: &HashMap) -> Result { let feed: Url = match args.get("feed") { None => { return Err(tera::Error::msg( "No argument of name 'feed' given to function.", )) } Some(val) => from_value(val.clone())?, }; if let Some(feed_config) = self.feeds.get(&feed) { Ok(tera::to_value(feed_config)?) } else { Err(tera::Error::msg( format!("Invalid feed URL: {}", feed), )) } } } struct GetAuthorFunction { feeds: HashMap, } impl tera::Function for GetAuthorFunction { fn call(&self, args: &HashMap) -> Result { let entry_val: tera::Map<_, _> = match args.get("entry") { None => { return Err(tera::Error::msg( "No argument of name 'entry' given to function.", )) } Some(val) => from_value(val.clone())?, }; let feed_url: String = from_value(entry_val.get("source").unwrap().clone())?; let authors_val: Vec> = from_value(entry_val.get("authors").unwrap().clone())?; let mut authors: Vec = Vec::new(); for author_val in authors_val { let name: String = from_value(author_val.get("name").unwrap().clone())?; if is_valid_name(&name) { authors.push(name.clone()); } } if authors.is_empty() { authors.append(&mut self.find_authors_from_feed(&feed_url)); } Ok(tera::Value::String(authors.join(", "))) } } impl GetAuthorFunction { fn find_authors_from_feed(&self, feed_url: &str) -> Vec { let feed = self.feeds.get(feed_url).unwrap(); feed.authors .clone() .into_iter() .map(|x| x.name) .filter(is_valid_name) .collect() } } fn is_valid_name(n: &String) -> bool { !n.is_empty() && n != "unknown" && n != "author" }