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    /// Enable in-development features that may be changed or removed at any
83    /// time.
84    #[arg(long = "features", value_delimiter = ',', env = "TYPST_FEATURES")]
85    pub features: Vec<Feature>,
86
87    /// Add a string key-value pair visible through `sys.inputs`.
88    ///
89    /// ### Examples
90    ///
91    /// Tell the script that `sys.inputs.foo` is `"bar"` (type: `str`).
92    ///
93    /// ```bash
94    /// tinymist compile --input foo=bar
95    /// ```
96    #[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    /// Configure the document's creation date formatted as a UNIX timestamp
105    /// (in seconds).
106    ///
107    /// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
108    #[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    /// Specify the PDF standards that Typst will enforce conformance with.
118    ///
119    /// If multiple standards are specified, they are separated by commas.
120    #[arg(long = "pdf-standard", value_delimiter = ',')]
121    pub pdf_standard: Vec<PdfStandard>,
122
123    /// Specify the path to CA certificate file for network access, especially
124    /// for downloading typst packages.
125    #[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
126    pub cert: Option<PathBuf>,
127}
128
129impl CompileOnceArgs {
130    /// Resolves the features.
131    pub fn resolve_features(&self) -> typst::Features {
132        typst::Features::from_iter(self.features.iter().map(|f| (*f).into()))
133    }
134
135    /// Resolves the inputs.
136    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    /// Resolves the entry options.
147    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    /// Resolves the arguments into a system universe. This is also a sample
196    /// implementation of how to resolve the arguments (user inputs) into a
197    /// universe.
198    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
213/// Parses key/value pairs split by the first equal sign.
214///
215/// This function will return an error if the argument contains no equals sign
216/// or contains the key (before the equals sign) is empty.
217fn 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
229/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
230pub 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/// Configure when to run a task.
249///
250/// By default, a `tinymist compile` only provides input information and
251/// doesn't change the `when` field. However, you can still specify a `when`
252/// argument to override the default behavior for specific tasks.
253///
254/// ## Examples
255///
256/// ```bash
257/// tinymist compile --when onSave main.typ
258/// alias typst="tinymist compile --when=onSave"
259/// typst compile main.typ
260/// ```
261#[derive(Debug, Clone, Eq, PartialEq, Default, Hash, ValueEnum, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase")]
263#[clap(rename_all = "camelCase")]
264pub enum TaskWhen {
265    /// Never watch to run task.
266    #[default]
267    Never,
268    /// Run task on saving the document, i.e. on `textDocument/didSave` events.
269    OnSave,
270    /// Run task on typing, i.e. on `textDocument/didChange` events.
271    OnType,
272    /// *DEPRECATED* Run task when a document has a title and on saved, which is
273    /// useful to filter out template files.
274    ///
275    /// Note: this is deprecating.
276    OnDocumentHasTitle,
277    /// Checks by running a typst script.
278    Script,
279}
280
281impl TaskWhen {
282    /// Returns `true` if the task should never be run automatically.
283    pub fn is_never(&self) -> bool {
284        matches!(self, TaskWhen::Never)
285    }
286}
287
288display_possible_values!(TaskWhen);
289
290/// Configure the format of the output file.
291#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
292pub enum OutputFormat {
293    /// Export to PDF.
294    Pdf,
295    /// Export to PNG.
296    Png,
297    /// Export to SVG.
298    Svg,
299    /// Export to HTML.
300    Html,
301}
302
303display_possible_values!(OutputFormat);
304
305/// Configure the current export target.
306///
307/// The design of this configuration is not yet finalized and for this reason it
308/// is guarded behind the html feature. Visit the HTML documentation page for
309/// more details.
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub enum ExportTarget {
313    /// The current export target is for PDF, PNG, and SVG export.
314    #[default]
315    Paged,
316    /// The current export target is for HTML export.
317    Html,
318}
319
320/// A PDF standard that Typst can enforce conformance with.
321#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, ValueEnum, Serialize, Deserialize)]
322#[allow(non_camel_case_types)]
323pub enum PdfStandard {
324    /// PDF 1.7.
325    #[value(name = "1.7")]
326    #[serde(rename = "1.7")]
327    V_1_7,
328    /// PDF/A-2b.
329    #[value(name = "a-2b")]
330    #[serde(rename = "a-2b")]
331    A_2b,
332    /// PDF/A-3b.
333    #[value(name = "a-3b")]
334    #[serde(rename = "a-3b")]
335    A_3b,
336}
337
338display_possible_values!(PdfStandard);
339
340/// An in-development feature that may be changed or removed at any time.
341#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
342pub enum Feature {
343    /// The HTML feature.
344    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}