tinymist_world/
args.rs

1//! Shared arguments to create a world.
2
3use 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/// The font arguments for the world to specify the way to search for fonts.
20#[derive(Debug, Clone, Default, Parser, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct CompileFontArgs {
23    /// Add additional directories that are recursively searched for fonts.
24    ///
25    /// If multiple paths are specified, they are separated by the system's path
26    /// separator (`:` on Unix-like systems and `;` on Windows).
27    #[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    /// Ensure system fonts won't be searched, unless explicitly included via
37    /// `--font-path`.
38    #[clap(long, default_value = "false")]
39    pub ignore_system_fonts: bool,
40}
41
42/// The package arguments for the world to specify where packages are stored in
43/// the system.
44#[derive(Debug, Clone, Parser, Default, PartialEq, Eq)]
45pub struct CompilePackageArgs {
46    /// Specify a custom path to local packages, defaults to system-dependent
47    /// location.
48    #[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
49    pub package_path: Option<PathBuf>,
50
51    /// Specify a custom path to package cache, defaults to system-dependent
52    /// location.
53    #[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/// Common arguments to create a world (environment) to run typst tasks, e.g.
62/// `compile`, `watch`, and `query`.
63#[derive(Debug, Clone, Parser, Default)]
64pub struct CompileOnceArgs {
65    /// Specify the path to input Typst file. If the path is relative, it will
66    /// be resolved relative to the current working directory (PWD).
67    #[clap(value_name = "INPUT")]
68    pub input: Option<String>,
69
70    /// Configure the project root (for absolute paths).
71    #[clap(long = "root", value_name = "DIR")]
72    pub root: Option<PathBuf>,
73
74    /// Specify the font related arguments.
75    #[clap(flatten)]
76    pub font: CompileFontArgs,
77
78    /// Specify the package related arguments.
79    #[clap(flatten)]
80    pub package: CompilePackageArgs,
81
82    /// Specify the PDF export related arguments.
83    #[clap(flatten)]
84    pub pdf: PdfExportArgs,
85
86    /// Specify the PNG export related arguments.
87    #[clap(flatten)]
88    pub png: PngExportArgs,
89
90    /// Enable in-development features that may be changed or removed at any
91    /// time.
92    #[arg(long = "features", value_delimiter = ',', env = "TYPST_FEATURES")]
93    pub features: Vec<Feature>,
94
95    /// Add a string key-value pair visible through `sys.inputs`.
96    ///
97    /// ### Examples
98    ///
99    /// Tell the script that `sys.inputs.foo` is `"bar"` (type: `str`).
100    ///
101    /// ```bash
102    /// tinymist compile --input foo=bar
103    /// ```
104    #[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    /// Configure the document's creation date formatted as a UNIX timestamp
113    /// (in seconds).
114    ///
115    /// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
116    #[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    /// Specify the path to CA certificate file for network access, especially
126    /// for downloading typst packages.
127    #[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
128    pub cert: Option<PathBuf>,
129}
130
131impl CompileOnceArgs {
132    /// Resolves the features.
133    pub fn resolve_features(&self) -> typst::Features {
134        typst::Features::from_iter(self.features.iter().map(|f| (*f).into()))
135    }
136
137    /// Resolves the inputs.
138    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    /// Resolves the entry options.
149    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    /// Resolves the arguments into a system universe. This is also a sample
198    /// implementation of how to resolve the arguments (user inputs) into a
199    /// universe.
200    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
215/// Parses key/value pairs split by the first equal sign.
216///
217/// This function will return an error if the argument contains no equals sign
218/// or contains the key (before the equals sign) is empty.
219fn 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
231/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
232pub 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/// Specify the PDF export related arguments.
238#[derive(Debug, Clone, Parser, Default)]
239pub struct PdfExportArgs {
240    /// Specify the PDF standards that Typst will enforce conformance with.
241    ///
242    /// If multiple standards are specified, they are separated by commas.
243    #[arg(long = "pdf-standard", value_delimiter = ',')]
244    pub standard: Vec<PdfStandard>,
245
246    /// By default, even when not producing a `PDF/UA-1` document, a tagged PDF
247    /// document is written to provide a baseline of accessibility. In some
248    /// circumstances (for example when trying to reduce the size of a document)
249    /// it can be desirable to disable tagged PDF.
250    #[arg(long = "no-pdf-tags")]
251    pub no_tags: bool,
252}
253
254/// Specify the PNG export related arguments.
255#[derive(Debug, Clone, Parser, Default)]
256pub struct PngExportArgs {
257    /// Specify the PPI (pixels per inch) to use for PNG export.
258    #[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/// Configure when to run a task.
276///
277/// By default, a `tinymist compile` only provides input information and
278/// doesn't change the `when` field. However, you can still specify a `when`
279/// argument to override the default behavior for specific tasks.
280///
281/// ## Examples
282///
283/// ```bash
284/// tinymist compile --when onSave main.typ
285/// alias typst="tinymist compile --when=onSave"
286/// typst compile main.typ
287/// ```
288#[derive(Debug, Clone, Eq, PartialEq, Default, Hash, ValueEnum, Serialize, Deserialize)]
289#[serde(rename_all = "camelCase")]
290#[clap(rename_all = "camelCase")]
291pub enum TaskWhen {
292    /// Never watch to run task.
293    #[default]
294    Never,
295    /// Run task on saving the document, i.e. on `textDocument/didSave` events.
296    OnSave,
297    /// Run task on typing, i.e. on `textDocument/didChange` events.
298    OnType,
299    /// *DEPRECATED* Run task when a document has a title and on saved, which is
300    /// useful to filter out template files.
301    ///
302    /// Note: this is deprecating.
303    OnDocumentHasTitle,
304    /// Checks by running a typst script.
305    Script,
306}
307
308impl TaskWhen {
309    /// Returns `true` if the task should never be run automatically.
310    pub fn is_never(&self) -> bool {
311        matches!(self, TaskWhen::Never)
312    }
313}
314
315display_possible_values!(TaskWhen);
316
317/// Configure the format of the output file.
318#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
319pub enum OutputFormat {
320    /// Export to PDF.
321    Pdf,
322    /// Export to PNG.
323    Png,
324    /// Export to SVG.
325    Svg,
326    /// Export to HTML.
327    Html,
328}
329
330display_possible_values!(OutputFormat);
331
332/// Configure the current export target.
333///
334/// The design of this configuration is not yet finalized and for this reason it
335/// is guarded behind the html feature. Visit the HTML documentation page for
336/// more details.
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
338#[serde(rename_all = "camelCase")]
339pub enum ExportTarget {
340    /// The current export target is for PDF, PNG, and SVG export.
341    #[default]
342    Paged,
343    /// The current export target is for HTML export.
344    Html,
345}
346
347/// A PDF standard that Typst can enforce conformance with.
348#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, ValueEnum, Serialize, Deserialize)]
349#[allow(non_camel_case_types)]
350pub enum PdfStandard {
351    /// PDF 1.4.
352    #[value(name = "1.4")]
353    #[serde(rename = "1.4")]
354    V_1_4,
355    /// PDF 1.5.
356    #[value(name = "1.5")]
357    #[serde(rename = "1.5")]
358    V_1_5,
359    /// PDF 1.6.
360    #[value(name = "1.6")]
361    #[serde(rename = "1.6")]
362    V_1_6,
363    /// PDF 1.7.
364    #[value(name = "1.7")]
365    #[serde(rename = "1.7")]
366    V_1_7,
367    /// PDF 2.0.
368    #[value(name = "2.0")]
369    #[serde(rename = "2.0")]
370    V_2_0,
371    /// PDF/A-1b.
372    #[value(name = "a-1b")]
373    #[serde(rename = "a-1b")]
374    A_1b,
375    /// PDF/A-1a.
376    #[value(name = "a-1a")]
377    #[serde(rename = "a-1a")]
378    A_1a,
379    /// PDF/A-2b.
380    #[value(name = "a-2b")]
381    #[serde(rename = "a-2b")]
382    A_2b,
383    /// PDF/A-2u.
384    #[value(name = "a-2u")]
385    #[serde(rename = "a-2u")]
386    A_2u,
387    /// PDF/A-2a.
388    #[value(name = "a-2a")]
389    #[serde(rename = "a-2a")]
390    A_2a,
391    /// PDF/A-3b.
392    #[value(name = "a-3b")]
393    #[serde(rename = "a-3b")]
394    A_3b,
395    /// PDF/A-3u.
396    #[value(name = "a-3u")]
397    #[serde(rename = "a-3u")]
398    A_3u,
399    /// PDF/A-3a.
400    #[value(name = "a-3a")]
401    #[serde(rename = "a-3a")]
402    A_3a,
403    /// PDF/A-4.
404    #[value(name = "a-4")]
405    #[serde(rename = "a-4")]
406    A_4,
407    /// PDF/A-4f.
408    #[value(name = "a-4f")]
409    #[serde(rename = "a-4f")]
410    A_4f,
411    /// PDF/A-4e.
412    #[value(name = "a-4e")]
413    #[serde(rename = "a-4e")]
414    A_4e,
415    /// PDF/UA-1.
416    #[value(name = "ua-1")]
417    #[serde(rename = "ua-1")]
418    Ua_1,
419}
420
421display_possible_values!(PdfStandard);
422
423/// An in-development feature that may be changed or removed at any time.
424#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
425pub enum Feature {
426    /// The HTML feature.
427    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}