use std::path::PathBuf;
use std::sync::Arc;
use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime, Dict, Duration};
use typst::syntax::{FileId, RootedPath, Source, VirtualRoot};
use typst::text::FontInfo;
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Feature, Library, LibraryExt, World};
use typst_kit::files::{FileLoader, FileStore};
use typst_kit::fonts::{FontSource, FontStore};
use crate::pack::Pack;
pub struct PackWorld {
library: LazyHash<Library>,
main: FileId,
store: FileStore<PackLoader>,
fonts: FontStore,
clock: Clock,
}
impl PackWorld {
pub fn builder(pack: Pack) -> PackWorldBuilder {
PackWorldBuilder::new(pack)
}
pub fn new(pack: Pack) -> Result<Self, PackWorldError> {
Self::builder(pack).build()
}
pub fn pack(&self) -> &Pack {
self.store.loader().pack.as_ref()
}
}
impl World for PackWorld {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
self.fonts.book()
}
fn main(&self) -> FileId {
self.main
}
fn source(&self, id: FileId) -> FileResult<Source> {
self.store.source(id)
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
self.store.file(id)
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.font(index)
}
fn today(&self, #[allow(unused_variables)] offset: Option<Duration>) -> Option<Datetime> {
match &self.clock {
Clock::None => None,
Clock::Fixed(datetime) => Some(*datetime),
#[cfg(feature = "fs")]
Clock::System(time) => time.today(offset),
}
}
}
enum Clock {
None,
Fixed(Datetime),
#[cfg(feature = "fs")]
System(typst_kit::datetime::Time),
}
struct PackLoader {
pack: Arc<Pack>,
package_loader: Option<Box<dyn FileLoader + Send + Sync>>,
}
impl FileLoader for PackLoader {
fn load(&self, id: FileId) -> FileResult<Bytes> {
let path = id.vpath().get_without_slash();
match id.root() {
VirtualRoot::Project => self
.pack
.file(path)
.cloned()
.ok_or_else(|| FileError::NotFound(PathBuf::from(path))),
VirtualRoot::Package(spec) => {
if self.pack.has_package(spec) {
self.pack
.package_file(spec, path)
.cloned()
.ok_or_else(|| FileError::NotFound(PathBuf::from(path)))
} else if let Some(loader) = &self.package_loader {
loader.load(id)
} else {
Err(FileError::Other(Some(
format!(
"package {spec} is not vendored in the pack \
and no package loader is configured"
)
.into(),
)))
}
}
}
}
}
pub struct PackWorldBuilder {
pack: Pack,
inputs: Dict,
features: Vec<Feature>,
clock: Clock,
#[cfg_attr(not(feature = "embedded-fonts"), allow(dead_code))]
embedded_fonts: bool,
extra_fonts: Vec<(BoxedFontSource, FontInfo)>,
package_loader: Option<Box<dyn FileLoader + Send + Sync>>,
}
struct BoxedFontSource(Box<dyn FontSource>);
impl FontSource for BoxedFontSource {
fn load(&self) -> Option<Font> {
self.0.load()
}
}
impl PackWorldBuilder {
fn new(pack: Pack) -> Self {
Self {
pack,
inputs: Dict::new(),
features: Vec::new(),
clock: Clock::None,
embedded_fonts: cfg!(feature = "embedded-fonts"),
extra_fonts: Vec::new(),
package_loader: None,
}
}
pub fn inputs(mut self, inputs: Dict) -> Self {
self.inputs = inputs;
self
}
pub fn feature(mut self, feature: Feature) -> Self {
self.features.push(feature);
self
}
pub fn fixed_date(mut self, datetime: Datetime) -> Self {
self.clock = Clock::Fixed(datetime);
self
}
#[cfg(feature = "fs")]
pub fn system_date(mut self) -> Self {
self.clock = Clock::System(typst_kit::datetime::Time::system());
self
}
#[cfg(feature = "embedded-fonts")]
pub fn embedded_fonts(mut self, include: bool) -> Self {
self.embedded_fonts = include;
self
}
pub fn extra_fonts<T: FontSource>(
mut self,
fonts: impl IntoIterator<Item = (T, FontInfo)>,
) -> Self {
self.extra_fonts.extend(
fonts
.into_iter()
.map(|(source, info)| (BoxedFontSource(Box::new(source)), info)),
);
self
}
pub fn package_loader(mut self, loader: impl FileLoader + Send + Sync + 'static) -> Self {
self.package_loader = Some(Box::new(loader));
self
}
pub fn build(self) -> Result<PackWorld, PackWorldError> {
let entrypoint = self
.pack
.manifest()
.entrypoint()
.map_err(|err| PackWorldError::InvalidPack(err.to_string()))?;
let main = RootedPath::new(VirtualRoot::Project, entrypoint).intern();
let mut fonts = FontStore::new();
for pack_font in self.pack.fonts() {
let font = Font::new(pack_font.data.clone(), pack_font.entry.index)
.ok_or_else(|| PackWorldError::InvalidFont(pack_font.entry.path.clone()))?;
let info = font.info().clone();
fonts.push((font, info));
}
fonts.extend(self.extra_fonts);
#[cfg(feature = "embedded-fonts")]
if self.embedded_fonts {
fonts.extend(typst_kit::fonts::embedded());
}
let library = Library::builder()
.with_inputs(self.inputs)
.with_features(self.features.into_iter().collect())
.build();
Ok(PackWorld {
library: LazyHash::new(library),
main,
store: FileStore::new(PackLoader {
pack: Arc::new(self.pack),
package_loader: self.package_loader,
}),
fonts,
clock: self.clock,
})
}
}
#[cfg(feature = "fs")]
pub struct SystemPackageLoader(pub typst_kit::packages::SystemPackages);
#[cfg(feature = "fs")]
impl SystemPackageLoader {
pub fn system() -> Self {
Self(typst_kit::packages::SystemPackages::new(
typst_kit::downloader::SystemDownloader::new(concat!(
"typst-pack/",
env!("CARGO_PKG_VERSION")
)),
))
}
pub fn offline() -> Self {
Self(typst_kit::packages::SystemPackages::new(OfflineDownloader))
}
}
#[cfg(feature = "fs")]
pub struct OfflineDownloader;
#[cfg(feature = "fs")]
impl typst_kit::downloader::Downloader for OfflineDownloader {
fn stream(
&self,
_key: &dyn std::any::Any,
_url: &str,
) -> std::io::Result<(Option<usize>, Box<dyn std::io::Read>)> {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"network access is disabled (offline mode)",
))
}
}
#[cfg(feature = "fs")]
impl FileLoader for SystemPackageLoader {
fn load(&self, id: FileId) -> FileResult<Bytes> {
match id.root() {
VirtualRoot::Project => Err(FileError::NotFound(PathBuf::from(
id.vpath().get_without_slash(),
))),
VirtualRoot::Package(spec) => Ok(self.0.obtain(spec)?.load(id.vpath())?),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum PackWorldError {
#[error("pack is not usable: {0}")]
InvalidPack(String),
#[error("embedded font `{0}` could not be loaded")]
InvalidFont(String),
}