use anyhow::{Result, anyhow};
use derivative::Derivative;
use globset::{Glob, GlobSet, GlobSetBuilder};
use onlyargs_derive::OnlyArgs;
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::exit;
#[derive(Clone, Debug, Eq, PartialEq, OnlyArgs)]
struct Args {
watch: bool,
ignore_initial: bool,
verbose: bool,
trace: bool,
}
#[derive(Deserialize)]
struct ConfigFile {
passthrough_copy: Option<Vec<String>>,
init: Option<Vec<String>>,
post_processing_typ: Option<Vec<String>>,
literal_paths: Option<bool>,
file_listing: Option<String>,
}
#[derive(Debug)]
pub enum FileListing {
Disabled,
Enabled,
IncludeData,
}
impl FileListing {
pub const DISABLED_STR: &str = "disabled";
pub const ENABLED_STR: &str = "enabled";
pub const INCLUDE_DATA_STR: &str = "include-data";
pub const DEFAULT_STR: &str = Self::DISABLED_STR;
}
impl Default for FileListing {
fn default() -> Self {
Self::Disabled
}
}
impl TryFrom<String> for FileListing {
type Error = anyhow::Error;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
match value.as_str() {
Self::DISABLED_STR => Ok(FileListing::Disabled),
Self::ENABLED_STR => Ok(FileListing::Enabled),
Self::INCLUDE_DATA_STR => Ok(FileListing::IncludeData),
_ => Err(anyhow!(
"TOML parsing error: file_listing must be one of \"{}\", \"{}\", \"{}\", not {}",
Self::DISABLED_STR,
Self::ENABLED_STR,
Self::INCLUDE_DATA_STR,
value
)),
}
}
}
#[derive(Derivative)]
#[derivative(Debug)]
pub struct Config {
pub watch: bool,
pub ignore_initial: bool,
pub verbose: bool,
pub trace: bool,
pub passthrough_copy: Vec<String>,
#[derivative(Debug = "ignore")]
pub passthrough_copy_globs: GlobSet,
pub passthrough_copy_globs_string_form: Vec<String>,
pub init: Vec<String>,
pub post_processing_typ: Vec<String>,
pub literal_paths: bool,
pub file_listing: FileListing,
pub project_root: PathBuf,
pub content_root: PathBuf,
pub output_root: PathBuf,
pub template_root: PathBuf,
}
const CONFIG_FNAME: &str = "compile-typst-site.toml";
impl Config {
pub fn new() -> Self {
Self::new_inner().unwrap_or_else(|err| {
eprintln!("{:?}", err);
exit(1)
})
}
fn new_inner() -> Result<Self> {
let content_root = PathBuf::from("src");
let output_root = PathBuf::from("_site");
let template_root = PathBuf::from("templates");
let Args {
watch,
ignore_initial,
verbose,
trace,
} = onlyargs::parse()?;
let project_root = Self::get_project_root()?;
let ConfigFile {
passthrough_copy,
init,
post_processing_typ,
literal_paths,
file_listing,
} = Self::get_configfile(&project_root)?;
let passthrough_copy = passthrough_copy.unwrap_or(vec![]);
let init = init.unwrap_or(vec![]);
let post_processing_typ = post_processing_typ.unwrap_or(vec![]);
let literal_paths = literal_paths.unwrap_or(false);
let file_listing = file_listing
.unwrap_or(FileListing::DEFAULT_STR.into())
.try_into()?;
let (passthrough_copy_globs, passthrough_copy_globs_string_form) =
Self::compile_globs(&passthrough_copy, &project_root, &content_root)?;
Ok(Self {
watch,
ignore_initial,
verbose,
trace,
passthrough_copy,
passthrough_copy_globs,
passthrough_copy_globs_string_form,
init,
post_processing_typ,
literal_paths,
file_listing,
project_root,
content_root,
output_root,
template_root,
})
}
fn get_project_root() -> Result<PathBuf> {
let mut root = std::env::current_dir()?;
loop {
let candidate = root.join(CONFIG_FNAME);
if candidate.exists() {
return Ok(root);
}
if !root.pop() {
return Err(anyhow!(
"Couldn't find a configuration file (looking for {CONFIG_FNAME}) in the current directory or any parent directories."
));
}
}
}
fn compile_globs(
globs: &[String],
project_root: &Path,
content_root: &Path,
) -> Result<(GlobSet, Vec<String>)> {
let mut builder = GlobSetBuilder::new();
let mut compiled_globs_string_form = Vec::new();
for glob in globs {
let glob = project_root
.join(content_root)
.join(glob)
.to_str()
.unwrap()
.to_string();
builder.add(Glob::new(&glob)?);
compiled_globs_string_form.push(glob);
}
Ok((builder.build()?, compiled_globs_string_form))
}
fn get_configfile(project_root: &Path) -> Result<ConfigFile> {
let file = project_root.join(CONFIG_FNAME);
let contents = fs::read_to_string(file)?;
let config = toml::from_str(&contents)?;
Ok(config)
}
}