summaryrefslogtreecommitdiff
path: root/crates/typst-cli/src/world.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-cli/src/world.rs')
-rw-r--r--crates/typst-cli/src/world.rs256
1 files changed, 256 insertions, 0 deletions
diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs
new file mode 100644
index 00000000..f09a3f6c
--- /dev/null
+++ b/crates/typst-cli/src/world.rs
@@ -0,0 +1,256 @@
+use std::cell::{OnceCell, RefCell, RefMut};
+use std::collections::HashMap;
+use std::fs;
+use std::hash::Hash;
+use std::path::{Path, PathBuf};
+
+use chrono::Datelike;
+use comemo::Prehashed;
+use same_file::Handle;
+use siphasher::sip128::{Hasher128, SipHasher13};
+use typst::diag::{FileError, FileResult, StrResult};
+use typst::eval::{eco_format, Datetime, Library};
+use typst::file::FileId;
+use typst::font::{Font, FontBook};
+use typst::syntax::Source;
+use typst::util::{Bytes, PathExt};
+use typst::World;
+
+use crate::args::CompileCommand;
+use crate::fonts::{FontSearcher, FontSlot};
+use crate::package::prepare_package;
+
+/// A world that provides access to the operating system.
+pub struct SystemWorld {
+ /// The root relative to which absolute paths are resolved.
+ root: PathBuf,
+ /// The input path.
+ main: FileId,
+ /// Typst's standard library.
+ library: Prehashed<Library>,
+ /// Metadata about discovered fonts.
+ book: Prehashed<FontBook>,
+ /// Locations of and storage for lazily loaded fonts.
+ fonts: Vec<FontSlot>,
+ /// Maps package-path combinations to canonical hashes. All package-path
+ /// combinations that point to the same file are mapped to the same hash. To
+ /// be used in conjunction with `paths`.
+ hashes: RefCell<HashMap<FileId, FileResult<PathHash>>>,
+ /// Maps canonical path hashes to source files and buffers.
+ paths: RefCell<HashMap<PathHash, PathSlot>>,
+ /// The current date if requested. This is stored here to ensure it is
+ /// always the same within one compilation. Reset between compilations.
+ today: OnceCell<Option<Datetime>>,
+}
+
+impl SystemWorld {
+ /// Create a new system world.
+ pub fn new(command: &CompileCommand) -> StrResult<Self> {
+ let mut searcher = FontSearcher::new();
+ searcher.search(&command.font_paths);
+
+ // Resolve the system-global input path.
+ let system_input = command.input.canonicalize().map_err(|_| {
+ eco_format!("input file not found (searched at {})", command.input.display())
+ })?;
+
+ // Resolve the system-global root directory.
+ let root = {
+ let path = command
+ .root
+ .as_deref()
+ .or_else(|| system_input.parent())
+ .unwrap_or(Path::new("."));
+ path.canonicalize().map_err(|_| {
+ eco_format!("root directory not found (searched at {})", path.display())
+ })?
+ };
+
+ // Resolve the input path within the project.
+ let project_input = system_input
+ .strip_prefix(&root)
+ .map(|path| Path::new("/").join(path))
+ .map_err(|_| "input file must be contained in project root")?;
+
+ Ok(Self {
+ root,
+ main: FileId::new(None, &project_input),
+ library: Prehashed::new(typst_library::build()),
+ book: Prehashed::new(searcher.book),
+ fonts: searcher.fonts,
+ hashes: RefCell::default(),
+ paths: RefCell::default(),
+ today: OnceCell::new(),
+ })
+ }
+
+ /// The id of the main source file.
+ pub fn main(&self) -> FileId {
+ self.main
+ }
+
+ /// Return all paths the last compilation depended on.
+ pub fn dependencies(&mut self) -> impl Iterator<Item = &Path> {
+ self.paths.get_mut().values().map(|slot| slot.system_path.as_path())
+ }
+
+ /// Reset the compilation state in preparation of a new compilation.
+ pub fn reset(&mut self) {
+ self.hashes.borrow_mut().clear();
+ self.paths.borrow_mut().clear();
+ self.today.take();
+ }
+
+ /// Lookup a source file by id.
+ #[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) -> &Prehashed<Library> {
+ &self.library
+ }
+
+ fn book(&self) -> &Prehashed<FontBook> {
+ &self.book
+ }
+
+ fn main(&self) -> Source {
+ self.source(self.main).unwrap()
+ }
+
+ fn source(&self, id: FileId) -> FileResult<Source> {
+ self.slot(id)?.source()
+ }
+
+ fn file(&self, id: FileId) -> FileResult<Bytes> {
+ self.slot(id)?.file()
+ }
+
+ fn font(&self, index: usize) -> Option<Font> {
+ self.fonts[index].get()
+ }
+
+ fn today(&self, offset: Option<i64>) -> Option<Datetime> {
+ *self.today.get_or_init(|| {
+ let naive = match offset {
+ None => chrono::Local::now().naive_local(),
+ Some(o) => (chrono::Utc::now() + chrono::Duration::hours(o)).naive_utc(),
+ };
+
+ Datetime::from_ymd(
+ naive.year(),
+ naive.month().try_into().ok()?,
+ naive.day().try_into().ok()?,
+ )
+ })
+ }
+}
+
+impl SystemWorld {
+ /// Access the canonical slot for the given file id.
+ #[tracing::instrument(skip_all)]
+ fn slot(&self, id: FileId) -> FileResult<RefMut<PathSlot>> {
+ let mut system_path = PathBuf::new();
+ let hash = self
+ .hashes
+ .borrow_mut()
+ .entry(id)
+ .or_insert_with(|| {
+ // Determine the root path relative to which the file path
+ // will be resolved.
+ let root = match id.package() {
+ Some(spec) => prepare_package(spec)?,
+ None => self.root.clone(),
+ };
+
+ // Join the path to the root. If it tries to escape, deny
+ // access. Note: It can still escape via symlinks.
+ system_path =
+ root.join_rooted(id.path()).ok_or(FileError::AccessDenied)?;
+
+ PathHash::new(&system_path)
+ })
+ .clone()?;
+
+ Ok(RefMut::map(self.paths.borrow_mut(), |paths| {
+ paths.entry(hash).or_insert_with(|| PathSlot {
+ id,
+ // This will only trigger if the `or_insert_with` above also
+ // triggered.
+ system_path,
+ source: OnceCell::new(),
+ buffer: OnceCell::new(),
+ })
+ }))
+ }
+}
+
+/// Holds canonical data for all paths pointing to the same entity.
+///
+/// Both fields can be populated if the file is both imported and read().
+struct PathSlot {
+ /// The slot's canonical file id.
+ id: FileId,
+ /// The slot's path on the system.
+ system_path: PathBuf,
+ /// The lazily loaded source file for a path hash.
+ source: OnceCell<FileResult<Source>>,
+ /// The lazily loaded buffer for a path hash.
+ buffer: OnceCell<FileResult<Bytes>>,
+}
+
+impl PathSlot {
+ fn source(&self) -> FileResult<Source> {
+ self.source
+ .get_or_init(|| {
+ let buf = read(&self.system_path)?;
+ let text = decode_utf8(buf)?;
+ Ok(Source::new(self.id, text))
+ })
+ .clone()
+ }
+
+ fn file(&self) -> FileResult<Bytes> {
+ self.buffer
+ .get_or_init(|| read(&self.system_path).map(Bytes::from))
+ .clone()
+ }
+}
+
+/// A hash that is the same for all paths pointing to the same entity.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+struct PathHash(u128);
+
+impl PathHash {
+ fn new(path: &Path) -> FileResult<Self> {
+ let f = |e| FileError::from_io(e, path);
+ let handle = Handle::from_path(path).map_err(f)?;
+ let mut state = SipHasher13::new();
+ handle.hash(&mut state);
+ Ok(Self(state.finish128().as_u128()))
+ }
+}
+
+/// Read a file.
+fn read(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)
+ }
+}
+
+/// Decode UTF-8 with an optional BOM.
+fn decode_utf8(buf: Vec<u8>) -> FileResult<String> {
+ Ok(if buf.starts_with(b"\xef\xbb\xbf") {
+ // Remove UTF-8 BOM.
+ std::str::from_utf8(&buf[3..])?.into()
+ } else {
+ // Assume UTF-8.
+ String::from_utf8(buf)?
+ })
+}