use anyhow::{Context, Result, anyhow};
use derivative::Derivative;
use glob::Pattern;
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 {
path: Option<String>,
watch: bool,
serve: 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 serve: bool,
pub ignore_initial: bool,
pub verbose: bool,
pub trace: bool,
pub passthrough_copy: Vec<String>,
#[derivative(Debug = "ignore")]
pub passthrough_copy_globs: Vec<Pattern>,
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_relpath: PathBuf,
pub output_relpath: PathBuf,
pub template_relpath: PathBuf,
}
pub 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)
})
}
pub fn content_root(&self) -> PathBuf {
self.project_root.join(&self.content_relpath)
}
pub fn output_root(&self) -> PathBuf {
self.project_root.join(&self.output_relpath)
}
pub fn template_root(&self) -> PathBuf {
self.project_root.join(&self.template_relpath)
}
fn new_inner() -> Result<Self> {
let content_relpath = PathBuf::from("src");
let output_relpath = PathBuf::from("_site");
let template_relpath = PathBuf::from("templates");
let Args {
path,
watch,
serve,
ignore_initial,
verbose,
trace,
} = onlyargs::parse()?;
let project_root = if let Some(path) = path {
path.parse()?
} else {
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_relpath)?;
Ok(Self {
watch,
serve,
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_relpath,
output_relpath,
template_relpath,
})
}
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(
string_globs: &[String],
project_root: &Path,
content_root: &Path,
) -> Result<(Vec<Pattern>, Vec<String>)> {
let mut compiled_globs = Vec::new();
let mut compiled_globs_string_form = Vec::new();
for glob in string_globs {
let string_glob = project_root
.join(content_root)
.join(glob)
.to_str()
.context(anyhow!("{glob} not utf8"))?
.to_owned();
let compiled_glob = string_glob.parse::<Pattern>()?;
compiled_globs.push(compiled_glob);
compiled_globs_string_form.push(string_glob);
}
Ok((compiled_globs, compiled_globs_string_form))
}
fn get_configfile(project_root: &Path) -> Result<ConfigFile> {
const PROJ_ROOT_REPLACEE: &str = "$PROJECT_ROOT";
let file = project_root.join(CONFIG_FNAME);
let contents = fs::read_to_string(file)?;
let mut config: ConfigFile = toml::from_str(&contents)?;
config.init = config.init.map(|init_args| {
init_args
.into_iter()
.map(|arg| arg.replace(PROJ_ROOT_REPLACEE, &project_root.to_string_lossy()))
.collect()
});
config.post_processing_typ = config.post_processing_typ.map(|init_args| {
init_args
.into_iter()
.map(|arg| arg.replace(PROJ_ROOT_REPLACEE, &project_root.to_string_lossy()))
.collect()
});
Ok(config)
}
}