embedded-typst 0.1.0

A typst::World for embedding the Typst compiler: lazily loaded fonts, a virtual filesystem over several roots, Typst packages and reproducible dates. Native and Wasm.
Documentation
//! Package resolution for `#import "@namespace/name:version"` (native only).

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Mutex;

use typst::diag::{PackageError, PackageResult};
use typst::syntax::package::PackageSpec;
use typst_kit::packages::FsPackages;

/// Where packages come from. They are looked up in the same places as the
/// typst CLI: the package data directory (locally installed packages) first,
/// then the package cache. With [`Packages::download`], a missing `@preview`
/// package is fetched from Typst Universe into the cache.
pub struct Packages {
    data: Option<FsPackages>,
    cache: Option<FsPackages>,
    #[cfg(feature = "download")]
    universe: Option<typst_kit::packages::UniversePackages>,
    /// Resolved package roots, so each package is located (and downloaded)
    /// at most once.
    roots: Mutex<HashMap<PackageSpec, PackageResult<PathBuf>>>,
}

impl Packages {
    /// Use `data` and `cache` as package directories, or the system's package
    /// data and cache directories where they are `None`.
    pub fn new(data: Option<PathBuf>, cache: Option<PathBuf>) -> Self {
        Self {
            data: data.map(FsPackages::new).or_else(FsPackages::system_data),
            cache: cache.map(FsPackages::new).or_else(FsPackages::system_cache),
            #[cfg(feature = "download")]
            universe: None,
            roots: Mutex::new(HashMap::new()),
        }
    }

    /// Download missing `@preview` packages into the cache directory, sending
    /// `user_agent` (such as `myapp/1.0`). Proxy variables are honoured.
    #[cfg(feature = "download")]
    pub fn download(mut self, user_agent: impl Into<String>) -> Self {
        self.universe = Some(typst_kit::packages::UniversePackages::new(download::Http {
            user_agent: user_agent.into(),
        }));
        self
    }

    /// The directory holding the files of `spec`.
    pub fn root(&self, spec: &PackageSpec) -> PackageResult<PathBuf> {
        let mut roots = self.roots.lock().expect("package lock poisoned");
        roots
            .entry(spec.clone())
            .or_insert_with(|| self.obtain(spec))
            .clone()
    }

    fn obtain(&self, spec: &PackageSpec) -> PackageResult<PathBuf> {
        for packages in [&self.data, &self.cache].into_iter().flatten() {
            if let Some(root) = packages.obtain(spec) {
                return Ok(root.path().to_path_buf());
            }
        }

        #[cfg(feature = "download")]
        if let (Some(universe), Some(cache)) = (&self.universe, &self.cache) {
            if spec.namespace == typst_kit::packages::UniversePackages::NAMESPACE {
                let mut archive = universe.package(spec)?;
                cache.store(spec, |dir| {
                    archive.unpack(dir).map_err(|error| {
                        PackageError::MalformedArchive(Some(error.to_string().into()))
                    })
                })?;
                if let Some(root) = cache.obtain(spec) {
                    return Ok(root.path().to_path_buf());
                }
            }
        }

        Err(PackageError::NotFound(spec.clone()))
    }
}

#[cfg(feature = "download")]
mod download {
    use std::any::Any;
    use std::io::{self, Read};

    /// Fetches Typst Universe archives over HTTPS, honouring proxy variables.
    pub(super) struct Http {
        pub(super) user_agent: String,
    }

    impl typst_kit::downloader::Downloader for Http {
        fn stream(&self, _: &dyn Any, url: &str) -> io::Result<(Option<usize>, Box<dyn Read>)> {
            let agent = ureq::AgentBuilder::new()
                .user_agent(&self.user_agent)
                .try_proxy_from_env(true)
                .build();
            let response = agent.get(url).call().map_err(|error| match error {
                ureq::Error::Status(404, _) => io::Error::new(io::ErrorKind::NotFound, error),
                error => io::Error::other(error),
            })?;
            let length = response
                .header("Content-Length")
                .and_then(|value| value.parse().ok());
            Ok((length, Box::new(response.into_reader())))
        }
    }
}