Skip to main content

reflexo_typst2hast/
lib.rs

1use std::sync::Arc;
2
3use base64::Engine;
4use ecow::{eco_format, EcoString};
5use reflexo::typst::TypstHtmlDocument;
6use typst::diag::SourceResult;
7use typst::layout::Frame;
8use typst::syntax::Span;
9use typst_html::{HtmlElement, HtmlNode};
10
11use crate::hast::{HastElement, HastElementContent, HastText};
12
13pub mod hast;
14
15/// Encodes an HTML document into a Hast.
16pub fn hast(document: &Arc<TypstHtmlDocument>) -> SourceResult<HastElementContent> {
17    write_element(&document.root)
18}
19
20/// Encode an HTML node into the writer.
21fn write_node(node: &HtmlNode, buf: &mut Vec<HastElementContent>) -> SourceResult<()> {
22    match node {
23        HtmlNode::Tag(_) => {}
24        HtmlNode::Text(text, span) => {
25            buf.push(write_text(text, *span)?);
26        }
27        HtmlNode::Element(element) => {
28            buf.push(write_element(element)?);
29        }
30        HtmlNode::Frame(frame) => {
31            write_frame(&frame.inner, buf);
32        }
33    }
34    Ok(())
35}
36
37/// Encode plain text into the writer.
38fn write_text(text: &EcoString, _span: Span) -> SourceResult<HastElementContent> {
39    Ok(HastElementContent::Text(HastText {
40        value: EcoString::from(text),
41        // todo: span mapping
42    }))
43}
44
45/// Encode one element into the write.
46fn write_element(element: &HtmlElement) -> SourceResult<HastElementContent> {
47    write_element_with_tag(element, &element.tag.resolve())
48}
49
50/// Encode one element into the write.
51fn write_element_with_tag(element: &HtmlElement, tag: &str) -> SourceResult<HastElementContent> {
52    let properties = element
53        .attrs
54        .0
55        .iter()
56        .map(|(attr, value)| (attr.resolve().as_str().into(), value.clone()))
57        .collect();
58
59    // todo: ignored: tag::is_void(element.tag)
60
61    let mut buf = Vec::new();
62
63    if !element.children.is_empty() {
64        for c in &element.children {
65            write_node(c, &mut buf)?;
66        }
67    }
68
69    Ok(HastElementContent::Element(Box::new(HastElement {
70        tag_name: EcoString::from(tag),
71        properties,
72        children: buf,
73        data: None,
74    })))
75}
76
77/// Encode a laid out frame into the writer.
78fn write_frame(frame: &Frame, buf: &mut Vec<HastElementContent>) {
79    // FIXME: This string replacement is obviously a hack.
80    let svg = typst_svg::svg_frame(frame)
81        .replace("<svg class", "<svg style=\"overflow: visible;\" class");
82
83    // create a img base64
84    let base64_svg = base64::engine::general_purpose::STANDARD.encode(svg.as_bytes());
85
86    buf.push(HastElementContent::Element(Box::new(HastElement {
87        tag_name: EcoString::from("img"),
88        properties: std::collections::BTreeMap::from([
89            (EcoString::inline("class"), EcoString::inline("typst-frame")),
90            (EcoString::inline("data-typst-doc"), EcoString::inline("1")),
91            (
92                EcoString::inline("src"),
93                EcoString::from(format!("data:image/svg+xml;base64,{base64_svg}")),
94            ),
95        ]),
96        children: vec![],
97        data: Some(crate::hast::HastElementData {
98            // Provides a cheap hash
99            hash: Some(eco_format!(
100                "siphash128_13:{:016x}",
101                reflexo::hash::hash128(&frame)
102            )),
103        }),
104    })));
105}