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 #[arg(long = "features", value_delimiter = ',', env = "TYPST_FEATURES")]
85 pub features: Vec<Feature>,
86
87 #[clap(
97 long = "input",
98 value_name = "key=value",
99 action = ArgAction::Append,
100 value_parser = ValueParser::new(parse_input_pair),
101 )]
102 pub inputs: Vec<(String, String)>,
103
104 #[clap(
109 long = "creation-timestamp",
110 env = "SOURCE_DATE_EPOCH",
111 value_name = "UNIX_TIMESTAMP",
112 value_parser = parse_source_date_epoch,
113 hide(true),
114 )]
115 pub creation_timestamp: Option<i64>,
116
117 #[arg(long = "pdf-standard", value_delimiter = ',')]
121 pub pdf_standard: Vec<PdfStandard>,
122
123 #[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
126 pub cert: Option<PathBuf>,
127}
128
129impl CompileOnceArgs {
130 pub fn resolve_features(&self) -> typst::Features {
132 typst::Features::from_iter(self.features.iter().map(|f| (*f).into()))
133 }
134
135 pub fn resolve_inputs(&self) -> Option<ImmutDict> {
137 if self.inputs.is_empty() {
138 return None;
139 }
140
141 let pairs = self.inputs.iter();
142 let pairs = pairs.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()));
143 Some(Arc::new(LazyHash::new(pairs.collect())))
144 }
145
146 pub fn resolve_sys_entry_opts(&self) -> Result<EntryOpts> {
148 let mut cwd = None;
149 let mut cwd = move || {
150 cwd.get_or_insert_with(|| {
151 std::env::current_dir().context("failed to get current directory")
152 })
153 .clone()
154 };
155
156 let main = {
157 let input = self.input.as_ref().context("entry file must be provided")?;
158 let input = Path::new(&input);
159 if input.is_absolute() {
160 input.to_owned()
161 } else {
162 cwd()?.join(input)
163 }
164 };
165
166 let root = if let Some(root) = &self.root {
167 if root.is_absolute() {
168 root.clone()
169 } else {
170 cwd()?.join(root)
171 }
172 } else {
173 main.parent()
174 .context("entry file don't have a valid parent as root")?
175 .to_owned()
176 };
177
178 let relative_main = match main.strip_prefix(&root) {
179 Ok(relative_main) => relative_main,
180 Err(_) => {
181 log::error!("entry file must be inside the root, file: {main:?}, root: {root:?}");
182 bail!("entry file must be inside the root, file: {main:?}, root: {root:?}");
183 }
184 };
185
186 Ok(EntryOpts::new_rooted(
187 root.clone(),
188 Some(relative_main.to_owned()),
189 ))
190 }
191}
192
193#[cfg(feature = "system")]
194impl CompileOnceArgs {
195 pub fn resolve_system(&self) -> Result<crate::TypstSystemUniverse> {
199 use crate::system::SystemUniverseBuilder;
200
201 let entry = self.resolve_sys_entry_opts()?.try_into()?;
202 let inputs = self.resolve_inputs().unwrap_or_default();
203 let fonts = Arc::new(SystemUniverseBuilder::resolve_fonts(self.font.clone())?);
204 let package = SystemUniverseBuilder::resolve_package(
205 self.cert.as_deref().map(From::from),
206 Some(&self.package),
207 );
208
209 Ok(SystemUniverseBuilder::build(entry, inputs, fonts, package))
210 }
211}
212
213fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
218 let (key, val) = raw
219 .split_once('=')
220 .ok_or("input must be a key and a value separated by an equal sign")?;
221 let key = key.trim().to_owned();
222 if key.is_empty() {
223 return Err("the key was missing or empty".to_owned());
224 }
225 let val = val.trim().to_owned();
226 Ok((key, val))
227}
228
229pub fn parse_source_date_epoch(raw: &str) -> Result<i64, String> {
231 raw.parse()
232 .map_err(|err| format!("timestamp must be decimal integer ({err})"))
233}
234
235macro_rules! display_possible_values {
236 ($ty:ty) => {
237 impl fmt::Display for $ty {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 self.to_possible_value()
240 .expect("no values are skipped")
241 .get_name()
242 .fmt(f)
243 }
244 }
245 };
246}
247
248#[derive(Debug, Clone, Eq, PartialEq, Default, Hash, ValueEnum, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase")]
263#[clap(rename_all = "camelCase")]
264pub enum TaskWhen {
265 #[default]
267 Never,
268 OnSave,
270 OnType,
272 OnDocumentHasTitle,
277 Script,
279}
280
281impl TaskWhen {
282 pub fn is_never(&self) -> bool {
284 matches!(self, TaskWhen::Never)
285 }
286}
287
288display_possible_values!(TaskWhen);
289
290#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
292pub enum OutputFormat {
293 Pdf,
295 Png,
297 Svg,
299 Html,
301}
302
303display_possible_values!(OutputFormat);
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub enum ExportTarget {
313 #[default]
315 Paged,
316 Html,
318}
319
320#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, ValueEnum, Serialize, Deserialize)]
322#[allow(non_camel_case_types)]
323pub enum PdfStandard {
324 #[value(name = "1.7")]
326 #[serde(rename = "1.7")]
327 V_1_7,
328 #[value(name = "a-2b")]
330 #[serde(rename = "a-2b")]
331 A_2b,
332 #[value(name = "a-3b")]
334 #[serde(rename = "a-3b")]
335 A_3b,
336}
337
338display_possible_values!(PdfStandard);
339
340#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
342pub enum Feature {
343 Html,
345}
346
347display_possible_values!(Feature);
348
349impl From<Feature> for typst::Feature {
350 fn from(f: Feature) -> typst::Feature {
351 match f {
352 Feature::Html => typst::Feature::Html,
353 }
354 }
355}