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 HTML task.
72    ExportSvgHtml(ExportHtmlTask),
73    /// An export Markdown task.
74    ExportMd(ExportMarkdownTask),
75    /// An export TeX task.
76    ExportTeX(ExportTeXTask),
77    /// An export Text task.
78    ExportText(ExportTextTask),
79    /// An query task.
80    Query(QueryTask),
81    // todo: compatibility
82    // An export task of another type.
83    // Other(serde_json::Value),
84}
85
86impl ProjectTask {
87    /// Returns the timing of executing the task.
88    pub fn when(&self) -> Option<&TaskWhen> {
89        Some(match self {
90            Self::Preview(task) => &task.when,
91            Self::ExportPdf(..)
92            | Self::ExportPng(..)
93            | Self::ExportSvg(..)
94            | Self::ExportHtml(..)
95            | Self::ExportSvgHtml(..)
96            | Self::ExportMd(..)
97            | Self::ExportTeX(..)
98            | Self::ExportText(..)
99            | Self::Query(..) => &self.as_export()?.when,
100        })
101    }
102
103    /// Returns the export configuration of a task.
104    pub fn as_export(&self) -> Option<&ExportTask> {
105        Some(match self {
106            Self::Preview(..) => return None,
107            Self::ExportPdf(task) => &task.export,
108            Self::ExportPng(task) => &task.export,
109            Self::ExportSvg(task) => &task.export,
110            Self::ExportHtml(task) => &task.export,
111            Self::ExportSvgHtml(task) => &task.export,
112            Self::ExportTeX(task) => &task.export,
113            Self::ExportMd(task) => &task.export,
114            Self::ExportText(task) => &task.export,
115            Self::Query(task) => &task.export,
116        })
117    }
118
119    /// Returns the export configuration of a task.
120    pub fn as_export_mut(&mut self) -> Option<&mut ExportTask> {
121        Some(match self {
122            Self::Preview(..) => return None,
123            Self::ExportPdf(task) => &mut task.export,
124            Self::ExportPng(task) => &mut task.export,
125            Self::ExportSvg(task) => &mut task.export,
126            Self::ExportHtml(task) => &mut task.export,
127            Self::ExportSvgHtml(task) => &mut task.export,
128            Self::ExportTeX(task) => &mut task.export,
129            Self::ExportMd(task) => &mut task.export,
130            Self::ExportText(task) => &mut task.export,
131            Self::Query(task) => &mut task.export,
132        })
133    }
134
135    /// Returns extension of the artifact.
136    pub fn extension(&self) -> &str {
137        match self {
138            Self::ExportPdf { .. } => "pdf",
139            Self::Preview(..) | Self::ExportSvgHtml { .. } | Self::ExportHtml { .. } => "html",
140            Self::ExportMd { .. } => "md",
141            Self::ExportTeX { .. } => "tex",
142            Self::ExportText { .. } => "txt",
143            Self::ExportSvg { .. } => "svg",
144            Self::ExportPng { .. } => "png",
145            Self::Query(QueryTask {
146                format,
147                output_extension,
148                ..
149            }) => output_extension.as_deref().unwrap_or(format),
150        }
151    }
152}
153
154/// A preview task specifier.
155#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
156#[serde(rename_all = "kebab-case")]
157pub struct PreviewTask {
158    /// When to run the task. See [`TaskWhen`] for more
159    /// information.
160    pub when: TaskWhen,
161}
162
163/// An export task specifier.
164#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
165#[serde(rename_all = "kebab-case")]
166pub struct ExportTask {
167    /// When to run the task
168    pub when: TaskWhen,
169    /// The output path pattern.
170    pub output: Option<PathPattern>,
171    /// The task's transforms.
172    #[serde(skip_serializing_if = "Vec::is_empty", default)]
173    pub transform: Vec<ExportTransform>,
174}
175
176impl ExportTask {
177    /// Creates a new unmounted export task.
178    pub fn new(when: TaskWhen) -> Self {
179        Self {
180            when,
181            output: None,
182            transform: Vec::new(),
183        }
184    }
185
186    /// Pretty prints the output whenever possible.
187    pub fn apply_pretty(&mut self) {
188        self.transform
189            .push(ExportTransform::Pretty { script: None });
190    }
191}
192
193/// A page merge specifier.
194#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
195#[serde(default)]
196pub struct PageMerge {
197    /// The gap between pages (in pt).
198    pub gap: Option<String>,
199}
200
201/// A project export transform specifier.
202#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
203#[serde(rename_all = "kebab-case")]
204pub enum ExportTransform {
205    /// Only pick a subset of pages.
206    Pages {
207        /// The page ranges to export.
208        ranges: Vec<Pages>,
209    },
210    /// Merge pages into a single page.
211    Merge {
212        /// The gap between pages (typst code expression, e.g. `1pt`).
213        gap: Option<String>,
214    },
215    /// Execute a transform script.
216    Script {
217        /// The postprocess script (typst script) to run.
218        #[serde(skip_serializing_if = "Option::is_none", default)]
219        script: Option<String>,
220    },
221    /// Uses a pretty printer to format the output.
222    Pretty {
223        /// The pretty command (typst script) to run.
224        ///
225        /// If not provided, the default pretty printer will be used.
226        /// Note: the builtin one may be only effective for json outputs.
227        #[serde(skip_serializing_if = "Option::is_none", default)]
228        script: Option<String>,
229    },
230}
231
232/// An export pdf task specifier.
233#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
234#[serde(rename_all = "kebab-case")]
235pub struct ExportPdfTask {
236    /// The shared export arguments.
237    #[serde(flatten)]
238    pub export: ExportTask,
239    /// Which pages to export. When unspecified, all pages are exported.
240    #[serde(skip_serializing_if = "Option::is_none", default)]
241    pub pages: Option<Vec<Pages>>,
242    /// One (or multiple comma-separated) PDF standards that Typst will enforce
243    /// conformance with.
244    #[serde(skip_serializing_if = "Vec::is_empty", default)]
245    pub pdf_standards: Vec<PdfStandard>,
246    /// The document's creation date formatted as a UNIX timestamp (in seconds).
247    ///
248    /// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
249    #[serde(skip_serializing_if = "Option::is_none", default)]
250    pub creation_timestamp: Option<i64>,
251}
252
253/// An export png task specifier.
254#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
255#[serde(rename_all = "kebab-case")]
256pub struct ExportPngTask {
257    /// The shared export arguments.
258    #[serde(flatten)]
259    pub export: ExportTask,
260    /// Which pages to export. When unspecified, all pages are exported.
261    #[serde(skip_serializing_if = "Option::is_none", default)]
262    pub pages: Option<Vec<Pages>>,
263    /// The page template to use for multiple pages.
264    #[serde(skip_serializing_if = "Option::is_none", default)]
265    pub page_number_template: Option<String>,
266    /// The page merge specifier.
267    #[serde(skip_serializing_if = "Option::is_none", default)]
268    pub merge: Option<PageMerge>,
269    /// The PPI (pixels per inch) to use for PNG export.
270    pub ppi: Scalar,
271    /// The expression constructing background fill color (in typst script).
272    /// e.g. `#ffffff`, `#000000`, `rgba(255, 255, 255, 0.5)`.
273    ///
274    /// If not provided, the default background color specified in the document
275    /// will be used.
276    #[serde(skip_serializing_if = "Option::is_none", default)]
277    pub fill: Option<String>,
278}
279
280/// An export svg task specifier.
281#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
282#[serde(rename_all = "kebab-case")]
283pub struct ExportSvgTask {
284    /// The shared export arguments.
285    #[serde(flatten)]
286    pub export: ExportTask,
287    /// The page template to use for multiple pages.
288    #[serde(skip_serializing_if = "Option::is_none", default)]
289    pub page_number_template: Option<String>,
290    /// Which pages to export. When unspecified, all pages are exported.
291    #[serde(skip_serializing_if = "Option::is_none", default)]
292    pub pages: Option<Vec<Pages>>,
293    /// The page merge specifier.
294    #[serde(skip_serializing_if = "Option::is_none", default)]
295    pub merge: Option<PageMerge>,
296}
297
298/// An export html task specifier.
299#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
300#[serde(rename_all = "kebab-case")]
301pub struct ExportHtmlTask {
302    /// The shared export arguments.
303    #[serde(flatten)]
304    pub export: ExportTask,
305}
306
307/// An export markdown task specifier.
308#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
309#[serde(rename_all = "kebab-case")]
310pub struct ExportMarkdownTask {
311    /// The processor to use for the markdown export.
312    pub processor: Option<String>,
313    /// The path of external assets directory.
314    pub assets_path: Option<PathBuf>,
315    /// The shared export arguments.
316    #[serde(flatten)]
317    pub export: ExportTask,
318}
319
320/// An export TeX task specifier.
321#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
322#[serde(rename_all = "kebab-case")]
323pub struct ExportTeXTask {
324    /// The processor to use for the TeX export.
325    pub processor: Option<String>,
326    /// The path of external assets directory.
327    pub assets_path: Option<PathBuf>,
328    /// The shared export arguments.
329    #[serde(flatten)]
330    pub export: ExportTask,
331}
332
333/// An export text task specifier.
334#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
335#[serde(rename_all = "kebab-case")]
336pub struct ExportTextTask {
337    /// The shared export arguments.
338    #[serde(flatten)]
339    pub export: ExportTask,
340}
341
342/// An export query task specifier.
343#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
344#[serde(rename_all = "kebab-case")]
345pub struct QueryTask {
346    /// The shared export arguments.
347    #[serde(flatten)]
348    pub export: ExportTask,
349    /// The format to serialize in. Can be `json`, `yaml`, or `txt`,
350    pub format: String,
351    /// Uses a different output extension from the one inferring from the
352    /// [`Self::format`].
353    pub output_extension: Option<String>,
354    /// Defines which elements to retrieve.
355    pub selector: String,
356    /// Extracts just one field from all retrieved elements.
357    pub field: Option<String>,
358    /// Expects and retrieves exactly one element.
359    pub one: bool,
360}