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 extension of the artifact.
120    pub fn extension(&self) -> &str {
121        match self {
122            Self::ExportPdf { .. } => "pdf",
123            Self::Preview(..) | Self::ExportSvgHtml { .. } | Self::ExportHtml { .. } => "html",
124            Self::ExportMd { .. } => "md",
125            Self::ExportTeX { .. } => "tex",
126            Self::ExportText { .. } => "txt",
127            Self::ExportSvg { .. } => "svg",
128            Self::ExportPng { .. } => "png",
129            Self::Query(QueryTask {
130                format,
131                output_extension,
132                ..
133            }) => output_extension.as_deref().unwrap_or(format),
134        }
135    }
136}
137
138/// A preview task specifier.
139#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
140#[serde(rename_all = "kebab-case")]
141pub struct PreviewTask {
142    /// When to run the task. See [`TaskWhen`] for more
143    /// information.
144    pub when: TaskWhen,
145}
146
147/// An export task specifier.
148#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
149#[serde(rename_all = "kebab-case")]
150pub struct ExportTask {
151    /// When to run the task
152    pub when: TaskWhen,
153    /// The output path pattern.
154    pub output: Option<PathPattern>,
155    /// The task's transforms.
156    #[serde(skip_serializing_if = "Vec::is_empty", default)]
157    pub transform: Vec<ExportTransform>,
158}
159
160impl ExportTask {
161    /// Creates a new unmounted export task.
162    pub fn new(when: TaskWhen) -> Self {
163        Self {
164            when,
165            output: None,
166            transform: Vec::new(),
167        }
168    }
169
170    /// Pretty prints the output whenever possible.
171    pub fn apply_pretty(&mut self) {
172        self.transform
173            .push(ExportTransform::Pretty { script: None });
174    }
175}
176
177/// The legacy page selection specifier.
178#[derive(Default, Debug, Clone, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub enum PageSelection {
181    /// Selects the first page.
182    #[default]
183    First,
184    /// Merges all pages into a single page.
185    Merged {
186        /// The gap between pages (in pt).
187        gap: Option<String>,
188    },
189}
190
191/// A project export transform specifier.
192#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
193#[serde(rename_all = "kebab-case")]
194pub enum ExportTransform {
195    /// Only pick a subset of pages.
196    Pages {
197        /// The page ranges to export.
198        ranges: Vec<Pages>,
199    },
200    /// Merge pages into a single page.
201    Merge {
202        /// The gap between pages (typst code expression, e.g. `1pt`).
203        gap: Option<String>,
204    },
205    /// Execute a transform script.
206    Script {
207        /// The postprocess script (typst script) to run.
208        #[serde(skip_serializing_if = "Option::is_none", default)]
209        script: Option<String>,
210    },
211    /// Uses a pretty printer to format the output.
212    Pretty {
213        /// The pretty command (typst script) to run.
214        ///
215        /// If not provided, the default pretty printer will be used.
216        /// Note: the builtin one may be only effective for json outputs.
217        #[serde(skip_serializing_if = "Option::is_none", default)]
218        script: Option<String>,
219    },
220}
221
222/// An export pdf task specifier.
223#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
224#[serde(rename_all = "kebab-case")]
225pub struct ExportPdfTask {
226    /// The shared export arguments.
227    #[serde(flatten)]
228    pub export: ExportTask,
229    /// One (or multiple comma-separated) PDF standards that Typst will enforce
230    /// conformance with.
231    #[serde(skip_serializing_if = "Vec::is_empty", default)]
232    pub pdf_standards: Vec<PdfStandard>,
233    /// The document's creation date formatted as a UNIX timestamp (in seconds).
234    ///
235    /// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
236    #[serde(skip_serializing_if = "Option::is_none", default)]
237    pub creation_timestamp: Option<i64>,
238}
239
240/// An export png task specifier.
241#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
242#[serde(rename_all = "kebab-case")]
243pub struct ExportPngTask {
244    /// The shared export arguments.
245    #[serde(flatten)]
246    pub export: ExportTask,
247    /// The PPI (pixels per inch) to use for PNG export.
248    pub ppi: Scalar,
249    /// The expression constructing background fill color (in typst script).
250    /// e.g. `#ffffff`, `#000000`, `rgba(255, 255, 255, 0.5)`.
251    ///
252    /// If not provided, the default background color specified in the document
253    /// will be used.
254    #[serde(skip_serializing_if = "Option::is_none", default)]
255    pub fill: Option<String>,
256}
257
258/// An export svg task specifier.
259#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
260#[serde(rename_all = "kebab-case")]
261pub struct ExportSvgTask {
262    /// The shared export arguments.
263    #[serde(flatten)]
264    pub export: ExportTask,
265}
266
267/// An export html task specifier.
268#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
269#[serde(rename_all = "kebab-case")]
270pub struct ExportHtmlTask {
271    /// The shared export arguments.
272    #[serde(flatten)]
273    pub export: ExportTask,
274}
275
276/// An export markdown task specifier.
277#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
278#[serde(rename_all = "kebab-case")]
279pub struct ExportMarkdownTask {
280    /// The processor to use for the markdown export.
281    pub processor: Option<String>,
282    /// The path of external assets directory.
283    pub assets_path: Option<PathBuf>,
284    /// The shared export arguments.
285    #[serde(flatten)]
286    pub export: ExportTask,
287}
288
289/// An export TeX task specifier.
290#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
291#[serde(rename_all = "kebab-case")]
292pub struct ExportTeXTask {
293    /// The processor to use for the TeX export.
294    pub processor: Option<String>,
295    /// The path of external assets directory.
296    pub assets_path: Option<PathBuf>,
297    /// The shared export arguments.
298    #[serde(flatten)]
299    pub export: ExportTask,
300}
301
302/// An export text task specifier.
303#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
304#[serde(rename_all = "kebab-case")]
305pub struct ExportTextTask {
306    /// The shared export arguments.
307    #[serde(flatten)]
308    pub export: ExportTask,
309}
310
311/// An export query task specifier.
312#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
313#[serde(rename_all = "kebab-case")]
314pub struct QueryTask {
315    /// The shared export arguments.
316    #[serde(flatten)]
317    pub export: ExportTask,
318    /// The format to serialize in. Can be `json`, `yaml`, or `txt`,
319    pub format: String,
320    /// Uses a different output extension from the one inferring from the
321    /// [`Self::format`].
322    pub output_extension: Option<String>,
323    /// Defines which elements to retrieve.
324    pub selector: String,
325    /// Extracts just one field from all retrieved elements.
326    pub field: Option<String>,
327    /// Expects and retrieves exactly one element.
328    pub one: bool,
329}