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 {
24 library: LazyHash<Library>,
25 main: FileId,
26 store: FileStore<PackLoader>,
27 fonts: FontStore,
28 clock: Clock,
29}
30
31impl PackWorld {
32 pub fn builder(pack: Pack) -> PackWorldBuilder {
34 PackWorldBuilder::new(pack)
35 }
36
37 pub fn new(pack: Pack) -> Result<Self, PackWorldError> {
39 Self::builder(pack).build()
40 }
41
42 pub fn pack(&self) -> &Pack {
44 self.store.loader().pack.as_ref()
45 }
46}
47
48impl World for PackWorld {
49 fn library(&self) -> &LazyHash<Library> {
50 &self.library
51 }
52
53 fn book(&self) -> &LazyHash<FontBook> {
54 self.fonts.book()
55 }
56
57 fn main(&self) -> FileId {
58 self.main
59 }
60
61 fn source(&self, id: FileId) -> FileResult<Source> {
62 self.store.source(id)
63 }
64
65 fn file(&self, id: FileId) -> FileResult<Bytes> {
66 self.store.file(id)
67 }
68
69 fn font(&self, index: usize) -> Option<Font> {
70 self.fonts.font(index)
71 }
72
73 fn today(&self, #[allow(unused_variables)] offset: Option<Duration>) -> Option<Datetime> {
74 match &self.clock {
75 Clock::None => None,
76 Clock::Fixed(datetime) => Some(*datetime),
79 #[cfg(feature = "fs")]
80 Clock::System(time) => time.today(offset),
81 }
82 }
83}
84
85enum Clock {
87 None,
89 Fixed(Datetime),
91 #[cfg(feature = "fs")]
93 System(typst_kit::datetime::Time),
94}
95
96struct PackLoader {
99 pack: Arc<Pack>,
100 package_loader: Option<Box<dyn FileLoader + Send + Sync>>,
101}
102
103impl FileLoader for PackLoader {
104 fn load(&self, id: FileId) -> FileResult<Bytes> {
105 let path = id.vpath().get_without_slash();
106 match id.root() {
107 VirtualRoot::Project => self
108 .pack
109 .file(path)
110 .cloned()
111 .ok_or_else(|| FileError::NotFound(PathBuf::from(path))),
112 VirtualRoot::Package(spec) => {
113 if self.pack.has_package(spec) {
114 self.pack
115 .package_file(spec, path)
116 .cloned()
117 .ok_or_else(|| FileError::NotFound(PathBuf::from(path)))
118 } else if let Some(loader) = &self.package_loader {
119 loader.load(id)
120 } else {
121 Err(FileError::Other(Some(
122 format!(
123 "package {spec} is not vendored in the pack \
124 and no package loader is configured"
125 )
126 .into(),
127 )))
128 }
129 }
130 }
131 }
132}
133
134pub struct PackWorldBuilder {
136 pack: Pack,
137 inputs: Dict,
138 features: Vec<Feature>,
139 clock: Clock,
140 #[cfg_attr(not(feature = "embedded-fonts"), allow(dead_code))]
141 embedded_fonts: bool,
142 extra_fonts: Vec<(BoxedFontSource, FontInfo)>,
143 package_loader: Option<Box<dyn FileLoader + Send + Sync>>,
144}
145
146struct BoxedFontSource(Box<dyn FontSource>);
148
149impl FontSource for BoxedFontSource {
150 fn load(&self) -> Option<Font> {
151 self.0.load()
152 }
153}
154
155impl PackWorldBuilder {
156 fn new(pack: Pack) -> Self {
157 Self {
158 pack,
159 inputs: Dict::new(),
160 features: Vec::new(),
161 clock: Clock::None,
162 embedded_fonts: cfg!(feature = "embedded-fonts"),
163 extra_fonts: Vec::new(),
164 package_loader: None,
165 }
166 }
167
168 pub fn inputs(mut self, inputs: Dict) -> Self {
170 self.inputs = inputs;
171 self
172 }
173
174 pub fn feature(mut self, feature: Feature) -> Self {
179 self.features.push(feature);
180 self
181 }
182
183 pub fn fixed_date(mut self, datetime: Datetime) -> Self {
185 self.clock = Clock::Fixed(datetime);
186 self
187 }
188
189 #[cfg(feature = "fs")]
191 pub fn system_date(mut self) -> Self {
192 self.clock = Clock::System(typst_kit::datetime::Time::system());
193 self
194 }
195
196 #[cfg(feature = "embedded-fonts")]
199 pub fn embedded_fonts(mut self, include: bool) -> Self {
200 self.embedded_fonts = include;
201 self
202 }
203
204 pub fn extra_fonts<T: FontSource>(
211 mut self,
212 fonts: impl IntoIterator<Item = (T, FontInfo)>,
213 ) -> Self {
214 self.extra_fonts.extend(
215 fonts
216 .into_iter()
217 .map(|(source, info)| (BoxedFontSource(Box::new(source)), info)),
218 );
219 self
220 }
221
222 pub fn package_loader(mut self, loader: impl FileLoader + Send + Sync + 'static) -> Self {
225 self.package_loader = Some(Box::new(loader));
226 self
227 }
228
229 pub fn build(self) -> Result<PackWorld, PackWorldError> {
231 let entrypoint = self
232 .pack
233 .manifest()
234 .entrypoint()
235 .map_err(|err| PackWorldError::InvalidPack(err.to_string()))?;
236 let main = RootedPath::new(VirtualRoot::Project, entrypoint).intern();
237
238 let mut fonts = FontStore::new();
239 for pack_font in self.pack.fonts() {
240 let font = Font::new(pack_font.data.clone(), pack_font.entry.index)
241 .ok_or_else(|| PackWorldError::InvalidFont(pack_font.entry.path.clone()))?;
242 let info = font.info().clone();
243 fonts.push((font, info));
244 }
245 fonts.extend(self.extra_fonts);
246 #[cfg(feature = "embedded-fonts")]
247 if self.embedded_fonts {
248 fonts.extend(typst_kit::fonts::embedded());
249 }
250
251 let library = Library::builder()
252 .with_inputs(self.inputs)
253 .with_features(self.features.into_iter().collect())
254 .build();
255
256 Ok(PackWorld {
257 library: LazyHash::new(library),
258 main,
259 store: FileStore::new(PackLoader {
260 pack: Arc::new(self.pack),
261 package_loader: self.package_loader,
262 }),
263 fonts,
264 clock: self.clock,
265 })
266 }
267}
268
269#[cfg(feature = "fs")]
274pub struct SystemPackageLoader(pub typst_kit::packages::SystemPackages);
275
276#[cfg(feature = "fs")]
277impl SystemPackageLoader {
278 pub fn system() -> Self {
281 Self(typst_kit::packages::SystemPackages::new(
282 typst_kit::downloader::SystemDownloader::new(concat!(
283 "typst-pack/",
284 env!("CARGO_PKG_VERSION")
285 )),
286 ))
287 }
288
289 pub fn offline() -> Self {
292 Self(typst_kit::packages::SystemPackages::new(OfflineDownloader))
293 }
294}
295
296#[cfg(feature = "fs")]
303pub struct OfflineDownloader;
304
305#[cfg(feature = "fs")]
306impl typst_kit::downloader::Downloader for OfflineDownloader {
307 fn stream(
308 &self,
309 _key: &dyn std::any::Any,
310 _url: &str,
311 ) -> std::io::Result<(Option<usize>, Box<dyn std::io::Read>)> {
312 Err(std::io::Error::new(
313 std::io::ErrorKind::NotFound,
314 "network access is disabled (offline mode)",
315 ))
316 }
317}
318
319#[cfg(feature = "fs")]
320impl FileLoader for SystemPackageLoader {
321 fn load(&self, id: FileId) -> FileResult<Bytes> {
322 match id.root() {
323 VirtualRoot::Project => Err(FileError::NotFound(PathBuf::from(
324 id.vpath().get_without_slash(),
325 ))),
326 VirtualRoot::Package(spec) => Ok(self.0.obtain(spec)?.load(id.vpath())?),
327 }
328 }
329}
330
331#[derive(Debug, thiserror::Error)]
333pub enum PackWorldError {
334 #[error("pack is not usable: {0}")]
335 InvalidPack(String),
336 #[error("embedded font `{0}` could not be loaded")]
337 InvalidFont(String),
338}