tinymist_task/
compute.rs

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