1use core::fmt;
4use std::{
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use clap::{ArgAction, Parser, ValueEnum, builder::ValueParser};
10use serde::{Deserialize, Serialize};
11use tinymist_std::{bail, error::prelude::*};
12use tinymist_vfs::ImmutDict;
13use typst::{foundations::IntoValue, utils::LazyHash};
14
15use crate::EntryOpts;
16
17const ENV_PATH_SEP: char = if cfg!(windows) { ';' } else { ':' };
18
19#[derive(Debug, Clone, Default, Parser, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct CompileFontArgs {
23 #[clap(
25 long = "font-path",
26 value_name = "DIR",
27 action = clap::ArgAction::Append,
28 env = "TYPST_FONT_PATHS",
29 value_delimiter = ENV_PATH_SEP
30 )]
31 pub font_paths: Vec<PathBuf>,
32
33 #[clap(long, default_value = "false")]
36 pub ignore_system_fonts: bool,
37}
38
39#[derive(Debug, Clone, Parser, Default, PartialEq, Eq)]
41pub struct CompilePackageArgs {
42 #[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
44 pub package_path: Option<PathBuf>,
45
46 #[clap(
48 long = "package-cache-path",
49 env = "TYPST_PACKAGE_CACHE_PATH",
50 value_name = "DIR"
51 )]
52 pub package_cache_path: Option<PathBuf>,
53}
54
55#[derive(Debug, Clone, Parser, Default)]
57pub struct CompileOnceArgs {
58 #[clap(value_name = "INPUT")]
60 pub input: Option<String>,
61
62 #[clap(long = "root", value_name = "DIR")]
64 pub root: Option<PathBuf>,
65
66 #[clap(flatten)]
68 pub font: CompileFontArgs,
69
70 #[clap(flatten)]
72 pub package: CompilePackageArgs,
73
74 #[arg(long = "features", value_delimiter = ',', env = "TYPST_FEATURES")]
77 pub features: Vec<Feature>,
78
79 #[clap(
81 long = "input",
82 value_name = "key=value",
83 action = ArgAction::Append,
84 value_parser = ValueParser::new(parse_input_pair),
85 )]
86 pub inputs: Vec<(String, String)>,
87
88 #[clap(
92 long = "creation-timestamp",
93 env = "SOURCE_DATE_EPOCH",
94 value_name = "UNIX_TIMESTAMP",
95 value_parser = parse_source_date_epoch,
96 hide(true),
97 )]
98 pub creation_timestamp: Option<i64>,
99
100 #[arg(long = "pdf-standard", value_delimiter = ',')]
103 pub pdf_standard: Vec<PdfStandard>,
104
105 #[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
108 pub cert: Option<PathBuf>,
109}
110
111impl CompileOnceArgs {
112 pub fn resolve_features(&self) -> typst::Features {
114 typst::Features::from_iter(self.features.iter().map(|f| (*f).into()))
115 }
116
117 pub fn resolve_inputs(&self) -> Option<ImmutDict> {
119 if self.inputs.is_empty() {
120 return None;
121 }
122
123 let pairs = self.inputs.iter();
124 let pairs = pairs.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()));
125 Some(Arc::new(LazyHash::new(pairs.collect())))
126 }
127
128 pub fn resolve_sys_entry_opts(&self) -> Result<EntryOpts> {
130 let mut cwd = None;
131 let mut cwd = move || {
132 cwd.get_or_insert_with(|| {
133 std::env::current_dir().context("failed to get current directory")
134 })
135 .clone()
136 };
137
138 let main = {
139 let input = self.input.as_ref().context("entry file must be provided")?;
140 let input = Path::new(&input);
141 if input.is_absolute() {
142 input.to_owned()
143 } else {
144 cwd()?.join(input)
145 }
146 };
147
148 let root = if let Some(root) = &self.root {
149 if root.is_absolute() {
150 root.clone()
151 } else {
152 cwd()?.join(root)
153 }
154 } else {
155 main.parent()
156 .context("entry file don't have a valid parent as root")?
157 .to_owned()
158 };
159
160 let relative_main = match main.strip_prefix(&root) {
161 Ok(relative_main) => relative_main,
162 Err(_) => {
163 log::error!("entry file must be inside the root, file: {main:?}, root: {root:?}");
164 bail!("entry file must be inside the root, file: {main:?}, root: {root:?}");
165 }
166 };
167
168 Ok(EntryOpts::new_rooted(
169 root.clone(),
170 Some(relative_main.to_owned()),
171 ))
172 }
173}
174
175#[cfg(feature = "system")]
176impl CompileOnceArgs {
177 pub fn resolve_system(&self) -> Result<crate::TypstSystemUniverse> {
181 use crate::system::SystemUniverseBuilder;
182
183 let entry = self.resolve_sys_entry_opts()?.try_into()?;
184 let inputs = self.resolve_inputs().unwrap_or_default();
185 let fonts = Arc::new(SystemUniverseBuilder::resolve_fonts(self.font.clone())?);
186 let package = SystemUniverseBuilder::resolve_package(
187 self.cert.as_deref().map(From::from),
188 Some(&self.package),
189 );
190
191 Ok(SystemUniverseBuilder::build(entry, inputs, fonts, package))
192 }
193}
194
195fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
200 let (key, val) = raw
201 .split_once('=')
202 .ok_or("input must be a key and a value separated by an equal sign")?;
203 let key = key.trim().to_owned();
204 if key.is_empty() {
205 return Err("the key was missing or empty".to_owned());
206 }
207 let val = val.trim().to_owned();
208 Ok((key, val))
209}
210
211pub fn parse_source_date_epoch(raw: &str) -> Result<i64, String> {
213 raw.parse()
214 .map_err(|err| format!("timestamp must be decimal integer ({err})"))
215}
216
217macro_rules! display_possible_values {
218 ($ty:ty) => {
219 impl fmt::Display for $ty {
220 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221 self.to_possible_value()
222 .expect("no values are skipped")
223 .get_name()
224 .fmt(f)
225 }
226 }
227 };
228}
229
230#[derive(Debug, Clone, Eq, PartialEq, Default, Hash, ValueEnum, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245#[clap(rename_all = "camelCase")]
246pub enum TaskWhen {
247 #[default]
249 Never,
250 OnSave,
252 OnType,
254 OnDocumentHasTitle,
259 Script,
261}
262
263impl TaskWhen {
264 pub fn is_never(&self) -> bool {
266 matches!(self, TaskWhen::Never)
267 }
268}
269
270display_possible_values!(TaskWhen);
271
272#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
274pub enum OutputFormat {
275 Pdf,
277 Png,
279 Svg,
281 Html,
283}
284
285display_possible_values!(OutputFormat);
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
293#[serde(rename_all = "camelCase")]
294pub enum ExportTarget {
295 #[default]
297 Paged,
298 Html,
300}
301
302#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, ValueEnum, Serialize, Deserialize)]
304#[allow(non_camel_case_types)]
305pub enum PdfStandard {
306 #[value(name = "1.7")]
308 #[serde(rename = "1.7")]
309 V_1_7,
310 #[value(name = "a-2b")]
312 #[serde(rename = "a-2b")]
313 A_2b,
314 #[value(name = "a-3b")]
316 #[serde(rename = "a-3b")]
317 A_3b,
318}
319
320display_possible_values!(PdfStandard);
321
322#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
324pub enum Feature {
325 Html,
327}
328
329display_possible_values!(Feature);
330
331impl From<Feature> for typst::Feature {
332 fn from(f: Feature) -> typst::Feature {
333 match f {
334 Feature::Html => typst::Feature::Html,
335 }
336 }
337}