use std::error;
use std::fmt;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use ecow::{EcoString, eco_format};
use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime, Dict, Duration, IntoValue, Repr};
use typst::syntax::{
FileId, PathError, RootedPath, Source, VirtualPath, VirtualRoot, VirtualizeError,
};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Library, LibraryExt, World};
use typst_kit::datetime::Time;
use typst_kit::diagnostics::DiagnosticWorld;
use typst_kit::files::{FileLoader, FileStore, FsRoot};
use typst_kit::fonts::FontStore;
use typst_kit::packages::SystemPackages;
use crate::args::{Feature, Input, ProcessArgs, WorldArgs};
pub struct SystemWorld {
workdir: Option<PathBuf>,
library: LazyHash<Library>,
fonts: LazyLock<FontStore, Box<dyn Fn() -> FontStore + Send + Sync>>,
files: FileStore<SystemFiles>,
now: Time,
}
impl SystemWorld {
pub fn new(
input: Option<&Input>,
world_args: &'static WorldArgs,
process_args: &ProcessArgs,
) -> Result<Self, WorldCreationError> {
if let Some(jobs) = process_args.jobs {
rayon::ThreadPoolBuilder::new()
.num_threads(jobs)
.use_current_thread()
.build_global()
.ok();
}
let library = {
let inputs: Dict = world_args
.inputs
.iter()
.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()))
.collect();
let features =
process_args.features.iter().copied().map(Into::into).collect();
Library::builder().with_inputs(inputs).with_features(features).build()
};
let now = match world_args.creation_timestamp {
Some(time) => Time::fixed_timestamp(time)
.map_err(|_| WorldCreationError::InvalidTimestamp)?,
None => Time::system(),
};
Ok(Self {
workdir: std::env::current_dir().ok(),
library: LazyHash::new(library),
fonts: LazyLock::new(Box::new(|| {
crate::fonts::discover_fonts(&world_args.font)
})),
files: FileStore::new(SystemFiles::new(input, world_args)?),
now,
})
}
pub fn root(&self) -> &Path {
self.files.loader().project.path()
}
pub fn workdir(&self) -> &Path {
self.workdir.as_deref().unwrap_or(Path::new("."))
}
pub fn dependencies(&mut self) -> impl Iterator<Item = PathBuf> + '_ {
let (loader, deps) = self.files.dependencies();
deps.filter_map(|id| loader.resolve(id).ok())
}
pub fn reset(&mut self) {
self.files.reset();
self.now.reset();
}
pub fn scan_fonts(&mut self) {
LazyLock::force(&self.fonts);
}
}
impl World for SystemWorld {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
self.fonts.book()
}
fn main(&self) -> FileId {
self.files.loader().main
}
fn source(&self, id: FileId) -> FileResult<Source> {
self.files.source(id)
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
self.files.file(id)
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.font(index)
}
fn today(&self, offset: Option<Duration>) -> Option<Datetime> {
self.now.today(offset)
}
}
impl DiagnosticWorld for SystemWorld {
fn name(&self, id: FileId) -> String {
let vpath = id.vpath();
match id.root() {
VirtualRoot::Project => {
vpath
.realize(self.root())
.ok()
.and_then(|rooted| pathdiff::diff_paths(rooted, self.workdir()))
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_else(|| vpath.get_without_slash().into())
}
VirtualRoot::Package(package) => {
format!("{package}{}", vpath.get_with_slash())
}
}
}
}
static STDIN_ID: LazyLock<FileId> = LazyLock::new(|| {
FileId::unique(RootedPath::new(
VirtualRoot::Project,
VirtualPath::new("<stdin>").unwrap(),
))
});
static EMPTY_ID: LazyLock<FileId> = LazyLock::new(|| {
FileId::unique(RootedPath::new(
VirtualRoot::Project,
VirtualPath::new("<empty>").unwrap(),
))
});
struct SystemFiles {
main: FileId,
project: FsRoot,
packages: SystemPackages,
}
impl SystemFiles {
pub fn new(
input: Option<&Input>,
world_args: &'static WorldArgs,
) -> Result<Self, WorldCreationError> {
let input_path = match input {
Some(Input::Path(path)) => {
Some(path.canonicalize().map_err(|err| match err.kind() {
io::ErrorKind::NotFound => {
WorldCreationError::InputNotFound(path.clone())
}
_ => WorldCreationError::Io(err),
})?)
}
_ => None,
};
let root = {
let path = world_args
.root
.as_deref()
.or_else(|| input_path.as_deref().and_then(|i| i.parent()))
.unwrap_or(Path::new("."));
path.canonicalize().map_err(|err| match err.kind() {
io::ErrorKind::NotFound => {
WorldCreationError::RootNotFound(path.to_path_buf())
}
_ => WorldCreationError::Io(err),
})?
};
let main = if let Some(path) = &input_path {
RootedPath::new(VirtualRoot::Project, VirtualPath::virtualize(&root, path)?)
.intern()
} else if matches!(input, Some(Input::Stdin)) {
*STDIN_ID
} else {
*EMPTY_ID
};
Ok(Self {
main,
project: FsRoot::new(root),
packages: crate::packages::system(&world_args.package),
})
}
pub fn resolve(&self, id: FileId) -> FileResult<PathBuf> {
self.root(id)?.resolve(id.vpath())
}
fn root(&self, id: FileId) -> FileResult<FsRoot> {
Ok(match id.root() {
VirtualRoot::Project => self.project.clone(),
VirtualRoot::Package(spec) => self.packages.obtain(spec)?,
})
}
}
impl FileLoader for SystemFiles {
fn load(&self, id: FileId) -> FileResult<Bytes> {
if id == *EMPTY_ID {
Ok(Bytes::new([]))
} else if id == *STDIN_ID {
read_from_stdin().map(Bytes::new)
} else {
self.root(id)?.load(id.vpath())
}
}
}
fn read_from_stdin() -> FileResult<Vec<u8>> {
let mut buf = Vec::new();
let result = io::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)
}
#[derive(Debug)]
pub enum WorldCreationError {
InputNotFound(PathBuf),
InputMalformed(VirtualizeError),
RootNotFound(PathBuf),
InvalidTimestamp,
Io(io::Error),
}
impl fmt::Display for WorldCreationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WorldCreationError::InputMalformed(err) => match err {
VirtualizeError::Path(PathError::Escapes) => {
write!(f, "source file must be contained in project root")
}
VirtualizeError::Path(PathError::Backslash) => {
write!(f, "source path must not contain a backslash")
}
VirtualizeError::Invalid(s) => {
write!(f, "source path contains invalid sequence `{}`", s.repr())
}
VirtualizeError::Utf8 => write!(f, "source path must be valid UTF-8"),
},
WorldCreationError::InputNotFound(path) => {
write!(f, "input file not found (searched at {})", path.display())
}
WorldCreationError::RootNotFound(path) => {
write!(f, "root directory not found (searched at {})", path.display())
}
WorldCreationError::InvalidTimestamp => {
write!(f, "creation timestamp out of range")
}
WorldCreationError::Io(err) => write!(f, "{err}"),
}
}
}
impl error::Error for WorldCreationError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
_ => None,
}
}
}
impl From<VirtualizeError> for WorldCreationError {
fn from(err: VirtualizeError) -> Self {
Self::InputMalformed(err)
}
}
impl From<WorldCreationError> for EcoString {
fn from(err: WorldCreationError) -> Self {
eco_format!("{err}")
}
}
impl From<Feature> for typst::Feature {
fn from(feature: Feature) -> Self {
match feature {
Feature::Html => typst::Feature::Html,
Feature::Bundle => typst::Feature::Bundle,
Feature::A11yExtras => typst::Feature::A11yExtras,
}
}
}