use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock, RwLock};
use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime, Dict, Duration};
use typst::syntax::{FileId, RootedPath, Source, VirtualPath, VirtualRoot};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Library, LibraryExt, World};
use crate::Fonts;
pub fn project_file(path: VirtualPath) -> FileId {
RootedPath::new(VirtualRoot::Project, path).intern()
}
struct FileSlot {
source: OnceLock<FileResult<Source>>,
bytes: OnceLock<FileResult<Bytes>>,
}
impl FileSlot {
fn new() -> Self {
Self {
source: OnceLock::new(),
bytes: OnceLock::new(),
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub struct Dependency {
pub name: String,
pub path: PathBuf,
pub data: Bytes,
}
pub struct EmbeddedWorld {
main: Source,
library: LazyHash<Library>,
fonts: Arc<Fonts>,
files: RwLock<HashMap<FileId, FileSlot>>,
roots: Vec<PathBuf>,
#[cfg(all(feature = "packages", not(target_arch = "wasm32")))]
packages: Option<crate::Packages>,
fixed_now: Option<chrono::DateTime<chrono::Utc>>,
#[cfg(not(target_arch = "wasm32"))]
now: OnceLock<chrono::DateTime<chrono::Utc>>,
}
impl EmbeddedWorld {
pub fn new(main: VirtualPath, source: impl Into<String>, fonts: Arc<Fonts>) -> Self {
Self {
main: Source::new(project_file(main), source.into()),
library: LazyHash::new(Library::default()),
fonts,
files: RwLock::new(HashMap::new()),
roots: Vec::new(),
#[cfg(all(feature = "packages", not(target_arch = "wasm32")))]
packages: None,
fixed_now: None,
#[cfg(not(target_arch = "wasm32"))]
now: OnceLock::new(),
}
}
pub fn with_roots(mut self, roots: Vec<PathBuf>) -> Self {
self.roots = roots;
self
}
pub fn with_inputs(mut self, inputs: Dict) -> Self {
self.library = LazyHash::new(Library::builder().with_inputs(inputs).build());
self
}
pub fn with_creation_timestamp(mut self, seconds: i64) -> Self {
self.fixed_now = chrono::DateTime::from_timestamp(seconds, 0);
self
}
#[cfg(all(feature = "packages", not(target_arch = "wasm32")))]
pub fn with_packages(mut self, packages: crate::Packages) -> Self {
self.packages = Some(packages);
self
}
pub fn add_file(&self, path: VirtualPath, contents: impl AsRef<[u8]> + Send + Sync + 'static) {
let mut files = self
.files
.write()
.expect("virtual filesystem lock poisoned");
let slot = files
.entry(project_file(path))
.or_insert_with(FileSlot::new);
let _ = slot.bytes.set(Ok(Bytes::new(contents)));
}
pub fn fonts(&self) -> &Fonts {
&self.fonts
}
pub fn path_for(&self, id: FileId) -> FileResult<PathBuf> {
let not_found = || FileError::NotFound(id.vpath().get_with_slash().into());
match id.root() {
VirtualRoot::Project => self
.roots
.iter()
.filter_map(|root| id.vpath().realize(root).ok())
.find(|path| path.is_file())
.ok_or_else(not_found),
#[cfg(all(feature = "packages", not(target_arch = "wasm32")))]
VirtualRoot::Package(spec) => {
let packages = self.packages.as_ref().ok_or_else(not_found)?;
let root = packages.root(spec)?;
id.vpath().realize(&root).map_err(|_| not_found())
}
#[cfg(not(all(feature = "packages", not(target_arch = "wasm32"))))]
VirtualRoot::Package(_) => Err(not_found()),
}
}
pub fn display_path(&self, id: FileId) -> String {
let Ok(path) = self.path_for(id) else {
return id.vpath().get_without_slash().to_owned();
};
std::env::current_dir()
.ok()
.and_then(|cwd| cwd.canonicalize().ok())
.and_then(|cwd| path.strip_prefix(cwd).ok().map(Path::to_path_buf))
.unwrap_or(path)
.display()
.to_string()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn dependencies(&self) -> Vec<Dependency> {
let files = self.files.read().expect("virtual filesystem lock poisoned");
let mut dependencies = Vec::new();
for (id, slot) in files.iter() {
let data = match (slot.bytes.get(), slot.source.get()) {
(Some(Ok(bytes)), _) => bytes.clone(),
(_, Some(Ok(source))) => Bytes::from_string(source.text().to_owned()),
_ => continue,
};
let Ok(path) = self.path_for(*id) else {
continue;
};
let name = match id.root() {
VirtualRoot::Project => id.vpath().get_without_slash().to_owned(),
VirtualRoot::Package(spec) => format!("{spec}/{}", id.vpath().get_without_slash()),
};
dependencies.push(Dependency { name, path, data });
}
dependencies
}
fn cached_or_load<T: Clone>(
&self,
id: FileId,
cell: impl Fn(&FileSlot) -> &OnceLock<FileResult<T>>,
load: impl FnOnce(&Path) -> FileResult<T>,
) -> FileResult<T> {
{
let files = self.files.read().expect("virtual filesystem lock poisoned");
if let Some(result) = files.get(&id).and_then(|slot| cell(slot).get()) {
return result.clone();
}
}
let result = self.path_for(id).and_then(|path| load(&path));
let mut files = self
.files
.write()
.expect("virtual filesystem lock poisoned");
let slot = files.entry(id).or_insert_with(FileSlot::new);
cell(slot).get_or_init(|| result.clone()).clone()
}
}
impl World for EmbeddedWorld {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
self.fonts.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 files = self.files.read().expect("virtual filesystem lock poisoned");
if let Some(Ok(bytes)) = files.get(&id).and_then(|slot| slot.bytes.get()) {
let text =
std::str::from_utf8(bytes.as_slice()).map_err(|_| FileError::InvalidUtf8)?;
return Ok(Source::new(id, text.to_owned()));
}
}
self.cached_or_load(
id,
|slot| &slot.source,
|path| {
std::fs::read_to_string(path)
.map_err(|error| FileError::from_io(error, path))
.map(|text| Source::new(id, text))
},
)
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
self.cached_or_load(
id,
|slot| &slot.bytes,
|path| {
std::fs::read(path)
.map_err(|error| FileError::from_io(error, path))
.map(Bytes::new)
},
)
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.font(index)
}
fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
use chrono::Datelike;
let (now, local) = match self.fixed_now {
Some(now) => (now, false),
#[cfg(not(target_arch = "wasm32"))]
None => (*self.now.get_or_init(chrono::Utc::now), true),
#[cfg(target_arch = "wasm32")]
None => return None,
};
let date = match offset {
#[cfg(not(target_arch = "wasm32"))]
None if local => now.with_timezone(&chrono::Local).date_naive(),
None => now.date_naive(),
Some(offset) => {
(now + chrono::Duration::try_seconds(offset.seconds() as i64)?).date_naive()
}
};
#[cfg(target_arch = "wasm32")]
let _ = local;
Datetime::from_ymd(
date.year(),
date.month().try_into().ok()?,
date.day().try_into().ok()?,
)
}
}