tinymist_task/compute/
text.rs

1use core::fmt;
2use std::sync::Arc;
3use typst::html::{tag, HtmlNode::*};
4
5use crate::ExportTextTask;
6use tinymist_std::error::prelude::*;
7use tinymist_std::typst::{TypstDocument, TypstPagedDocument};
8use tinymist_world::{CompilerFeat, ExportComputation, WorldComputeGraph};
9
10pub struct TextExport;
11
12impl TextExport {
13    pub fn run_on_doc(doc: &TypstDocument) -> Result<String> {
14        Ok(format!("{}", FullTextDigest(doc)))
15    }
16}
17
18impl<F: CompilerFeat> ExportComputation<F, TypstPagedDocument> for TextExport {
19    type Output = String;
20    type Config = ExportTextTask;
21
22    fn run(
23        _g: &Arc<WorldComputeGraph<F>>,
24        doc: &Arc<TypstPagedDocument>,
25        _config: &ExportTextTask,
26    ) -> Result<String> {
27        Self::run_on_doc(&TypstDocument::Paged(doc.clone()))
28    }
29}
30
31/// A full text digest of a document.
32struct FullTextDigest<'a>(&'a TypstDocument);
33
34impl FullTextDigest<'_> {
35    fn export_frame(f: &mut fmt::Formatter<'_>, doc: &typst::layout::Frame) -> fmt::Result {
36        for (_, item) in doc.items() {
37            Self::export_item(f, item)?;
38        }
39        #[cfg(not(feature = "no-content-hint"))]
40        {
41            use std::fmt::Write;
42            let c = doc.content_hint();
43            if c != '\0' {
44                f.write_char(c)?;
45            }
46        }
47
48        Ok(())
49    }
50
51    fn export_item(f: &mut fmt::Formatter<'_>, item: &typst::layout::FrameItem) -> fmt::Result {
52        use typst::layout::FrameItem::*;
53        match item {
54            Group(g) => Self::export_frame(f, &g.frame),
55            Text(t) => f.write_str(t.text.as_str()),
56            Link(..) | Tag(..) | Shape(..) | Image(..) => Ok(()),
57        }
58    }
59
60    fn export_element(f: &mut fmt::Formatter<'_>, elem: &typst::html::HtmlElement) -> fmt::Result {
61        for child in elem.children.iter() {
62            Self::export_html_node(f, child)?;
63        }
64        Ok(())
65    }
66
67    fn export_html_node(f: &mut fmt::Formatter<'_>, node: &typst::html::HtmlNode) -> fmt::Result {
68        match node {
69            Tag(_) => Ok(()),
70            Element(elem) => {
71                // Skips certain tags that do not contribute to text content.
72                if matches!(elem.tag, tag::style | tag::script) {
73                    Ok(())
74                } else {
75                    Self::export_element(f, elem)
76                }
77            }
78            Text(t, _) => f.write_str(t.as_str()),
79            Frame(frame) => Self::export_frame(f, frame),
80        }
81    }
82}
83
84impl fmt::Display for FullTextDigest<'_> {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match &self.0 {
87            TypstDocument::Paged(paged_doc) => {
88                for page in paged_doc.pages.iter() {
89                    Self::export_frame(f, &page.frame)?;
90                }
91                Ok(())
92            }
93            TypstDocument::Html(html_doc) => {
94                Self::export_element(f, &html_doc.root)?;
95                Ok(())
96            }
97        }
98    }
99}