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#[derive(Debug, Clone, Default, Parser, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct CompileFontArgs {
21 #[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 #[clap(long, default_value = "false")]
34 pub ignore_system_fonts: bool,
35}
36
37#[derive(Debug, Clone, Parser, Default, PartialEq, Eq)]
39pub struct CompilePackageArgs {
40 #[clap(long = "package-path", env = "TYPST_PACKAGE_PATH", value_name = "DIR")]
42 pub package_path: Option<PathBuf>,
43
44 #[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#[derive(Debug, Clone, Parser, Default)]
55pub struct CompileOnceArgs {
56 #[clap(value_name = "INPUT")]
58 pub input: Option<String>,
59
60 #[clap(long = "root", value_name = "DIR")]
62 pub root: Option<PathBuf>,
63
64 #[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 #[clap(flatten)]
75 pub font: CompileFontArgs,
76
77 #[clap(flatten)]
79 pub package: CompilePackageArgs,
80
81 #[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 #[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 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 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
177fn 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
193pub 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
199pub 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}