breb 0.5.0

the blog/reblog library and command-line tool
Documentation
//! defining the structure of a blog in a direct, very predictable way
//!
//! this is the intended programmatic api --
//! if you're using blog/reblog as a library, you should use this.
//! on the other hand, if you want to parse a `breb.yml` config file,
//! you probably want to use [`crate::friendly`] instead.
//!
//! it is, however, used in any config file parsing that needs doing.

use std::{
	borrow::Cow,
	fs, io,
	path::{Path, PathBuf},
	sync::Arc,
};

use crate::{
	Blog,
	serve::{Feed, Serve},
};

/// granularly defines the structure of a blog
///
/// you need to provide at least:
/// - the `name` and `url` of the blog
/// - one author, one serve, and one feed
///
/// you can give more than one of each of the latter, of course! but that's the minimum.
///
/// this deliberately doesn't try to offer any way to "dry" code using it --
/// it's meant to be an extremely direct, 1:1, low-level representation of the blog you want built.
// /// if you want a higher-level config, use [`crate::FriendlyConfig`].
#[derive(Clone, Debug, Default)]
pub struct Config {
	/// the name of the blog
	pub name: String,
	/// the url where the blog will live
	pub url: String,
	/// templates to render html with
	pub template_srcs: Vec<TemplateSrc>,
	/// the authors who write posts on the blog
	///
	/// the first element is the default author if one isn't specified for a post.
	/// posts which specify authors should refer to them by their name.
	pub authors: Vec<Author>,
	/// serving the files in this blog
	pub serves: Vec<Arc<dyn Serve>>,
	/// post syndication feeds for automated integration
	pub feeds: Vec<Feed>,
}

impl Config {
	/// set the name of the blog
	pub fn name(mut self, name: impl Into<String>) -> Self {
		self.name = name.into();
		self
	}

	/// set the url the blog is served at
	pub fn url(mut self, url: impl Into<String>) -> Self {
		self.url = url.into();
		self
	}

	/// add a template from [a string source](TemplateSrc::InMem)
	pub fn template_str(
		mut self,
		name: impl Into<Cow<'static, str>>,
		body: impl Into<Cow<'static, str>>,
	) -> Self {
		self.template_srcs.push(TemplateSrc::InMem {
			name: name.into(),
			body: body.into(),
		});
		self
	}
	/// add a template from [a file source](TemplateSrc::Stored)
	pub fn template_file(mut self, path: impl AsRef<Path>) -> Self {
		self.template_srcs.push(TemplateSrc::Stored {
			name: None,
			path: path.as_ref().to_path_buf(),
		});
		self
	}
	/// add a template from [a file source](TemplateSrc::Stored) with a name
	pub fn template_file_named(
		mut self,
		name: impl Into<Cow<'static, str>>,
		path: impl AsRef<Path>,
	) -> Self {
		self.template_srcs.push(TemplateSrc::Stored {
			name: Some(name.into()),
			path: path.as_ref().to_path_buf(),
		});
		self
	}

	/// add an author to the blog
	///
	/// there's not currently a way to set which author wrote which files --
	/// all authors share credit (and blame) for everything.
	pub fn author(mut self, author: Author) -> Self {
		self.authors.push(author);
		self
	}
	/// add multiple [`Self::author`]s at once
	pub fn authors(mut self, authors: impl IntoIterator<Item = Author>) -> Self {
		self.authors.extend(authors);
		self
	}

	/// add a serve to the blog, handling some subpath
	pub fn serve(mut self, serve: impl Serve + 'static) -> Self {
		self.serves.push(Arc::new(serve));
		self
	}
	/// add multiple [`Self::serve`]s at once
	pub fn serves(mut self, serves: impl IntoIterator<Item = impl Serve + 'static>) -> Self {
		// TODO: why doesn't extend(map) work for this....
		for serve in serves {
			self.serves.push(Arc::new(serve));
		}
		self
	}

	/// add a machine-readable post feed to the blog
	pub fn feed(mut self, feed: Feed) -> Self {
		self.feeds.push(feed);
		self
	}

	/// build a blog from the config
	///
	/// (this is actually just a wrapper around [`Blog::load`], go see its docs)
	pub fn build(self, dir: &Path) -> io::Result<Blog> {
		Blog::load(self, dir)
	}
}

/// info for the atom feed about the blog's author(s).
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Author {
	/// human-friendly name for this author -- not necessarily their legal identity.
	pub name: String,
	/// url of the author's homepage, if they have one they want to link.
	#[serde(default, skip_serializing_if = "Option::is_none")]
	pub website: Option<String>,
	/// email address to contact the author, if they have one they want to link.
	#[serde(default, skip_serializing_if = "Option::is_none")]
	pub email: Option<String>,
}

impl Author {
	/// create an author.
	pub fn new(name: impl Into<String>) -> Self {
		Self {
			name: name.into(),
			..Default::default()
		}
	}

	/// set author's public-facing webpage.
	pub fn uri(mut self, uri: impl Into<String>) -> Self {
		self.website = Some(uri.into());
		self
	}

	/// set author's public-facing email address.
	pub fn email(mut self, email: impl Into<String>) -> Self {
		self.email = Some(email.into());
		self
	}
}

/// tells the blog where to load its templates [when it loads](crate::Blog::load)
///
/// some builtin functionality depends on specifically named templates:
/// - [`Pages`](crate::serve::Pages) requires `"page"`
/// - [`Posts`](crate::serve::Posts) requires `"post"`
/// - **the default templates** for both require `"base"`
///
/// non-builtin serves should document which templates, if any, they depend on.
///
/// by default,
#[derive(Clone, Debug)]
pub enum TemplateSrc {
	/// install a template from memory
	InMem {
		/// name of the template, used verbatim
		name: Cow<'static, str>,
		/// body of the template
		body: Cow<'static, str>,
	},
	/// install a template from storage
	Stored {
		/// name of the template, used verbatim if provided
		///
		/// if not, the template's name will be the filename,
		/// with any (one) of these extensions stripped:
		/// - `.t.html`
		/// - `.jinja.html`
		/// - `.html`
		/// - `.html.jinja`
		/// - `.jinja`
		name: Option<Cow<'static, str>>,
		/// file to load the body from
		path: PathBuf,
	},
}
impl TemplateSrc {
	/// load a template from the source and add it to the environment
	pub fn add_to(self, dir: &Path, env: &mut minijinja::Environment) -> io::Result<()> {
		match self {
			Self::InMem {
				name,
				body,
			} => env.add_template_owned(name, body),
			Self::Stored {
				name,
				path: body,
			} => {
				let name = name.unwrap_or_else(|| {
					// TODO: prettier errors than a panic?
					let mut name = body.file_name().unwrap().to_str().unwrap();
					for ext in [".t.html", ".jinja.html", ".html", ".html.jinja", ".jinja"] {
						if let Some(n) = name.strip_suffix(ext) {
							name = n;
							break;
						}
					}
					name.to_string().into()
				});
				let contents = fs::read_to_string(dir.join(body))?;
				env.add_template_owned(name, contents)
			}
		}
		.map_err(|e| io::Error::other(format!("{e}")))
	}
}