tinymist_project/
args.rs

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