tinymist_project/
args.rs

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