tinymist_task/
model.rs

1//! Project task models.
2
3use std::{hash::Hash, path::PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use super::{Id, Pages, PathPattern, PdfStandard, Scalar, TaskWhen};
8
9/// A project task application specifier. This is used for specifying tasks to
10/// run in a project. When the language service notifies an update event of the
11/// project, it will check whether any associated tasks need to be run.
12///
13/// Each task can have different timing and conditions for running. See
14/// [`TaskWhen`] for more information.
15///
16/// The available task types listed in the [`ProjectTask`] only represent the
17/// direct formats supported by the typst compiler. More task types can be
18/// customized by the [`ExportTransform`].
19///
20/// ## Examples
21///
22/// Export a JSON file with the pdfpc notes of the document:
23///
24/// ```bash
25/// tinymist project query main.typ --format json --selector "<pdfpc-notes>" --field value --one
26/// ```
27///
28/// Export a PDF file and then runs a ghostscript command to compress it:
29///
30/// ```bash
31/// tinymist project compile main.typ --pipe 'import "@local/postprocess:0.0.1": ghostscript; ghostscript(output.path)'
32/// ```
33#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(rename_all = "kebab-case", tag = "type")]
35pub struct ApplyProjectTask {
36    /// The task's ID.
37    pub id: Id,
38    /// The document's ID.
39    pub document: Id,
40    /// The task to run.
41    #[serde(flatten)]
42    pub task: ProjectTask,
43}
44
45impl ApplyProjectTask {
46    /// Returns the document's ID.
47    pub fn doc_id(&self) -> &Id {
48        &self.document
49    }
50
51    /// Returns the task's ID.
52    pub fn id(&self) -> &Id {
53        &self.id
54    }
55}
56
57/// A project task specifier. This structure specifies the arguments for a task.
58#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
59#[serde(rename_all = "kebab-case", tag = "type")]
60pub enum ProjectTask {
61    /// A preview task.
62    Preview(PreviewTask),
63    /// An export PDF task.
64    ExportPdf(ExportPdfTask),
65    /// An export PNG task.
66    ExportPng(ExportPngTask),
67    /// An export SVG task.
68    ExportSvg(ExportSvgTask),
69    /// An export HTML task.
70    ExportHtml(ExportHtmlTask),
71    /// An export bundle task.
72    ExportBundle(ExportBundleTask),
73    /// An export HTML task.
74    ExportSvgHtml(ExportHtmlTask),
75    /// An export Markdown task.
76    ExportMd(ExportMarkdownTask),
77    /// An export TeX task.
78    ExportTeX(ExportTeXTask),
79    /// An export Text task.
80    ExportText(ExportTextTask),
81    /// An query task.
82    Query(QueryTask),
83    // todo: compatibility
84    // An export task of another type.
85    // Other(serde_json::Value),
86}
87
88impl ProjectTask {
89    /// Returns the timing of executing the task.
90    pub fn when(&self) -> Option<&TaskWhen> {
91        Some(match self {
92            Self::Preview(task) => &task.when,
93            Self::ExportPdf(..)
94            | Self::ExportPng(..)
95            | Self::ExportSvg(..)
96            | Self::ExportHtml(..)
97            | Self::ExportBundle(..)
98            | Self::ExportSvgHtml(..)
99            | Self::ExportMd(..)
100            | Self::ExportTeX(..)
101            | Self::ExportText(..)
102            | Self::Query(..) => &self.as_export()?.when,
103        })
104    }
105
106    /// Returns the export configuration of a task.
107    pub fn as_export(&self) -> Option<&ExportTask> {
108        Some(match self {
109            Self::Preview(..) => return None,
110            Self::ExportPdf(task) => &task.export,
111            Self::ExportPng(task) => &task.export,
112            Self::ExportSvg(task) => &task.export,
113            Self::ExportHtml(task) => &task.export,
114            Self::ExportBundle(task) => &task.export,
115            Self::ExportSvgHtml(task) => &task.export,
116            Self::ExportTeX(task) => &task.export,
117            Self::ExportMd(task) => &task.export,
118            Self::ExportText(task) => &task.export,
119            Self::Query(task) => &task.export,
120        })
121    }
122
123    /// Returns the export configuration of a task.
124    pub fn as_export_mut(&mut self) -> Option<&mut ExportTask> {
125        Some(match self {
126            Self::Preview(..) => return None,
127            Self::ExportPdf(task) => &mut task.export,
128            Self::ExportPng(task) => &mut task.export,
129            Self::ExportSvg(task) => &mut task.export,
130            Self::ExportHtml(task) => &mut task.export,
131            Self::ExportBundle(task) => &mut task.export,
132            Self::ExportSvgHtml(task) => &mut task.export,
133            Self::ExportTeX(task) => &mut task.export,
134            Self::ExportMd(task) => &mut task.export,
135            Self::ExportText(task) => &mut task.export,
136            Self::Query(task) => &mut task.export,
137        })
138    }
139
140    /// Returns extension of the artifact.
141    pub fn extension(&self) -> &str {
142        match self {
143            Self::ExportPdf { .. } => "pdf",
144            Self::Preview(..) | Self::ExportSvgHtml { .. } | Self::ExportHtml { .. } => "html",
145            Self::ExportBundle { .. } => "",
146            Self::ExportMd { .. } => "md",
147            Self::ExportTeX { .. } => "tex",
148            Self::ExportText { .. } => "txt",
149            Self::ExportSvg { .. } => "svg",
150            Self::ExportPng { .. } => "png",
151            Self::Query(QueryTask {
152                format,
153                output_extension,
154                ..
155            }) => output_extension.as_deref().unwrap_or(format),
156        }
157    }
158}
159
160/// A preview task specifier.
161#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
162#[serde(rename_all = "kebab-case")]
163pub struct PreviewTask {
164    /// When to run the task. See [`TaskWhen`] for more
165    /// information.
166    pub when: TaskWhen,
167}
168
169/// An export task specifier.
170#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
171#[serde(rename_all = "kebab-case")]
172pub struct ExportTask {
173    /// When to run the task
174    pub when: TaskWhen,
175    /// The output path pattern.
176    pub output: Option<PathPattern>,
177    /// The task's transforms.
178    #[serde(skip_serializing_if = "Vec::is_empty", default)]
179    pub transform: Vec<ExportTransform>,
180}
181
182impl ExportTask {
183    /// Creates a new unmounted export task.
184    pub fn new(when: TaskWhen) -> Self {
185        Self {
186            when,
187            output: None,
188            transform: Vec::new(),
189        }
190    }
191
192    /// Pretty prints the output whenever possible.
193    pub fn apply_pretty(&mut self) {
194        self.transform
195            .push(ExportTransform::Pretty { script: None });
196    }
197}
198
199/// A page merge specifier.
200#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
201#[serde(default)]
202pub struct PageMerge {
203    /// The gap between pages (in pt).
204    pub gap: Option<String>,
205}
206
207/// A project export transform specifier.
208#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
209#[serde(rename_all = "kebab-case")]
210pub enum ExportTransform {
211    /// Only pick a subset of pages.
212    Pages {
213        /// The page ranges to export.
214        ranges: Vec<Pages>,
215    },
216    /// Merge pages into a single page.
217    Merge {
218        /// The gap between pages (typst code expression, e.g. `1pt`).
219        gap: Option<String>,
220    },
221    /// Execute a transform script.
222    Script {
223        /// The postprocess script (typst script) to run.
224        #[serde(skip_serializing_if = "Option::is_none", default)]
225        script: Option<String>,
226    },
227    /// Uses a pretty printer to format the output.
228    Pretty {
229        /// The pretty command (typst script) to run.
230        ///
231        /// If not provided, the default pretty printer will be used.
232        /// Note: the builtin one may be only effective for json outputs.
233        #[serde(skip_serializing_if = "Option::is_none", default)]
234        script: Option<String>,
235    },
236}
237
238/// An export pdf task specifier.
239#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
240#[serde(rename_all = "kebab-case")]
241pub struct ExportPdfTask {
242    /// The shared export arguments.
243    #[serde(flatten)]
244    pub export: ExportTask,
245    /// Which pages to export. When unspecified, all pages are exported.
246    #[serde(skip_serializing_if = "Option::is_none", default)]
247    pub pages: Option<Vec<Pages>>,
248    /// One (or multiple comma-separated) PDF standards that Typst will enforce
249    /// conformance with.
250    #[serde(skip_serializing_if = "Vec::is_empty", default)]
251    pub pdf_standards: Vec<PdfStandard>,
252    /// By default, even when not producing a `PDF/UA-1` document, a tagged PDF
253    /// document is written to provide a baseline of accessibility. In some
254    /// circumstances (for example when trying to reduce the size of a document)
255    /// it can be desirable to disable tagged PDF.
256    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
257    pub no_pdf_tags: bool,
258    /// The document's creation date formatted as a UNIX timestamp (in seconds).
259    ///
260    /// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
261    #[serde(skip_serializing_if = "Option::is_none", default)]
262    pub creation_timestamp: Option<i64>,
263}
264
265/// An export png task specifier.
266#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
267#[serde(rename_all = "kebab-case")]
268pub struct ExportPngTask {
269    /// The shared export arguments.
270    #[serde(flatten)]
271    pub export: ExportTask,
272    /// Which pages to export. When unspecified, all pages are exported.
273    #[serde(skip_serializing_if = "Option::is_none", default)]
274    pub pages: Option<Vec<Pages>>,
275    /// The page template to use for multiple pages.
276    #[serde(skip_serializing_if = "Option::is_none", default)]
277    pub page_number_template: Option<String>,
278    /// The page merge specifier.
279    #[serde(skip_serializing_if = "Option::is_none", default)]
280    pub merge: Option<PageMerge>,
281    /// The PPI (pixels per inch) to use for PNG export.
282    pub ppi: Scalar,
283    /// The expression constructing background fill color (in typst script).
284    /// e.g. `#ffffff`, `#000000`, `rgba(255, 255, 255, 0.5)`.
285    ///
286    /// If not provided, the default background color specified in the document
287    /// will be used.
288    #[serde(skip_serializing_if = "Option::is_none", default)]
289    pub fill: Option<String>,
290}
291
292/// An export svg task specifier.
293#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
294#[serde(rename_all = "kebab-case")]
295pub struct ExportSvgTask {
296    /// The shared export arguments.
297    #[serde(flatten)]
298    pub export: ExportTask,
299    /// The page template to use for multiple pages.
300    #[serde(skip_serializing_if = "Option::is_none", default)]
301    pub page_number_template: Option<String>,
302    /// Which pages to export. When unspecified, all pages are exported.
303    #[serde(skip_serializing_if = "Option::is_none", default)]
304    pub pages: Option<Vec<Pages>>,
305    /// The page merge specifier.
306    #[serde(skip_serializing_if = "Option::is_none", default)]
307    pub merge: Option<PageMerge>,
308}
309
310/// An export html task specifier.
311#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
312#[serde(rename_all = "kebab-case")]
313pub struct ExportHtmlTask {
314    /// The shared export arguments.
315    #[serde(flatten)]
316    pub export: ExportTask,
317}
318
319/// An export bundle task specifier.
320#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
321#[serde(rename_all = "kebab-case")]
322pub struct ExportBundleTask {
323    /// The shared export arguments.
324    #[serde(flatten)]
325    pub export: ExportTask,
326    /// Which pages to export in PDF documents. When unspecified, all pages are
327    /// exported.
328    #[serde(skip_serializing_if = "Option::is_none", default)]
329    pub pages: Option<Vec<Pages>>,
330    /// One (or multiple comma-separated) PDF standards that Typst will enforce
331    /// conformance with for PDF documents.
332    #[serde(skip_serializing_if = "Vec::is_empty", default)]
333    pub pdf_standards: Vec<PdfStandard>,
334    /// Disable tagged PDF output for PDF documents.
335    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
336    pub no_pdf_tags: bool,
337    /// The document's creation date formatted as a UNIX timestamp (in seconds).
338    #[serde(skip_serializing_if = "Option::is_none", default)]
339    pub creation_timestamp: Option<i64>,
340    /// The PPI (pixels per inch) to use for PNG documents.
341    pub ppi: Scalar,
342}
343
344/// An export markdown task specifier.
345#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
346#[serde(rename_all = "kebab-case")]
347pub struct ExportMarkdownTask {
348    /// The processor to use for the markdown export.
349    pub processor: Option<String>,
350    /// The path of external assets directory.
351    pub assets_path: Option<PathBuf>,
352    /// The shared export arguments.
353    #[serde(flatten)]
354    pub export: ExportTask,
355}
356
357/// An export TeX task specifier.
358#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
359#[serde(rename_all = "kebab-case")]
360pub struct ExportTeXTask {
361    /// The processor to use for the TeX export.
362    pub processor: Option<String>,
363    /// The path of external assets directory.
364    pub assets_path: Option<PathBuf>,
365    /// The shared export arguments.
366    #[serde(flatten)]
367    pub export: ExportTask,
368}
369
370/// An export text task specifier.
371#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
372#[serde(rename_all = "kebab-case")]
373pub struct ExportTextTask {
374    /// The shared export arguments.
375    #[serde(flatten)]
376    pub export: ExportTask,
377}
378
379/// An export query task specifier.
380#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
381#[serde(rename_all = "kebab-case")]
382pub struct QueryTask {
383    /// The shared export arguments.
384    #[serde(flatten)]
385    pub export: ExportTask,
386    /// The format to serialize in. Can be `json`, `yaml`, or `txt`,
387    pub format: String,
388    /// Uses a different output extension from the one inferring from the
389    /// [`Self::format`].
390    pub output_extension: Option<String>,
391    /// Defines which elements to retrieve.
392    pub selector: String,
393    /// Extracts just one field from all retrieved elements.
394    pub field: Option<String>,
395    /// Expects and retrieves exactly one element.
396    pub one: bool,
397}