webdog 0.1.4

static site generator fit for a dog
Documentation
mod builder;
mod extras;
pub mod frontmatter;
mod link_list;
pub mod resource;
#[cfg(feature = "serve")]
pub mod serving;
mod util;

use std::{
	collections::HashMap,
	path::{Path, PathBuf},
};

use extras::ExtraData;
use eyre::Context;
use resource::{EmbedMetadata, ResourceBuilderConfig};
use serde::{Deserialize, Serialize};
use url::Url;
use walkdir::WalkDir;

use builder::SiteBuilder;

/// Source base path for normal site pages.
pub const PAGES_PATH: &str = "pages";
/// Source base path for site templates.
pub const TEMPLATES_PATH: &str = "templates";
/// Source base path for SASS stylesheets.
pub const SASS_PATH: &str = "sass";
/// Source base path for files which will be copied to the site root.
pub const ROOT_PATH: &str = "root";
/// Source base path for resources.
pub const RESOURCES_PATH: &str = "resources";

/// Struct for the site's configuration.
#[derive(Debug, Serialize, Deserialize)]
pub struct SiteConfig {
	/// The location the site is at.
	pub base_url: Url,
	/// The site's title.
	pub title: String,
	/// The site's description? Not sure if this will actually be used or not
	pub description: String,
	/// the site's theme color for embed metadata
	pub theme_color: String,
	/// The site's build directory. Defaults to <site>/build if not specified.
	pub build: Option<String>,
	/// A list of Sass stylesheets that will be built.
	pub sass_styles: Vec<PathBuf>,
	/// URL to the CDN used for the site's images.
	pub cdn_url: Url,
	/// The path to output webdog static resources to. Defaults to "webdog"
	pub webdog_path: Option<String>,
	/// The theme to use for the site's code blocks.
	/// TODO: dark/light themes
	/// TODO: export themes as CSS instead of styling HTML directly
	/// TODO: allow loading user themes
	pub code_theme: String,

	/// List of resources the site should build.
	pub resources: HashMap<String, ResourceBuilderConfig>,
}

impl SiteConfig {
	/// The filename for site config files.
	pub const FILENAME: &str = "config.yaml";

	/// Creates a new site config from the given title.
	pub fn new(base_url: Url, cdn_url: Url, title: String) -> Self {
		Self {
			base_url,
			title,
			description: Default::default(),
			theme_color: "#ffc4fc".to_string(),
			build: None,
			sass_styles: vec!["index.scss".into()],
			cdn_url,
			webdog_path: None,
			code_theme: "base16-ocean.dark".to_string(),
			resources: Default::default(),
		}
	}

	/// Gets a CDN url from the given file name.
	pub fn cdn_url(&self, file: &str) -> eyre::Result<Url> {
		Ok(self.cdn_url.join(file)?)
	}

	/// Checks the site config for errors.
	pub fn check(&self, builder: &SiteBuilder) -> eyre::Result<()> {
		builder
			.theme_set
			.themes
			.contains_key(&self.code_theme)
			.then_some(())
			.ok_or_else(|| eyre::eyre!("missing code theme: {}", self.code_theme))?;
		Ok(())
	}

	/// Helper to read the site config from the given path.
	pub fn read(site_path: &Path) -> eyre::Result<Self> {
		let config_path = site_path.join(SiteConfig::FILENAME);
		if !config_path.exists() {
			eyre::bail!("no site config found!");
		}
		Ok(serde_yaml_ng::from_str(&std::fs::read_to_string(
			config_path,
		)?)?)
	}
}

/// Struct for the front matter in templates. (nothing here yet)
#[derive(Debug, Default, Deserialize)]
pub struct TemplateMetadata {}

/// Struct for the front matter in pages.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct PageMetadata {
	/// The page's title.
	pub title: Option<String>,
	/// The template to use for the page. If not specified, it defaults to "base".
	pub template: Option<String>,
	/// custom embed info for a template
	#[serde(default)]
	pub embed: Option<EmbedMetadata>,
	/// The page's custom scripts, if any.
	#[serde(default)]
	pub scripts: Vec<String>,
	/// the page's custom styles, if any.
	#[serde(default)]
	pub styles: Vec<String>,
	/// The extra stuff to run for the page, if any.
	#[serde(default)]
	pub extra: Option<ExtraData>,
	/// Custom values passed to the base template.
	#[serde(default)]
	pub userdata: serde_yaml_ng::Value,
	/// Whether this page being rendered is a partial. Set by the builder, not your page metadata.
	#[serde(skip)]
	pub is_partial: bool,
}

/// Struct containing information about the site.
#[derive(Debug)]
pub struct Site {
	/// The path to the site.
	pub site_path: PathBuf,
	/// The site's configuration.
	pub config: SiteConfig,
	/// An index of available pages.
	pub page_index: HashMap<String, PathBuf>,
}

impl Site {
	/// Creates a new site from the given path.
	pub fn new(site_path: &Path) -> eyre::Result<Self> {
		let config = SiteConfig::read(site_path)?;

		let mut page_index = HashMap::new();
		let pages_path = site_path.join(PAGES_PATH);
		for entry in WalkDir::new(&pages_path).into_iter() {
			let entry = entry.wrap_err("Failed to read page entry")?;
			let path = entry.path();

			if let Some(ext) = path.extension()
				&& ext == "md"
				&& entry.file_type().is_file()
			{
				page_index.insert(
					path.strip_prefix(&pages_path)
						.wrap_err("This really shouldn't have happened")?
						.with_extension("")
						.to_string_lossy()
						.to_string(),
					path.to_owned(),
				);
			}
		}

		Ok(Self {
			site_path: site_path.to_owned(),
			config,
			page_index,
		})
	}

	/// Builds the site once.
	pub fn build_once(self) -> eyre::Result<()> {
		SiteBuilder::new(self, false)?.prepare()?.build_all()
	}
}