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)),
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); 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); 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)
}
}