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