1#[path = "export.rs"]
4mod export_;
5mod introspect;
6mod link;
7
8use crate::introspect::BundleIntrospector;
9
10pub use self::export_::{BundleOptions, VirtualFs, export};
11
12use std::collections::hash_map::Entry;
13use std::sync::Arc;
14
15use comemo::{Tracked, TrackedMut};
16use ecow::{EcoString, EcoVec};
17use indexmap::IndexMap;
18use rustc_hash::{FxBuildHasher, FxHashMap};
19use typst_html::HtmlDocument;
20use typst_layout::PagedDocument;
21use typst_library::diag::{At, CollectCombinedResult, SourceResult, bail, error};
22use typst_library::engine::{Engine, Route, Sink, Traced};
23use typst_library::foundations::{
24 Bytes, Content, Output, Packed, StyleChain, Target, TargetElem,
25};
26use typst_library::introspection::{
27 Introspector, Location, Locator, SplitLocator, Tag, TagElem,
28};
29use typst_library::model::{
30 AssetElem, Document, DocumentElem, DocumentFormat, DocumentInfo, PagedFormat,
31};
32use typst_library::routines::{Arenas, Pair, RealizationKind};
33use typst_library::{Feature, Library, World};
34use typst_syntax::VirtualPath;
35use typst_utils::{LazyHash, Protected};
36
37#[derive(Debug, Clone)]
45pub struct Bundle {
46 pub files: Arc<IndexMap<VirtualPath, BundleFile, FxBuildHasher>>,
48 pub introspector: Arc<BundleIntrospector>,
54}
55
56impl Output for Bundle {
57 fn introspector(&self) -> &dyn Introspector {
58 self.introspector.as_ref()
59 }
60
61 fn target() -> Target {
62 Target::Bundle
63 }
64
65 fn create(
66 engine: &mut Engine,
67 content: &Content,
68 styles: StyleChain,
69 ) -> SourceResult<Self> {
70 bundle(engine, content, styles)
71 }
72}
73
74#[derive(Debug, Clone)]
76pub enum BundleFile {
77 Document(BundleDocument),
80 Asset(Bytes),
82}
83
84#[derive(Debug, Clone)]
87pub enum BundleDocument {
88 Paged(Box<PagedDocument>, PagedExtras),
90 Html(Box<HtmlDocument>),
92}
93
94impl Document for BundleDocument {
95 fn info(&self) -> &DocumentInfo {
96 match self {
97 BundleDocument::Paged(doc, _) => doc.info(),
98 BundleDocument::Html(doc) => doc.info(),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Hash)]
105pub struct PagedExtras {
106 pub format: PagedFormat,
108 pub anchors: Vec<(Location, EcoString)>,
114}
115
116#[typst_macros::time]
121pub fn bundle(
122 engine: &mut Engine,
123 content: &Content,
124 styles: StyleChain,
125) -> SourceResult<Bundle> {
126 bundle_impl(
127 engine.world,
128 engine.library,
129 engine.introspector.into_raw(),
130 engine.traced,
131 TrackedMut::reborrow_mut(&mut engine.sink),
132 engine.route.track(),
133 content,
134 styles,
135 )
136}
137
138#[comemo::memoize]
140#[allow(clippy::too_many_arguments)]
141fn bundle_impl(
142 world: Tracked<dyn World + '_>,
143 library: &LazyHash<Library>,
144 introspector: Tracked<dyn Introspector + '_>,
145 traced: Tracked<Traced>,
146 sink: TrackedMut<Sink>,
147 route: Tracked<Route>,
148 content: &Content,
149 styles: StyleChain,
150) -> SourceResult<Bundle> {
151 let introspector = Protected::from_raw(introspector);
152 let mut locator = Locator::root().split();
153 let mut engine = Engine {
154 library,
155 world,
156 introspector,
157 traced,
158 sink,
159 route: Route::extend(route).unnested(),
160 };
161
162 let styles = styles.to_map().outside();
165 let styles = StyleChain::new(&styles);
166
167 let arenas = Arenas::default();
168 let children = (engine.library.routines.realize)(
169 RealizationKind::Bundle,
170 &mut engine,
171 &mut locator,
172 &arenas,
173 content,
174 styles,
175 )?;
176
177 let children = collect(&children, &mut engine, &mut locator)?;
178
179 let mut items = engine
180 .parallelize(children, |engine, child| -> SourceResult<_> {
181 Ok(match child {
182 Child::Tag(tag) => Item::Tag(tag.clone()),
183 Child::Asset(asset) => Item::Asset(
184 asset.path.clone().into_inner(),
185 asset.data.0.clone(),
186 asset.location().unwrap(),
187 ),
188 Child::Document(document, styles, locator) => Item::Document(
189 document.path.clone().into_inner(),
190 compile_document(engine, document, styles, locator)?,
191 document.location().unwrap(),
192 ),
193 })
194 })
195 .collect_combined_result::<Vec<_>>()?;
196
197 let mut introspector = BundleIntrospector::new(&items);
198 let targets = introspector.link_targets();
199 let anchors = crate::link::create_link_anchors(&mut items, &targets);
200 introspector.set_anchors(anchors);
201
202 let mut files = IndexMap::default();
203 for item in items {
204 match item {
205 Item::Tag(_) => {}
206 Item::Asset(path, bytes, _) => {
207 files.insert(path, BundleFile::Asset(bytes));
208 }
209 Item::Document(path, doc, _) => {
210 files.insert(path, BundleFile::Document(doc));
211 }
212 }
213 }
214
215 Ok(Bundle {
216 files: Arc::new(files),
217 introspector: Arc::new(introspector),
218 })
219}
220
221enum Child<'a> {
223 Tag(&'a Tag),
224 Asset(&'a Packed<AssetElem>),
225 Document(&'a Packed<DocumentElem>, StyleChain<'a>, Locator<'a>),
226}
227
228enum Item {
230 Tag(Tag),
231 Asset(VirtualPath, Bytes, Location),
232 Document(VirtualPath, BundleDocument, Location),
233}
234
235fn collect<'a>(
237 children: &'a [Pair<'a>],
238 engine: &mut Engine,
239 locator: &mut SplitLocator<'a>,
240) -> SourceResult<Vec<Child<'a>>> {
241 let mut items = Vec::new();
242 let mut errors = EcoVec::new();
243 let mut seen = FxHashMap::default();
244
245 for (elem, styles) in children {
246 let path = if let Some(elem) = elem.to_packed::<TagElem>() {
247 items.push(Child::Tag(&elem.tag));
248 continue;
249 } else if let Some(elem) = elem.to_packed::<AssetElem>() {
250 items.push(Child::Asset(elem));
251 elem.path.as_ref()
252 } else if let Some(elem) = elem.to_packed::<DocumentElem>() {
253 items.push(Child::Document(elem, *styles, locator.next(&elem.span())));
254 elem.path.as_ref()
255 } else {
256 errors.push(error!(
257 elem.span(), "{} is not allowed at the top-level in bundle export",
258 elem.func().name();
259 hint: "try wrapping the content in a `document` instead";
260 ));
261 continue;
262 };
263
264 match seen.entry(path) {
265 Entry::Vacant(entry) => {
266 entry.insert(elem.span());
267 }
268 Entry::Occupied(entry) => {
269 engine.sink.delayed_error(error!(
270 elem.span(), "path `{}` occurs multiple times in the bundle",
271 path.get_without_slash();
272 hint: "{} paths must be unique in the bundle",
273 elem.func().name();
274 hint[*entry.get()]: "path is already in use here";
275 ));
276 }
277 }
278 }
279
280 if !errors.is_empty() {
281 return Err(errors);
282 }
283
284 Ok(items)
285}
286
287fn compile_document<'a>(
289 engine: &mut Engine,
290 document: &'a Packed<DocumentElem>,
291 styles: StyleChain<'a>,
292 locator: Locator<'a>,
293) -> SourceResult<BundleDocument> {
294 let format = document.determine_format(styles).at(document.span())?;
295 let target = TargetElem::target.set(format.target()).wrap();
296 let styles = styles.chain(&target);
297 Ok(match format {
298 DocumentFormat::Paged(format) => {
299 let doc = typst_layout::layout_document_for_bundle(
300 engine,
301 &document.body,
302 locator,
303 styles,
304 )?;
305
306 let num_pages = doc.pages().len();
307 if num_pages != 1 && matches!(format, PagedFormat::Png | PagedFormat::Svg) {
308 bail!(
309 document.span(),
310 "expected document to have a single page";
311 hint: "the document resulted in {num_pages} pages";
312 hint: "documents exported to an image format only support a single page";
313 );
314 }
315
316 BundleDocument::Paged(
317 Box::new(doc),
318 PagedExtras { format, anchors: Vec::new() },
319 )
320 }
321 DocumentFormat::Html => {
322 if !engine.library.features.is_enabled(Feature::Html) {
323 bail!(
324 document.span(),
325 "html export is only available when the `html` feature is enabled";
326 hint: "html export is under active development and incomplete";
327 hint: "to enable both bundle and html export, pass `--features bundle,html`";
328 );
329 }
330
331 let doc = typst_html::html_document_for_bundle(
332 engine,
333 &document.body,
334 locator,
335 styles,
336 )?;
337 BundleDocument::Html(Box::new(doc))
338 }
339 })
340}