use chrono::Datelike; use std::collections::HashMap; 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('\\', "/") } 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 book = FontBook::new(); let mut fonts = Vec::new(); for data in typst_assets::fonts() { let buffer = Bytes::new(data); for font in Font::iter(buffer) { book.push(font.info().clone()); fonts.push(font); } } for (name, data) in &files { let lower = name.to_lowercase(); if [".ttf", ".otf", ".ttc", ".otc"] .iter() .any(|ext| lower.ends_with(ext)) { for font in Font::iter(Bytes::new(data.clone())) { book.push(font.info().clone()); fonts.push(font); } } } 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) } }