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(
28 long = "font-path",
29 value_name = "DIR",
30 action = clap::ArgAction::Append,
31 env = "TYPST_FONT_PATHS",
32 value_delimiter = ENV_PATH_SEP
33 )]
34 pub font_paths: Vec<PathBuf>,
35
36 #[clap(long, default_value = "false")]
39 pub ignore_system_fonts: bool,
40}
41
42#[derive(Debug, Clone, Parser, Default, PartialEq, Eq)]
45pub struct CompilePackageArgs {
46 #[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
49 pub package_path: Option<PathBuf>,
50
51 #[clap(
54 long = "package-cache-path",
55 env = "TYPST_PACKAGE_CACHE_PATH",
56 value_name = "DIR"
57 )]
58 pub package_cache_path: Option<PathBuf>,
59}
60
61#[derive(Debug, Clone, Parser, Default)]
64pub struct CompileOnceArgs {
65 #[clap(value_name = "INPUT")]
68 pub input: Option<String>,
69
70 #[clap(long = "root", value_name = "DIR")]
72 pub root: Option<PathBuf>,
73
74 #[clap(flatten)]
76 pub font: CompileFontArgs,
77
78 #[clap(flatten)]
80 pub package: CompilePackageArgs,
81
82 #[clap(flatten)]
84 pub pdf: PdfExportArgs,
85
86 #[clap(flatten)]
88 pub png: PngExportArgs,
89
90 #[arg(long = "features", value_delimiter = ',', env = "TYPST_FEATURES")]
93 pub features: Vec<Feature>,
94
95 #[clap(
105 long = "input",
106 value_name = "key=value",
107 action = ArgAction::Append,
108 value_parser = ValueParser::new(parse_input_pair),
109 )]
110 pub inputs: Vec<(String, String)>,
111
112 #[clap(
117 long = "creation-timestamp",
118 env = "SOURCE_DATE_EPOCH",
119 value_name = "UNIX_TIMESTAMP",
120 value_parser = parse_source_date_epoch,
121 hide(true),
122 )]
123 pub creation_timestamp: Option<i64>,
124
125 #[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
128 pub cert: Option<PathBuf>,
129}
130
131impl CompileOnceArgs {
132 pub fn resolve_features(&self) -> typst::Features {
134 typst::Features::from_iter(self.features.iter().map(|f| (*f).into()))
135 }
136
137 pub fn resolve_inputs(&self) -> Option<ImmutDict> {
139 if self.inputs.is_empty() {
140 return None;
141 }
142
143 let pairs = self.inputs.iter();
144 let pairs = pairs.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()));
145 Some(Arc::new(LazyHash::new(pairs.collect())))
146 }
147
148 pub fn resolve_sys_entry_opts(&self) -> Result<EntryOpts> {
150 let mut cwd = None;
151 let mut cwd = move || {
152 cwd.get_or_insert_with(|| {
153 std::env::current_dir().context("failed to get current directory")
154 })
155 .clone()
156 };
157
158 let main = {
159 let input = self.input.as_ref().context("entry file must be provided")?;
160 let input = Path::new(&input);
161 if input.is_absolute() {
162 input.to_owned()
163 } else {
164 cwd()?.join(input)
165 }
166 };
167
168 let root = if let Some(root) = &self.root {
169 if root.is_absolute() {
170 root.clone()
171 } else {
172 cwd()?.join(root)
173 }
174 } else {
175 main.parent()
176 .context("entry file don't have a valid parent as root")?
177 .to_owned()
178 };
179
180 let relative_main = match main.strip_prefix(&root) {
181 Ok(relative_main) => relative_main,
182 Err(_) => {
183 log::error!("entry file must be inside the root, file: {main:?}, root: {root:?}");
184 bail!("entry file must be inside the root, file: {main:?}, root: {root:?}");
185 }
186 };
187
188 Ok(EntryOpts::new_rooted(
189 root.clone(),
190 Some(relative_main.to_owned()),
191 ))
192 }
193}
194
195#[cfg(feature = "system")]
196impl CompileOnceArgs {
197 pub fn resolve_system(&self) -> Result<crate::TypstSystemUniverse> {
201 use crate::system::SystemUniverseBuilder;
202
203 let entry = self.resolve_sys_entry_opts()?.try_into()?;
204 let inputs = self.resolve_inputs().unwrap_or_default();
205 let fonts = Arc::new(SystemUniverseBuilder::resolve_fonts(self.font.clone())?);
206 let package = SystemUniverseBuilder::resolve_package(
207 self.cert.as_deref().map(From::from),
208 Some(&self.package),
209 );
210
211 Ok(SystemUniverseBuilder::build(entry, inputs, fonts, package))
212 }
213}
214
215fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
220 let (key, val) = raw
221 .split_once('=')
222 .ok_or("input must be a key and a value separated by an equal sign")?;
223 let key = key.trim().to_owned();
224 if key.is_empty() {
225 return Err("the key was missing or empty".to_owned());
226 }
227 let val = val.trim().to_owned();
228 Ok((key, val))
229}
230
231pub fn parse_source_date_epoch(raw: &str) -> Result<i64, String> {
233 raw.parse()
234 .map_err(|err| format!("timestamp must be decimal integer ({err})"))
235}
236
237#[derive(Debug, Clone, Parser, Default)]
239pub struct PdfExportArgs {
240 #[arg(long = "pdf-standard", value_delimiter = ',')]
244 pub standard: Vec<PdfStandard>,
245
246 #[arg(long = "no-pdf-tags")]
251 pub no_tags: bool,
252}
253
254#[derive(Debug, Clone, Parser, Default)]
256pub struct PngExportArgs {
257 #[arg(long = "ppi", default_value_t = 144.0)]
259 pub ppi: f32,
260}
261
262macro_rules! display_possible_values {
263 ($ty:ty) => {
264 impl fmt::Display for $ty {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 self.to_possible_value()
267 .expect("no values are skipped")
268 .get_name()
269 .fmt(f)
270 }
271 }
272 };
273}
274
275#[derive(Debug, Clone, Eq, PartialEq, Default, Hash, ValueEnum, Serialize, Deserialize)]
289#[serde(rename_all = "camelCase")]
290#[clap(rename_all = "camelCase")]
291pub enum TaskWhen {
292 #[default]
294 Never,
295 OnSave,
297 OnType,
299 OnDocumentHasTitle,
304 Script,
306}
307
308impl TaskWhen {
309 pub fn is_never(&self) -> bool {
311 matches!(self, TaskWhen::Never)
312 }
313}
314
315display_possible_values!(TaskWhen);
316
317#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
319pub enum OutputFormat {
320 Pdf,
322 Png,
324 Svg,
326 Html,
328}
329
330display_possible_values!(OutputFormat);
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
338#[serde(rename_all = "camelCase")]
339pub enum ExportTarget {
340 #[default]
342 Paged,
343 Html,
345}
346
347#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, ValueEnum, Serialize, Deserialize)]
349#[allow(non_camel_case_types)]
350pub enum PdfStandard {
351 #[value(name = "1.4")]
353 #[serde(rename = "1.4")]
354 V_1_4,
355 #[value(name = "1.5")]
357 #[serde(rename = "1.5")]
358 V_1_5,
359 #[value(name = "1.6")]
361 #[serde(rename = "1.6")]
362 V_1_6,
363 #[value(name = "1.7")]
365 #[serde(rename = "1.7")]
366 V_1_7,
367 #[value(name = "2.0")]
369 #[serde(rename = "2.0")]
370 V_2_0,
371 #[value(name = "a-1b")]
373 #[serde(rename = "a-1b")]
374 A_1b,
375 #[value(name = "a-1a")]
377 #[serde(rename = "a-1a")]
378 A_1a,
379 #[value(name = "a-2b")]
381 #[serde(rename = "a-2b")]
382 A_2b,
383 #[value(name = "a-2u")]
385 #[serde(rename = "a-2u")]
386 A_2u,
387 #[value(name = "a-2a")]
389 #[serde(rename = "a-2a")]
390 A_2a,
391 #[value(name = "a-3b")]
393 #[serde(rename = "a-3b")]
394 A_3b,
395 #[value(name = "a-3u")]
397 #[serde(rename = "a-3u")]
398 A_3u,
399 #[value(name = "a-3a")]
401 #[serde(rename = "a-3a")]
402 A_3a,
403 #[value(name = "a-4")]
405 #[serde(rename = "a-4")]
406 A_4,
407 #[value(name = "a-4f")]
409 #[serde(rename = "a-4f")]
410 A_4f,
411 #[value(name = "a-4e")]
413 #[serde(rename = "a-4e")]
414 A_4e,
415 #[value(name = "ua-1")]
417 #[serde(rename = "ua-1")]
418 Ua_1,
419}
420
421display_possible_values!(PdfStandard);
422
423#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
425pub enum Feature {
426 Html,
428}
429
430display_possible_values!(Feature);
431
432impl From<Feature> for typst::Feature {
433 fn from(f: Feature) -> typst::Feature {
434 match f {
435 Feature::Html => typst::Feature::Html,
436 }
437 }
438}