use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use chrono::{DateTime, Datelike, FixedOffset, Local, Utc};
use typst::diag::{FileError, FileResult, SourceDiagnostic};
use typst::foundations::{Bytes, Datetime, Smart};
use typst::layout::PagedDocument;
use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Feature, Features, Library, LibraryExt, World};
use typst_html::HtmlDocument;
use typst_kit::fonts::{FontSlot, Fonts};
use typst_pdf::{PdfOptions, PdfStandards};
use crate::{Error, Job, Result, extension_of};
pub fn typ_to(job: &Job) -> Result<()> {
let to = extension_of(job.output).unwrap_or_default();
let input_abs = fs::canonicalize(job.input)?;
let root = input_abs
.parent()
.ok_or_else(|| Error::other("typst: input has no parent directory"))?
.to_path_buf();
let main_vpath = VirtualPath::within_root(&input_abs, &root)
.ok_or_else(|| Error::other("typst: input is outside project root"))?;
let main = FileId::new(None, main_vpath);
let world = build_world(root, main, None, &to);
emit(&world, &to, job.output)
}
pub fn compile_source(
source_text: &str,
root: PathBuf,
target: &str,
output: &Path,
) -> Result<()> {
let main_vpath = VirtualPath::new("__univert_main.typ");
let main = FileId::new(None, main_vpath);
let source = Source::new(main, source_text.to_owned());
let world = build_world(root, main, Some(source), target);
emit(&world, target, output)
}
fn build_world(
root: PathBuf,
main: FileId,
preloaded: Option<Source>,
target: &str,
) -> SystemWorld {
let mut builder = Library::builder();
if target == "html" {
let features: Features = [Feature::Html].into_iter().collect();
builder = builder.with_features(features);
}
let library = builder.build();
let fonts = Fonts::searcher().search();
let mut slots: HashMap<FileId, FileSlot> = HashMap::new();
if let Some(src) = preloaded {
slots.insert(
main,
FileSlot {
source: Some(Ok(src)),
bytes: None,
},
);
}
SystemWorld {
root,
main,
library: LazyHash::new(library),
book: LazyHash::new(fonts.book),
fonts: fonts.fonts,
slots: Mutex::new(slots),
now: Utc::now(),
}
}
fn emit(world: &SystemWorld, target: &str, output: &Path) -> Result<()> {
match target {
"html" => {
let document = typst::compile::<HtmlDocument>(world)
.output
.map_err(diag_error)?;
let html = typst_html::html(&document).map_err(diag_error)?;
fs::write(output, html)?;
}
"pdf" => {
let document = typst::compile::<PagedDocument>(world)
.output
.map_err(diag_error)?;
let options = PdfOptions {
ident: Smart::Auto,
timestamp: None,
page_ranges: None,
standards: PdfStandards::default(),
tagged: false,
};
let buffer = typst_pdf::pdf(&document, &options).map_err(diag_error)?;
fs::write(output, buffer)?;
}
"png" => {
let document = typst::compile::<PagedDocument>(world)
.output
.map_err(diag_error)?;
let page = document
.pages
.first()
.ok_or_else(|| Error::other("typst: document has no pages"))?;
let pixmap = typst_render::render(page, 144.0 / 72.0);
let bytes = pixmap
.encode_png()
.map_err(|e| Error::other(format!("typst png encode: {e}")))?;
fs::write(output, bytes)?;
}
"svg" => {
let document = typst::compile::<PagedDocument>(world)
.output
.map_err(diag_error)?;
let page = document
.pages
.first()
.ok_or_else(|| Error::other("typst: document has no pages"))?;
let svg = typst_svg::svg(page);
fs::write(output, svg)?;
}
other => return Err(Error::other(format!("typst cannot emit .{other}"))),
}
Ok(())
}
fn diag_error<I>(diags: I) -> Error
where
I: IntoIterator,
I::Item: std::borrow::Borrow<SourceDiagnostic>,
{
use std::borrow::Borrow;
let msgs: Vec<String> = diags
.into_iter()
.map(|d| d.borrow().message.to_string())
.collect();
Error::other(format!("typst: {}", msgs.join("; ")))
}
struct SystemWorld {
root: PathBuf,
main: FileId,
library: LazyHash<Library>,
book: LazyHash<FontBook>,
fonts: Vec<FontSlot>,
slots: Mutex<HashMap<FileId, FileSlot>>,
now: DateTime<Utc>,
}
#[derive(Default)]
struct FileSlot {
source: Option<FileResult<Source>>,
bytes: Option<FileResult<Bytes>>,
}
impl SystemWorld {
fn resolve(&self, id: FileId) -> FileResult<PathBuf> {
if id.package().is_some() {
return Err(FileError::Other(Some(
"typst package imports are not supported by univert".into(),
)));
}
id.vpath()
.resolve(&self.root)
.ok_or(FileError::AccessDenied)
}
fn load(&self, id: FileId) -> FileResult<Vec<u8>> {
let path = self.resolve(id)?;
fs::read(&path).map_err(|e| FileError::from_io(e, &path))
}
}
impl World for SystemWorld {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
&self.book
}
fn main(&self) -> FileId {
self.main
}
fn source(&self, id: FileId) -> FileResult<Source> {
let mut slots = self.slots.lock().unwrap();
let slot = slots.entry(id).or_default();
if let Some(r) = &slot.source {
return r.clone();
}
let r = self.load(id).and_then(|data| {
let buf = data.strip_prefix(b"\xef\xbb\xbf").unwrap_or(&data);
let text = std::str::from_utf8(buf).map_err(FileError::from)?;
Ok(Source::new(id, text.into()))
});
slot.source = Some(r.clone());
r
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
let mut slots = self.slots.lock().unwrap();
let slot = slots.entry(id).or_default();
if let Some(r) = &slot.bytes {
return r.clone();
}
let r = self.load(id).map(Bytes::new);
slot.bytes = Some(r.clone());
r
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.get(index)?.get()
}
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
let with_offset = match offset {
None => self.now.with_timezone(&Local).fixed_offset(),
Some(hours) => {
let seconds = i32::try_from(hours).ok()?.checked_mul(3600)?;
self.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()?,
)
}
}