use std::{
collections::HashMap,
fmt,
fmt::{Display, Formatter},
fs, io,
io::{Error, Read},
mem,
path::{Path, PathBuf},
str::from_utf8,
sync::OnceLock,
};
use chrono::{DateTime, Datelike, Duration, Local};
use ecow::{EcoString, eco_format};
use fs::metadata;
use io::stdin;
use mem::replace;
use once_cell::sync::Lazy;
use package::storage;
use parking_lot::Mutex;
use typst::{
Library, LibraryExt, World,
diag::{FileError, FileResult},
foundations::{Bytes, Datetime, Dict, IntoValue},
syntax::{FileId, Source, VirtualPath},
text::{Font, FontBook},
utils::LazyHash,
};
use typst_kit::{download::ProgressSink, package::PackageStorage};
use typst_timing::{TimingScope, timed};
use crate::{
fonts::{FontSearcher, FontSlot},
package,
};
static STDIN_ID: Lazy<FileId> = Lazy::new(|| FileId::new_fake(VirtualPath::new("<stdin>")));
pub struct SystemWorld {
root: PathBuf,
main: FileId,
library: LazyHash<Library>,
book: LazyHash<FontBook>,
fonts: Vec<FontSlot>,
slots: Mutex<HashMap<FileId, FileSlot>>,
package_storage: PackageStorage,
now: OnceLock<DateTime<Local>>,
}
impl SystemWorld {
pub fn new(
input: &Path,
font_paths: &[PathBuf],
inputs: Vec<(String, String)>,
package_path: &Option<PathBuf>,
package_cache_path: &Option<PathBuf>,
) -> Result<Self, WorldCreationError> {
let input = input.canonicalize().map_err(|err| match err.kind() {
io::ErrorKind::NotFound => {
WorldCreationError::InputNotFound(input.to_path_buf().clone())
}
_ => WorldCreationError::Io(err),
})?;
let root =
input
.parent()
.unwrap_or(Path::new("."))
.canonicalize()
.map_err(|err| match err.kind() {
io::ErrorKind::NotFound => {
WorldCreationError::RootNotFound(input.to_path_buf())
}
_ => WorldCreationError::Io(err),
})?;
let main_path =
VirtualPath::within_root(&input, &root).ok_or(WorldCreationError::InputOutsideRoot)?;
let main = FileId::new(None, main_path);
let library = {
let inputs: Dict = inputs
.iter()
.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()))
.collect();
Library::builder().with_inputs(inputs).build()
};
let mut searcher = FontSearcher::new();
searcher.search(font_paths);
Ok(Self {
root,
main,
library: LazyHash::new(library),
book: LazyHash::new(searcher.book),
fonts: searcher.fonts,
slots: Mutex::new(HashMap::new()),
package_storage: storage(package_path, package_cache_path),
now: OnceLock::new(),
})
}
}
impl World for SystemWorld {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
&self.book
}
fn main(&self) -> FileId {
self.main
}
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 now = self.now.get_or_init(Local::now);
let naive = match offset {
None => now.naive_local(),
Some(o) => now.naive_utc() + Duration::try_hours(o)?,
};
Datetime::from_ymd(
naive.year(),
naive.month().try_into().ok()?,
naive.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();
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 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 name = if prev.is_some() { "reparsing file" } else { "parsing file" };
let _scope = TimingScope::new(name);
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 get_or_init(
&mut self,
load: impl FnOnce() -> FileResult<Vec<u8>>,
f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
) -> FileResult<T> {
if replace(&mut self.accessed, true)
&& let Some(data) = &self.data
{
return data.clone();
}
let result = timed!("loading file", load());
let fingerprint = timed!("hashing file", typst_utils::hash128(&result));
if replace(&mut self.fingerprint, fingerprint) == fingerprint
&& 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>> {
if id == *STDIN_ID {
read_from_stdin()
} else {
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 metadata(path).map_err(f)?.is_dir() {
Err(FileError::IsDirectory)
} else {
fs::read(path).map_err(f)
}
}
fn read_from_stdin() -> FileResult<Vec<u8>> {
let mut buf = Vec::new();
let result = stdin().read_to_end(&mut buf);
match result {
Ok(_) => (),
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => (),
Err(err) => return Err(FileError::from_io(err, Path::new("<stdin>"))),
}
Ok(buf)
}
fn decode_utf8(buf: &[u8]) -> FileResult<&str> {
Ok(from_utf8(buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf))?)
}
#[derive(Debug)]
pub enum WorldCreationError {
InputNotFound(PathBuf),
InputOutsideRoot,
RootNotFound(PathBuf),
Io(Error),
}
impl Display for WorldCreationError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
WorldCreationError::InputNotFound(path) => {
write!(f, "input file not found (searched at {})", path.display())
}
WorldCreationError::InputOutsideRoot => {
write!(f, "source file must be contained in project root")
}
WorldCreationError::RootNotFound(path) => {
write!(f, "root directory not found (searched at {})", path.display())
}
WorldCreationError::Io(err) => write!(f, "{err}"),
}
}
}
impl From<WorldCreationError> for EcoString {
fn from(err: WorldCreationError) -> Self {
eco_format!("{err}")
}
}