use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use comemo::track;
use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime};
use typst::syntax::{FileId, Source};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Library, World};
use typst_kit::fonts::{FontSlot, Fonts};
struct FontsHolder {
book: LazyHash<FontBook>,
fonts: Vec<FontSlot>,
}
lazy_static::lazy_static! {
static ref FONTS: FontsHolder = {
let fonts = Fonts::searcher().include_system_fonts(false).search();
FontsHolder { book: fonts.book.into(), fonts: fonts.fonts }
};
}
pub struct TypstWrapperWorld {
source: Source,
pub(crate) library: LazyHash<Library>,
time: time::OffsetDateTime,
files: Arc<Mutex<HashMap<FileId, FileEntry>>>,
}
impl TypstWrapperWorld {
pub fn new(source: impl Into<String>) -> Self {
Self {
library: LazyHash::new(Library::default()),
source: Source::detached(source),
time: time::OffsetDateTime::now_utc(),
files: Arc::new(Mutex::new(HashMap::new())),
}
}
fn get_file(&self, id: FileId) -> FileResult<FileEntry> {
let mut files = self.files.lock().map_err(|_| FileError::AccessDenied)?;
if let Some(entry) = files.get(&id) {
return Ok(entry.clone());
}
let path = if let Some(package) = id.package() {
Err(typst::diag::PackageError::NotFound(package.clone()))?
} else {
id.vpath().resolve(&std::env::current_dir().unwrap())
}
.ok_or(FileError::AccessDenied)?;
let content = std::fs::read(&path).map_err(|error| FileError::from_io(error, &path))?;
Ok(files
.entry(id)
.or_insert(FileEntry::new(content, None))
.clone())
}
}
#[derive(Clone, Debug)]
struct FileEntry {
bytes: Bytes,
source: Option<Source>,
}
impl FileEntry {
fn new(bytes: Vec<u8>, source: Option<Source>) -> Self {
Self {
bytes: bytes.into(),
source,
}
}
fn source(&mut self, id: FileId) -> FileResult<Source> {
let source = if let Some(source) = &self.source {
source
} else {
let contents = std::str::from_utf8(&self.bytes).map_err(|_| FileError::InvalidUtf8)?;
let contents = contents.trim_start_matches('\u{feff}');
let source = Source::new(id, contents.into());
self.source.insert(source)
};
Ok(source.clone())
}
}
#[track]
impl typst::World for TypstWrapperWorld {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
&FONTS.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.get_file(id)?.source(id)
}
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
self.get_file(id).map(|file| file.bytes.clone())
}
fn font(&self, id: usize) -> Option<Font> {
FONTS.fonts[id].get()
}
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
let offset = offset.unwrap_or(0);
let offset = time::UtcOffset::from_hms(offset.try_into().ok()?, 0, 0).ok()?;
let time = self.time.checked_to_offset(offset)?;
Some(Datetime::Date(time.date()))
}
}