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
use std::fs;
use std::sync::Arc;

use embedded_typst::{EmbeddedWorld, Fonts};
use typst::foundations::{Dict, IntoValue};
use typst::syntax::VirtualPath;
use typst::World;
use typst_layout::PagedDocument;

fn fonts() -> Arc<Fonts> {
    Arc::new(Fonts::builder().typst_fonts().build())
}

fn vpath(path: &str) -> VirtualPath {
    VirtualPath::new(path).unwrap()
}

/// Compile and return the text of the document.
fn text(world: &EmbeddedWorld) -> String {
    let document = typst::compile::<PagedDocument>(world)
        .output
        .unwrap_or_else(|errors| panic!("{errors:?}"));
    let mut text = String::new();
    for page in document.pages() {
        collect(&page.frame, &mut text);
    }
    text
}

fn collect(frame: &typst::layout::Frame, text: &mut String) {
    for (_, item) in frame.items() {
        match item {
            typst::layout::FrameItem::Group(group) => collect(&group.frame, text),
            typst::layout::FrameItem::Text(item) => text.push_str(item.text.as_str()),
            _ => {}
        }
    }
}

#[test]
fn in_memory_files_are_imported() {
    let world = EmbeddedWorld::new(vpath("main.typ"), "#import \"lib.typ\": x\n#x", fonts());
    world.add_file(vpath("lib.typ"), b"#let x = [memory]".to_vec());
    assert_eq!(text(&world), "memory");
}

#[test]
fn roots_are_searched_in_order() {
    let first = tempfile::tempdir().unwrap();
    let second = tempfile::tempdir().unwrap();
    fs::write(second.path().join("a.typ"), "#let a = [second-a]").unwrap();
    fs::write(second.path().join("b.typ"), "#let b = [second-b]").unwrap();
    fs::write(first.path().join("a.typ"), "#let a = [first-a]").unwrap();

    let world = EmbeddedWorld::new(
        vpath("main.typ"),
        "#import \"a.typ\": a\n#import \"b.typ\": b\n#a #b",
        fonts(),
    )
    .with_roots(vec![first.path().into(), second.path().into()]);
    assert_eq!(text(&world), "first-a second-b");

    let mut names: Vec<_> = world.dependencies().into_iter().map(|d| d.name).collect();
    names.sort();
    assert_eq!(names, ["a.typ", "b.typ"]);
}

#[test]
fn missing_files_are_errors() {
    let world = EmbeddedWorld::new(vpath("main.typ"), "#import \"nope.typ\": x", fonts());
    assert!(typst::compile::<PagedDocument>(&world).output.is_err());
}

#[test]
fn inputs_reach_sys_inputs() {
    let mut inputs = Dict::new();
    inputs.insert("name".into(), "Ada".into_value());
    let world =
        EmbeddedWorld::new(vpath("main.typ"), "#sys.inputs.name", fonts()).with_inputs(inputs);
    assert_eq!(text(&world), "Ada");
}

#[test]
fn creation_timestamp_fixes_today() {
    let world = EmbeddedWorld::new(vpath("main.typ"), "#datetime.today().display()", fonts())
        .with_creation_timestamp(1_700_000_000);
    assert_eq!(text(&world), "2023-11-14");
}

#[test]
fn font_files_load_lazily() {
    let dir = tempfile::tempdir().unwrap();
    let data = typst_assets::fonts().next().unwrap();
    fs::write(dir.path().join("font.otf"), data).unwrap();

    let fonts = Fonts::builder().path(dir.path()).build();
    assert!(fonts.book().families().count() > 0);
    assert!(fonts.loaded_files().is_empty());

    assert!(fonts.font(0).is_some());
    assert_eq!(fonts.loaded_files().len(), 1);
}

#[test]
fn data_fonts_are_registered() {
    let data = typst_assets::fonts().next().unwrap();
    let fonts = Fonts::builder().data(data).build();
    assert!(fonts.font(0).is_some());

    let world = EmbeddedWorld::new(vpath("main.typ"), "", Arc::new(fonts));
    assert!(world.book().families().count() > 0);
}

#[cfg(feature = "packages")]
#[test]
fn local_packages_resolve() {
    let data = tempfile::tempdir().unwrap();
    let cache = tempfile::tempdir().unwrap();
    let package = data.path().join("local/greet/0.1.0");
    fs::create_dir_all(&package).unwrap();
    fs::write(
        package.join("typst.toml"),
        "[package]\nname = \"greet\"\nversion = \"0.1.0\"\nentrypoint = \"lib.typ\"\n",
    )
    .unwrap();
    fs::write(package.join("lib.typ"), "#let hello = [package]").unwrap();

    let packages =
        embedded_typst::Packages::new(Some(data.path().into()), Some(cache.path().into()));
    let world = EmbeddedWorld::new(
        vpath("main.typ"),
        "#import \"@local/greet:0.1.0\": hello\n#hello",
        fonts(),
    )
    .with_packages(packages);
    assert_eq!(text(&world), "package");
    let mut names: Vec<_> = world.dependencies().into_iter().map(|d| d.name).collect();
    names.sort();
    assert_eq!(
        names,
        [
            "@local/greet:0.1.0/lib.typ",
            "@local/greet:0.1.0/typst.toml"
        ]
    );
}