1use std::path::PathBuf;
4use std::sync::Arc;
5
6use typst::diag::{FileError, FileResult};
7use typst::foundations::{Bytes, Datetime, Dict, Duration};
8use typst::syntax::{FileId, RootedPath, Source, VirtualRoot};
9use typst::text::FontInfo;
10use typst::text::{Font, FontBook};
11use typst::utils::LazyHash;
12use typst::{Feature, Library, LibraryExt, World};
13use typst_kit::files::{FileLoader, FileStore};
14use typst_kit::fonts::{FontSource, FontStore};
15
16use crate::pack::Pack;
17
18pub struct PackWorld {
27 library: LazyHash<Library>,
28 main: FileId,
29 store: FileStore<PackLoader>,
30 fonts: FontStore,
31 clock: Clock,
32}
33
34impl PackWorld {
35 pub fn builder(pack: Pack) -> PackWorldBuilder {
37 PackWorldBuilder::new(pack)
38 }
39
40 pub fn new(pack: Pack) -> Result<Self, PackWorldError> {
42 Self::builder(pack).build()
43 }
44
45 pub fn pack(&self) -> &Pack {
47 self.store.loader().pack.as_ref()
48 }
49}
50
51impl World for PackWorld {
52 fn library(&self) -> &LazyHash<Library> {
53 &self.library
54 }
55
56 fn book(&self) -> &LazyHash<FontBook> {
57 self.fonts.book()
58 }
59
60 fn main(&self) -> FileId {
61 self.main
62 }
63
64 fn source(&self, id: FileId) -> FileResult<Source> {
65 if matches!(id.root(), VirtualRoot::Project)
66 && self
67 .store
68 .loader()
69 .pack
70 .file(id.vpath().get_without_slash())
71 .is_none()
72 {
73 return Err(FileError::NotFound(PathBuf::from(
74 id.vpath().get_without_slash(),
75 )));
76 }
77 self.store.source(id)
78 }
79
80 fn file(&self, id: FileId) -> FileResult<Bytes> {
81 self.store.file(id)
82 }
83
84 fn font(&self, index: usize) -> Option<Font> {
85 self.fonts.font(index)
86 }
87
88 fn today(&self, #[allow(unused_variables)] offset: Option<Duration>) -> Option<Datetime> {
89 match &self.clock {
90 Clock::None => None,
91 Clock::Fixed(datetime) => Some(*datetime),
94 #[cfg(feature = "fs")]
95 Clock::System(time) => time.today(offset),
96 }
97 }
98}
99
100enum Clock {
102 None,
104 Fixed(Datetime),
106 #[cfg(feature = "fs")]
108 System(typst_kit::datetime::Time),
109}
110
111struct PackLoader {
114 pack: Arc<Pack>,
115 external_resource_loaders: Vec<Box<dyn FileLoader + Send + Sync>>,
116 package_loader: Option<Box<dyn FileLoader + Send + Sync>>,
117}
118
119pub(crate) fn load_external_resource(
120 loaders: &[Box<dyn FileLoader + Send + Sync>],
121 id: FileId,
122) -> FileResult<Bytes> {
123 for loader in loaders {
124 match loader.load(id) {
125 Err(FileError::NotFound(_)) => {}
126 result => return result,
127 }
128 }
129 Err(FileError::NotFound(PathBuf::from(
130 id.vpath().get_without_slash(),
131 )))
132}
133
134impl FileLoader for PackLoader {
135 fn load(&self, id: FileId) -> FileResult<Bytes> {
136 let path = id.vpath().get_without_slash();
137 match id.root() {
138 VirtualRoot::Project => {
139 if let Some(data) = self.pack.file(path) {
140 return Ok(data.clone());
141 }
142 if !self.pack.is_external_resource(path) {
143 return Err(FileError::NotFound(PathBuf::from(path)));
144 }
145 load_external_resource(&self.external_resource_loaders, id)
146 }
147 VirtualRoot::Package(spec) => {
148 if self.pack.has_package(spec) {
149 self.pack
150 .package_file(spec, path)
151 .cloned()
152 .ok_or_else(|| FileError::NotFound(PathBuf::from(path)))
153 } else if let Some(loader) = &self.package_loader {
154 loader.load(id)
155 } else {
156 Err(FileError::Other(Some(
157 format!(
158 "package {spec} is not vendored in the pack \
159 and no package loader is configured"
160 )
161 .into(),
162 )))
163 }
164 }
165 }
166 }
167}
168
169pub struct PackWorldBuilder {
171 pack: Pack,
172 inputs: Dict,
173 features: Vec<Feature>,
174 clock: Clock,
175 #[cfg_attr(not(feature = "embedded-fonts"), allow(dead_code))]
176 embedded_fonts: bool,
177 extra_fonts: Vec<(BoxedFontSource, FontInfo)>,
178 external_resource_loaders: Vec<Box<dyn FileLoader + Send + Sync>>,
179 package_loader: Option<Box<dyn FileLoader + Send + Sync>>,
180}
181
182struct BoxedFontSource(Box<dyn FontSource>);
184
185impl FontSource for BoxedFontSource {
186 fn load(&self) -> Option<Font> {
187 self.0.load()
188 }
189}
190
191impl PackWorldBuilder {
192 fn new(pack: Pack) -> Self {
193 Self {
194 pack,
195 inputs: Dict::new(),
196 features: Vec::new(),
197 clock: Clock::None,
198 embedded_fonts: cfg!(feature = "embedded-fonts"),
199 extra_fonts: Vec::new(),
200 external_resource_loaders: Vec::new(),
201 package_loader: None,
202 }
203 }
204
205 pub fn inputs(mut self, inputs: Dict) -> Self {
207 self.inputs = inputs;
208 self
209 }
210
211 pub fn feature(mut self, feature: Feature) -> Self {
216 self.features.push(feature);
217 self
218 }
219
220 pub fn fixed_date(mut self, datetime: Datetime) -> Self {
222 self.clock = Clock::Fixed(datetime);
223 self
224 }
225
226 #[cfg(feature = "fs")]
228 pub fn system_date(mut self) -> Self {
229 self.clock = Clock::System(typst_kit::datetime::Time::system());
230 self
231 }
232
233 #[cfg(feature = "embedded-fonts")]
236 pub fn embedded_fonts(mut self, include: bool) -> Self {
237 self.embedded_fonts = include;
238 self
239 }
240
241 pub fn extra_fonts<T: FontSource>(
248 mut self,
249 fonts: impl IntoIterator<Item = (T, FontInfo)>,
250 ) -> Self {
251 self.extra_fonts.extend(
252 fonts
253 .into_iter()
254 .map(|(source, info)| (BoxedFontSource(Box::new(source)), info)),
255 );
256 self
257 }
258
259 pub fn package_loader(mut self, loader: impl FileLoader + Send + Sync + 'static) -> Self {
262 self.package_loader = Some(Box::new(loader));
263 self
264 }
265
266 pub fn external_resource_loader(
270 mut self,
271 loader: impl FileLoader + Send + Sync + 'static,
272 ) -> Self {
273 self.external_resource_loaders.push(Box::new(loader));
274 self
275 }
276
277 pub fn build(self) -> Result<PackWorld, PackWorldError> {
279 let entrypoint = self
280 .pack
281 .manifest()
282 .entrypoint()
283 .map_err(|err| PackWorldError::InvalidPack(err.to_string()))?;
284 let main = RootedPath::new(VirtualRoot::Project, entrypoint).intern();
285
286 let mut fonts = FontStore::new();
287 for pack_font in self.pack.fonts() {
288 let font = Font::new(pack_font.data.clone(), pack_font.entry.index)
289 .ok_or_else(|| PackWorldError::InvalidFont(pack_font.entry.path.clone()))?;
290 let info = font.info().clone();
291 fonts.push((font, info));
292 }
293 fonts.extend(self.extra_fonts);
294 #[cfg(feature = "embedded-fonts")]
295 if self.embedded_fonts {
296 fonts.extend(typst_kit::fonts::embedded());
297 }
298
299 let library = Library::builder()
300 .with_inputs(self.inputs)
301 .with_features(self.features.into_iter().collect())
302 .build();
303
304 Ok(PackWorld {
305 library: LazyHash::new(library),
306 main,
307 store: FileStore::new(PackLoader {
308 pack: Arc::new(self.pack),
309 external_resource_loaders: self.external_resource_loaders,
310 package_loader: self.package_loader,
311 }),
312 fonts,
313 clock: self.clock,
314 })
315 }
316}
317
318#[cfg(feature = "fs")]
323pub struct SystemPackageLoader(pub typst_kit::packages::SystemPackages);
324
325#[cfg(feature = "fs")]
326impl SystemPackageLoader {
327 pub fn system() -> Self {
330 Self(typst_kit::packages::SystemPackages::new(
331 typst_kit::downloader::SystemDownloader::new(concat!(
332 "typst-pack/",
333 env!("CARGO_PKG_VERSION")
334 )),
335 ))
336 }
337
338 pub fn offline() -> Self {
341 Self(typst_kit::packages::SystemPackages::new(OfflineDownloader))
342 }
343}
344
345#[cfg(feature = "fs")]
352pub struct OfflineDownloader;
353
354#[cfg(feature = "fs")]
355impl typst_kit::downloader::Downloader for OfflineDownloader {
356 fn stream(
357 &self,
358 _key: &dyn std::any::Any,
359 _url: &str,
360 ) -> std::io::Result<(Option<usize>, Box<dyn std::io::Read>)> {
361 Err(std::io::Error::new(
362 std::io::ErrorKind::NotFound,
363 "network access is disabled (offline mode)",
364 ))
365 }
366}
367
368#[cfg(feature = "fs")]
369impl FileLoader for SystemPackageLoader {
370 fn load(&self, id: FileId) -> FileResult<Bytes> {
371 match id.root() {
372 VirtualRoot::Project => Err(FileError::NotFound(PathBuf::from(
373 id.vpath().get_without_slash(),
374 ))),
375 VirtualRoot::Package(spec) => Ok(self.0.obtain(spec)?.load(id.vpath())?),
376 }
377 }
378}
379
380#[derive(Debug, thiserror::Error)]
382pub enum PackWorldError {
383 #[error("pack is not usable: {0}")]
384 InvalidPack(String),
385 #[error("embedded font `{0}` could not be loaded")]
386 InvalidFont(String),
387}