diff --git a/src/eval/func.rs b/src/eval/func.rs index 4c5761ab6..f15b0241e 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use super::{Args, Eval, Flow, Scope, Scopes, Value}; use crate::diag::{StrResult, TypResult}; use crate::model::{Content, NodeId, StyleMap}; +use crate::source::SourceId; use crate::syntax::ast::Expr; use crate::util::EcoString; use crate::Context; @@ -174,6 +175,8 @@ pub trait Node: 'static { /// A user-defined closure. #[derive(Hash)] pub struct Closure { + /// The location where the closure was defined. + pub location: Option, /// The name of the closure. pub name: Option, /// Captured values from outer scopes. @@ -212,18 +215,28 @@ impl Closure { // Backup the old control flow state. let prev_flow = ctx.flow.take(); + let detached = ctx.route.is_empty(); + if detached { + ctx.route = self.location.into_iter().collect(); + } // Evaluate the body. - let mut value = self.body.eval(ctx, &mut scp)?; + let result = self.body.eval(ctx, &mut scp); + + // Restore the old control flow state. + let flow = std::mem::replace(&mut ctx.flow, prev_flow); + if detached { + ctx.route.clear(); + } // Handle control flow. - match std::mem::replace(&mut ctx.flow, prev_flow) { - Some(Flow::Return(_, Some(explicit))) => value = explicit, + match flow { + Some(Flow::Return(_, Some(explicit))) => return Ok(explicit), Some(Flow::Return(_, None)) => {} Some(flow) => return Err(flow.forbidden())?, None => {} } - Ok(value) + result } } diff --git a/src/eval/mod.rs b/src/eval/mod.rs index b35cf1ef9..790601373 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -707,6 +707,7 @@ impl Eval for ClosureExpr { // Define the actual function. Ok(Value::Func(Func::from_closure(Closure { + location: ctx.route.last().copied(), name, captured, params, @@ -765,6 +766,7 @@ impl Eval for ShowExpr { let body = self.body(); let span = body.span(); let func = Func::from_closure(Closure { + location: ctx.route.last().copied(), name: None, captured, params, @@ -945,9 +947,11 @@ impl Eval for IncludeExpr { /// Process an import of a module relative to the current location. fn import(ctx: &mut Context, path: &str, span: Span) -> TypResult { // Load the source file. - let full = ctx.complete_path(path); + let full = ctx.locate(&path).at(span)?; let id = ctx.sources.load(&full).map_err(|err| match err.kind() { - std::io::ErrorKind::NotFound => error!(span, "file not found"), + std::io::ErrorKind::NotFound => { + error!(span, "file not found (searched at {})", full.display()) + } _ => error!(span, "failed to load source file ({})", err), })?; diff --git a/src/image.rs b/src/image.rs index 24a0deecc..87c093d38 100644 --- a/src/image.rs +++ b/src/image.rs @@ -48,7 +48,8 @@ impl ImageStore { } } - /// Load and decode an image file from a path. + /// Load and decode an image file from a path relative to the compilation + /// environment's root. pub fn load(&mut self, path: &Path) -> io::Result { let hash = self.loader.resolve(path)?; Ok(*match self.files.entry(hash) { diff --git a/src/lib.rs b/src/lib.rs index efddc239b..eb6e8f722 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,7 +57,7 @@ use std::hash::Hash; use std::path::PathBuf; use std::sync::Arc; -use crate::diag::TypResult; +use crate::diag::{StrResult, TypResult}; use crate::eval::{Eval, Flow, Module, Scope, Scopes}; use crate::font::FontStore; use crate::frame::Frame; @@ -65,6 +65,7 @@ use crate::image::ImageStore; use crate::loading::Loader; use crate::model::StyleMap; use crate::source::{SourceId, SourceStore}; +use crate::util::PathExt; /// The core context which holds the loader, configuration and cached artifacts. pub struct Context { @@ -76,6 +77,8 @@ pub struct Context { pub fonts: FontStore, /// Stores decoded images. pub images: ImageStore, + /// The compilation root. + root: PathBuf, /// The standard library scope. std: Arc, /// The default styles. @@ -172,51 +175,64 @@ impl Context { self.evaluate(id)?.content.layout(self) } - /// Resolve a user-entered path (relative to the current evaluation - /// location) to be relative to the compilation environment's root. - pub fn complete_path(&self, path: &str) -> PathBuf { + /// Resolve a user-entered path to be relative to the compilation + /// environment's root. + pub fn locate(&self, path: &str) -> StrResult { if let Some(&id) = self.route.last() { + if let Some(path) = path.strip_prefix('/') { + return Ok(self.root.join(path).normalize()); + } + if let Some(dir) = self.sources.get(id).path().parent() { - return dir.join(path); + return Ok(dir.join(path).normalize()); } } - path.into() + return Err("cannot access file system from here".into()); } } /// A builder for a [`Context`]. /// /// This struct is created by [`Context::builder`]. +#[derive(Default)] pub struct ContextBuilder { + root: PathBuf, std: Option>, styles: Option>, } impl ContextBuilder { + /// The compilation root, relative to which absolute paths are. + pub fn root(&mut self, root: impl Into) -> &mut Self { + self.root = root.into(); + self + } + /// The scope containing definitions that are available everywhere /// (the standard library). - pub fn std(mut self, std: impl Into>) -> Self { + pub fn std(&mut self, std: impl Into>) -> &mut Self { self.std = Some(std.into()); self } /// The default properties for page size, font selection and so on. - pub fn styles(mut self, styles: impl Into>) -> Self { + pub fn styles(&mut self, styles: impl Into>) -> &mut Self { self.styles = Some(styles.into()); self } /// Finish building the context by providing the `loader` used to load /// fonts, images, source files and other resources. - pub fn build(self, loader: Arc) -> Context { + pub fn build(&self, loader: Arc) -> Context { Context { sources: SourceStore::new(Arc::clone(&loader)), fonts: FontStore::new(Arc::clone(&loader)), images: ImageStore::new(Arc::clone(&loader)), loader, - std: self.std.unwrap_or_else(|| Arc::new(library::new())), - styles: self.styles.unwrap_or_default(), + root: self.root.clone(), + std: self.std.clone().unwrap_or_else(|| Arc::new(library::new())), + styles: self.styles.clone().unwrap_or_default(), modules: HashMap::new(), cache: HashMap::new(), route: vec![], @@ -226,12 +242,6 @@ impl ContextBuilder { } } -impl Default for ContextBuilder { - fn default() -> Self { - Self { std: None, styles: None } - } -} - /// An entry in the query cache. struct CacheEntry { /// The query's results. diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs index ee854130c..6fd465cbf 100644 --- a/src/library/graphics/image.rs +++ b/src/library/graphics/image.rs @@ -12,11 +12,15 @@ impl ImageNode { pub const FIT: ImageFit = ImageFit::Cover; fn construct(ctx: &mut Context, args: &mut Args) -> TypResult { - let path = args.expect::>("path to image file")?; - let full = ctx.complete_path(&path.v); + let Spanned { v: path, span } = + args.expect::>("path to image file")?; + + let full = ctx.locate(&path).at(span)?; let id = ctx.images.load(&full).map_err(|err| match err.kind() { - std::io::ErrorKind::NotFound => error!(path.span, "file not found"), - _ => error!(path.span, "failed to load image ({})", err), + std::io::ErrorKind::NotFound => { + error!(span, "file not found (searched at {})", full.display()) + } + _ => error!(span, "failed to load image ({})", err), })?; let width = args.named("width")?; diff --git a/src/main.rs b/src/main.rs index daeff033e..59ad5a717 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,9 +22,10 @@ USAGE: OPTIONS: -h, --help Print this help + --root Configure the root for absolute paths ARGS: - Path input Typst file + Path to input Typst file [output.pdf] Path to output PDF "; @@ -44,9 +45,17 @@ fn main() { fn try_main(args: Args) -> Result<(), String> { // Create a loader for fonts and files. let mut loader = FsLoader::new(); + let mut builder = Context::builder(); + if let Some(root) = &args.root { + builder.root(root); + } // Search for fonts in the project directory. if let Some(dir) = args.input.parent() { + if args.root.is_none() { + builder.root(dir); + } + if dir.as_os_str().is_empty() { // Just a filename, so directory is current directory. loader.search_path("."); @@ -60,7 +69,7 @@ fn try_main(args: Args) -> Result<(), String> { // Create the context which holds loaded source files, fonts, images and // cached artifacts. - let mut ctx = Context::new(loader.wrap()); + let mut ctx = builder.build(loader.wrap()); // Ensure that the source file is not overwritten. if is_same_file(&args.input, &args.output).unwrap_or(false) { @@ -94,6 +103,7 @@ fn try_main(args: Args) -> Result<(), String> { struct Args { input: PathBuf, output: PathBuf, + root: Option, } /// Parse command line arguments. @@ -104,7 +114,8 @@ fn parse_args() -> Result { std::process::exit(0); } - let input = args.free_from_str::().map_err(|_| "missing input file")?; + let root = args.opt_value_from_str("--root").map_err(|_| "malformed root")?; + let input: PathBuf = args.free_from_str().map_err(|_| "missing input file")?; let output = match args.opt_free_from_str().ok().flatten() { Some(output) => output, None => { @@ -118,7 +129,7 @@ fn parse_args() -> Result { Err("too many arguments")?; } - Ok(Args { input, output }) + Ok(Args { input, output, root }) } /// Print an application-level error (independent from a source file). diff --git a/src/source.rs b/src/source.rs index a7c952556..780e12a8b 100644 --- a/src/source.rs +++ b/src/source.rs @@ -54,7 +54,8 @@ impl SourceStore { } } - /// Load a source file from a path using the `loader`. + /// Load a source file from a path relative to the compilation environment's + /// root. /// /// If there already exists a source file for this path, it is /// [replaced](SourceFile::replace). diff --git a/tests/ref/style/set.png b/tests/ref/style/set.png index c63ddb7c9..52512b855 100644 Binary files a/tests/ref/style/set.png and b/tests/ref/style/set.png differ diff --git a/tests/ref/style/show-text.png b/tests/ref/style/show-text.png index 53c9d1321..ae5230f16 100644 Binary files a/tests/ref/style/show-text.png and b/tests/ref/style/show-text.png differ diff --git a/tests/typ/code/import.typ b/tests/typ/code/import.typ index dab91daed..aef5e9c47 100644 --- a/tests/typ/code/import.typ +++ b/tests/typ/code/import.typ @@ -38,11 +38,11 @@ #import a, c, from "target.typ" --- -// Error: 19-21 file not found +// Error: 19-21 file not found (searched at typ/code) #import name from "" --- -// Error: 16-27 file not found +// Error: 16-27 file not found (searched at typ/code/lib/0.2.1) #import * from "lib/0.2.1" --- diff --git a/tests/typ/code/include.typ b/tests/typ/code/include.typ index 3510cb063..cd3328a23 100644 --- a/tests/typ/code/include.typ +++ b/tests/typ/code/include.typ @@ -6,7 +6,7 @@ = Document // Include a file -#include "importable/chap1.typ" +#include "/typ/code/importable/chap1.typ" // Expression as a file name. #let chap2 = include "import" + "able/chap" + "2.typ" @@ -16,7 +16,7 @@ --- { - // Error: 19-41 file not found + // Error: 19-41 file not found (searched at typ/code/importable/chap3.typ) let x = include "importable/chap3.typ" } diff --git a/tests/typ/coma.typ b/tests/typ/coma.typ index 0e228d148..e312fc788 100644 --- a/tests/typ/coma.typ +++ b/tests/typ/coma.typ @@ -21,4 +21,4 @@ Die Tiefe eines Knotens _v_ ist die Länge des eindeutigen Weges von der Wurzel zu _v_, und die Höhe von _v_ ist die Länge eines längsten (absteigenden) Weges von _v_ zu einem Blatt. Die Höhe des Baumes ist die Höhe der Wurzel. -#align(center, image("../res/graph.png", width: 75%)) +#align(center, image("/res/graph.png", width: 75%)) diff --git a/tests/typ/graphics/image.typ b/tests/typ/graphics/image.typ index 8817713f3..e64b6c450 100644 --- a/tests/typ/graphics/image.typ +++ b/tests/typ/graphics/image.typ @@ -4,7 +4,7 @@ // Test loading different image formats. // Load an RGBA PNG image. -#image("../../res/rhino.png") +#image("/res/rhino.png") // Load an RGB JPEG image. #set page(height: 60pt) @@ -14,14 +14,14 @@ // Test configuring the size and fitting behaviour of images. // Set width and height explicitly. -#image("../../res/rhino.png", width: 30pt) -#image("../../res/rhino.png", height: 30pt) +#image("/res/rhino.png", width: 30pt) +#image("/res/rhino.png", height: 30pt) // Set width and height explicitly and force stretching. -#image("../../res/monkey.svg", width: 100%, height: 20pt, fit: "stretch") +#image("/res/monkey.svg", width: 100%, height: 20pt, fit: "stretch") // Make sure the bounding-box of the image is correct. -#align(bottom + right, image("../../res/tiger.jpg", width: 40pt)) +#align(bottom + right, image("/res/tiger.jpg", width: 40pt)) --- // Test all three fit modes. @@ -30,9 +30,9 @@ columns: (1fr, 1fr, 1fr), rows: 100%, gutter: 3pt, - image("../../res/tiger.jpg", width: 100%, height: 100%, fit: "contain"), - image("../../res/tiger.jpg", width: 100%, height: 100%, fit: "cover"), - image("../../res/monkey.svg", width: 100%, height: 100%, fit: "stretch"), + image("/res/tiger.jpg", width: 100%, height: 100%, fit: "contain"), + image("/res/tiger.jpg", width: 100%, height: 100%, fit: "cover"), + image("/res/monkey.svg", width: 100%, height: 100%, fit: "stretch"), ) --- @@ -40,18 +40,18 @@ #set page(height: 60pt) Stuff \ Stuff -#image("../../res/rhino.png") +#image("/res/rhino.png") --- // Test baseline. -A #image("../../res/tiger.jpg", height: 1cm, width: 80%) B +A #image("/res/tiger.jpg", height: 1cm, width: 80%) B --- // Test advanced SVG features. -#image("../../res/pattern.svg") +#image("/res/pattern.svg") --- -// Error: 8-29 file not found +// Error: 8-29 file not found (searched at typ/graphics/path/does/not/exist) #image("path/does/not/exist") --- diff --git a/tests/typ/graphics/transform.typ b/tests/typ/graphics/transform.typ index e06793060..5d2a1729a 100644 --- a/tests/typ/graphics/transform.typ +++ b/tests/typ/graphics/transform.typ @@ -31,13 +31,13 @@ nor #xetex! // Test combination of scaling and rotation. #set page(height: 80pt) #align(center + horizon, - rotate(20deg, scale(70%, image("../../res/tiger.jpg"))) + rotate(20deg, scale(70%, image("/res/tiger.jpg"))) ) --- // Test setting rotation origin. #rotate(10deg, origin: top + left, - image("../../res/tiger.jpg", width: 50%) + image("/res/tiger.jpg", width: 50%) ) --- diff --git a/tests/typ/layout/grid-3.typ b/tests/typ/layout/grid-3.typ index 8d04722e6..6b7dc47fd 100644 --- a/tests/typ/layout/grid-3.typ +++ b/tests/typ/layout/grid-3.typ @@ -23,7 +23,7 @@ columns: 4 * (1fr,), row-gutter: 10pt, column-gutter: (0pt, 10%), - align(top, image("../../res/rhino.png")), + align(top, image("/res/rhino.png")), align(top, rect(fill: eastern, align(right)[LoL])), [rofl], [\ A] * 3, diff --git a/tests/typ/layout/pad.typ b/tests/typ/layout/pad.typ index 502137ec2..9791eae43 100644 --- a/tests/typ/layout/pad.typ +++ b/tests/typ/layout/pad.typ @@ -21,7 +21,7 @@ Hi #box(pad(left: 10pt)[A]) there // Test that the pad node doesn't consume the whole region. #set page(height: 6cm) #align(left)[Before] -#pad(10pt, image("../../res/tiger.jpg")) +#pad(10pt, image("/res/tiger.jpg")) #align(right)[After] --- diff --git a/tests/typ/layout/place-background.typ b/tests/typ/layout/place-background.typ index f64bf0eef..fafedec1a 100644 --- a/tests/typ/layout/place-background.typ +++ b/tests/typ/layout/place-background.typ @@ -7,7 +7,7 @@ dx: -10pt, dy: -10pt, image( - "../../res/tiger.jpg", + "/res/tiger.jpg", fit: "cover", width: 100% + 20pt, height: 100% + 20pt, diff --git a/tests/typ/layout/place.typ b/tests/typ/layout/place.typ index 95049bdc6..58f9d06d5 100644 --- a/tests/typ/layout/place.typ +++ b/tests/typ/layout/place.typ @@ -5,7 +5,7 @@ #place(bottom + center)[© Typst] = Placement -#place(right, image("../../res/tiger.jpg", width: 1.8cm)) +#place(right, image("/res/tiger.jpg", width: 1.8cm)) Hi there. This is \ a placed node. \ Unfortunately, \ diff --git a/tests/typ/style/set.typ b/tests/typ/style/set.typ index 2c12d3e97..830c66618 100644 --- a/tests/typ/style/set.typ +++ b/tests/typ/style/set.typ @@ -36,6 +36,18 @@ Hello *{x}* [Not blue] } +--- +// Test relative path resolving in layout phase. +#let choice = ("monkey.svg", "rhino.png", "tiger.jpg") +#set enum(label: n => { + let path = "../../res/" + choice(n - 1) + move(dy: -0.15em, image(path, width: 1em, height: 1em)) +}) + +. Monkey +. Rhino +. Tiger + --- // Error: 11-25 set is only allowed directly in code and content blocks { let x = set text(blue) } diff --git a/tests/typ/style/show-text.typ b/tests/typ/style/show-text.typ index f4ecb7e11..283a28879 100644 --- a/tests/typ/style/show-text.typ +++ b/tests/typ/style/show-text.typ @@ -56,3 +56,10 @@ Rust is memory-safe and blazingly fast. Let's rewrite everything in rust. World - World + +--- +// Test absolute path in layout phase. + +#show "GRAPH" as image("/res/graph.png") + +The GRAPH has nodes. diff --git a/tests/typ/text/bidi.typ b/tests/typ/text/bidi.typ index 7058638ac..11c0cafa1 100644 --- a/tests/typ/text/bidi.typ +++ b/tests/typ/text/bidi.typ @@ -43,7 +43,7 @@ Lריווח #h(1cm) R --- // Test inline object. #set text(lang: "he", "IBM Plex Serif") -קרנפיםRh#image("../../res/rhino.png", height: 11pt)inoחיים +קרנפיםRh#image("/res/rhino.png", height: 11pt)inoחיים --- // Test whether L1 whitespace resetting destroys stuff. diff --git a/tests/typ/text/indent.typ b/tests/typ/text/indent.typ index 897e360cc..92b121803 100644 --- a/tests/typ/text/indent.typ +++ b/tests/typ/text/indent.typ @@ -8,10 +8,10 @@ The first paragraph has no indent. But the second one does. -#image("../../res/tiger.jpg", height: 6pt) +#image("/res/tiger.jpg", height: 6pt) starts a paragraph without indent. -#align(center, image("../../res/rhino.png", width: 1cm)) +#align(center, image("/res/rhino.png", width: 1cm)) = Headings - And lists. diff --git a/tests/typ/text/link.typ b/tests/typ/text/link.typ index ad5381877..64697bff5 100644 --- a/tests/typ/text/link.typ +++ b/tests/typ/text/link.typ @@ -31,5 +31,5 @@ My cool #move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink))) // Link containing a block. #link("https://example.com/", underline: false, block[ My cool rhino - #move(dx: 10pt, image("../../res/rhino.png", width: 1cm)) + #move(dx: 10pt, image("/res/rhino.png", width: 1cm)) ])