tinymist_world/
args.rs

1use std::{
2    path::{Path, PathBuf},
3    sync::Arc,
4};
5
6use chrono::{DateTime, Utc};
7use clap::{builder::ValueParser, ArgAction, Parser};
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    /// Add a string key-value pair visible through `sys.inputs`
65    #[clap(
66        long = "input",
67        value_name = "key=value",
68        action = ArgAction::Append,
69        value_parser = ValueParser::new(parse_input_pair),
70    )]
71    pub inputs: Vec<(String, String)>,
72
73    /// Font related arguments.
74    #[clap(flatten)]
75    pub font: CompileFontArgs,
76
77    /// Package related arguments.
78    #[clap(flatten)]
79    pub package: CompilePackageArgs,
80
81    /// The document's creation date formatted as a UNIX timestamp (in seconds).
82    ///
83    /// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
84    #[clap(
85        long = "creation-timestamp",
86        env = "SOURCE_DATE_EPOCH",
87        value_name = "UNIX_TIMESTAMP",
88        value_parser = parse_source_date_epoch,
89        hide(true),
90    )]
91    pub creation_timestamp: Option<i64>,
92
93    /// Path to CA certificate file for network access, especially for
94    /// downloading typst packages.
95    #[clap(long = "cert", env = "TYPST_CERT", value_name = "CERT_PATH")]
96    pub cert: Option<PathBuf>,
97}
98
99impl CompileOnceArgs {
100    pub fn resolve_inputs(&self) -> Option<ImmutDict> {
101        if self.inputs.is_empty() {
102            return None;
103        }
104
105        let pairs = self.inputs.iter();
106        let pairs = pairs.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()));
107        Some(Arc::new(LazyHash::new(pairs.collect())))
108    }
109
110    /// Resolves the entry options.
111    pub fn resolve_sys_entry_opts(&self) -> Result<EntryOpts> {
112        let mut cwd = None;
113        let mut cwd = move || {
114            cwd.get_or_insert_with(|| {
115                std::env::current_dir().context("failed to get current directory")
116            })
117            .clone()
118        };
119
120        let main = {
121            let input = self.input.as_ref().context("entry file must be provided")?;
122            let input = Path::new(&input);
123            if input.is_absolute() {
124                input.to_owned()
125            } else {
126                cwd()?.join(input)
127            }
128        };
129
130        let root = if let Some(root) = &self.root {
131            if root.is_absolute() {
132                root.clone()
133            } else {
134                cwd()?.join(root)
135            }
136        } else {
137            main.parent()
138                .context("entry file don't have a valid parent as root")?
139                .to_owned()
140        };
141
142        let relative_main = match main.strip_prefix(&root) {
143            Ok(relative_main) => relative_main,
144            Err(_) => {
145                log::error!("entry file must be inside the root, file: {main:?}, root: {root:?}");
146                bail!("entry file must be inside the root, file: {main:?}, root: {root:?}");
147            }
148        };
149
150        Ok(EntryOpts::new_rooted(
151            root.clone(),
152            Some(relative_main.to_owned()),
153        ))
154    }
155}
156
157#[cfg(feature = "system")]
158impl CompileOnceArgs {
159    /// Resolves the arguments into a system universe. This is also a sample
160    /// implementation of how to resolve the arguments (user inputs) into a
161    /// universe.
162    pub fn resolve_system(&self) -> Result<crate::TypstSystemUniverse> {
163        use crate::system::SystemUniverseBuilder;
164
165        let entry = self.resolve_sys_entry_opts()?.try_into()?;
166        let inputs = self.resolve_inputs().unwrap_or_default();
167        let fonts = Arc::new(SystemUniverseBuilder::resolve_fonts(self.font.clone())?);
168        let package = SystemUniverseBuilder::resolve_package(
169            self.cert.as_deref().map(From::from),
170            Some(&self.package),
171        );
172
173        Ok(SystemUniverseBuilder::build(entry, inputs, fonts, package))
174    }
175}
176
177/// Parses key/value pairs split by the first equal sign.
178///
179/// This function will return an error if the argument contains no equals sign
180/// or contains the key (before the equals sign) is empty.
181fn parse_input_pair(raw: &str) -> Result<(String, String), String> {
182    let (key, val) = raw
183        .split_once('=')
184        .ok_or("input must be a key and a value separated by an equal sign")?;
185    let key = key.trim().to_owned();
186    if key.is_empty() {
187        return Err("the key was missing or empty".to_owned());
188    }
189    let val = val.trim().to_owned();
190    Ok((key, val))
191}
192
193/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
194pub fn parse_source_date_epoch(raw: &str) -> Result<i64, String> {
195    raw.parse()
196        .map_err(|err| format!("timestamp must be decimal integer ({err})"))
197}
198
199/// Parses a UNIX timestamp according to <https://reproducible-builds.org/specs/source-date-epoch/>
200pub fn convert_source_date_epoch(seconds: i64) -> Result<chrono::DateTime<Utc>, String> {
201    DateTime::from_timestamp(seconds, 0).ok_or_else(|| "timestamp out of range".to_string())
202}