use core::fmt;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use clap::{builder::ValueParser, ArgAction, Parser, ValueEnum};
use serde::{Deserialize, Serialize};
use tinymist_std::{bail, error::prelude::*};
use tinymist_vfs::ImmutDict;
use typst::{foundations::IntoValue, utils::LazyHash};
use crate::EntryOpts;
const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
#[derive(Debug, Clone, Default, Parser, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompileFontArgs {
#[clap(
long = "font-path",
value_name = "DIR",
action = clap::ArgAction::Append,
env = "TYPST_FONT_PATHS",
value_delimiter = ENV_PATH_SEP
)]
pub font_paths: Vec<PathBuf>,
#[clap(long, default_value = "false")]
pub ignore_system_fonts: bool,
}
#[derive(Debug, Clone, Parser, Default, PartialEq, Eq)]
pub struct CompilePackageArgs {
#[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
pub package_path: Option<PathBuf>,
#[clap(
long = "package-cache-path",
env = "TYPST_PACKAGE_CACHE_PATH",
value_name = "DIR"
)]
pub package_cache_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Parser, Default)]
pub struct CompileOnceArgs {
#[clap(value_name = "INPUT")]
pub input: Option<String>,
#[clap(long = "root", value_name = "DIR")]
pub root: Option<PathBuf>,
#[clap(flatten)]
pub font: CompileFontArgs,
#[clap(flatten)]
pub package: CompilePackageArgs,
#[arg(long = "features", value_delimiter = ',', env = "TYPST_FEATURES")]
pub features: Vec<Feature>,
#[clap(
long = "input",
value_name = "key=value",
action = ArgAction::Append,
value_parser = ValueParser::new(parse_input_pair),
)]
pub inputs: Vec<(String, String)>,
#[clap(
long = "creation-timestamp",
env = "SOURCE_DATE_EPOCH",
value_name = "UNIX_TIMESTAMP",
value_parser = parse_source_date_epoch,
hide(true),
)]
pub creation_timestamp: Option<i64>,
#[arg(long = "pdf-standard", value_delimiter = ',')]
pub pdf_standard: Vec<PdfStandard>,
#[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
pub cert: Option<PathBuf>,
}
impl CompileOnceArgs {
pub fn resolve_features(&self) -> typst::Features {
typst::Features::from_iter(self.features.iter().map(|f| (*f).into()))
}
pub fn resolve_inputs(&self) -> Option<ImmutDict> {
if self.inputs.is_empty() {
return None;
}
let pairs = self.inputs.iter();
let pairs = pairs.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()));
Some(Arc::new(LazyHash::new(pairs.collect())))
}
pub fn resolve_sys_entry_opts(&self) -> Result<EntryOpts> {
let mut cwd = None;
let mut cwd = move || {
cwd.get_or_insert_with(|| {
std::env::current_dir().context("failed to get current directory")
})
.clone()
};
let main = {
let input = self.input.as_ref().context("entry file must be provided")?;
let input = Path::new(&input);
if input.is_absolute() {
input.to_owned()
} else {
cwd()?.join(input)
}
};
let root = if let Some(root) = &self.root {
if root.is_absolute() {
root.clone()
} else {
cwd()?.join(root)
}
} else {
main.parent()
.context("entry file don't have a valid parent as root")?
.to_owned()
};
let relative_main = match main.strip_prefix(&root) {
Ok(relative_main) => relative_main,
Err(_) => {
log::error!("entry file must be inside the root, file: {main:?}, root: {root:?}");
bail!("entry file must be inside the root, file: {main:?}, root: {root:?}");
}
};
Ok(EntryOpts::new_rooted(
root.clone(),
Some(relative_main.to_owned()),
))
}
}
#[cfg(feature = "system")]
impl CompileOnceArgs {
pub fn resolve_system(&self) -> Result<crate::TypstSystemUniverse> {
use crate::system::SystemUniverseBuilder;
let entry = self.resolve_sys_entry_opts()?.try_into()?;
let inputs = self.resolve_inputs().unwrap_or_default();
let fonts = Arc::new(SystemUniverseBuilder::resolve_fonts(self.font.clone())?);
let package = SystemUniverseBuilder::resolve_package(
self.cert.as_deref().map(From::from),
Some(&self.package),
);
Ok(SystemUniverseBuilder::build(entry, inputs, fonts, package))
}
}
fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
let (key, val) = raw
.split_once('=')
.ok_or("input must be a key and a value separated by an equal sign")?;
let key = key.trim().to_owned();
if key.is_empty() {
return Err("the key was missing or empty".to_owned());
}
let val = val.trim().to_owned();
Ok((key, val))
}
pub fn parse_source_date_epoch(raw: &str) -> Result<i64, String> {
raw.parse()
.map_err(|err| format!("timestamp must be decimal integer ({err})"))
}
macro_rules! display_possible_values {
($ty:ty) => {
impl fmt::Display for $ty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.to_possible_value()
.expect("no values are skipped")
.get_name()
.fmt(f)
}
}
};
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash, ValueEnum, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[clap(rename_all = "camelCase")]
pub enum TaskWhen {
#[default]
Never,
OnSave,
OnType,
OnDocumentHasTitle,
}
impl TaskWhen {
pub fn is_never(&self) -> bool {
matches!(self, TaskWhen::Never)
}
}
display_possible_values!(TaskWhen);
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
pub enum OutputFormat {
Pdf,
Png,
Svg,
Html,
}
display_possible_values!(OutputFormat);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ExportTarget {
#[default]
Paged,
Html,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, ValueEnum, Serialize, Deserialize)]
#[allow(non_camel_case_types)]
pub enum PdfStandard {
#[value(name = "1.7")]
#[serde(rename = "1.7")]
V_1_7,
#[value(name = "a-2b")]
#[serde(rename = "a-2b")]
A_2b,
#[value(name = "a-3b")]
#[serde(rename = "a-3b")]
A_3b,
}
display_possible_values!(PdfStandard);
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
pub enum Feature {
Html,
}
display_possible_values!(Feature);
impl From<Feature> for typst::Feature {
fn from(f: Feature) -> typst::Feature {
match f {
Feature::Html => typst::Feature::Html,
}
}
}