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.
20#[derive(Debug, Clone, Default, Parser, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct CompileFontArgs {
23    /// Font paths
24    #[clap(
25        long = "font-path",
26        value_name = "DIR",
27        action = clap::ArgAction::Append,
28        env = "TYPST_FONT_PATHS",
29        value_delimiter = ENV_PATH_SEP
30    )]
31    pub font_paths: Vec<PathBuf>,
32
33    /// Ensures system fonts won't be searched, unless explicitly included via
34    /// `--font-path`
35    #[clap(long, default_value = "false")]
36    pub ignore_system_fonts: bool,
37}
38
39/// Arguments related to where packages are stored in the system.
40#[derive(Debug, Clone, Parser, Default, PartialEq, Eq)]
41pub struct CompilePackageArgs {
42    /// Custom path to local packages, defaults to system-dependent location
43    #[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
44    pub package_path: Option<PathBuf>,
45
46    /// Custom path to package cache, defaults to system-dependent location
47    #[clap(
48        long = "package-cache-path",
49        env = "TYPST_PACKAGE_CACHE_PATH",
50        value_name = "DIR"
51    )]
52    pub package_cache_path: Option<PathBuf>,
53}
54
55/// Common arguments of compile, watch, and query.
56#[derive(Debug, Clone, Parser, Default)]
57pub struct CompileOnceArgs {
58    /// Path to input Typst file
59    #[clap(value_name = "INPUT")]
60    pub input: Option<String>,
61
62    /// Configures the project root (for absolute paths)
63    #[clap(long = "root", value_name = "DIR")]
64    pub root: Option<PathBuf>,
65
66    /// Font related arguments.
67    #[clap(flatten)]
68    pub font: CompileFontArgs,
69
70    /// Package related arguments.
71    #[clap(flatten)]
72    pub package: CompilePackageArgs,
73
74    /// Enables in-development features that may be changed or removed at any
75    /// time.
76    #[arg(long = "features", value_delimiter = ',', env = "TYPST_FEATURES")]
77    pub features: Vec<Feature>,
78
79    /// Add a string key-value pair visible through `sys.inputs`
80    #[clap(
81        long = "input",
82        value_name = "key=value",
83        action = ArgAction::Append,
84        value_parser = ValueParser::new(parse_input_pair),
85    )]
86    pub inputs: Vec<(String, String)>,
87
88    /// The document's creation date formatted as a UNIX timestamp (in seconds).
89    ///
90    /// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
91    #[clap(
92        long = "creation-timestamp",
93        env = "SOURCE_DATE_EPOCH",
94        value_name = "UNIX_TIMESTAMP",
95        value_parser = parse_source_date_epoch,
96        hide(true),
97    )]
98    pub creation_timestamp: Option<i64>,
99
100    /// One (or multiple comma-separated) PDF standards that Typst will enforce
101    /// conformance with.
102    #[arg(long = "pdf-standard", value_delimiter = ',')]
103    pub pdf_standard: Vec<PdfStandard>,
104
105    /// Path to CA certificate file for network access, especially for
106    /// downloading typst packages.
107    #[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
108    pub cert: Option<PathBuf>,
109}
110
111impl CompileOnceArgs {
112    /// Resolves the features.
113    pub fn resolve_features(&self) -> typst::Features {
114        typst::Features::from_iter(self.features.iter().map(|f| (*f).into()))
115    }
116
117    /// Resolves the inputs.
118    pub fn resolve_inputs(&self) -> Option<ImmutDict> {
119        if self.inputs.is_empty() {
120            return None;
121        }
122
123        let pairs = self.inputs.iter();
124        let pairs = pairs.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()));
125        Some(Arc::new(LazyHash::new(pairs.collect())))
126    }
127
128    /// Resolves the entry options.
129    pub fn resolve_sys_entry_opts(&self) -> Result<EntryOpts> {
130        let mut cwd = None;
131        let mut cwd = move || {
132            cwd.get_or_insert_with(|| {
133                std::env::current_dir().context("failed to get current directory")
134            })
135            .clone()
136        };
137
138        let main = {
139            let input = self.input.as_ref().context("entry file must be provided")?;
140            let input = Path::new(&input);
141            if input.is_absolute() {
142                input.to_owned()
143            } else {
144                cwd()?.join(input)
145            }
146        };
147
148        let root = if let Some(root) = &self.root {
149            if root.is_absolute() {
150                root.clone()
151            } else {
152                cwd()?.join(root)
153            }
154        } else {
155            main.parent()
156                .context("entry file don't have a valid parent as root")?
157                .to_owned()
158        };
159
160        let relative_main = match main.strip_prefix(&root) {
161            Ok(relative_main) => relative_main,
162            Err(_) => {
163                log::error!("entry file must be inside the root, file: {main:?}, root: {root:?}");
164                bail!("entry file must be inside the root, file: {main:?}, root: {root:?}");
165            }
166        };
167
168        Ok(EntryOpts::new_rooted(
169            root.clone(),
170            Some(relative_main.to_owned()),
171        ))
172    }
173}
174
175#[cfg(feature = "system")]
176impl CompileOnceArgs {
177    /// Resolves the arguments into a system universe. This is also a sample
178    /// implementation of how to resolve the arguments (user inputs) into a
179    /// universe.
180    pub fn resolve_system(&self) -> Result<crate::TypstSystemUniverse> {
181        use crate::system::SystemUniverseBuilder;
182
183        let entry = self.resolve_sys_entry_opts()?.try_into()?;
184        let inputs = self.resolve_inputs().unwrap_or_default();
185        let fonts = Arc::new(SystemUniverseBuilder::resolve_fonts(self.font.clone())?);
186        let package = SystemUniverseBuilder::resolve_package(
187            self.cert.as_deref().map(From::from),
188            Some(&self.package),
189        );
190
191        Ok(SystemUniverseBuilder::build(entry, inputs, fonts, package))
192    }
193}
194
195/// Parses key/value pairs split by the first equal sign.
196///
197/// This function will return an error if the argument contains no equals sign
198/// or contains the key (before the equals sign) is empty.
199fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
200    let (key, val) = raw
201        .split_once('=')
202        .ok_or("input must be a key and a value separated by an equal sign")?;
203    let key = key.trim().to_owned();
204    if key.is_empty() {
205        return Err("the key was missing or empty".to_owned());
206    }
207    let val = val.trim().to_owned();
208    Ok((key, val))
209}
210
211/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
212pub fn parse_source_date_epoch(raw: &str) -> Result<i64, String> {
213    raw.parse()
214        .map_err(|err| format!("timestamp must be decimal integer ({err})"))
215}
216
217macro_rules! display_possible_values {
218    ($ty:ty) => {
219        impl fmt::Display for $ty {
220            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221                self.to_possible_value()
222                    .expect("no values are skipped")
223                    .get_name()
224                    .fmt(f)
225            }
226        }
227    };
228}
229
230/// When to export an output file.
231///
232/// By default, a `tinymist compile` only provides input information and
233/// doesn't change the `when` field. However, you can still specify a `when`
234/// argument to override the default behavior for specific tasks.
235///
236/// ## Examples
237///
238/// ```bash
239/// tinymist compile --when onSave main.typ
240/// alias typst="tinymist compile --when=onSave"
241/// typst compile main.typ
242/// ```
243#[derive(Debug, Clone, Eq, PartialEq, Default, Hash, ValueEnum, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245#[clap(rename_all = "camelCase")]
246pub enum TaskWhen {
247    /// Never watch to run task.
248    #[default]
249    Never,
250    /// Run task on saving the document, i.e. on `textDocument/didSave` events.
251    OnSave,
252    /// Run task on typing, i.e. on `textDocument/didChange` events.
253    OnType,
254    /// *DEPRECATED* Run task when a document has a title and on saved, which is
255    /// useful to filter out template files.
256    ///
257    /// Note: this is deprecating.
258    OnDocumentHasTitle,
259    /// Checks by running a typst script.
260    Script,
261}
262
263impl TaskWhen {
264    /// Returns `true` if the task should never be run automatically.
265    pub fn is_never(&self) -> bool {
266        matches!(self, TaskWhen::Never)
267    }
268}
269
270display_possible_values!(TaskWhen);
271
272/// Which format to use for the generated output file.
273#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
274pub enum OutputFormat {
275    /// Export to PDF.
276    Pdf,
277    /// Export to PNG.
278    Png,
279    /// Export to SVG.
280    Svg,
281    /// Export to HTML.
282    Html,
283}
284
285display_possible_values!(OutputFormat);
286
287/// Specifies the current export target.
288///
289/// The design of this configuration is not yet finalized and for this reason it
290/// is guarded behind the html feature. Visit the HTML documentation page for
291/// more details.
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
293#[serde(rename_all = "camelCase")]
294pub enum ExportTarget {
295    /// The current export target is for PDF, PNG, and SVG export.
296    #[default]
297    Paged,
298    /// The current export target is for HTML export.
299    Html,
300}
301
302/// A PDF standard that Typst can enforce conformance with.
303#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, ValueEnum, Serialize, Deserialize)]
304#[allow(non_camel_case_types)]
305pub enum PdfStandard {
306    /// PDF 1.7.
307    #[value(name = "1.7")]
308    #[serde(rename = "1.7")]
309    V_1_7,
310    /// PDF/A-2b.
311    #[value(name = "a-2b")]
312    #[serde(rename = "a-2b")]
313    A_2b,
314    /// PDF/A-3b.
315    #[value(name = "a-3b")]
316    #[serde(rename = "a-3b")]
317    A_3b,
318}
319
320display_possible_values!(PdfStandard);
321
322/// An in-development feature that may be changed or removed at any time.
323#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
324pub enum Feature {
325    /// The HTML feature.
326    Html,
327}
328
329display_possible_values!(Feature);
330
331impl From<Feature> for typst::Feature {
332    fn from(f: Feature) -> typst::Feature {
333        match f {
334            Feature::Html => typst::Feature::Html,
335        }
336    }
337}