#![allow(dead_code)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::{fs, io, mem};
use chrono::{DateTime, Datelike, FixedOffset, Local, Utc};
use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime};
use typst::syntax::{FileId, Source};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Library, World};
use typst_kit::download::ProgressSink;
use typst_kit::fonts::{FontSlot, Fonts};
use typst_kit::package::PackageStorage;
use tytanic_core::library::augmented_default_library;
pub struct SystemWorld {
workdir: Option<PathBuf>,
root: PathBuf,
library: LazyHash<Library>,
book: LazyHash<FontBook>,
fonts: Vec<FontSlot>,
slots: Mutex<HashMap<FileId, FileSlot>>,
package_storage: PackageStorage,
now: DateTime<Utc>,
}
impl SystemWorld {
pub fn new(
root: PathBuf,
fonts: Fonts,
package_storage: PackageStorage,
now: DateTime<Utc>,
) -> io::Result<Self> {
Ok(Self {
workdir: std::env::current_dir().ok(),
root,
library: LazyHash::new(augmented_default_library()),
book: LazyHash::new(fonts.book),
fonts: fonts.fonts,
slots: Mutex::new(HashMap::new()),
package_storage,
now,
})
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn workdir(&self) -> &Path {
self.workdir.as_deref().unwrap_or(Path::new("."))
}
pub fn reset(&mut self) {
for slot in self.slots.get_mut().unwrap().values_mut() {
slot.reset();
}
}
#[track_caller]
pub fn lookup(&self, id: FileId) -> Source {
self.source(id)
.expect("file id does not point to any source file")
}
}
impl World for SystemWorld {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
&self.book
}
fn main(&self) -> FileId {
panic!("system world does not have a main file")
}
fn source(&self, id: FileId) -> FileResult<Source> {
self.slot(id, |slot| slot.source(&self.root, &self.package_storage))
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
self.slot(id, |slot| slot.file(&self.root, &self.package_storage))
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts[index].get()
}
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
let with_offset = match offset {
None => self.now.with_timezone(&Local).fixed_offset(),
Some(hours) => {
let seconds = i32::try_from(hours).ok()?.checked_mul(3600)?;
self.now.with_timezone(&FixedOffset::east_opt(seconds)?)
}
};
Datetime::from_ymd(
with_offset.year(),
with_offset.month().try_into().ok()?,
with_offset.day().try_into().ok()?,
)
}
}
impl SystemWorld {
fn slot<F, T>(&self, id: FileId, f: F) -> T
where
F: FnOnce(&mut FileSlot) -> T,
{
let mut map = self.slots.lock().unwrap();
f(map.entry(id).or_insert_with(|| FileSlot::new(id)))
}
}
struct FileSlot {
id: FileId,
source: SlotCell<Source>,
file: SlotCell<Bytes>,
}
impl FileSlot {
fn new(id: FileId) -> Self {
Self {
id,
file: SlotCell::new(),
source: SlotCell::new(),
}
}
fn reset(&mut self) {
self.source.reset();
self.file.reset();
}
fn source(
&mut self,
project_root: &Path,
package_storage: &PackageStorage,
) -> FileResult<Source> {
self.source.get_or_init(
|| read(self.id, project_root, package_storage),
|data, prev| {
let text = decode_utf8(&data)?;
if let Some(mut prev) = prev {
prev.replace(text);
Ok(prev)
} else {
Ok(Source::new(self.id, text.into()))
}
},
)
}
fn file(&mut self, project_root: &Path, package_storage: &PackageStorage) -> FileResult<Bytes> {
self.file.get_or_init(
|| read(self.id, project_root, package_storage),
|data, _| Ok(Bytes::new(data)),
)
}
}
struct SlotCell<T> {
data: Option<FileResult<T>>,
fingerprint: u128,
accessed: bool,
}
impl<T: Clone> SlotCell<T> {
fn new() -> Self {
Self {
data: None,
fingerprint: 0,
accessed: false,
}
}
fn reset(&mut self) {
self.accessed = false;
}
fn get_or_init(
&mut self,
load: impl FnOnce() -> FileResult<Vec<u8>>,
f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
) -> FileResult<T> {
if mem::replace(&mut self.accessed, true) {
if let Some(data) = &self.data {
return data.clone();
}
}
let result = load();
let fingerprint = typst::utils::hash128(&result);
if mem::replace(&mut self.fingerprint, fingerprint) == fingerprint {
if let Some(data) = &self.data {
return data.clone();
}
}
let prev = self.data.take().and_then(Result::ok);
let value = result.and_then(|data| f(data, prev));
self.data = Some(value.clone());
value
}
}
fn system_path(
project_root: &Path,
id: FileId,
package_storage: &PackageStorage,
) -> FileResult<PathBuf> {
let buf;
let mut root = project_root;
if let Some(spec) = id.package() {
buf = package_storage.prepare_package(spec, &mut ProgressSink)?;
root = &buf;
}
id.vpath().resolve(root).ok_or(FileError::AccessDenied)
}
fn read(id: FileId, project_root: &Path, package_storage: &PackageStorage) -> FileResult<Vec<u8>> {
read_from_disk(&system_path(project_root, id, package_storage)?)
}
fn read_from_disk(path: &Path) -> FileResult<Vec<u8>> {
let f = |e| FileError::from_io(e, path);
if fs::metadata(path).map_err(f)?.is_dir() {
Err(FileError::IsDirectory)
} else {
fs::read(path).map_err(f)
}
}
fn decode_utf8(buf: &[u8]) -> FileResult<&str> {
Ok(std::str::from_utf8(
buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf),
)?)
}
type CodespanResult<T> = Result<T, CodespanError>;
type CodespanError = codespan_reporting::files::Error;
impl<'a> codespan_reporting::files::Files<'a> for SystemWorld {
type FileId = FileId;
type Name = String;
type Source = Source;
fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> {
let vpath = id.vpath();
Ok(if let Some(package) = id.package() {
format!("{package}{}", vpath.as_rooted_path().display())
} else {
vpath
.resolve(self.root())
.unwrap_or_else(|| vpath.as_rootless_path().to_path_buf())
.to_string_lossy()
.into()
})
}
fn source(&'a self, id: FileId) -> CodespanResult<Self::Source> {
Ok(self.lookup(id))
}
fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult<usize> {
let source = self.lookup(id);
source
.byte_to_line(given)
.ok_or_else(|| CodespanError::IndexTooLarge {
given,
max: source.len_bytes(),
})
}
fn line_range(&'a self, id: FileId, given: usize) -> CodespanResult<std::ops::Range<usize>> {
let source = self.lookup(id);
source
.line_to_range(given)
.ok_or_else(|| CodespanError::LineTooLarge {
given,
max: source.len_lines(),
})
}
fn column_number(&'a self, id: FileId, _: usize, given: usize) -> CodespanResult<usize> {
let source = self.lookup(id);
source.byte_to_column(given).ok_or_else(|| {
let max = source.len_bytes();
if given <= max {
CodespanError::InvalidCharBoundary { given }
} else {
CodespanError::IndexTooLarge { given, max }
}
})
}
}