Skip to main content

typst_batch/html/
document.rs

1//! HTML document wrapper.
2
3use typst::foundations::{Label, Selector};
4use typst::introspection::MetadataElem;
5use typst::utils::PicoStr;
6
7use super::HtmlElement;
8#[cfg(feature = "svg")]
9use super::HtmlFrame;
10
11#[cfg(all(feature = "svg", feature = "batch"))]
12use rayon::prelude::*;
13
14/// A compiled HTML document.
15///
16/// This is a stable wrapper around the internal typst HTML document type.
17#[derive(Debug, Clone)]
18pub struct HtmlDocument(pub(crate) typst_html::HtmlDocument);
19
20impl HtmlDocument {
21    /// Create a new HtmlDocument from a typst HtmlDocument.
22    #[inline]
23    pub fn new(doc: typst_html::HtmlDocument) -> Self {
24        Self(doc)
25    }
26
27    /// Get the root element of the document.
28    #[inline]
29    pub fn root(&self) -> HtmlElement<'_> {
30        HtmlElement(&self.0.root)
31    }
32
33    /// Query metadata by label name.
34    ///
35    /// In Typst: `#metadata((title: "Hello")) <my-meta>`
36    ///
37    /// ```ignore
38    /// let meta = doc.query_metadata("my-meta");
39    /// ```
40    pub fn query_metadata(&self, label: &str) -> Option<serde_json::Value> {
41        let label = Label::new(PicoStr::intern(label))?;
42        let elem = self
43            .0
44            .introspector
45            .query_unique(&Selector::Label(label))
46            .ok()?;
47        metadata_to_json(&elem)
48    }
49
50    /// Query all metadata values by label name, preserving document order.
51    ///
52    /// Unlike [`query_metadata`](Self::query_metadata), this method supports
53    /// labels that occur multiple times in the same document.
54    ///
55    /// In Typst:
56    /// `#metadata((id: 1)) <item>`
57    /// `#metadata((id: 2)) <item>`
58    ///
59    /// ```ignore
60    /// let all = doc.query_metadata_all("item");
61    /// assert_eq!(all.len(), 2);
62    /// ```
63    pub fn query_metadata_all(&self, label: &str) -> Vec<serde_json::Value> {
64        let Some(label) = Label::new(PicoStr::intern(label)) else {
65            return Vec::new();
66        };
67
68        self.0
69            .introspector
70            .query(&Selector::Label(label))
71            .iter()
72            .filter_map(metadata_to_json)
73            .collect()
74    }
75
76    /// Render a frame to SVG.
77    #[cfg(feature = "svg")]
78    pub(crate) fn render_frame_svg(&self, frame: &HtmlFrame<'_>) -> String {
79        self.render_raw_frame_svg(frame.0)
80    }
81
82    /// Render a frame to SVG (internal, takes raw typst frame).
83    #[cfg(feature = "svg")]
84    #[inline]
85    fn render_raw_frame_svg(&self, frame: &typst_html::HtmlFrame) -> String {
86        typst_svg::svg_html_frame(
87            &frame.inner,
88            frame.text_size,
89            frame.id.as_deref(),
90            &frame.link_points,
91            &self.0.introspector,
92        )
93    }
94
95    /// Render multiple frames to SVG.
96    ///
97    /// When the `batch` feature is enabled, frames are rendered in parallel
98    /// using rayon. Otherwise, they are rendered sequentially.
99    ///
100    /// This is the recommended way to render multiple frames, as it
101    /// automatically uses the best strategy based on available features.
102    ///
103    /// # Arguments
104    ///
105    /// * `frames` - Slice of frames to render
106    ///
107    /// # Returns
108    ///
109    /// Vector of SVG strings in the same order as input frames.
110    ///
111    /// # Example
112    ///
113    /// ```ignore
114    /// let frames: Vec<_> = collect_frames(&doc);
115    /// let svgs = doc.render_frames(&frames);
116    /// ```
117    #[cfg(feature = "svg")]
118    pub fn render_frames(&self, frames: &[HtmlFrame<'_>]) -> Vec<String> {
119        #[cfg(feature = "batch")]
120        {
121            frames
122                .par_iter()
123                .map(|frame| self.render_raw_frame_svg(frame.0))
124                .collect()
125        }
126
127        #[cfg(not(feature = "batch"))]
128        {
129            frames
130                .iter()
131                .map(|frame| self.render_raw_frame_svg(frame.0))
132                .collect()
133        }
134    }
135
136    /// Get the inner typst document.
137    #[inline]
138    pub fn into_inner(self) -> typst_html::HtmlDocument {
139        self.0
140    }
141
142    /// Get a reference to the inner typst document.
143    #[inline]
144    pub fn as_inner(&self) -> &typst_html::HtmlDocument {
145        &self.0
146    }
147}
148
149#[inline]
150fn metadata_to_json(elem: &typst::foundations::Content) -> Option<serde_json::Value> {
151    elem.to_packed::<MetadataElem>()
152        .and_then(|meta| serde_json::to_value(&meta.value).ok())
153}
154
155impl From<typst_html::HtmlDocument> for HtmlDocument {
156    fn from(doc: typst_html::HtmlDocument) -> Self {
157        Self::new(doc)
158    }
159}