use std::env;
use std::fs;
use std::path::{Path, PathBuf};
fn read_manifest(manifest_dir: &Path) -> Result<toml::Table, String> {
let cargo_toml_path = manifest_dir.join("Cargo.toml");
let content = fs::read_to_string(&cargo_toml_path)
.map_err(|e| format!("Failed to read Cargo.toml: {e}"))?;
content
.parse()
.map_err(|e| format!("Failed to parse Cargo.toml: {e}"))
}
fn get_metadata_value<'a>(manifest: &'a toml::Table, key: &str) -> Option<&'a toml::Value> {
manifest
.get("package")
.and_then(|p| p.get("metadata"))
.and_then(|m| m.get("typst-bake"))
.and_then(|t| t.get(key))
}
fn get_metadata_str<'a>(manifest: &'a toml::Table, key: &str) -> Option<&'a str> {
get_metadata_value(manifest, key).and_then(toml::Value::as_str)
}
fn resolve_path(manifest_dir: &Path, path: &str) -> PathBuf {
if Path::new(path).is_absolute() {
PathBuf::from(path)
} else {
manifest_dir.join(path)
}
}
fn get_config_dir(
env_var: &str,
metadata_key: &str,
not_configured_msg: &str,
dir_kind: &str,
) -> Result<PathBuf, String> {
let manifest_dir =
env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR not set".to_owned())?;
let manifest_dir = Path::new(&manifest_dir);
let path = if let Ok(dir) = env::var(env_var) {
resolve_path(manifest_dir, &dir)
} else {
let manifest = read_manifest(manifest_dir)?;
let dir = get_metadata_str(&manifest, metadata_key)
.ok_or_else(|| not_configured_msg.to_owned())?;
resolve_path(manifest_dir, dir)
};
if !path.exists() {
return Err(format!(
"{dir_kind} directory does not exist: {}",
path.display()
));
}
Ok(path)
}
pub fn get_template_dir() -> Result<PathBuf, String> {
get_config_dir(
"TYPST_BAKE_TEMPLATE_DIR",
"template-dir",
"Template directory not configured.\n\n\
Add to your Cargo.toml:\n\n\
[package.metadata.typst-bake]\n\
template-dir = \"./templates\"\n\n\
Or set environment variable:\n\
export TYPST_BAKE_TEMPLATE_DIR=./templates",
"Template",
)
}
pub fn should_refresh_cache() -> bool {
env::var("TYPST_BAKE_PKG_NOCACHE").is_ok()
}
pub fn get_fonts_dir() -> Result<PathBuf, String> {
let path = get_config_dir(
"TYPST_BAKE_FONTS_DIR",
"fonts-dir",
"Fonts directory not configured.\n\n\
Add to your Cargo.toml:\n\n\
[package.metadata.typst-bake]\n\
fonts-dir = \"./fonts\"\n\n\
Or set environment variable:\n\
export TYPST_BAKE_FONTS_DIR=./fonts",
"Fonts",
)?;
let has_fonts = walkdir::WalkDir::new(&path)
.into_iter()
.filter_map(Result::ok)
.any(|entry| is_font_file(entry.path()));
if !has_fonts {
return Err(format!(
"No font files found in fonts directory: {}\n\n\
Supported formats: .ttf, .otf, .ttc",
path.display()
));
}
Ok(path)
}
pub fn is_hidden(path: &Path) -> bool {
path.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with('.'))
}
pub fn is_font_file(path: &Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.is_some_and(|ext| {
ext.eq_ignore_ascii_case("ttf")
|| ext.eq_ignore_ascii_case("otf")
|| ext.eq_ignore_ascii_case("ttc")
})
}
const ZSTD_LEVEL_MIN: i32 = 1;
const ZSTD_LEVEL_MAX: i32 = 22;
const ZSTD_LEVEL_DEFAULT: i32 = 19;
pub fn get_compression_level() -> i32 {
if let Some(level) = env::var("TYPST_BAKE_COMPRESSION_LEVEL")
.ok()
.and_then(|v| v.parse::<i32>().ok())
{
return level.clamp(ZSTD_LEVEL_MIN, ZSTD_LEVEL_MAX);
}
if let Some(level) = env::var("CARGO_MANIFEST_DIR")
.ok()
.and_then(|dir| read_manifest(Path::new(&dir)).ok())
.and_then(|manifest| {
get_metadata_value(&manifest, "compression-level").and_then(toml::Value::as_integer)
})
{
return (level as i32).clamp(ZSTD_LEVEL_MIN, ZSTD_LEVEL_MAX);
}
ZSTD_LEVEL_DEFAULT
}
pub fn get_compression_cache_dir() -> Result<PathBuf, String> {
let pkg_name = env::var("CARGO_PKG_NAME").map_err(|_| "CARGO_PKG_NAME not set".to_owned())?;
let cache_in = |target: PathBuf| target.join("typst-bake-cache").join(&pkg_name);
if let Ok(target_dir) = env::var("CARGO_TARGET_DIR") {
return Ok(cache_in(PathBuf::from(target_dir)));
}
let manifest_dir =
env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR not set".to_owned())?;
let manifest_dir = Path::new(&manifest_dir);
let local_target = manifest_dir.join("target");
if local_target.is_dir() {
return Ok(cache_in(local_target));
}
let mut dir = manifest_dir.parent();
while let Some(d) = dir {
let candidate = d.join("target");
if candidate.is_dir() {
return Ok(cache_in(candidate));
}
dir = d.parent();
}
let cache_base =
dirs::cache_dir().ok_or_else(|| "Could not determine cache directory".to_owned())?;
Ok(cache_base
.join("typst-bake")
.join("compression-cache")
.join(&pkg_name))
}