breb 0.5.0

the blog/reblog library and command-line tool
Documentation
use std::{borrow::Cow, collections::HashMap, fmt, path::{self, Path, PathBuf}, slice};

use url::Url;

use crate::{
	friendly::{AdjacentInfo, Config, ConfigSection, FeedInfo, SectionList, ServeInfo, Translation}, raw, serve::{self, HasCssRules, HasNav},
};

#[derive(Debug)]
pub struct FriendlyToRawError {
	message: Cow<'static, str>,
	fields: Vec<Cow<'static, str>>,
}
impl fmt::Display for FriendlyToRawError {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		write!(f, "friendly config is invalid: {} ({})", self.message, self.fields.join(", "))
	}
}
impl std::error::Error for FriendlyToRawError {}

type ToRawResult<T> = Result<T, FriendlyToRawError>;

fn to_cow(fmt: fmt::Arguments) -> Cow<'static, str> {
	if let Some(s) = fmt.as_str() {
		s.into()
	} else {
		fmt.to_string().into()
	}
}
macro_rules! f2re {
	(
		$message:literal$(($($m_arg:expr),* $(,)?))?,
		$($field:literal$(($($f_arg:expr),* $(,)?))?),*
		$(,)?
	) => {
		FriendlyToRawError {
			message: to_cow(format_args!($message $($(, $m_arg)*)?)),
			fields: vec![$(
				to_cow(format_args!($field $($(, $f_arg)*)?))
			),*],
		}
	}
}

fn merge(prev: &HashMap<String, PathBuf>, cur: &HashMap<String, PathBuf>) -> HashMap<String, PathBuf> {
	prev.iter().chain(cur.iter()).map(|(b, t)| (b.clone(), t.clone())).collect()
}

macro_rules! any {
	(
		$first:expr $(, $opt:expr)*
		$(, else: $last:expr)?
		$(,)?
	) => {
		($first).as_ref()
			$(.or(($opt).as_ref()))*
			.cloned()
			$(.unwrap_or_else(|| $last))?
	}
}

impl AdjacentInfo {
	fn fill(&self, prev: &AdjacentInfo) -> AdjacentInfo {
		AdjacentInfo {
			metafree: self.metafree.or(prev.metafree),
			locale: any!(self.locale, prev.locale),
			template: any!(self.template, prev.template),
			bases: merge(&prev.bases, &self.bases),
			styles: any!(self.styles, prev.styles),
			nav: any!(self.nav, prev.nav),
		}
	}
}

impl ServeInfo {
	fn fill(&self, def: &AdjacentInfo) -> FilledServeInfo {
		let AdjacentInfo {
			metafree: d_metafree,
			locale: d_locale,
			template: d_template,
			bases: d_bases,
			styles: d_styles,
			nav: d_nav,
		} = def;
		match self {
			Self::AsIs { serve_at, from, metafree } => FilledServeInfo::AsIs {
				serve_at: serve_at.clone(),
				from: any!(from, else: deroot(serve_at)),
				metafree: metafree.or(*d_metafree).unwrap_or(false),
			},
			Self::Pages { serve_at, from, template, bases, styles, nav, locale } => FilledServeInfo::Pages {
				serve_at: serve_at.clone(),
				from: any!(from, else: deroot(serve_at)),
				template: any!(template, d_template),
				bases: merge(d_bases, bases),
				styles: any!(styles, d_styles, else: vec![]),
				nav: any!(nav, d_nav, else: vec![]),
				locale: any!(locale, d_locale),
			},
			Self::Posts { serve_at, from, template, bases, styles, nav, index, locale } => FilledServeInfo::Posts {
				serve_at: serve_at.clone(),
				from: any!(from, else: deroot(serve_at)),
				template: any!(template, d_template),
				bases: merge(d_bases, bases),
				styles: any!(styles, d_styles, else: vec![]),
				nav: any!(nav, d_nav, else: vec![]),
				locale: any!(locale, d_locale),
				index: any!(index, else: deroot(serve_at).to_string_lossy().into_owned()),
			},
			Self::LocalGitRepo { serve_at, source } => {
				FilledServeInfo::LocalGitRepo {
					serve_at: serve_at.clone(),
					source: any!(source, else: deroot(serve_at)),
				}
			}
			Self::RemoteGitRepo { serve_at, url } => {
				FilledServeInfo::RemoteGitRepo {
					serve_at: serve_at.clone(),
					url: url.clone(),
				}
			}
		}
	}
}

impl Translation {
	fn path_prefix(&self) -> &Path {
		self.path_prefix.as_ref().unwrap_or(&self.url_prefix)
	}
}

#[derive(Debug)]
pub enum FilledServeInfo {
	AsIs {
		serve_at: PathBuf,
		from: PathBuf,
		metafree: bool,
	},
	Pages {
		serve_at: PathBuf,
		from: PathBuf,
		template: Option<PathBuf>,
		bases: HashMap<String, PathBuf>,
		styles: Vec<String>,
		nav: Vec<HashMap<String, String>>,
		locale: Option<String>,
	},
	Posts {
		serve_at: PathBuf,
		from: PathBuf,
		template: Option<PathBuf>,
		bases: HashMap<String, PathBuf>,
		styles: Vec<String>,
		nav: Vec<HashMap<String, String>>,
		locale: Option<String>,
		index: String,
	},
	LocalGitRepo {
		serve_at: PathBuf,
		source: PathBuf,
	},
	RemoteGitRepo {
		serve_at: PathBuf,
		url: Url,
	},
}

impl FilledServeInfo {
	fn translate(&self, lang: &Translation) -> Self {
		match self {
			Self::AsIs { serve_at, from, metafree } => {
				let serve_at = lang.url_prefix.join(deroot(serve_at));
				let lang_prefix = lang.path_prefix.as_ref().unwrap_or(&lang.url_prefix);
				let from = lang_prefix.join(from);
				Self::AsIs { serve_at, from, metafree: *metafree }
			}
			Self::Pages { serve_at, from, template, bases, styles, nav, locale } => {
				let serve_at = lang.url_prefix.join(deroot(serve_at));
				let lang_prefix = lang.path_prefix.as_ref().unwrap_or(&lang.url_prefix);
				let from = lang_prefix.join(from);
				Self::Pages {
					serve_at,
					from,
					template: template.clone(),
					bases: bases.clone(),
					styles: styles.clone(),
					nav: nav.clone(),
					locale: Some(locale.clone().unwrap_or(lang.locale.clone())),
				}
			}
			Self::Posts { serve_at, from, template, bases, styles, nav, index, locale } => {
				let serve_at = lang.url_prefix.join(deroot(serve_at));
				let lang_prefix = lang.path_prefix.as_ref().unwrap_or(&lang.url_prefix);
				let from = lang_prefix.join(from);
				Self::Posts {
					serve_at,
					from,
					template: template.clone(),
					bases: bases.clone(),
					styles: styles.clone(),
					nav: nav.clone(),
					locale: Some(locale.clone().unwrap_or(lang.locale.clone())),
					index: format!("{}/{}", lang.locale, index),
				}
			}
			Self::LocalGitRepo { serve_at, source } => {
				Self::LocalGitRepo {
					serve_at: lang.url_prefix.join(deroot(serve_at)),
					// NOT derooting `source`: we want to allow outside paths!
					source: lang.path_prefix().join(source),
				}
			}
			Self::RemoteGitRepo { serve_at, url } => {
				Self::RemoteGitRepo {
					serve_at: lang.url_prefix.join(deroot(serve_at)),
					url: url.clone(),
				}
			}
		}
	}
}

impl FeedInfo {
	fn translate(&self, lang: &Translation) -> Self {
		match self {
			FeedInfo::Atom { serve_at, index, length, link } => {
				FeedInfo::Atom {
					serve_at: lang.url_prefix.join(deroot(serve_at)),
					index: format!("{}/{}", lang.locale, index),
					length: *length,
					link: link.clone(),
				}
			}
		}
	}
}

impl ConfigSection {
	fn infos(&self, prev: &AdjacentInfo) -> (Vec<FilledServeInfo>, Vec<FeedInfo>) {
		match self {
			Self::Normal(base) => {
				let adj = base.serves.adjacent.fill(prev);
				let serves = base.serves.serves
					.iter()
					.map(|s| s.fill(&adj))
					.collect();
				(serves, base.feeds.clone())
			}
			Self::Translated { base, languages } => {
				let mut serves = Vec::with_capacity(base.serves.serves.len() * languages.len());
				for language in languages {
					let mut lang_adj = language.adjacent.clone();
					lang_adj.locale = Some(language.locale.clone());
					let adj = base.serves.adjacent.fill(&lang_adj).fill(prev);
					let base_serves = base.serves.serves
						.iter()
						.map(|s| s.fill(&adj));
					for serve in base_serves {
						serves.push(serve.translate(language));
					}
				}
				let mut feeds = Vec::with_capacity(base.feeds.len() * languages.len());
				for language in languages {
					for feed in &base.feeds {
						feeds.push(feed.translate(language));
					}
				}
				(serves, feeds)
			}
		}
	}
}

fn add_rules<H: HasCssRules>(mut serve: H, section: usize, styles: Vec<String>) -> Result<H, FriendlyToRawError> {
	for style in styles {
		let parsed = style.parse()
			.map_err(|e| f2re! {
				"invalid style rule: {}" (e),
				".sections[{}]" (section),
			})?;
		serve = serve.add_rule(parsed);
	}
	Ok(serve)
}

fn add_navs<H: HasNav>(mut serve: H, section: usize, navs: Vec<HashMap<String, String>>) -> Result<H, FriendlyToRawError> {
	for nav in navs {
		if nav.len() != 1 {
			return Err(f2re! {
				"expected exactly 1 'label: url' pair",
				".sections[{}]" (section),
			});
		}
		for (name, url) in nav {
			serve = serve.nav(name, url);
		}
	}
	Ok(serve)
}

fn deroot(orig: &Path) -> PathBuf {
	orig
		.components()
		.filter(|c| matches!(c, path::Component::CurDir | path::Component::ParentDir | path::Component::Normal(_)))
		.collect()
}

impl TryFrom<Config> for raw::Config {
	type Error = FriendlyToRawError;
	fn try_from(friendly: Config) -> ToRawResult<Self> {
		let mut res = raw::Config::default()
			.name(friendly.blog.name)
			.authors(friendly.authors)
			.url(friendly.blog.url.clone());

		let (slice, top_adj) = match &friendly.sections {
			SectionList::One(sec) => (slice::from_ref(&**sec), &Default::default()),
			SectionList::Many { sections, adjacent } => (&**sections, adjacent),
		};

		for (i, section) in slice.iter().enumerate() {
			let (infos, extra_feeds) = section.infos(top_adj);
			for feed in extra_feeds {
				res = res.feed(match feed {
					FeedInfo::Atom { serve_at, index, length, link } => {
						let mut feed = serve::Feed::atom(serve_at);
						feed = feed.index(index);
						if let Some(len) = length {
							feed = feed.len(len);
						}
						if let Some(link) = link {
							feed = feed.link(link);
						}
						feed
					}
				});
			}
			for info in infos {
				match info {
					FilledServeInfo::AsIs { serve_at, from, metafree } => {
						let mut serve = serve::AsIs::new(serve_at, from);
						if metafree {
							serve = serve.metafree();
						}
						res = res.serve(serve);
					}
					FilledServeInfo::Pages { serve_at, from, template, bases, styles, nav, locale } => {
						let mut serve = serve::Pages::new(serve_at, from);
						let _ = (template, bases); // TODO: push templates lower
						if let Some(locale) = locale {
							serve = serve.locale(locale);
						}
						serve = add_rules(serve, i, styles)?;
						serve = add_navs(serve, i, nav)?;
						res = res.serve(serve);
					}
					FilledServeInfo::Posts { serve_at, from, template, bases, styles, nav, index, locale } => {
						let mut feed = serve::Feed::atom(serve_at.join("feed.xml"));
						let mut serve = serve::Posts::new(serve_at, from);
						let _ = (template, bases); // TODO: push templates lower
						if let Some(locale) = locale {
							serve = serve.locale(locale);
						}
						serve = add_rules(serve, i, styles)?;
						serve = add_navs(serve, i, nav)?;
						feed = feed.index(&index);
						serve = serve.index(&index);
						res = res.serve(serve).feed(feed);
					}
					FilledServeInfo::LocalGitRepo { serve_at, source } => {
						let serve = serve::GitRepo::local(serve_at, &source)
							.map_err(|e| f2re! {
								"unable to setup local repo: {}" (e),
								".sections[{}]" (i),
							})?;
						res = res.serve(serve);
					}
					FilledServeInfo::RemoteGitRepo { serve_at, url } => {
						let serve = serve::GitRepo::remote(serve_at, url)
							.map_err(|e| f2re! {
								"unable to setup remote repo: {}" (e),
								".sections[{}]" (i),
							})?;
						res = res.serve(serve);
					}
				}
			}
		}

		Ok(res)
	}
}