tinymist_world/
args.rs

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/// The font arguments for the compiler.
18#[derive(Debug, Clone, Default, Parser, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct CompileFontArgs {
21    /// Font paths
22    #[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    /// Ensures system fonts won't be searched, unless explicitly included via
32    /// `--font-path`
33    #[clap(long, default_value = "false")]
34    pub ignore_system_fonts: bool,
35}
36
37/// Arguments related to where packages are stored in the system.
38#[derive(Debug, Clone, Parser, Default, PartialEq, Eq)]
39pub struct CompilePackageArgs {
40    /// Custom path to local packages, defaults to system-dependent location
41    #[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
42    pub package_path: Option<PathBuf>,
43
44    /// Custom path to package cache, defaults to system-dependent location
45    #[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/// Common arguments of compile, watch, and query.
54#[derive(Debug, Clone, Parser, Default)]
55pub struct CompileOnceArgs {
56    /// Path to input Typst file
57    #[clap(value_name = "INPUT")]
58    pub input: Option<String>,
59
60    /// Configures the project root (for absolute paths)
61    #[clap(long = "root", value_name = "DIR")]
62    pub root: Option<PathBuf>,
63
64    /// Font related arguments.
65    #[clap(flatten)]
66    pub font: CompileFontArgs,
67
68    /// Package related arguments.
69    #[clap(flatten)]
70    pub package: CompilePackageArgs,
71
72    /// Enables in-development features that may be changed or removed at any
73    /// time.
74    #[arg(long = "features", value_delimiter = ',', env = "TYPST_FEATURES")]
75    pub features: Vec<Feature>,
76
77    /// Add a string key-value pair visible through `sys.inputs`
78    #[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    /// The document's creation date formatted as a UNIX timestamp (in seconds).
87    ///
88    /// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
89    #[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    /// One (or multiple comma-separated) PDF standards that Typst will enforce
99    /// conformance with.
100    #[arg(long = "pdf-standard", value_delimiter = ',')]
101    pub pdf_standard: Vec<PdfStandard>,
102
103    /// Path to CA certificate file for network access, especially for
104    /// downloading typst packages.
105    #[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    /// Resolves the entry options.
125    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    /// Resolves the arguments into a system universe. This is also a sample
174    /// implementation of how to resolve the arguments (user inputs) into a
175    /// universe.
176    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
191/// Parses key/value pairs split by the first equal sign.
192///
193/// This function will return an error if the argument contains no equals sign
194/// or contains the key (before the equals sign) is empty.
195fn 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
207/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
208pub 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/// When to export an output file.
227///
228/// By default, a `tinymist compile` only provides input information and
229/// doesn't change the `when` field. However, you can still specify a `when`
230/// argument to override the default behavior for specific tasks.
231///
232/// ## Examples
233///
234/// ```bash
235/// tinymist compile --when onSave main.typ
236/// alias typst="tinymist compile --when=onSave"
237/// typst compile main.typ
238/// ```
239#[derive(Debug, Clone, Eq, PartialEq, Default, Hash, ValueEnum, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241#[clap(rename_all = "camelCase")]
242pub enum TaskWhen {
243    /// Never watch to run task.
244    #[default]
245    Never,
246    /// Run task on saving the document, i.e. on `textDocument/didSave` events.
247    OnSave,
248    /// Run task on typing, i.e. on `textDocument/didChange` events.
249    OnType,
250    /// *DEPRECATED* Run task when a document has a title and on saved, which is
251    /// useful to filter out template files.
252    ///
253    /// Note: this is deprecating.
254    OnDocumentHasTitle,
255    /// Checks by running a typst script.
256    Script,
257}
258
259impl TaskWhen {
260    /// Returns `true` if the task should never be run automatically.
261    pub fn is_never(&self) -> bool {
262        matches!(self, TaskWhen::Never)
263    }
264}
265
266display_possible_values!(TaskWhen);
267
268/// Which format to use for the generated output file.
269#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
270pub enum OutputFormat {
271    /// Export to PDF.
272    Pdf,
273    /// Export to PNG.
274    Png,
275    /// Export to SVG.
276    Svg,
277    /// Export to HTML.
278    Html,
279}
280
281display_possible_values!(OutputFormat);
282
283/// Specifies the current export target.
284///
285/// The design of this configuration is not yet finalized and for this reason it
286/// is guarded behind the html feature. Visit the HTML documentation page for
287/// more details.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
289#[serde(rename_all = "camelCase")]
290pub enum ExportTarget {
291    /// The current export target is for PDF, PNG, and SVG export.
292    #[default]
293    Paged,
294    /// The current export target is for HTML export.
295    Html,
296}
297
298/// A PDF standard that Typst can enforce conformance with.
299#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, ValueEnum, Serialize, Deserialize)]
300#[allow(non_camel_case_types)]
301pub enum PdfStandard {
302    /// PDF 1.7.
303    #[value(name = "1.7")]
304    #[serde(rename = "1.7")]
305    V_1_7,
306    /// PDF/A-2b.
307    #[value(name = "a-2b")]
308    #[serde(rename = "a-2b")]
309    A_2b,
310    /// PDF/A-3b.
311    #[value(name = "a-3b")]
312    #[serde(rename = "a-3b")]
313    A_3b,
314}
315
316display_possible_values!(PdfStandard);
317
318/// An in-development feature that may be changed or removed at any time.
319#[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}