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