tinymist_task/
compute.rs

1//! The computations for the tasks.
2
3use std::str::FromStr;
4use std::sync::Arc;
5
6use comemo::Track;
7use ecow::EcoString;
8use tinymist_std::error::prelude::*;
9use tinymist_std::typst::{TypstDocument, TypstHtmlDocument, TypstPagedDocument};
10use tinymist_world::{CompileSnapshot, CompilerFeat, ExportComputation, WorldComputeGraph};
11use typst::World;
12use typst::diag::{SourceResult, StrResult};
13use typst::foundations::{Bytes, Content, IntoValue, LocatableSelector, Scope, Value};
14use typst::layout::Abs;
15use typst::routines::EvalMode;
16use typst::syntax::{Span, SyntaxNode, ast};
17use typst::visualize::Color;
18use typst_eval::eval_string;
19
20use crate::model::{ExportHtmlTask, ExportPngTask, ExportSvgTask};
21use crate::primitives::TaskWhen;
22use crate::{ExportTransform, Pages, QueryTask};
23
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 computation for svg export.
56pub struct SvgExport;
57
58impl<F: CompilerFeat> ExportComputation<F, TypstPagedDocument> for SvgExport {
59    type Output = String;
60    type Config = ExportSvgTask;
61
62    fn run(
63        _graph: &Arc<WorldComputeGraph<F>>,
64        doc: &Arc<TypstPagedDocument>,
65        config: &ExportSvgTask,
66    ) -> Result<String> {
67        let (is_first, merged_gap) = get_page_selection(&config.export)?;
68
69        let first_page = doc.pages.first();
70
71        Ok(if is_first {
72            if let Some(first_page) = first_page {
73                typst_svg::svg(first_page)
74            } else {
75                typst_svg::svg_merged(doc, merged_gap)
76            }
77        } else {
78            typst_svg::svg_merged(doc, merged_gap)
79        })
80    }
81}
82
83// impl<F: CompilerFeat> WorldComputable<F> for SvgExport {
84//     type Output = Option<String>;
85
86//     fn compute(graph: &Arc<WorldComputeGraph<F>>) -> Result<Self::Output> {
87//         OptionDocumentTask::run_export::<F, Self>(graph)
88//     }
89// }
90
91/// The computation for png export.
92pub struct PngExport;
93
94impl<F: CompilerFeat> ExportComputation<F, TypstPagedDocument> for PngExport {
95    type Output = Bytes;
96    type Config = ExportPngTask;
97
98    fn run(
99        _graph: &Arc<WorldComputeGraph<F>>,
100        doc: &Arc<TypstPagedDocument>,
101        config: &ExportPngTask,
102    ) -> Result<Bytes> {
103        let ppi = config.ppi.to_f32();
104        if ppi <= 1e-6 {
105            tinymist_std::bail!("invalid ppi: {ppi}");
106        }
107
108        let fill = if let Some(fill) = &config.fill {
109            parse_color(fill.clone()).map_err(|err| anyhow::anyhow!("invalid fill ({err})"))?
110        } else {
111            Color::WHITE
112        };
113
114        let (is_first, merged_gap) = get_page_selection(&config.export)?;
115
116        let ppp = ppi / 72.;
117        let pixmap = if is_first {
118            if let Some(first_page) = doc.pages.first() {
119                typst_render::render(first_page, ppp)
120            } else {
121                typst_render::render_merged(doc, ppp, merged_gap, Some(fill))
122            }
123        } else {
124            typst_render::render_merged(doc, ppp, merged_gap, Some(fill))
125        };
126
127        pixmap
128            .encode_png()
129            .map(Bytes::new)
130            .context_ut("failed to encode PNG")
131    }
132}
133
134// impl<F: CompilerFeat> WorldComputable<F> for PngExport {
135//     type Output = Option<Bytes>;
136
137//     fn compute(graph: &Arc<WorldComputeGraph<F>>) -> Result<Self::Output> {
138//         OptionDocumentTask::run_export::<F, Self>(graph)
139//     }
140// }
141
142/// The computation for html export.
143pub struct HtmlExport;
144
145impl<F: CompilerFeat> ExportComputation<F, TypstHtmlDocument> for HtmlExport {
146    type Output = String;
147    type Config = ExportHtmlTask;
148
149    fn run(
150        _graph: &Arc<WorldComputeGraph<F>>,
151        doc: &Arc<TypstHtmlDocument>,
152        _config: &ExportHtmlTask,
153    ) -> Result<String> {
154        Ok(typst_html::html(doc)?)
155    }
156}
157
158// impl<F: CompilerFeat> WorldComputable<F> for HtmlExport {
159//     type Output = Option<String>;
160
161//     fn compute(graph: &Arc<WorldComputeGraph<F>>) -> Result<Self::Output> {
162//         OptionDocumentTask::run_export::<F, Self>(graph)
163//     }
164// }
165
166/// The computation for document query.
167pub struct DocumentQuery;
168
169impl DocumentQuery {
170    // todo: query exporter
171    /// Retrieve the matches for the selector.
172    pub fn retrieve<D: typst::Document>(
173        world: &dyn World,
174        selector: &str,
175        document: &D,
176    ) -> StrResult<Vec<Content>> {
177        let selector = eval_string(
178            &typst::ROUTINES,
179            world.track(),
180            selector,
181            Span::detached(),
182            EvalMode::Code,
183            Scope::default(),
184        )
185        .map_err(|errors| {
186            let mut message = EcoString::from("failed to evaluate selector");
187            for (i, error) in errors.into_iter().enumerate() {
188                message.push_str(if i == 0 { ": " } else { ", " });
189                message.push_str(&error.message);
190            }
191            message
192        })?
193        .cast::<LocatableSelector>()
194        .map_err(|e| EcoString::from(format!("failed to cast: {}", e.message())))?;
195
196        Ok(document
197            .introspector()
198            .query(&selector.0)
199            .into_iter()
200            .collect::<Vec<_>>())
201    }
202
203    fn run_inner<F: CompilerFeat, D: typst::Document>(
204        g: &Arc<WorldComputeGraph<F>>,
205        doc: &Arc<D>,
206        config: &QueryTask,
207    ) -> Result<Vec<Value>> {
208        let selector = &config.selector;
209        let elements = Self::retrieve(&g.snap.world, selector, doc.as_ref())
210            .map_err(|e| anyhow::anyhow!("failed to retrieve: {e}"))?;
211        if config.one && elements.len() != 1 {
212            bail!("expected exactly one element, found {}", elements.len());
213        }
214
215        Ok(elements
216            .into_iter()
217            .filter_map(|c| match &config.field {
218                Some(field) => c.get_by_name(field).ok(),
219                _ => Some(c.into_value()),
220            })
221            .collect())
222    }
223
224    /// Queries the document and returns the result as a value.
225    pub fn doc_get_as_value<F: CompilerFeat>(
226        g: &Arc<WorldComputeGraph<F>>,
227        doc: &TypstDocument,
228        config: &QueryTask,
229    ) -> Result<serde_json::Value> {
230        match doc {
231            TypstDocument::Paged(doc) => Self::get_as_value(g, doc, config),
232            TypstDocument::Html(doc) => Self::get_as_value(g, doc, config),
233        }
234    }
235
236    /// Queries the document and returns the result as a value.
237    pub fn get_as_value<F: CompilerFeat, D: typst::Document>(
238        g: &Arc<WorldComputeGraph<F>>,
239        doc: &Arc<D>,
240        config: &QueryTask,
241    ) -> Result<serde_json::Value> {
242        let mapped = Self::run_inner(g, doc, config)?;
243
244        let res = if config.one {
245            let Some(value) = mapped.first() else {
246                bail!("no such field found for element");
247            };
248            serde_json::to_value(value)
249        } else {
250            serde_json::to_value(&mapped)
251        };
252
253        res.context("failed to serialize")
254    }
255}
256
257impl<F: CompilerFeat, D: typst::Document> ExportComputation<F, D> for DocumentQuery {
258    type Output = SourceResult<String>;
259    type Config = QueryTask;
260
261    fn run(
262        g: &Arc<WorldComputeGraph<F>>,
263        doc: &Arc<D>,
264        config: &QueryTask,
265    ) -> Result<SourceResult<String>> {
266        let pretty = false;
267        let mapped = Self::run_inner(g, doc, config)?;
268
269        let res = if config.one {
270            let Some(value) = mapped.first() else {
271                bail!("no such field found for element");
272            };
273            serialize(value, &config.format, pretty)
274        } else {
275            serialize(&mapped, &config.format, pretty)
276        };
277
278        res.map(Ok)
279    }
280}
281
282/// Serialize data to the output format.
283fn serialize(data: &impl serde::Serialize, format: &str, pretty: bool) -> Result<String> {
284    Ok(match format {
285        "json" if pretty => serde_json::to_string_pretty(data).context("serialize query")?,
286        "json" => serde_json::to_string(data).context("serialize query")?,
287        "yaml" => serde_yaml::to_string(&data).context_ut("serialize query")?,
288        "txt" => {
289            use serde_json::Value::*;
290            let value = serde_json::to_value(data).context("serialize query")?;
291            match value {
292                String(s) => s,
293                _ => {
294                    let kind = match value {
295                        Null => "null",
296                        Bool(_) => "boolean",
297                        Number(_) => "number",
298                        String(_) => "string",
299                        Array(_) => "array",
300                        Object(_) => "object",
301                    };
302                    bail!("expected a string value for format: {format}, got {kind}")
303                }
304            }
305        }
306        _ => bail!("unsupported format for query: {format}"),
307    })
308}
309
310/// Gets legacy page selection
311pub fn get_page_selection(task: &crate::ExportTask) -> Result<(bool, Abs)> {
312    let is_first = task
313        .transform
314        .iter()
315        .any(|t| matches!(t, ExportTransform::Pages { ranges, .. } if ranges == &[Pages::FIRST]));
316
317    let mut gap_res = Abs::default();
318    if !is_first {
319        for trans in &task.transform {
320            if let ExportTransform::Merge { gap } = trans {
321                let gap = gap
322                    .as_deref()
323                    .map(parse_length)
324                    .transpose()
325                    .context_ut("failed to parse gap")?;
326                gap_res = gap.unwrap_or_default();
327            }
328        }
329    }
330
331    Ok((is_first, gap_res))
332}
333
334fn parse_length(gap: &str) -> Result<Abs> {
335    let length = typst::syntax::parse_code(gap);
336    if length.erroneous() {
337        bail!("invalid length: {gap}, errors: {:?}", length.errors());
338    }
339
340    let length: Option<ast::Numeric> = descendants(&length).into_iter().find_map(SyntaxNode::cast);
341
342    let Some(length) = length else {
343        bail!("not a length: {gap}");
344    };
345
346    let (value, unit) = length.get();
347    match unit {
348        ast::Unit::Pt => Ok(Abs::pt(value)),
349        ast::Unit::Mm => Ok(Abs::mm(value)),
350        ast::Unit::Cm => Ok(Abs::cm(value)),
351        ast::Unit::In => Ok(Abs::inches(value)),
352        _ => bail!("invalid unit: {unit:?} in {gap}"),
353    }
354}
355
356/// Low performance but simple recursive iterator.
357fn descendants(node: &SyntaxNode) -> impl IntoIterator<Item = &SyntaxNode> + '_ {
358    let mut res = vec![];
359    for child in node.children() {
360        res.push(child);
361        res.extend(descendants(child));
362    }
363
364    res
365}
366
367fn parse_color(fill: String) -> anyhow::Result<Color> {
368    match fill.as_str() {
369        "black" => Ok(Color::BLACK),
370        "white" => Ok(Color::WHITE),
371        "red" => Ok(Color::RED),
372        "green" => Ok(Color::GREEN),
373        "blue" => Ok(Color::BLUE),
374        hex if hex.starts_with('#') => {
375            Color::from_str(&hex[1..]).map_err(|e| anyhow::anyhow!("failed to parse color: {e}"))
376        }
377        _ => anyhow::bail!("invalid color: {fill}"),
378    }
379}
380
381#[cfg(test)]
382mod tests {
383
384    use super::*;
385
386    #[test]
387    fn test_parse_color() {
388        assert_eq!(parse_color("black".to_owned()).unwrap(), Color::BLACK);
389        assert_eq!(parse_color("white".to_owned()).unwrap(), Color::WHITE);
390        assert_eq!(parse_color("red".to_owned()).unwrap(), Color::RED);
391        assert_eq!(parse_color("green".to_owned()).unwrap(), Color::GREEN);
392        assert_eq!(parse_color("blue".to_owned()).unwrap(), Color::BLUE);
393        assert_eq!(
394            parse_color("#000000".to_owned()).unwrap().to_hex(),
395            "#000000"
396        );
397        assert_eq!(
398            parse_color("#ffffff".to_owned()).unwrap().to_hex(),
399            "#ffffff"
400        );
401        assert_eq!(
402            parse_color("#000000cc".to_owned()).unwrap().to_hex(),
403            "#000000cc"
404        );
405        assert!(parse_color("invalid".to_owned()).is_err());
406    }
407
408    #[test]
409    fn test_parse_length() {
410        assert_eq!(parse_length("1pt").unwrap(), Abs::pt(1.));
411        assert_eq!(parse_length("1mm").unwrap(), Abs::mm(1.));
412        assert_eq!(parse_length("1cm").unwrap(), Abs::cm(1.));
413        assert_eq!(parse_length("1in").unwrap(), Abs::inches(1.));
414        assert!(parse_length("1").is_err());
415        assert!(parse_length("1px").is_err());
416    }
417}