tinymist_task/
compute.rs

1//! The computations for the tasks.
2
3use std::str::FromStr;
4use std::sync::Arc;
5
6use tinymist_std::error::prelude::*;
7use tinymist_std::typst::TypstPagedDocument;
8use tinymist_world::{CompileSnapshot, CompilerFeat, ExportComputation, WorldComputeGraph};
9use typst::foundations::Bytes;
10use typst::layout::Abs;
11use typst::model::Document;
12use typst::syntax::{SyntaxNode, ast};
13use typst::visualize::Color;
14use typst_layout::Page;
15
16use crate::{Pages, TaskWhen, exported_page_ranges};
17
18mod html;
19pub use html::*;
20mod png;
21pub use png::*;
22mod query;
23pub use query::*;
24mod svg;
25pub use svg::*;
26#[cfg(feature = "pdf")]
27pub mod pdf;
28#[cfg(feature = "pdf")]
29pub use pdf::*;
30#[cfg(feature = "text")]
31pub mod text;
32#[cfg(feature = "text")]
33pub use text::*;
34
35/// The flag indicating that the svg export is needed.
36pub struct SvgFlag;
37/// The flag indicating that the png export is needed.
38pub struct PngFlag;
39/// The flag indicating that the html export is needed.
40pub struct HtmlFlag;
41
42/// The computation to check if the export is needed.
43pub struct ExportTimings;
44
45impl ExportTimings {
46    /// Checks if the export is needed.
47    pub fn needs_run<F: CompilerFeat, D: Document>(
48        snap: &CompileSnapshot<F>,
49        timing: Option<&TaskWhen>,
50        docs: Option<&D>,
51    ) -> Option<bool> {
52        snap.signal
53            .should_run_task(timing.unwrap_or(&TaskWhen::Never), docs)
54    }
55}
56
57/// The output of image exports, either paged or merged.
58pub enum ImageOutput<T> {
59    /// Each page exported separately.
60    Paged(Vec<PagedOutput<T>>),
61    /// All pages merged into one output.
62    Merged(T),
63}
64
65/// The output of a single page.
66pub struct PagedOutput<T> {
67    /// The page number (0-based).
68    pub page: usize,
69    /// The value of the page.
70    pub value: T,
71}
72
73fn select_pages<'a>(
74    document: &'a TypstPagedDocument,
75    pages: &Option<Vec<Pages>>,
76) -> Vec<(usize, &'a Page)> {
77    let pages = pages.as_ref().map(|pages| exported_page_ranges(pages));
78    document
79        .pages()
80        .iter()
81        .enumerate()
82        .filter(|(i, _)| {
83            pages
84                .as_ref()
85                .is_none_or(|exported_page_ranges| exported_page_ranges.includes_page_index(*i))
86        })
87        .collect::<Vec<_>>()
88}
89
90fn parse_length(gap: &str) -> Result<Abs> {
91    let length = typst::syntax::parse_code(gap);
92    if length.diagnosis().errors {
93        bail!(
94            "invalid length: {gap}, errors: {:?}",
95            length.errors_and_warnings().0
96        );
97    }
98
99    let length: Option<ast::Numeric> = descendants(&length).into_iter().find_map(SyntaxNode::cast);
100
101    let Some(length) = length else {
102        bail!("not a length: {gap}");
103    };
104
105    let (value, unit) = length.get();
106    match unit {
107        ast::Unit::Pt => Ok(Abs::pt(value)),
108        ast::Unit::Mm => Ok(Abs::mm(value)),
109        ast::Unit::Cm => Ok(Abs::cm(value)),
110        ast::Unit::In => Ok(Abs::inches(value)),
111        _ => bail!("invalid unit: {unit:?} in {gap}"),
112    }
113}
114
115/// Low performance but simple recursive iterator.
116fn descendants(node: &SyntaxNode) -> impl IntoIterator<Item = &SyntaxNode> + '_ {
117    let mut res = vec![];
118    for child in node.children() {
119        res.push(child);
120        res.extend(descendants(child));
121    }
122
123    res
124}
125
126fn parse_color(fill: &str) -> anyhow::Result<Color> {
127    match fill {
128        "black" => Ok(Color::BLACK),
129        "white" => Ok(Color::WHITE),
130        "red" => Ok(Color::RED),
131        "green" => Ok(Color::GREEN),
132        "blue" => Ok(Color::BLUE),
133        hex if hex.starts_with('#') => {
134            Color::from_str(&hex[1..]).map_err(|e| anyhow::anyhow!("failed to parse color: {e}"))
135        }
136        _ => anyhow::bail!("invalid color: {fill}"),
137    }
138}
139
140#[cfg(test)]
141mod tests {
142
143    use super::*;
144
145    #[test]
146    fn test_parse_color() {
147        assert_eq!(parse_color("black").unwrap(), Color::BLACK);
148        assert_eq!(parse_color("white").unwrap(), Color::WHITE);
149        assert_eq!(parse_color("red").unwrap(), Color::RED);
150        assert_eq!(parse_color("green").unwrap(), Color::GREEN);
151        assert_eq!(parse_color("blue").unwrap(), Color::BLUE);
152        assert_eq!(parse_color("#000000").unwrap().to_hex(), "#000000");
153        assert_eq!(parse_color("#ffffff").unwrap().to_hex(), "#ffffff");
154        assert_eq!(parse_color("#000000cc").unwrap().to_hex(), "#000000cc");
155        assert!(parse_color("invalid").is_err());
156    }
157
158    #[test]
159    fn test_parse_length() {
160        assert_eq!(parse_length("1pt").unwrap(), Abs::pt(1.));
161        assert_eq!(parse_length("1mm").unwrap(), Abs::mm(1.));
162        assert_eq!(parse_length("1cm").unwrap(), Abs::cm(1.));
163        assert_eq!(parse_length("1in").unwrap(), Abs::inches(1.));
164        assert!(parse_length("1").is_err());
165        assert!(parse_length("1px").is_err());
166    }
167}