diff --git a/Cargo.toml b/Cargo.toml index 58f5c9379..066ca4945 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [features] default = ["cli", "fs"] -cli = ["anyhow", "fs"] +cli = ["anyhow", "fs", "same-file"] fs = ["dirs", "memmap2", "same-file", "walkdir"] [workspace] diff --git a/bench/src/bench.rs b/bench/src/bench.rs index 2a9014903..d4e297bf3 100644 --- a/bench/src/bench.rs +++ b/bench/src/bench.rs @@ -41,16 +41,16 @@ fn benchmarks(c: &mut Criterion) { // Prepare intermediate results, run warm and fill caches. let src = std::fs::read_to_string(&path).unwrap(); let tree = Rc::new(parse(&src).output); - let evaluated = eval(&mut loader, &mut cache, &path, tree.clone(), &scope); + let evaluated = eval(&mut loader, &mut cache, Some(&path), tree.clone(), &scope); let executed = exec(&evaluated.output.template, state.clone()); let layouted = layout(&mut loader, &mut cache, &executed.output); // Bench! bench!("parse": parse(&src)); - bench!("eval": eval(&mut loader, &mut cache, &path, tree.clone(), &scope)); + bench!("eval": eval(&mut loader, &mut cache, Some(&path), tree.clone(), &scope)); bench!("exec": exec(&evaluated.output.template, state.clone())); bench!("layout": layout(&mut loader, &mut cache, &executed.output)); - bench!("typeset": typeset(&mut loader, &mut cache, &path, &src, &scope, state.clone())); + bench!("typeset": typeset(&mut loader, &mut cache, Some(&path), &src, &scope, state.clone())); bench!("pdf": pdf(&cache, &layouted)); } } diff --git a/src/eval/capture.rs b/src/eval/capture.rs index bee523ef7..74da4747e 100644 --- a/src/eval/capture.rs +++ b/src/eval/capture.rs @@ -17,7 +17,7 @@ impl<'a> CapturesVisitor<'a> { pub fn new(external: &'a Scopes) -> Self { Self { external, - internal: Scopes::new(), + internal: Scopes::new(None), captures: Scope::new(), } } diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 38c6263e0..c480ddfec 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -23,22 +23,17 @@ use crate::loading::{FileHash, Loader}; use crate::parse::parse; use crate::syntax::visit::Visit; use crate::syntax::*; +use crate::util::PathExt; -/// Evaluated a parsed source file into a module. -/// -/// The `path` should point to the source file for the `tree` and is used to -/// resolve relative path names. -/// -/// The `scope` consists of the base definitions that are present from the -/// beginning (typically, the standard library). +/// Evaluate a parsed source file into a module. pub fn eval( loader: &mut dyn Loader, cache: &mut Cache, - path: &Path, + path: Option<&Path>, tree: Rc, - base: &Scope, + scope: &Scope, ) -> Pass { - let mut ctx = EvalContext::new(loader, cache, path, base); + let mut ctx = EvalContext::new(loader, cache, path, scope); let map = tree.eval(&mut ctx); let module = Module { scope: ctx.scopes.top, @@ -67,7 +62,7 @@ pub struct EvalContext<'a> { /// Evaluation diagnostics. pub diags: DiagSet, /// The location of the currently evaluated file. - pub path: PathBuf, + pub path: Option, /// The stack of imported files that led to evaluation of the current file. pub route: Vec, /// A map of loaded module. @@ -79,20 +74,24 @@ impl<'a> EvalContext<'a> { pub fn new( loader: &'a mut dyn Loader, cache: &'a mut Cache, - path: &Path, - base: &'a Scope, + path: Option<&Path>, + scope: &'a Scope, ) -> Self { + let path = path.map(PathExt::normalize); + let mut route = vec![]; - if let Some(hash) = loader.resolve(path) { - route.push(hash); + if let Some(path) = &path { + if let Some(hash) = loader.resolve(path) { + route.push(hash); + } } Self { loader, cache, - scopes: Scopes::with_base(Some(base)), + scopes: Scopes::new(Some(scope)), diags: DiagSet::new(), - path: path.to_owned(), + path, route, modules: HashMap::new(), } @@ -102,10 +101,13 @@ impl<'a> EvalContext<'a> { /// /// Generates an error if the file is not found. pub fn resolve(&mut self, path: &str, span: Span) -> Option<(PathBuf, FileHash)> { - let dir = self.path.parent().expect("location is a file"); - let path = dir.join(path); + let path = match &self.path { + Some(current) => current.parent()?.join(path), + None => PathBuf::from(path), + }; + match self.loader.resolve(&path) { - Some(hash) => Some((path, hash)), + Some(hash) => Some((path.normalize(), hash)), None => { self.diag(error!(span, "file not found")); None @@ -142,10 +144,10 @@ impl<'a> EvalContext<'a> { let parsed = parse(string); // Prepare the new context. - let new_scopes = Scopes::with_base(self.scopes.base); + let new_scopes = Scopes::new(self.scopes.base); let old_scopes = mem::replace(&mut self.scopes, new_scopes); let old_diags = mem::replace(&mut self.diags, parsed.diags); - let old_path = mem::replace(&mut self.path, resolved); + let old_path = mem::replace(&mut self.path, Some(resolved)); self.route.push(hash); // Evaluate the module. diff --git a/src/eval/scope.rs b/src/eval/scope.rs index cfa2bccd6..e5afb6b00 100644 --- a/src/eval/scope.rs +++ b/src/eval/scope.rs @@ -22,16 +22,7 @@ pub struct Scopes<'a> { impl<'a> Scopes<'a> { /// Create a new, empty hierarchy of scopes. - pub fn new() -> Self { - Self { - top: Scope::new(), - scopes: vec![], - base: None, - } - } - - /// Create a new hierarchy of scopes with a base scope. - pub fn with_base(base: Option<&'a Scope>) -> Self { + pub fn new(base: Option<&'a Scope>) -> Self { Self { top: Scope::new(), scopes: vec![], base } } diff --git a/src/eval/value.rs b/src/eval/value.rs index d10d734af..e2ff5383b 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -664,7 +664,6 @@ impl From for Value { /// This would allow the type `FontFamily` to be cast from: /// - a [`Value::Any`] variant already containing a `FontFamily`, /// - a string, producing a named font family. -#[macro_export] macro_rules! value { ($type:ty: $type_name:literal diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 643d5b44a..35e6b55cc 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -15,9 +15,6 @@ use crate::pretty::pretty; use crate::syntax::*; /// Execute a template to produce a layout tree. -/// -/// The `state` is the base state that may be updated over the course of -/// execution. pub fn exec(template: &TemplateValue, state: State) -> Pass { let mut ctx = ExecContext::new(state); template.exec(&mut ctx); @@ -53,7 +50,7 @@ impl ExecWithMap for Tree { impl ExecWithMap for Node { fn exec_with_map(&self, ctx: &mut ExecContext, map: &NodeMap) { match self { - Node::Text(text) => ctx.push_text(text.clone()), + Node::Text(text) => ctx.push_text(text), Node::Space => ctx.push_word_space(), _ => map[&(self as *const _)].exec(ctx), } @@ -66,7 +63,7 @@ impl Exec for Value { Value::None => {} Value::Int(v) => ctx.push_text(pretty(v)), Value::Float(v) => ctx.push_text(pretty(v)), - Value::Str(v) => ctx.push_text(v.clone()), + Value::Str(v) => ctx.push_text(v), Value::Template(v) => v.exec(ctx), Value::Error => {} other => { @@ -93,7 +90,7 @@ impl Exec for TemplateNode { fn exec(&self, ctx: &mut ExecContext) { match self { Self::Tree { tree, map } => tree.exec_with_map(ctx, &map), - Self::Str(v) => ctx.push_text(v.clone()), + Self::Str(v) => ctx.push_text(v), Self::Func(v) => v.exec(ctx), } } diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 0ca4df388..1cc62332a 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -139,7 +139,7 @@ impl<'a> PdfExporter<'a> { // We only write font switching actions when the used face changes. To // do that, we need to remember the active face. - let mut face = FaceId::MAX; + let mut face = None; let mut size = Length::zero(); let mut fill: Option = None; @@ -158,8 +158,8 @@ impl<'a> PdfExporter<'a> { // Then, also check if we need to issue a font switching // action. - if shaped.face_id != face || shaped.size != size { - face = shaped.face_id; + if face != Some(shaped.face_id) || shaped.size != size { + face = Some(shaped.face_id); size = shaped.size; let name = format!("F{}", self.fonts.map(shaped.face_id)); diff --git a/src/font.rs b/src/font.rs index 69a309006..516d4bbe7 100644 --- a/src/font.rs +++ b/src/font.rs @@ -6,8 +6,7 @@ use std::fmt::{self, Debug, Display, Formatter}; use serde::{Deserialize, Serialize}; use crate::geom::Length; -use crate::loading::Buffer; -use crate::loading::Loader; +use crate::loading::{Buffer, Loader}; /// A font face. pub struct Face { @@ -171,7 +170,7 @@ impl FontCache { let mut families = HashMap::>::new(); for (i, info) in loader.faces().iter().enumerate() { - let id = FaceId(i as u32); + let id = FaceId(i as u64); faces.push(None); families .entry(info.family.to_lowercase()) @@ -259,22 +258,19 @@ impl FontCache { /// A unique identifier for a loaded font face. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct FaceId(u32); +pub struct FaceId(u64); impl FaceId { - /// A blank initialization value. - pub const MAX: Self = Self(u32::MAX); - /// Create a face id from the raw underlying value. /// /// This should only be called with values returned by /// [`into_raw`](Self::into_raw). - pub fn from_raw(v: u32) -> Self { + pub fn from_raw(v: u64) -> Self { Self(v) } /// Convert into the raw underlying value. - pub fn into_raw(self) -> u32 { + pub fn into_raw(self) -> u64 { self.0 } } diff --git a/src/image.rs b/src/image.rs index 3c5c85732..90252ff33 100644 --- a/src/image.rs +++ b/src/image.rs @@ -9,7 +9,7 @@ use image::io::Reader as ImageReader; use image::{DynamicImage, GenericImageView, ImageFormat}; use serde::{Deserialize, Serialize}; -use crate::loading::{FileHash, Loader}; +use crate::loading::Loader; /// A loaded image. pub struct Image { @@ -55,10 +55,8 @@ impl Debug for Image { /// Caches decoded images. pub struct ImageCache { - /// Loaded images indexed by [`ImageId`]. - images: Vec, /// Maps from file hashes to ids of decoded images. - map: HashMap, + images: HashMap, /// Callback for loaded images. on_load: Option>, } @@ -66,28 +64,22 @@ pub struct ImageCache { impl ImageCache { /// Create a new, empty image cache. pub fn new() -> Self { - Self { - images: vec![], - map: HashMap::new(), - on_load: None, - } + Self { images: HashMap::new(), on_load: None } } /// Load and decode an image file from a path. pub fn load(&mut self, loader: &mut dyn Loader, path: &Path) -> Option { - Some(match self.map.entry(loader.resolve(path)?) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - let buffer = loader.load_file(path)?; - let image = Image::parse(&buffer)?; - let id = ImageId(self.images.len() as u32); - if let Some(callback) = &self.on_load { - callback(id, &image); - } - self.images.push(image); - *entry.insert(id) + let hash = loader.resolve(path)?; + let id = ImageId(hash.into_raw()); + if let Entry::Vacant(entry) = self.images.entry(id) { + let buffer = loader.load_file(path)?; + let image = Image::parse(&buffer)?; + if let Some(callback) = &self.on_load { + callback(id, &image); } - }) + entry.insert(image); + } + Some(id) } /// Get a reference to a loaded image. @@ -96,7 +88,7 @@ impl ImageCache { /// only be called with ids returned by [`load()`](Self::load). #[track_caller] pub fn get(&self, id: ImageId) -> &Image { - &self.images[id.0 as usize] + &self.images[&id] } /// Register a callback which is invoked each time an image is loaded. @@ -110,19 +102,19 @@ impl ImageCache { /// A unique identifier for a loaded image. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct ImageId(u32); +pub struct ImageId(u64); impl ImageId { /// Create an image id from the raw underlying value. /// /// This should only be called with values returned by /// [`into_raw`](Self::into_raw). - pub fn from_raw(v: u32) -> Self { + pub fn from_raw(v: u64) -> Self { Self(v) } /// Convert into the raw underlying value. - pub fn into_raw(self) -> u32 { + pub fn into_raw(self) -> u64 { self.0 } } diff --git a/src/lib.rs b/src/lib.rs index 65e23c799..3c50230f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,16 +59,33 @@ use crate::layout::Frame; use crate::loading::Loader; /// Process source code directly into a collection of layouted frames. +/// +/// # Parameters +/// - The `loader` is used to load fonts, images and other source files. +/// - The `cache` stores things that are reusable across several compilations +/// like loaded fonts, decoded images and layouting artifacts. +/// - The `path` should point to the source file if `src` comes from the file +/// system and is used to resolve relative paths (for importing and image +/// loading). +/// - The `src` is the _Typst_ source code to typeset. +/// - The `scope` contains definitions that are available everywhere, +/// typically the standard library. +/// - The `state` defines initial properties for page size, font selection and +/// so on. +/// +/// # Return value +/// Returns a vector of frames representing individual pages alongside +/// diagnostic information (errors and warnings). pub fn typeset( loader: &mut dyn Loader, cache: &mut Cache, - path: &Path, + path: Option<&Path>, src: &str, - base: &Scope, + scope: &Scope, state: State, ) -> Pass> { let parsed = parse::parse(src); - let evaluated = eval::eval(loader, cache, path, Rc::new(parsed.output), base); + let evaluated = eval::eval(loader, cache, path, Rc::new(parsed.output), scope); let executed = exec::exec(&evaluated.output.template, state); let layouted = layout::layout(loader, cache, &executed.output); diff --git a/src/library/markup.rs b/src/library/markup.rs index c218746ba..0a80fe747 100644 --- a/src/library/markup.rs +++ b/src/library/markup.rs @@ -160,7 +160,7 @@ pub fn raw(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let snapshot = ctx.state.clone(); ctx.set_monospace(); - ctx.push_text(text.clone()); + ctx.push_text(&text); ctx.state = snapshot; if block { diff --git a/src/loading/fs.rs b/src/loading/fs.rs index ac3f37065..7fa1c1202 100644 --- a/src/loading/fs.rs +++ b/src/loading/fs.rs @@ -24,8 +24,7 @@ pub struct FsLoader { cache: FileCache, } -/// Maps from paths to loaded file buffers. When the buffer is `None` the file -/// does not exist or couldn't be read. +/// Maps from resolved file hashes to loaded file buffers. type FileCache = HashMap; impl FsLoader { @@ -169,38 +168,29 @@ impl Loader for FsLoader { } fn load_face(&mut self, idx: usize) -> Option { - load(&mut self.cache, &self.files[idx]) + self.load_file(&self.files[idx].clone()) } fn load_file(&mut self, path: &Path) -> Option { - load(&mut self.cache, path) + let hash = self.resolve(path)?; + Some(Rc::clone(match self.cache.entry(hash) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let buffer = std::fs::read(path).ok()?; + entry.insert(Rc::new(buffer)) + } + })) } fn resolve(&self, path: &Path) -> Option { - hash(path) - } -} - -/// Load from the file system using a cache. -fn load(cache: &mut FileCache, path: &Path) -> Option { - Some(match cache.entry(hash(path)?) { - Entry::Occupied(entry) => entry.get().clone(), - Entry::Vacant(entry) => { - let buffer = std::fs::read(path).ok()?; - entry.insert(Rc::new(buffer)).clone() + let file = File::open(path).ok()?; + let meta = file.metadata().ok()?; + if meta.is_file() { + let handle = Handle::from_file(file).ok()?; + Some(FileHash::from_raw(fxhash::hash64(&handle))) + } else { + None } - }) -} - -/// Create a hash that is the same for all paths pointing to the same file. -fn hash(path: &Path) -> Option { - let file = File::open(path).ok()?; - let meta = file.metadata().ok()?; - if meta.is_file() { - let handle = Handle::from_file(file).ok()?; - Some(FileHash(fxhash::hash64(&handle))) - } else { - None } } diff --git a/src/loading/mod.rs b/src/loading/mod.rs index f57b5c730..0e171796b 100644 --- a/src/loading/mod.rs +++ b/src/loading/mod.rs @@ -36,7 +36,19 @@ pub trait Loader { /// /// Should be the same for all paths pointing to the same file. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct FileHash(pub u64); +pub struct FileHash(u64); + +impl FileHash { + /// Create an file hash from a raw hash value. + pub fn from_raw(v: u64) -> Self { + Self(v) + } + + /// Convert into the raw underlying hash value. + pub fn into_raw(self) -> u64 { + self.0 + } +} /// A loader which serves nothing. pub struct BlankLoader; diff --git a/src/main.rs b/src/main.rs index 449cad20a..d0762b3e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,7 @@ use std::fs; use std::path::{Path, PathBuf}; use anyhow::{anyhow, bail, Context}; - -use typst::loading::Loader; +use same_file::is_same_file; fn main() -> anyhow::Result<()> { let args: Vec<_> = std::env::args().collect(); @@ -33,9 +32,7 @@ fn main() -> anyhow::Result<()> { }; // Ensure that the source file is not overwritten. - let src_hash = loader.resolve(&src_path); - let dest_hash = loader.resolve(&dest_path); - if src_hash.is_some() && src_hash == dest_hash { + if is_same_file(src_path, &dest_path).unwrap_or(false) { bail!("source and destination files are the same"); } @@ -47,7 +44,14 @@ fn main() -> anyhow::Result<()> { let mut cache = typst::cache::Cache::new(&loader); let scope = typst::library::new(); let state = typst::exec::State::default(); - let pass = typst::typeset(&mut loader, &mut cache, &src_path, &src, &scope, state); + let pass = typst::typeset( + &mut loader, + &mut cache, + Some(&src_path), + &src, + &scope, + state, + ); // Print diagnostics. let map = typst::parse::LineMap::new(&src); diff --git a/src/util.rs b/src/util.rs index 72db4518b..8a8c04b6d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -2,6 +2,7 @@ use std::cmp::Ordering; use std::ops::Range; +use std::path::{Component, Path, PathBuf}; /// Additional methods for slices. pub trait SliceExt { @@ -79,3 +80,28 @@ impl RangeExt for Range { } } } + +/// Additional methods for [`Path`]. +pub trait PathExt { + /// Lexically normalize a path. + fn normalize(&self) -> PathBuf; +} + +impl PathExt for Path { + fn normalize(&self) -> PathBuf { + let mut out = PathBuf::new(); + for component in self.components() { + match component { + Component::CurDir => {} + Component::ParentDir => match out.components().next_back() { + Some(Component::Normal(_)) => { + out.pop(); + } + _ => out.push(component), + }, + _ => out.push(component), + } + } + out + } +} diff --git a/tests/typeset.rs b/tests/typeset.rs index 3f9bbd1d5..604a82758 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -203,7 +203,7 @@ fn test( fn test_part( loader: &mut FsLoader, cache: &mut Cache, - path: &Path, + src_path: &Path, src: &str, i: usize, compare_ref: bool, @@ -224,7 +224,7 @@ fn test_part( state.page.size = Size::new(Length::pt(120.0), Length::raw(f64::INFINITY)); state.page.margins = Sides::splat(Some(Length::pt(10.0).into())); - let mut pass = typst::typeset(loader, cache, path, &src, &scope, state); + let mut pass = typst::typeset(loader, cache, Some(src_path), &src, &scope, state); if !compare_ref { pass.output.clear(); }