The full code is about 600 lines long.
Full example
[package]
name = "example"
version = "0.1.0"
edition = "2024"
[dependencies]
chrono = { version = "0.4.40", default-features = false, features = ["clock", "std"] }
comemo = "0.4.0"
typst = "0.13.1"
typst-kit = "0.13.1"
typst-pdf = "0.13.1"
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::{fmt, fs, io, mem, process};
use chrono::{DateTime, Datelike, FixedOffset, Local, Timelike, Utc};
use comemo::{Track, Tracked, Validate};
use typst::diag::{At, FileError, FileResult, SourceResult, StrResult, Warned, warning};
use typst::ecow::{EcoString, eco_format};
use typst::engine::{Engine, Route, Sink, Traced};
use typst::foundations::{
Bytes, Content, Datetime, NativeElement, Packed, Smart, StyleChain, TargetElem,
};
use typst::introspection::Introspector;
use typst::layout::{PagedDocument, Sizing, TrackSizings};
use typst::model::{TableCell, TableChild, TableElem, TableItem};
use typst::syntax::{FileId, Source, Span};
use typst::text::{Font, FontBook, TextElem};
use typst::utils::LazyHash;
use typst::{Document, Library, World};
use typst_kit::download::{DownloadState, Downloader, Progress};
use typst_kit::fonts::{FontSlot, Fonts};
use typst_kit::package::PackageStorage;
use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
fn main() {
let source_code = r#"
#import "@preview/codly:1.3.0": *
#import "@preview/codly-languages:0.1.8": codly-languages
#show: codly-init
#codly(languages: codly-languages)
```rs
println!("Hello there.");
```
"#;
if compile(source_code, "output.pdf").is_err() {
process::exit(1)
}
let text = TextElem::new("Hello".into()).pack();
let table_children = (0..10)
.map(|_| TableChild::Item(TableItem::Cell(Packed::new(TableCell::new(text.clone())))))
.collect();
let table = TableElem::new(table_children)
.with_columns(TrackSizings([Sizing::Auto].repeat(2).into()))
.pack();
let content = table;
if compile_content(content, "output2.pdf").is_err() {
process::exit(1)
}
}
fn compile_content<T: AsRef<Path>>(content: Content, path: T) -> Result<(), ()> {
let world: &dyn World = &mut SimpleWorld::new("").unwrap();
let world = world.track();
let binding = Traced::default();
let traced = binding.track();
let document = relayout::<PagedDocument>(world, traced, content).unwrap();
let output = Output::Path(path.as_ref().to_path_buf());
export_pdf(&document, &output).unwrap();
Ok(())
}
fn relayout<D: Document>(
world: Tracked<dyn World + '_>,
traced: Tracked<Traced>,
content: Content,
) -> SourceResult<D> {
let library = world.library();
let base = StyleChain::new(&library.styles);
let target = TargetElem::set_target(D::TARGET).wrap();
let styles = base.chain(&target);
let empty_introspector = Introspector::default();
let mut iter = 0;
let mut subsink;
let mut introspector = &empty_introspector;
let mut document: D;
// Relayout until all introspections stabilize.
// If that doesn't happen within five attempts, we give up.
loop {
subsink = Sink::new();
let constraint = <Introspector as Validate>::Constraint::new();
let mut engine = Engine {
world,
introspector: introspector.track_with(&constraint),
traced,
sink: subsink.track_mut(),
route: Route::default(),
routines: &typst::ROUTINES,
};
// Layout!
document = D::create(&mut engine, &content, styles)?;
introspector = document.introspector();
iter += 1;
if introspector.validate(&constraint) {
break;
}
if iter >= 5 {
subsink.warn(warning!(
Span::detached(), "layout did not converge within 5 attempts";
hint: "check if any states or queries are updating themselves"
));
break;
}
}
Ok(document)
}
/// A very simple in-memory world.
struct SimpleWorld {
/// The root relative to which absolute paths are resolved.
root: PathBuf,
/// Main file source code.
source: Source,
/// Typst's standard library.
library: LazyHash<Library>,
/// Metadata about discovered fonts.
book: LazyHash<FontBook>,
/// Locations of and storage for lazily loaded fonts.
fonts: Vec<FontSlot>,
/// Maps file ids to source files and buffers.
slots: Mutex<HashMap<FileId, FileSlot>>,
/// Holds information about where packages are stored.
package_storage: PackageStorage,
/// The current datetime if requested. This is stored here to ensure it is
/// always the same within one compilation.
/// Reset between compilations if not [`Now::Fixed`].
now: Now,
}
impl SimpleWorld {
fn new(source_code: &str) -> Result<Self, WorldCreationError> {
// Resolve the system-global root directory.
let root = {
let path = Path::new(".");
path.canonicalize().map_err(|err| match err.kind() {
io::ErrorKind::NotFound => WorldCreationError::RootNotFound(path.to_path_buf()),
_ => WorldCreationError::Io(err),
})?
};
let library = {
Library::builder()
.with_inputs([].into_iter().collect())
.with_features([].into_iter().collect())
.build()
};
let fonts = Fonts::searcher()
.include_system_fonts(true)
.search_with([""]);
let now = Now::System(OnceLock::new());
Ok(Self {
source: Source::detached(source_code),
root,
library: LazyHash::new(library),
book: LazyHash::new(fonts.book),
fonts: fonts.fonts,
slots: Mutex::new(HashMap::new()),
package_storage: storage(),
now,
})
}
}
/// Returns a new package storage for the given args.
pub fn storage() -> PackageStorage {
PackageStorage::new(None, None, downloader())
}
impl World for SimpleWorld {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
&self.book
}
fn main(&self) -> FileId {
self.source.id()
}
/// Accessing a specified source file (based on `FileId`).
fn source(&self, id: FileId) -> FileResult<Source> {
if id == self.source.id() {
Ok(self.source.clone())
} else {
self.slot(id, |slot| slot.source(&self.root, &self.package_storage))
}
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
self.slot(id, |slot| slot.file(&self.root, &self.package_storage))
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.get(index)?.get()
}
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
let now = match &self.now {
Now::System(time) => time.get_or_init(Utc::now),
};
// The time with the specified UTC offset, or within the local time zone.
let with_offset = match offset {
None => now.with_timezone(&Local).fixed_offset(),
Some(hours) => {
let seconds = i32::try_from(hours).ok()?.checked_mul(3600)?;
now.with_timezone(&FixedOffset::east_opt(seconds)?)
}
};
Datetime::from_ymd(
with_offset.year(),
with_offset.month().try_into().ok()?,
with_offset.day().try_into().ok()?,
)
}
}
impl SimpleWorld {
/// Access the canonical slot for the given file id.
fn slot<F, T>(&self, id: FileId, f: F) -> T
where
F: FnOnce(&mut FileSlot) -> T,
{
let mut map = self.slots.lock().unwrap();
f(map.entry(id).or_insert_with(|| FileSlot::new(id)))
}
}
/// Holds the processed data for a file ID.
///
/// Both fields can be populated if the file is both imported and read().
struct FileSlot {
/// The slot's file id.
id: FileId,
/// The lazily loaded and incrementally updated source file.
source: SlotCell<Source>,
/// The lazily loaded raw byte buffer.
file: SlotCell<Bytes>,
}
impl FileSlot {
/// Create a new file slot.
fn new(id: FileId) -> Self {
Self {
id,
file: SlotCell::new(),
source: SlotCell::new(),
}
}
/// Retrieve the source for this file.
fn source(
&mut self,
project_root: &Path,
package_storage: &PackageStorage,
) -> FileResult<Source> {
self.source.get_or_init(
|| read(self.id, project_root, package_storage),
|data, prev| {
let text = decode_utf8(&data)?;
if let Some(mut prev) = prev {
prev.replace(text);
Ok(prev)
} else {
Ok(Source::new(self.id, text.into()))
}
},
)
}
/// Retrieve the file's bytes.
fn file(&mut self, project_root: &Path, package_storage: &PackageStorage) -> FileResult<Bytes> {
self.file.get_or_init(
|| read(self.id, project_root, package_storage),
|data, _| Ok(Bytes::new(data)),
)
}
}
/// Lazily processes data for a file.
struct SlotCell<T> {
/// The processed data.
data: Option<FileResult<T>>,
/// A hash of the raw file contents / access error.
fingerprint: u128,
/// Whether the slot has been accessed in the current compilation.
accessed: bool,
}
impl<T: Clone> SlotCell<T> {
/// Creates a new, empty cell.
fn new() -> Self {
Self {
data: None,
fingerprint: 0,
accessed: false,
}
}
/// Gets the contents of the cell or initialize them.
fn get_or_init(
&mut self,
load: impl FnOnce() -> FileResult<Vec<u8>>,
f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
) -> FileResult<T> {
// If we accessed the file already in this compilation, retrieve it.
if mem::replace(&mut self.accessed, true) {
if let Some(data) = &self.data {
return data.clone();
}
}
// Read and hash the file.
let result = load();
let fingerprint = typst::utils::hash128(&result);
// If the file contents didn't change, yield the old processed data.
if mem::replace(&mut self.fingerprint, fingerprint) == fingerprint {
if let Some(data) = &self.data {
return data.clone();
}
}
let prev = self.data.take().and_then(Result::ok);
let value = result.and_then(|data| f(data, prev));
self.data = Some(value.clone());
value
}
}
/// Resolves the path of a file id on the system, downloading a package if
/// necessary.
fn system_path(
project_root: &Path,
id: FileId,
package_storage: &PackageStorage,
) -> FileResult<PathBuf> {
// Determine the root path relative to which the file path
// will be resolved.
let buf;
let mut root = project_root;
if let Some(spec) = id.package() {
buf = package_storage.prepare_package(spec, &mut PrintDownload(&spec))?;
root = &buf;
}
// Join the path to the root. If it tries to escape, deny
// access. Note: It can still escape via symlinks.
id.vpath().resolve(root).ok_or(FileError::AccessDenied)
}
/// Reads a file from a `FileId`.
///
/// If the ID represents stdin it will read from standard input,
/// otherwise it gets the file path of the ID and reads the file from disk.
fn read(id: FileId, project_root: &Path, package_storage: &PackageStorage) -> FileResult<Vec<u8>> {
read_from_disk(&system_path(project_root, id, package_storage)?)
}
/// Read a file from disk.
fn read_from_disk(path: &Path) -> FileResult<Vec<u8>> {
let f = |e| FileError::from_io(e, path);
if fs::metadata(path).map_err(f)?.is_dir() {
Err(FileError::IsDirectory)
} else {
fs::read(path).map_err(f)
}
}
/// Decode UTF-8 with an optional BOM.
fn decode_utf8(buf: &[u8]) -> FileResult<&str> {
// Remove UTF-8 BOM.
Ok(std::str::from_utf8(
buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf),
)?)
}
/// The current date and time.
enum Now {
/// The current date and time if the time is not externally fixed.
System(OnceLock<DateTime<Utc>>),
}
/// An error that occurs during world construction.
#[derive(Debug)]
pub enum WorldCreationError {
/// The input file does not appear to exist.
InputNotFound(PathBuf),
/// The input file is not contained within the root folder.
InputOutsideRoot,
/// The root directory does not appear to exist.
RootNotFound(PathBuf),
/// Another type of I/O error.
Io(io::Error),
}
impl fmt::Display for WorldCreationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WorldCreationError::InputNotFound(path) => {
write!(f, "input file not found (searched at {})", path.display())
}
WorldCreationError::InputOutsideRoot => {
write!(f, "source file must be contained in project root")
}
WorldCreationError::RootNotFound(path) => {
write!(
f,
"root directory not found (searched at {})",
path.display()
)
}
WorldCreationError::Io(err) => write!(f, "{err}"),
}
}
}
impl From<WorldCreationError> for EcoString {
fn from(err: WorldCreationError) -> Self {
eco_format!("{err}")
}
}
fn compile<T: AsRef<Path>, U: AsRef<str>>(source_code: U, path: T) -> Result<(), ()> {
let mut world = SimpleWorld::new(source_code.as_ref()).unwrap();
let output = Output::Path(path.as_ref().to_path_buf());
let Warned { output, warnings } = compile_and_export(&mut world, &output);
match output {
Ok(_) => {
for diagnostic in warnings {
eprintln!("{}", diagnostic.message);
}
Ok(())
}
Err(errors) => {
for diagnostic in warnings.into_iter().chain(errors) {
eprintln!("{}", diagnostic.message);
}
Err(())
}
}
}
/// Compile and then export the document.
fn compile_and_export(
world: &mut SimpleWorld,
output: &Output,
) -> Warned<SourceResult<Vec<Output>>> {
let Warned {
output: document,
warnings,
} = typst::compile::<PagedDocument>(world);
let result =
document.and_then(|document| export_pdf(&document, output).map(|()| vec![output.clone()]));
Warned {
output: result,
warnings,
}
}
/// Export to a PDF.
fn export_pdf(document: &PagedDocument, output: &Output) -> SourceResult<()> {
// If the timestamp is provided through the CLI, use UTC suffix,
// else, use the current local time and timezone.
let timestamp = {
let local_datetime = chrono::Local::now();
convert_datetime(local_datetime).and_then(|datetime| {
Timestamp::new_local(datetime, local_datetime.offset().local_minus_utc() / 60)
})
};
let options = PdfOptions {
ident: Smart::Auto,
timestamp,
page_ranges: None,
standards: PdfStandards::default(),
};
let buffer = typst_pdf::pdf(document, &options)?;
output
.write(&buffer)
.map_err(|err| eco_format!("failed to write PDF file ({err})"))
.at(Span::detached())?;
Ok(())
}
/// An output that is either stdout or a real path.
#[derive(Debug, Clone)]
pub enum Output {
/// Stdout, represented by `-`.
Stdout,
/// A non-empty path.
Path(PathBuf),
}
impl Output {
fn write(&self, buffer: &[u8]) -> StrResult<()> {
match self {
Output::Stdout => io::Write::write_all(&mut std::io::stdout(), buffer),
Output::Path(path) => fs::write(path, buffer),
}
.map_err(|err| eco_format!("{err}"))
}
}
impl Display for Output {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Output::Stdout => f.pad("stdout"),
Output::Path(path) => path.display().fmt(f),
}
}
}
/// Convert [`chrono::DateTime`] to [`Datetime`]
fn convert_datetime<Tz: chrono::TimeZone>(date_time: chrono::DateTime<Tz>) -> Option<Datetime> {
Datetime::from_ymd_hms(
date_time.year(),
date_time.month().try_into().ok()?,
date_time.day().try_into().ok()?,
date_time.hour().try_into().ok()?,
date_time.minute().try_into().ok()?,
date_time.second().try_into().ok()?,
)
}
/// Prints download progress by writing `downloading {0}` followed by repeatedly
/// updating the last terminal line.
pub struct PrintDownload<T>(pub T);
impl<T: Display> Progress for PrintDownload<T> {
fn print_start(&mut self) {}
fn print_progress(&mut self, _state: &DownloadState) {}
fn print_finish(&mut self, _state: &DownloadState) {}
}
/// Returns a new downloader.
pub fn downloader() -> Downloader {
let user_agent = concat!("typst/", env!("CARGO_PKG_VERSION"));
Downloader::new(user_agent)
}