use chrono::Datelike; use std::collections::HashMap; use std::sync::{Mutex, OnceLock}; use typst::diag::{FileError, FileResult}; use typst::foundations::{Bytes, Datetime, Duration}; use typst::syntax::{FileId, RootedPath, Source, VirtualPath, VirtualRoot}; use typst::text::{Font, FontBook}; use typst::World; use typst::{Library, LibraryExt}; use typst_kit::downloader::SystemDownloader; use typst_kit::packages::SystemPackages; pub struct ProjectWorld { library: typst::utils::LazyHash, main: FileId, files: HashMap>, book: typst::utils::LazyHash, fonts: Vec, packages: SystemPackages, } fn normalize_path(path: &str) -> String { path.trim_start_matches('/').replace('\\', "/") } fn bundled_fonts() -> &'static Vec { static FONTS: OnceLock> = OnceLock::new(); FONTS.get_or_init(|| { let mut fonts = Vec::new(); for data in typst_assets::fonts() { fonts.extend(Font::iter(Bytes::new(data))); } fonts }) } fn custom_fonts(name: &str, data: &[u8]) -> Vec { static CACHE: OnceLock>>> = OnceLock::new(); let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); let key = (name.to_string(), data.len()); if let Ok(map) = cache.lock() { if let Some(fonts) = map.get(&key) { return fonts.clone(); } } let fonts: Vec = Font::iter(Bytes::new(data.to_vec())).collect(); if let Ok(mut map) = cache.lock() { map.insert(key, fonts.clone()); } fonts } impl ProjectWorld { pub fn new(entrypoint: String, files: HashMap>, enable_html: bool) -> Self { let main = FileId::new(RootedPath::new( VirtualRoot::Project, VirtualPath::new(&entrypoint).unwrap_or_else(|_| VirtualPath::new("main.typ").unwrap()), )); let downloader = SystemDownloader::new("TypstDesktop (typst-kit)"); let packages = SystemPackages::new(downloader); let mut fonts = bundled_fonts().clone(); for (name, data) in &files { let lower = name.to_lowercase(); if [".ttf", ".otf", ".ttc", ".otc"] .iter() .any(|ext| lower.ends_with(ext)) { fonts.extend(custom_fonts(name, data)); } } let mut book = FontBook::new(); for font in &fonts { book.push(font.info().clone()); } let library = if enable_html { Library::builder() .with_features([typst::Feature::Html].into_iter().collect()) .build() } else { Library::builder().build() }; Self { library: typst::utils::LazyHash::new(library), main, files, book: typst::utils::LazyHash::new(book), fonts, packages, } } fn load_bytes(&self, id: FileId) -> FileResult> { let path = normalize_path(id.vpath().get_without_slash()); if let VirtualRoot::Package(package) = id.root() { let root = self .packages .obtain(package) .map_err(|e| FileError::Other(Some(e.to_string().into())))?; return root.load(id.vpath()).map(|bytes| bytes.to_vec()); } self.files .get(&path) .cloned() .ok_or_else(|| FileError::NotFound(path.into())) } } impl World for ProjectWorld { fn library(&self) -> &typst::utils::LazyHash { &self.library } fn book(&self) -> &typst::utils::LazyHash { &self.book } fn main(&self) -> FileId { self.main } fn source(&self, id: FileId) -> FileResult { let data = self.load_bytes(id)?; let text = std::str::from_utf8(&data) .map_err(|_| FileError::InvalidUtf8)? .to_owned(); Ok(Source::new(id, text)) } fn file(&self, id: FileId) -> FileResult { let data = self.load_bytes(id)?; Ok(Bytes::new(data)) } fn font(&self, index: usize) -> Option { self.fonts.get(index).cloned() } fn today(&self, offset: Option) -> Option { let now = chrono::Local::now(); let date = if let Some(offset) = offset { let offset_secs = offset.hours() as i32 * 3600; let offset_chrono = chrono::FixedOffset::east_opt(offset_secs)?; now.with_timezone(&offset_chrono).date_naive() } else { now.date_naive() }; Datetime::from_ymd(date.year(), date.month() as u8, date.day() as u8) } }