tinymist_project/
args.rs

1use std::{path::Path, sync::OnceLock};
2
3use clap::ValueHint;
4use tinymist_std::{bail, error::prelude::Result};
5
6pub use tinymist_world::args::{CompileFontArgs, CompilePackageArgs};
7
8use crate::PROJECT_ROUTE_USER_ACTION_PRIORITY;
9use crate::model::*;
10
11/// Project document commands.
12#[derive(Debug, Clone, clap::Subcommand)]
13#[clap(rename_all = "kebab-case")]
14pub enum DocCommands {
15    /// Declare a document (project input).
16    New(DocNewArgs),
17    /// Configure document priority in workspace.
18    Configure(DocConfigureArgs),
19}
20
21/// Declare a document (project's input).
22#[derive(Debug, Clone, clap::Parser)]
23pub struct DocNewArgs {
24    /// Specify the argument to identify a project.
25    #[clap(flatten)]
26    pub id: DocIdArgs,
27    /// Configure the project root (for absolute paths). If the path is
28    /// relative, it will be resolved relative to the current working directory
29    /// (PWD).
30    #[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")]
31    pub root: Option<String>,
32    /// Specify the font related arguments.
33    #[clap(flatten)]
34    pub font: CompileFontArgs,
35    /// Specify the package related arguments.
36    #[clap(flatten)]
37    pub package: CompilePackageArgs,
38}
39
40impl DocNewArgs {
41    /// Converts to project input.
42    pub fn to_input(&self, ctx: CtxPath) -> ProjectInput {
43        let id: Id = self.id.id(ctx);
44
45        let root = self
46            .root
47            .as_ref()
48            .map(|root| ResourcePath::from_user_sys(Path::new(root), ctx));
49        let main = ResourcePath::from_user_sys(Path::new(&self.id.input), ctx);
50
51        let font_paths = self
52            .font
53            .font_paths
54            .iter()
55            .map(|p| ResourcePath::from_user_sys(p, ctx))
56            .collect::<Vec<_>>();
57
58        let package_path = self
59            .package
60            .package_path
61            .as_ref()
62            .map(|p| ResourcePath::from_user_sys(p, ctx));
63
64        let package_cache_path = self
65            .package
66            .package_cache_path
67            .as_ref()
68            .map(|p| ResourcePath::from_user_sys(p, ctx));
69
70        ProjectInput {
71            id: id.clone(),
72            lock_dir: Some(ctx.1.to_path_buf()),
73            root,
74            main,
75            // todo: inputs
76            inputs: vec![],
77            font_paths,
78            system_fonts: !self.font.ignore_system_fonts,
79            package_path,
80            package_cache_path,
81        }
82    }
83}
84
85/// Specify the id of a document.
86///
87/// If an identifier is not provided, the document's path is used as the id.
88#[derive(Debug, Clone, clap::Parser)]
89pub struct DocIdArgs {
90    /// Give a task name to the document.
91    #[clap(long = "name")]
92    pub name: Option<String>,
93    /// Specify the path to input Typst file.
94    #[clap(value_hint = ValueHint::FilePath)]
95    pub input: String,
96}
97
98impl DocIdArgs {
99    /// Converts to a document ID.
100    pub fn id(&self, ctx: CtxPath) -> Id {
101        if let Some(id) = &self.name {
102            Id::new(id.clone())
103        } else {
104            (&ResourcePath::from_user_sys(Path::new(&self.input), ctx)).into()
105        }
106    }
107}
108
109/// Configure project's priorities.
110#[derive(Debug, Clone, clap::Parser)]
111pub struct DocConfigureArgs {
112    /// Specify the argument to identify a project.
113    #[clap(flatten)]
114    pub id: DocIdArgs,
115    /// Set the unsigned priority of these task (lower numbers are higher
116    /// priority).
117    #[clap(long = "priority", default_value_t = PROJECT_ROUTE_USER_ACTION_PRIORITY)]
118    pub priority: u32,
119}
120
121/// Declare an compile task.
122#[derive(Debug, Clone, clap::Parser)]
123pub struct TaskCompileArgs {
124    /// Specify the argument to identify a project.
125    #[clap(flatten)]
126    pub declare: DocNewArgs,
127
128    /// Configure when to run the task.
129    #[arg(long = "when")]
130    pub when: Option<TaskWhen>,
131
132    /// Provide the path to output file (PDF, PNG, SVG, or HTML). Use `-` to
133    /// write output to stdout.
134    ///
135    /// For output formats emitting one file per page (PNG & SVG), a page number
136    /// template must be present if the source document renders to multiple
137    /// pages. Use `{p}` for page numbers, `{0p}` for zero padded page numbers
138    /// and `{t}` for page count. For example, `page-{0p}-of-{t}.png` creates
139    /// `page-01-of-10.png`, `page-02-of-10.png`, and so on.
140    #[clap(value_hint = ValueHint::FilePath)]
141    pub output: Option<String>,
142
143    /// Specify the format of the output file, inferred from the extension by
144    /// default.
145    #[arg(long = "format", short = 'f')]
146    pub format: Option<OutputFormat>,
147
148    /// Specify which pages to export. When unspecified, all pages are exported.
149    ///
150    /// Pages to export are separated by commas, and can be either simple page
151    /// numbers (e.g. '2,5' to export only pages 2 and 5) or page ranges (e.g.
152    /// '2,3-6,8-' to export page 2, pages 3 to 6 (inclusive), page 8 and any
153    /// pages after it).
154    ///
155    /// Page numbers are one-indexed and correspond to physical page numbers in
156    /// the document (therefore not being affected by the document's page
157    /// counter).
158    #[arg(long = "pages", value_delimiter = ',')]
159    pub pages: Option<Vec<Pages>>,
160
161    /// Specify the PDF export related arguments.
162    #[clap(flatten)]
163    pub pdf: PdfExportArgs,
164
165    /// Specify the PNG export related arguments.
166    #[clap(flatten)]
167    pub png: PngExportArgs,
168
169    /// Specify the output format.
170    #[clap(skip)]
171    pub output_format: OnceLock<Result<OutputFormat>>,
172}
173
174impl TaskCompileArgs {
175    /// Converts the arguments to a project task.
176    pub fn to_task(self, doc_id: Id, cwd: &Path) -> Result<ApplyProjectTask> {
177        let new_task_id = self.declare.id.name.map(Id::new);
178        let task_id = new_task_id.unwrap_or(doc_id.clone());
179
180        let output_format = if let Some(specified) = self.format {
181            specified
182        } else if let Some(output) = &self.output {
183            let output = Path::new(output);
184
185            match output.extension() {
186                Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
187                Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
188                Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
189                Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
190                _ => bail!(
191                    "could not infer output format for path {output:?}.\n\
192                         consider providing the format manually with `--format/-f`",
193                ),
194            }
195        } else {
196            OutputFormat::Pdf
197        };
198
199        let output = self.output.as_ref().map(|output| {
200            let output = Path::new(output);
201            let output = if output.is_absolute() {
202                output.to_path_buf()
203            } else {
204                cwd.join(output)
205            };
206
207            PathPattern::new(&output.with_extension("").to_string_lossy())
208        });
209
210        let when = self.when.unwrap_or(TaskWhen::Never);
211
212        let mut transforms = vec![];
213
214        if let Some(pages) = &self.pages {
215            transforms.push(ExportTransform::Pages {
216                ranges: pages.clone(),
217            });
218        }
219
220        let export = ExportTask {
221            when,
222            output,
223            transform: transforms,
224        };
225
226        let config = match output_format {
227            OutputFormat::Pdf => ProjectTask::ExportPdf(ExportPdfTask {
228                export,
229                pdf_standards: self.pdf.pdf_standard.clone(),
230                creation_timestamp: None,
231            }),
232            OutputFormat::Png => ProjectTask::ExportPng(ExportPngTask {
233                export,
234                ppi: self.png.ppi.try_into().unwrap(),
235                fill: None,
236            }),
237            OutputFormat::Svg => ProjectTask::ExportSvg(ExportSvgTask { export }),
238            OutputFormat::Html => ProjectTask::ExportSvg(ExportSvgTask { export }),
239        };
240
241        Ok(ApplyProjectTask {
242            id: task_id.clone(),
243            document: doc_id,
244            task: config,
245        })
246    }
247}
248
249/// Specify the PDF export related arguments.
250#[derive(Debug, Clone, clap::Parser)]
251pub struct PdfExportArgs {
252    /// Specify the PDF standards that Typst will enforce conformance with.
253    ///
254    /// If multiple standards are specified, they are separated by commas.
255    #[arg(long = "pdf-standard", value_delimiter = ',')]
256    pub pdf_standard: Vec<PdfStandard>,
257}
258
259/// Specify the PNG export related arguments.
260#[derive(Debug, Clone, clap::Parser)]
261pub struct PngExportArgs {
262    /// Specify the PPI (pixels per inch) to use for PNG export.
263    #[arg(long = "ppi", default_value_t = 144.0)]
264    pub ppi: f32,
265}