use std::cell::OnceCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{OnceLock, RwLock};
use fontdb::Database;
use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime};
use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook};
use typst::{Library, World};
use crate::engine::EngineConfig;
pub struct ReportWorld {
main: Source,
library: typst::utils::LazyHash<Library>,
book: typst::utils::LazyHash<FontBook>,
fonts: Vec<Font>,
slots: RwLock<HashMap<FileId, FileSlot>>,
root: PathBuf,
now: OnceLock<Option<Datetime>>,
}
struct FileSlot {
source: OnceCell<FileResult<Source>>,
bytes: OnceCell<FileResult<Bytes>>,
}
impl FileSlot {
fn new() -> Self {
Self {
source: OnceCell::new(),
bytes: OnceCell::new(),
}
}
}
unsafe impl Send for FileSlot {}
unsafe impl Sync for FileSlot {}
impl ReportWorld {
pub fn new(source: String, config: &EngineConfig) -> Self {
let main = Source::detached(source);
let library = typst::utils::LazyHash::new(Library::default());
let mut fontdb = Database::new();
if config.use_system_fonts {
fontdb.load_system_fonts();
}
for path in &config.font_paths {
if path.is_dir() {
fontdb.load_fonts_dir(path);
} else if path.is_file() {
let _ = fontdb.load_font_file(path);
}
}
let mut book = FontBook::new();
let mut fonts = Vec::new();
for face in fontdb.faces() {
let source_data: Option<Vec<u8>> = match &face.source {
fontdb::Source::File(path) => std::fs::read(path).ok(),
fontdb::Source::Binary(data) => {
let slice: &[u8] = data.as_ref().as_ref();
Some(slice.to_vec())
}
fontdb::Source::SharedFile(_, data) => {
let slice: &[u8] = data.as_ref().as_ref();
Some(slice.to_vec())
}
};
if let Some(data) = source_data {
let buffer = Bytes::new(data);
for font in Font::iter(buffer) {
book.push(font.info().clone());
fonts.push(font);
}
}
}
Self {
main,
library,
book: typst::utils::LazyHash::new(book),
fonts,
slots: RwLock::new(HashMap::new()),
root: std::env::current_dir().unwrap_or_default(),
now: OnceLock::new(),
}
}
pub fn with_root(mut self, root: impl Into<PathBuf>) -> Self {
self.root = root.into();
self
}
pub fn add_file(&self, path: impl AsRef<Path>, content: impl Into<Vec<u8>>) {
let id = FileId::new(None, VirtualPath::new(path.as_ref()));
let mut slots = self.slots.write().unwrap();
let slot = slots.entry(id).or_insert_with(FileSlot::new);
let _ = slot.bytes.set(Ok(Bytes::new(content.into())));
}
}
impl World for ReportWorld {
fn library(&self) -> &typst::utils::LazyHash<Library> {
&self.library
}
fn book(&self) -> &typst::utils::LazyHash<FontBook> {
&self.book
}
fn main(&self) -> FileId {
self.main.id()
}
fn source(&self, id: FileId) -> FileResult<Source> {
if id == self.main.id() {
return Ok(self.main.clone());
}
let slots = self.slots.read().unwrap();
if let Some(slot) = slots.get(&id) {
if let Some(result) = slot.source.get() {
return result.clone();
}
}
drop(slots);
let path = id.vpath().as_rooted_path();
let full_path = self.root.join(path.strip_prefix("/").unwrap_or(path));
let result = std::fs::read_to_string(&full_path)
.map_err(|e| FileError::from_io(e, &full_path))
.map(|text| Source::new(id, text));
let mut slots = self.slots.write().unwrap();
let slot = slots.entry(id).or_insert_with(FileSlot::new);
let _ = slot.source.set(result.clone());
result
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
let slots = self.slots.read().unwrap();
if let Some(slot) = slots.get(&id) {
if let Some(result) = slot.bytes.get() {
return result.clone();
}
}
drop(slots);
let path = id.vpath().as_rooted_path();
let full_path = self.root.join(path.strip_prefix("/").unwrap_or(path));
let result = std::fs::read(&full_path)
.map_err(|e| FileError::from_io(e, &full_path))
.map(Bytes::new);
let mut slots = self.slots.write().unwrap();
let slot = slots.entry(id).or_insert_with(FileSlot::new);
let _ = slot.bytes.set(result.clone());
result
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.get(index).cloned()
}
fn today(&self, _offset: Option<i64>) -> Option<Datetime> {
*self.now.get_or_init(|| {
let now = chrono::Local::now();
Datetime::from_ymd_hms(
now.format("%Y").to_string().parse().ok()?,
now.format("%m").to_string().parse().ok()?,
now.format("%d").to_string().parse().ok()?,
now.format("%H").to_string().parse().ok()?,
now.format("%M").to_string().parse().ok()?,
now.format("%S").to_string().parse().ok()?,
)
})
}
}