Files
typst-desktop/src-tauri/src/world.rs
T

164 lines
4.7 KiB
Rust

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<Library>,
main: FileId,
files: HashMap<String, Vec<u8>>,
book: typst::utils::LazyHash<FontBook>,
fonts: Vec<Font>,
packages: SystemPackages,
}
fn normalize_path(path: &str) -> String {
path.trim_start_matches('/').replace('\\', "/")
}
fn bundled_fonts() -> &'static Vec<Font> {
static FONTS: OnceLock<Vec<Font>> = 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<Font> {
static CACHE: OnceLock<Mutex<HashMap<(String, usize), Vec<Font>>>> = 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> = 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<String, Vec<u8>>, 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<Vec<u8>> {
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<Library> {
&self.library
}
fn book(&self) -> &typst::utils::LazyHash<FontBook> {
&self.book
}
fn main(&self) -> FileId {
self.main
}
fn source(&self, id: FileId) -> FileResult<Source> {
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<Bytes> {
let data = self.load_bytes(id)?;
Ok(Bytes::new(data))
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.get(index).cloned()
}
fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
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)
}
}