use eyre::Result;
use std::{collections::HashMap, io::Write, path::PathBuf, sync::Mutex};
use typst::layout::PagedDocument;
use typst::{
diag::{eco_format, FileError, FileResult, PackageError, PackageResult},
foundations::{Bytes, Datetime},
syntax::{package::PackageSpec, FileId, Source},
text::{Font, FontBook},
utils::LazyHash,
Library, World,
};
pub mod templates;
pub trait MathCompiler {
fn compile(&self, input: &str) -> Result<(String, Vec<u8>)>;
}
use crate::cargo_target_directory;
fn fonts() -> Vec<Font> {
typst_assets::fonts()
.flat_map(|bytes| {
let buffer = Bytes::new(bytes);
let face_count = ttf_parser::fonts_in_collection(&buffer).unwrap_or(1);
(0..face_count).map(move |face| {
Font::new(buffer.clone(), face).expect("failed to load font from typst-assets")
})
})
.collect()
}
pub struct TypstFile {
bytes: Bytes,
source: Option<Source>,
}
impl TypstFile {
fn source(&mut self, id: FileId) -> FileResult<Source> {
let source = match &self.source {
Some(source) => source,
None => {
let contents =
std::str::from_utf8(&self.bytes).map_err(|_| FileError::InvalidUtf8)?;
let source = Source::new(id, contents.into());
self.source.insert(source)
}
};
Ok(source.clone())
}
}
pub struct TypstCompiler {
library: LazyHash<Library>,
book: LazyHash<FontBook>,
fonts: Vec<Font>,
packages_cache_path: PathBuf,
files: Mutex<HashMap<FileId, TypstFile>>,
}
impl TypstCompiler {
pub fn new() -> Self {
let fonts = fonts();
Self {
library: LazyHash::new(Library::default()),
book: LazyHash::new(FontBook::from_fonts(&fonts)),
fonts,
packages_cache_path: cargo_target_directory()
.unwrap()
.to_path_buf()
.join("typst-packages"),
files: Mutex::new(HashMap::new()),
}
}
pub fn wrap_source(&self, source: impl Into<String>) -> WrapSource<'_> {
WrapSource {
compiler: self,
source: Source::detached(source),
time: time::OffsetDateTime::now_local().unwrap_or(time::OffsetDateTime::now_utc()),
}
}
fn package(&self, package: &PackageSpec) -> PackageResult<PathBuf> {
let package_subdir = format!("{}/{}/{}", package.namespace, package.name, package.version);
let path = self.packages_cache_path.join(package_subdir);
if path.exists() {
return Ok(path);
}
let package_url = format!(
"https://packages.typst.org/{}/{}-{}.tar.gz",
package.namespace, package.name, package.version
);
let mut response = reqwest::blocking::get(package_url).map_err(|e| {
PackageError::NetworkFailed(Some(eco_format!(
"Failed to download package {}: {}",
package.name,
e
)))
})?;
let mut compressed = Vec::new();
response.copy_to(&mut compressed).map_err(|e| {
PackageError::NetworkFailed(Some(eco_format!(
"Failed to save package {}: {}",
package.name,
e
)))
})?;
let mut decompressed = Vec::new();
let mut decoder = flate2::write::GzDecoder::new(decompressed);
decoder.write_all(&compressed).map_err(|e| {
PackageError::MalformedArchive(Some(eco_format!(
"Failed to decompress package {}: {}",
package.name,
e
)))
})?;
decoder.try_finish().map_err(|e| {
PackageError::MalformedArchive(Some(eco_format!(
"Failed to decompress package {}: {}",
package.name,
e
)))
})?;
decompressed = decoder.finish().map_err(|e| {
PackageError::MalformedArchive(Some(eco_format!(
"Failed to decompress package {}: {}",
package.name,
e
)))
})?;
let mut archive = tar::Archive::new(decompressed.as_slice());
archive.unpack(&path).map_err(|e| {
std::fs::remove_dir_all(&path).ok();
PackageError::MalformedArchive(Some(eco_format!(
"Failed to unpack package {}: {}",
package.name,
e
)))
})?;
Ok(path)
}
fn file<T>(&self, id: FileId, map: impl FnOnce(&mut TypstFile) -> T) -> FileResult<T> {
let mut files = self.files.lock().unwrap();
if let Some(entry) = files.get_mut(&id) {
return Ok(map(entry));
}
'x: {
if let Some(package) = id.package() {
let package_dir = self.package(package)?;
let Some(path) = id.vpath().resolve(&package_dir) else {
break 'x;
};
let contents =
std::fs::read(&path).map_err(|error| FileError::from_io(error, &path))?;
let entry = files.entry(id).or_insert(TypstFile {
bytes: Bytes::new(contents),
source: None,
});
return Ok(map(entry));
}
}
Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
}
}
impl MathCompiler for TypstCompiler {
fn compile(&self, source: &str) -> Result<(String, Vec<u8>)> {
let source = templates::raw(&source);
let world = self.wrap_source(source);
let document = typst::compile(&world);
let warnings = document.warnings;
let has_error = warnings
.iter()
.any(|w| w.severity == typst::diag::Severity::Error);
if has_error {
return Err(eyre::Report::msg(format!("{:?}", warnings)));
}
let document: PagedDocument = document
.output
.map_err(|diags| eyre::Report::msg(format!("{:?}", diags)))?;
let page = document
.pages
.first()
.ok_or(eyre::Report::msg("No pages found"))?;
Ok((
typst_svg::svg(page),
typst_pdf::pdf(
&PagedDocument {
pages: vec![page.clone()],
info: Default::default(),
introspector: Default::default(),
},
&Default::default(),
)
.unwrap(),
))
}
}
pub struct WrapSource<'a> {
compiler: &'a TypstCompiler,
source: Source,
time: time::OffsetDateTime,
}
impl World for WrapSource<'_> {
fn library(&self) -> &LazyHash<Library> {
&self.compiler.library
}
fn book(&self) -> &LazyHash<FontBook> {
&self.compiler.book
}
fn main(&self) -> FileId {
self.source.id()
}
fn source(&self, id: FileId) -> FileResult<Source> {
if id == self.source.id() {
Ok(self.source.clone())
} else {
self.compiler.file(id, |file| file.source(id))?
}
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
self.compiler.file(id, |file| file.bytes.clone())
}
fn font(&self, index: usize) -> Option<Font> {
self.compiler.fonts.get(index).cloned()
}
fn today(&self, _offset: Option<i64>) -> Option<Datetime> {
Some(Datetime::Date(self.time.date()))
}
}