summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-05-29 15:45:57 +0200
committerLaurenz <laurmaedje@gmail.com>2021-05-31 22:33:40 +0200
commite023bf2ac9f5796355d9485afc16781196bf212b (patch)
tree26d4487de0c4e2d0f69182483301de867cb5fa34
parent9f77f09aacd1fb0fd6138a6d16ed2755f6bfae3f (diff)
Module loading system
Detects cyclic imports and loads each module only once per compilation.
-rw-r--r--bench/src/bench.rs22
-rw-r--r--src/eval/mod.rs198
-rw-r--r--src/eval/scope.rs13
-rw-r--r--src/image.rs13
-rw-r--r--src/layout/mod.rs8
-rw-r--r--src/lib.rs4
-rw-r--r--src/library/image.rs14
-rw-r--r--src/loading/fs.rs24
-rw-r--r--src/loading/mod.rs18
-rw-r--r--src/main.rs43
-rw-r--r--tests/typ/full/coma.typ2
-rw-r--r--tests/typ/library/image.typ22
-rw-r--r--tests/typ/text/bidi.typ2
-rw-r--r--tests/typeset.rs5
14 files changed, 276 insertions, 112 deletions
diff --git a/bench/src/bench.rs b/bench/src/bench.rs
index 8563f115..2a901490 100644
--- a/bench/src/bench.rs
+++ b/bench/src/bench.rs
@@ -24,8 +24,8 @@ fn benchmarks(c: &mut Criterion) {
let state = typst::exec::State::default();
for case in CASES {
- let case = Path::new(case);
- let name = case.file_stem().unwrap().to_string_lossy();
+ let path = Path::new(TYP_DIR).join(case);
+ let name = path.file_stem().unwrap().to_string_lossy();
macro_rules! bench {
($step:literal: $code:expr) => {
@@ -39,18 +39,18 @@ fn benchmarks(c: &mut Criterion) {
}
// Prepare intermediate results, run warm and fill caches.
- let src = std::fs::read_to_string(Path::new(TYP_DIR).join(case)).unwrap();
- let parsed = Rc::new(parse(&src).output);
- let evaluated = eval(&mut loader, &mut cache, parsed.clone(), &scope).output;
- let executed = exec(&evaluated.template, state.clone()).output;
- let layouted = layout(&mut loader, &mut cache, &executed);
+ 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 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, parsed.clone(), &scope));
- bench!("exec": exec(&evaluated.template, state.clone()));
- bench!("layout": layout(&mut loader, &mut cache, &executed));
- bench!("typeset": typeset(&mut loader, &mut cache, &src, &scope, state.clone()));
+ bench!("eval": eval(&mut loader, &mut cache, &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!("pdf": pdf(&cache, &layouted));
}
}
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 0af9dd6b..e40f91da 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -10,27 +10,34 @@ pub use capture::*;
pub use scope::*;
pub use value::*;
+use std::collections::HashMap;
+use std::path::{Path, PathBuf};
use std::rc::Rc;
use crate::cache::Cache;
use crate::color::Color;
use crate::diag::{Diag, DiagSet, Pass};
use crate::geom::{Angle, Length, Relative};
-use crate::loading::Loader;
+use crate::loading::{FileHash, Loader};
+use crate::parse::parse;
use crate::syntax::visit::Visit;
use crate::syntax::*;
/// 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).
pub fn eval(
loader: &mut dyn Loader,
cache: &mut Cache,
+ path: &Path,
tree: Rc<Tree>,
base: &Scope,
) -> Pass<Module> {
- let mut ctx = EvalContext::new(loader, cache, base);
+ let mut ctx = EvalContext::new(loader, cache, path, base);
let map = tree.eval(&mut ctx);
let module = Module {
scope: ctx.scopes.top,
@@ -58,6 +65,12 @@ pub struct EvalContext<'a> {
pub scopes: Scopes<'a>,
/// Evaluation diagnostics.
pub diags: DiagSet,
+ /// The stack of imported files that led to evaluation of the current file.
+ pub route: Vec<FileHash>,
+ /// The location of the currently evaluated file.
+ pub path: PathBuf,
+ /// A map of loaded module.
+ pub modules: HashMap<FileHash, Module>,
}
impl<'a> EvalContext<'a> {
@@ -65,20 +78,116 @@ impl<'a> EvalContext<'a> {
pub fn new(
loader: &'a mut dyn Loader,
cache: &'a mut Cache,
+ path: &Path,
base: &'a Scope,
) -> Self {
+ let mut route = vec![];
+ if let Some(hash) = loader.resolve(path) {
+ route.push(hash);
+ }
+
Self {
loader,
cache,
- scopes: Scopes::with_base(base),
+ scopes: Scopes::with_base(Some(base)),
diags: DiagSet::new(),
+ route,
+ path: path.to_owned(),
+ modules: HashMap::new(),
+ }
+ }
+
+ /// Resolve a path relative to the current file.
+ ///
+ /// 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);
+ match self.loader.resolve(&path) {
+ Some(hash) => Some((path, hash)),
+ None => {
+ self.diag(error!(span, "file not found"));
+ None
+ }
}
}
+ /// Process an import of a module relative to the current location.
+ pub fn import(&mut self, path: &str, span: Span) -> Option<FileHash> {
+ let (resolved, hash) = self.resolve(path, span)?;
+
+ // Prevent cycling importing.
+ if self.route.contains(&hash) {
+ self.diag(error!(span, "cyclic import"));
+ return None;
+ }
+
+ if self.modules.get(&hash).is_some() {
+ return Some(hash);
+ }
+
+ let buffer = self.loader.load_file(&resolved).or_else(|| {
+ self.diag(error!(span, "failed to load file"));
+ None
+ })?;
+
+ let string = std::str::from_utf8(&buffer).ok().or_else(|| {
+ self.diag(error!(span, "file is not valid utf-8"));
+ None
+ })?;
+
+ // Prepare the new context.
+ self.route.push(hash);
+ let new_scopes = Scopes::with_base(self.scopes.base);
+ let old_scopes = std::mem::replace(&mut self.scopes, new_scopes);
+
+ // Evaluate the module.
+ let tree = Rc::new(parse(string).output);
+ let map = tree.eval(self);
+
+ // Restore the old context.
+ let new_scopes = std::mem::replace(&mut self.scopes, old_scopes);
+ self.route.pop();
+
+ self.modules.insert(hash, Module {
+ scope: new_scopes.top,
+ template: vec![TemplateNode::Tree { tree, map }],
+ });
+
+ Some(hash)
+ }
+
/// Add a diagnostic.
pub fn diag(&mut self, diag: Diag) {
self.diags.insert(diag);
}
+
+ /// Cast a value to a type and diagnose a possible error / warning.
+ pub fn cast<T>(&mut self, value: Value, span: Span) -> Option<T>
+ where
+ T: Cast<Value>,
+ {
+ if value == Value::Error {
+ return None;
+ }
+
+ match T::cast(value) {
+ CastResult::Ok(t) => Some(t),
+ CastResult::Warn(t, m) => {
+ self.diag(warning!(span, "{}", m));
+ Some(t)
+ }
+ CastResult::Err(value) => {
+ self.diag(error!(
+ span,
+ "expected {}, found {}",
+ T::TYPE_NAME,
+ value.type_name(),
+ ));
+ None
+ }
+ }
+ }
}
/// Evaluate an expression.
@@ -349,24 +458,14 @@ impl Eval for CallExpr {
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
let callee = self.callee.eval(ctx);
-
- if let Value::Func(func) = callee {
- let func = func.clone();
-
+ if let Some(func) = ctx.cast::<FuncValue>(callee, self.callee.span()) {
let mut args = self.args.eval(ctx);
let returned = func(ctx, &mut args);
args.finish(ctx);
-
- return returned;
- } else if callee != Value::Error {
- ctx.diag(error!(
- self.callee.span(),
- "expected function, found {}",
- callee.type_name(),
- ));
+ returned
+ } else {
+ Value::Error
}
-
- Value::Error
}
}
@@ -449,7 +548,7 @@ impl Eval for IfExpr {
fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
let condition = self.condition.eval(ctx);
- if let Value::Bool(condition) = condition {
+ if let Some(condition) = ctx.cast(condition, self.condition.span()) {
if condition {
self.if_body.eval(ctx)
} else if let Some(else_body) = &self.else_body {
@@ -458,13 +557,6 @@ impl Eval for IfExpr {
Value::None
}
} else {
- if condition != Value::Error {
- ctx.diag(error!(
- self.condition.span(),
- "expected boolean, found {}",
- condition.type_name(),
- ));
- }
Value::Error
}
}
@@ -477,7 +569,7 @@ impl Eval for WhileExpr {
let mut output = vec![];
loop {
let condition = self.condition.eval(ctx);
- if let Value::Bool(condition) = condition {
+ if let Some(condition) = ctx.cast(condition, self.condition.span()) {
if condition {
match self.body.eval(ctx) {
Value::Template(v) => output.extend(v),
@@ -489,13 +581,6 @@ impl Eval for WhileExpr {
return Value::Template(output);
}
} else {
- if condition != Value::Error {
- ctx.diag(error!(
- self.condition.span(),
- "expected boolean, found {}",
- condition.type_name(),
- ));
- }
return Value::Error;
}
}
@@ -571,15 +656,54 @@ impl Eval for ForExpr {
impl Eval for ImportExpr {
type Output = Value;
- fn eval(&self, _: &mut EvalContext) -> Self::Output {
- todo!()
+ fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
+ let span = self.path.span();
+ let path = self.path.eval(ctx);
+
+ if let Some(path) = ctx.cast::<String>(path, span) {
+ if let Some(hash) = ctx.import(&path, span) {
+ let mut module = &ctx.modules[&hash];
+ match &self.imports {
+ Imports::Wildcard => {
+ for (var, slot) in module.scope.iter() {
+ let value = slot.borrow().clone();
+ ctx.scopes.def_mut(var, value);
+ }
+ }
+ Imports::Idents(idents) => {
+ for ident in idents {
+ if let Some(slot) = module.scope.get(&ident) {
+ let value = slot.borrow().clone();
+ ctx.scopes.def_mut(ident.as_str(), value);
+ } else {
+ ctx.diag(error!(ident.span, "unresolved import"));
+ module = &ctx.modules[&hash];
+ }
+ }
+ }
+ }
+
+ return Value::None;
+ }
+ }
+
+ Value::Error
}
}
impl Eval for IncludeExpr {
type Output = Value;
- fn eval(&self, _: &mut EvalContext) -> Self::Output {
- todo!()
+ fn eval(&self, ctx: &mut EvalContext) -> Self::Output {
+ let span = self.path.span();
+ let path = self.path.eval(ctx);
+
+ if let Some(path) = ctx.cast::<String>(path, span) {
+ if let Some(hash) = ctx.import(&path, span) {
+ return Value::Template(ctx.modules[&hash].template.clone());
+ }
+ }
+
+ Value::Error
}
}
diff --git a/src/eval/scope.rs b/src/eval/scope.rs
index a3c9234b..cfa2bccd 100644
--- a/src/eval/scope.rs
+++ b/src/eval/scope.rs
@@ -31,12 +31,8 @@ impl<'a> Scopes<'a> {
}
/// Create a new hierarchy of scopes with a base scope.
- pub fn with_base(base: &'a Scope) -> Self {
- Self {
- top: Scope::new(),
- scopes: vec![],
- base: Some(base),
- }
+ pub fn with_base(base: Option<&'a Scope>) -> Self {
+ Self { top: Scope::new(), scopes: vec![], base }
}
/// Enter a new scope.
@@ -131,6 +127,11 @@ impl Scope {
pub fn get(&self, var: &str) -> Option<&Slot> {
self.values.get(var)
}
+
+ /// Iterate over all definitions.
+ pub fn iter(&self) -> impl Iterator<Item = (&str, &Slot)> {
+ self.values.iter().map(|(k, v)| (k.as_str(), v))
+ }
}
impl Debug for Scope {
diff --git a/src/image.rs b/src/image.rs
index bdfc19a6..3c5c8573 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -3,12 +3,13 @@
use std::collections::{hash_map::Entry, HashMap};
use std::fmt::{self, Debug, Formatter};
use std::io::Cursor;
+use std::path::Path;
use image::io::Reader as ImageReader;
use image::{DynamicImage, GenericImageView, ImageFormat};
use serde::{Deserialize, Serialize};
-use crate::loading::Loader;
+use crate::loading::{FileHash, Loader};
/// A loaded image.
pub struct Image {
@@ -56,8 +57,8 @@ impl Debug for Image {
pub struct ImageCache {
/// Loaded images indexed by [`ImageId`].
images: Vec<Image>,
- /// Maps from paths to loaded images.
- paths: HashMap<String, ImageId>,
+ /// Maps from file hashes to ids of decoded images.
+ map: HashMap<FileHash, ImageId>,
/// Callback for loaded images.
on_load: Option<Box<dyn Fn(ImageId, &Image)>>,
}
@@ -67,14 +68,14 @@ impl ImageCache {
pub fn new() -> Self {
Self {
images: vec![],
- paths: HashMap::new(),
+ map: HashMap::new(),
on_load: None,
}
}
/// Load and decode an image file from a path.
- pub fn load(&mut self, loader: &mut dyn Loader, path: &str) -> Option<ImageId> {
- Some(match self.paths.entry(path.to_string()) {
+ pub fn load(&mut self, loader: &mut dyn Loader, path: &Path) -> Option<ImageId> {
+ Some(match self.map.entry(loader.resolve(path)?) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let buffer = loader.load_file(path)?;
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 30776fa2..9d5ccdc0 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -22,7 +22,6 @@ use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use decorum::N64;
-use fxhash::FxHasher64;
use crate::cache::Cache;
use crate::geom::*;
@@ -81,12 +80,7 @@ impl AnyNode {
where
T: Layout + Debug + Clone + PartialEq + Hash + 'static,
{
- let hash = {
- let mut state = FxHasher64::default();
- node.hash(&mut state);
- state.finish()
- };
-
+ let hash = fxhash::hash64(&node);
Self { node: Box::new(node), hash }
}
}
diff --git a/src/lib.rs b/src/lib.rs
index c435c2dd..65e23c79 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -48,6 +48,7 @@ pub mod pretty;
pub mod syntax;
pub mod util;
+use std::path::Path;
use std::rc::Rc;
use crate::cache::Cache;
@@ -61,12 +62,13 @@ use crate::loading::Loader;
pub fn typeset(
loader: &mut dyn Loader,
cache: &mut Cache,
+ path: &Path,
src: &str,
base: &Scope,
state: State,
) -> Pass<Vec<Frame>> {
let parsed = parse::parse(src);
- let evaluated = eval::eval(loader, cache, Rc::new(parsed.output), base);
+ let evaluated = eval::eval(loader, cache, path, Rc::new(parsed.output), base);
let executed = exec::exec(&evaluated.output.template, state);
let layouted = layout::layout(loader, cache, &executed.output);
diff --git a/src/library/image.rs b/src/library/image.rs
index cd6a97d1..7fabfe35 100644
--- a/src/library/image.rs
+++ b/src/library/image.rs
@@ -20,12 +20,14 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
let mut node = None;
if let Some(path) = &path {
- if let Some(id) = ctx.cache.image.load(ctx.loader, &path.v) {
- let img = ctx.cache.image.get(id);
- let dimensions = img.buf.dimensions();
- node = Some(ImageNode { id, dimensions, width, height });
- } else {
- ctx.diag(error!(path.span, "failed to load image"));
+ if let Some((resolved, _)) = ctx.resolve(&path.v, path.span) {
+ if let Some(id) = ctx.cache.image.load(ctx.loader, &resolved) {
+ let img = ctx.cache.image.get(id);
+ let dimensions = img.buf.dimensions();
+ node = Some(ImageNode { id, dimensions, width, height });
+ } else {
+ ctx.diag(error!(path.span, "failed to load image"));
+ }
}
}
diff --git a/src/loading/fs.rs b/src/loading/fs.rs
index 969ee9e0..bf768bd5 100644
--- a/src/loading/fs.rs
+++ b/src/loading/fs.rs
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use ttf_parser::{name_id, Face};
use walkdir::WalkDir;
-use super::{Buffer, Loader};
+use super::{Buffer, FileHash, Loader};
use crate::font::{FaceInfo, FontStretch, FontStyle, FontVariant, FontWeight};
/// Loads fonts and images from the local file system.
@@ -25,7 +25,7 @@ pub struct FsLoader {
/// Maps from paths to loaded file buffers. When the buffer is `None` the file
/// does not exist or couldn't be read.
-type FileCache = HashMap<PathBuf, Option<Buffer>>;
+type FileCache = HashMap<FileHash, Buffer>;
impl FsLoader {
/// Create a new loader without any fonts.
@@ -167,24 +167,32 @@ impl Loader for FsLoader {
&self.faces
}
+ fn resolve(&self, path: &Path) -> Option<FileHash> {
+ hash(path)
+ }
+
fn load_face(&mut self, idx: usize) -> Option<Buffer> {
load(&mut self.cache, &self.files[idx])
}
- fn load_file(&mut self, path: &str) -> Option<Buffer> {
- load(&mut self.cache, Path::new(path))
+ fn load_file(&mut self, path: &Path) -> Option<Buffer> {
+ load(&mut self.cache, path)
}
}
/// Load from the file system using a cache.
fn load(cache: &mut FileCache, path: &Path) -> Option<Buffer> {
- match cache.entry(path.to_owned()) {
+ Some(match cache.entry(hash(path)?) {
Entry::Occupied(entry) => entry.get().clone(),
Entry::Vacant(entry) => {
- let buffer = std::fs::read(path).ok().map(Rc::new);
- entry.insert(buffer).clone()
+ let buffer = std::fs::read(path).ok()?;
+ entry.insert(Rc::new(buffer)).clone()
}
- }
+ })
+}
+
+fn hash(path: &Path) -> Option<FileHash> {
+ path.canonicalize().ok().map(|p| FileHash(fxhash::hash64(&p)))
}
#[cfg(test)]
diff --git a/src/loading/mod.rs b/src/loading/mod.rs
index 818e7e3c..b4e5d160 100644
--- a/src/loading/mod.rs
+++ b/src/loading/mod.rs
@@ -6,6 +6,7 @@ mod fs;
#[cfg(feature = "fs")]
pub use fs::*;
+use std::path::Path;
use std::rc::Rc;
use crate::font::FaceInfo;
@@ -18,13 +19,22 @@ pub trait Loader {
/// Descriptions of all font faces this loader serves.
fn faces(&self) -> &[FaceInfo];
+ /// Resolve a hash that is the same for all paths pointing to the same file.
+ ///
+ /// Should return `None` if the file does not exist.
+ fn resolve(&self, path: &Path) -> Option<FileHash>;
+
/// Load the font face with the given index in [`faces()`](Self::faces).
fn load_face(&mut self, idx: usize) -> Option<Buffer>;
/// Load a file from a path.
- fn load_file(&mut self, path: &str) -> Option<Buffer>;
+ fn load_file(&mut self, path: &Path) -> Option<Buffer>;
}
+/// A hash that must be the same for all paths pointing to the same file.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct FileHash(pub u64);
+
/// A loader which serves nothing.
pub struct BlankLoader;
@@ -33,11 +43,15 @@ impl Loader for BlankLoader {
&[]
}
+ fn resolve(&self, _: &Path) -> Option<FileHash> {
+ None
+ }
+
fn load_face(&mut self, _: usize) -> Option<Buffer> {
None
}
- fn load_file(&mut self, _: &str) -> Option<Buffer> {
+ fn load_file(&mut self, _: &Path) -> Option<Buffer> {
None
}
}
diff --git a/src/main.rs b/src/main.rs
index 5370f6a8..449cad20 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,37 +3,53 @@ use std::path::{Path, PathBuf};
use anyhow::{anyhow, bail, Context};
+use typst::loading::Loader;
+
fn main() -> anyhow::Result<()> {
let args: Vec<_> = std::env::args().collect();
if args.len() < 2 || args.len() > 3 {
- println!("Usage: typst src.typ [out.pdf]");
+ println!("usage: typst src.typ [out.pdf]");
return Ok(());
}
+ // Create a loader for fonts and files.
+ let mut loader = typst::loading::FsLoader::new();
+ loader.search_path("fonts");
+ loader.search_system();
+
+ // Resolve the canonical path because the compiler needs it for module
+ // loading.
let src_path = Path::new(&args[1]);
+
+ // Find out the file name to create the output file.
+ let name = src_path
+ .file_name()
+ .ok_or_else(|| anyhow!("source path is not a file"))?;
+
let dest_path = if args.len() <= 2 {
- let name = src_path
- .file_name()
- .ok_or_else(|| anyhow!("Source path is not a file."))?;
Path::new(name).with_extension("pdf")
} else {
PathBuf::from(&args[2])
};
- if src_path == dest_path {
- bail!("Source and destination path are the same.");
+ // 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 {
+ bail!("source and destination files are the same");
}
- let src = fs::read_to_string(src_path).context("Failed to read from source file.")?;
-
- let mut loader = typst::loading::FsLoader::new();
- loader.search_path("fonts");
- loader.search_system();
+ // Read the source.
+ let src = fs::read_to_string(&src_path)
+ .map_err(|_| anyhow!("failed to read source file"))?;
+ // Compile.
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, &scope, state);
+ let pass = typst::typeset(&mut loader, &mut cache, &src_path, &src, &scope, state);
+
+ // Print diagnostics.
let map = typst::parse::LineMap::new(&src);
for diag in pass.diags {
let start = map.location(diag.span.start).unwrap();
@@ -48,8 +64,9 @@ fn main() -> anyhow::Result<()> {
);
}
+ // Export the PDF.
let buffer = typst::export::pdf(&cache, &pass.output);
- fs::write(&dest_path, buffer).context("Failed to write PDF file.")?;
+ fs::write(&dest_path, buffer).context("failed to write PDF file")?;
Ok(())
}
diff --git a/tests/typ/full/coma.typ b/tests/typ/full/coma.typ
index 619843d7..4941fb71 100644
--- a/tests/typ/full/coma.typ
+++ b/tests/typ/full/coma.typ
@@ -46,4 +46,4 @@ von _v_ zu einem Blatt. Die Höhe des Baumes ist die Höhe der Wurzel.
// The `image` function returns a "template" value of the same type as
// the `[...]` literals.
-#align(center, image("res/graph.png", width: 75%))
+#align(center, image("../../res/graph.png", width: 75%))
diff --git a/tests/typ/library/image.typ b/tests/typ/library/image.typ
index a5737f4f..1fa128f0 100644
--- a/tests/typ/library/image.typ
+++ b/tests/typ/library/image.typ
@@ -4,35 +4,35 @@
// Test loading different image formats.
// Load an RGBA PNG image.
-#image("res/rhino.png")
+#image("../../res/rhino.png")
#pagebreak()
// Load an RGB JPEG image.
-#image("res/tiger.jpg")
+#image("../../res/tiger.jpg")
-// Error: 8-29 failed to load image
+// Error: 8-29 file not found
#image("path/does/not/exist")
-// Error: 8-29 failed to load image
-#image("typ/image-error.typ")
+// Error: 8-20 failed to load image
+#image("./font.typ")
---
// Test configuring the size and fitting behaviour of images.
// Fit to width of page.
-#image("res/rhino.png")
+#image("../../res/rhino.png")
// Fit to height of page.
-#page(height: 40pt, image("res/rhino.png"))
+#page(height: 40pt, image("../../res/rhino.png"))
// Set width explicitly.
-#image("res/rhino.png", width: 50pt)
+#image("../../res/rhino.png", width: 50pt)
// Set height explicitly.
-#image("res/rhino.png", height: 50pt)
+#image("../../res/rhino.png", height: 50pt)
// Set width and height explicitly and force stretching.
-#image("res/rhino.png", width: 25pt, height: 50pt)
+#image("../../res/rhino.png", width: 25pt, height: 50pt)
// Make sure the bounding-box of the image is correct.
-#align(bottom, right, image("res/tiger.jpg", width: 60pt))
+#align(bottom, right, image("../../res/tiger.jpg", width: 60pt))
diff --git a/tests/typ/text/bidi.typ b/tests/typ/text/bidi.typ
index 44f0cc15..0d589930 100644
--- a/tests/typ/text/bidi.typ
+++ b/tests/typ/text/bidi.typ
@@ -46,4 +46,4 @@ Lריווח #h(1cm) R
// Test inline object.
#font("Noto Serif Hebrew", "EB Garamond")
#lang("he")
-קרנפיםRh#image("res/rhino.png", height: 11pt)inoחיים
+קרנפיםRh#image("../../res/rhino.png", height: 11pt)inoחיים
diff --git a/tests/typeset.rs b/tests/typeset.rs
index faf76f7e..3f9bbd1d 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -162,7 +162,7 @@ fn test(
}
} else {
let (part_ok, compare_here, part_frames) =
- test_part(loader, cache, part, i, compare_ref, lines);
+ test_part(loader, cache, src_path, part, i, compare_ref, lines);
ok &= part_ok;
compare_ever |= compare_here;
frames.extend(part_frames);
@@ -203,6 +203,7 @@ fn test(
fn test_part(
loader: &mut FsLoader,
cache: &mut Cache,
+ path: &Path,
src: &str,
i: usize,
compare_ref: bool,
@@ -223,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, &src, &scope, state);
+ let mut pass = typst::typeset(loader, cache, path, &src, &scope, state);
if !compare_ref {
pass.output.clear();
}