Absolute paths

This commit is contained in:
Laurenz 2022-05-16 17:56:23 +02:00
parent 6536e9e069
commit a741bd6b83
23 changed files with 123 additions and 60 deletions

View File

@ -5,6 +5,7 @@ use std::sync::Arc;
use super::{Args, Eval, Flow, Scope, Scopes, Value}; use super::{Args, Eval, Flow, Scope, Scopes, Value};
use crate::diag::{StrResult, TypResult}; use crate::diag::{StrResult, TypResult};
use crate::model::{Content, NodeId, StyleMap}; use crate::model::{Content, NodeId, StyleMap};
use crate::source::SourceId;
use crate::syntax::ast::Expr; use crate::syntax::ast::Expr;
use crate::util::EcoString; use crate::util::EcoString;
use crate::Context; use crate::Context;
@ -174,6 +175,8 @@ pub trait Node: 'static {
/// A user-defined closure. /// A user-defined closure.
#[derive(Hash)] #[derive(Hash)]
pub struct Closure { pub struct Closure {
/// The location where the closure was defined.
pub location: Option<SourceId>,
/// The name of the closure. /// The name of the closure.
pub name: Option<EcoString>, pub name: Option<EcoString>,
/// Captured values from outer scopes. /// Captured values from outer scopes.
@ -212,18 +215,28 @@ impl Closure {
// Backup the old control flow state. // Backup the old control flow state.
let prev_flow = ctx.flow.take(); let prev_flow = ctx.flow.take();
let detached = ctx.route.is_empty();
if detached {
ctx.route = self.location.into_iter().collect();
}
// Evaluate the body. // 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. // Handle control flow.
match std::mem::replace(&mut ctx.flow, prev_flow) { match flow {
Some(Flow::Return(_, Some(explicit))) => value = explicit, Some(Flow::Return(_, Some(explicit))) => return Ok(explicit),
Some(Flow::Return(_, None)) => {} Some(Flow::Return(_, None)) => {}
Some(flow) => return Err(flow.forbidden())?, Some(flow) => return Err(flow.forbidden())?,
None => {} None => {}
} }
Ok(value) result
} }
} }

View File

@ -707,6 +707,7 @@ impl Eval for ClosureExpr {
// Define the actual function. // Define the actual function.
Ok(Value::Func(Func::from_closure(Closure { Ok(Value::Func(Func::from_closure(Closure {
location: ctx.route.last().copied(),
name, name,
captured, captured,
params, params,
@ -765,6 +766,7 @@ impl Eval for ShowExpr {
let body = self.body(); let body = self.body();
let span = body.span(); let span = body.span();
let func = Func::from_closure(Closure { let func = Func::from_closure(Closure {
location: ctx.route.last().copied(),
name: None, name: None,
captured, captured,
params, params,
@ -945,9 +947,11 @@ impl Eval for IncludeExpr {
/// Process an import of a module relative to the current location. /// Process an import of a module relative to the current location.
fn import(ctx: &mut Context, path: &str, span: Span) -> TypResult<Module> { fn import(ctx: &mut Context, path: &str, span: Span) -> TypResult<Module> {
// Load the source file. // 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() { 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), _ => error!(span, "failed to load source file ({})", err),
})?; })?;

View File

@ -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<ImageId> { pub fn load(&mut self, path: &Path) -> io::Result<ImageId> {
let hash = self.loader.resolve(path)?; let hash = self.loader.resolve(path)?;
Ok(*match self.files.entry(hash) { Ok(*match self.files.entry(hash) {

View File

@ -57,7 +57,7 @@ use std::hash::Hash;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use crate::diag::TypResult; use crate::diag::{StrResult, TypResult};
use crate::eval::{Eval, Flow, Module, Scope, Scopes}; use crate::eval::{Eval, Flow, Module, Scope, Scopes};
use crate::font::FontStore; use crate::font::FontStore;
use crate::frame::Frame; use crate::frame::Frame;
@ -65,6 +65,7 @@ use crate::image::ImageStore;
use crate::loading::Loader; use crate::loading::Loader;
use crate::model::StyleMap; use crate::model::StyleMap;
use crate::source::{SourceId, SourceStore}; use crate::source::{SourceId, SourceStore};
use crate::util::PathExt;
/// The core context which holds the loader, configuration and cached artifacts. /// The core context which holds the loader, configuration and cached artifacts.
pub struct Context { pub struct Context {
@ -76,6 +77,8 @@ pub struct Context {
pub fonts: FontStore, pub fonts: FontStore,
/// Stores decoded images. /// Stores decoded images.
pub images: ImageStore, pub images: ImageStore,
/// The compilation root.
root: PathBuf,
/// The standard library scope. /// The standard library scope.
std: Arc<Scope>, std: Arc<Scope>,
/// The default styles. /// The default styles.
@ -172,51 +175,64 @@ impl Context {
self.evaluate(id)?.content.layout(self) self.evaluate(id)?.content.layout(self)
} }
/// Resolve a user-entered path (relative to the current evaluation /// Resolve a user-entered path to be relative to the compilation
/// location) to be relative to the compilation environment's root. /// environment's root.
pub fn complete_path(&self, path: &str) -> PathBuf { pub fn locate(&self, path: &str) -> StrResult<PathBuf> {
if let Some(&id) = self.route.last() { 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() { 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`]. /// A builder for a [`Context`].
/// ///
/// This struct is created by [`Context::builder`]. /// This struct is created by [`Context::builder`].
#[derive(Default)]
pub struct ContextBuilder { pub struct ContextBuilder {
root: PathBuf,
std: Option<Arc<Scope>>, std: Option<Arc<Scope>>,
styles: Option<Arc<StyleMap>>, styles: Option<Arc<StyleMap>>,
} }
impl ContextBuilder { impl ContextBuilder {
/// The compilation root, relative to which absolute paths are.
pub fn root(&mut self, root: impl Into<PathBuf>) -> &mut Self {
self.root = root.into();
self
}
/// The scope containing definitions that are available everywhere /// The scope containing definitions that are available everywhere
/// (the standard library). /// (the standard library).
pub fn std(mut self, std: impl Into<Arc<Scope>>) -> Self { pub fn std(&mut self, std: impl Into<Arc<Scope>>) -> &mut Self {
self.std = Some(std.into()); self.std = Some(std.into());
self self
} }
/// The default properties for page size, font selection and so on. /// The default properties for page size, font selection and so on.
pub fn styles(mut self, styles: impl Into<Arc<StyleMap>>) -> Self { pub fn styles(&mut self, styles: impl Into<Arc<StyleMap>>) -> &mut Self {
self.styles = Some(styles.into()); self.styles = Some(styles.into());
self self
} }
/// Finish building the context by providing the `loader` used to load /// Finish building the context by providing the `loader` used to load
/// fonts, images, source files and other resources. /// fonts, images, source files and other resources.
pub fn build(self, loader: Arc<dyn Loader>) -> Context { pub fn build(&self, loader: Arc<dyn Loader>) -> Context {
Context { Context {
sources: SourceStore::new(Arc::clone(&loader)), sources: SourceStore::new(Arc::clone(&loader)),
fonts: FontStore::new(Arc::clone(&loader)), fonts: FontStore::new(Arc::clone(&loader)),
images: ImageStore::new(Arc::clone(&loader)), images: ImageStore::new(Arc::clone(&loader)),
loader, loader,
std: self.std.unwrap_or_else(|| Arc::new(library::new())), root: self.root.clone(),
styles: self.styles.unwrap_or_default(), std: self.std.clone().unwrap_or_else(|| Arc::new(library::new())),
styles: self.styles.clone().unwrap_or_default(),
modules: HashMap::new(), modules: HashMap::new(),
cache: HashMap::new(), cache: HashMap::new(),
route: vec![], 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. /// An entry in the query cache.
struct CacheEntry { struct CacheEntry {
/// The query's results. /// The query's results.

View File

@ -12,11 +12,15 @@ impl ImageNode {
pub const FIT: ImageFit = ImageFit::Cover; pub const FIT: ImageFit = ImageFit::Cover;
fn construct(ctx: &mut Context, args: &mut Args) -> TypResult<Content> { fn construct(ctx: &mut Context, args: &mut Args) -> TypResult<Content> {
let path = args.expect::<Spanned<EcoString>>("path to image file")?; let Spanned { v: path, span } =
let full = ctx.complete_path(&path.v); args.expect::<Spanned<EcoString>>("path to image file")?;
let full = ctx.locate(&path).at(span)?;
let id = ctx.images.load(&full).map_err(|err| match err.kind() { let id = ctx.images.load(&full).map_err(|err| match err.kind() {
std::io::ErrorKind::NotFound => error!(path.span, "file not found"), std::io::ErrorKind::NotFound => {
_ => error!(path.span, "failed to load image ({})", err), error!(span, "file not found (searched at {})", full.display())
}
_ => error!(span, "failed to load image ({})", err),
})?; })?;
let width = args.named("width")?; let width = args.named("width")?;

View File

@ -22,9 +22,10 @@ USAGE:
OPTIONS: OPTIONS:
-h, --help Print this help -h, --help Print this help
--root <dir> Configure the root for absolute paths
ARGS: ARGS:
<input.typ> Path input Typst file <input.typ> Path to input Typst file
[output.pdf] Path to output PDF [output.pdf] Path to output PDF
"; ";
@ -44,9 +45,17 @@ fn main() {
fn try_main(args: Args) -> Result<(), String> { fn try_main(args: Args) -> Result<(), String> {
// Create a loader for fonts and files. // Create a loader for fonts and files.
let mut loader = FsLoader::new(); 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. // Search for fonts in the project directory.
if let Some(dir) = args.input.parent() { if let Some(dir) = args.input.parent() {
if args.root.is_none() {
builder.root(dir);
}
if dir.as_os_str().is_empty() { if dir.as_os_str().is_empty() {
// Just a filename, so directory is current directory. // Just a filename, so directory is current directory.
loader.search_path("."); 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 // Create the context which holds loaded source files, fonts, images and
// cached artifacts. // cached artifacts.
let mut ctx = Context::new(loader.wrap()); let mut ctx = builder.build(loader.wrap());
// Ensure that the source file is not overwritten. // Ensure that the source file is not overwritten.
if is_same_file(&args.input, &args.output).unwrap_or(false) { if is_same_file(&args.input, &args.output).unwrap_or(false) {
@ -94,6 +103,7 @@ fn try_main(args: Args) -> Result<(), String> {
struct Args { struct Args {
input: PathBuf, input: PathBuf,
output: PathBuf, output: PathBuf,
root: Option<PathBuf>,
} }
/// Parse command line arguments. /// Parse command line arguments.
@ -104,7 +114,8 @@ fn parse_args() -> Result<Args, String> {
std::process::exit(0); std::process::exit(0);
} }
let input = args.free_from_str::<PathBuf>().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() { let output = match args.opt_free_from_str().ok().flatten() {
Some(output) => output, Some(output) => output,
None => { None => {
@ -118,7 +129,7 @@ fn parse_args() -> Result<Args, String> {
Err("too many arguments")?; Err("too many arguments")?;
} }
Ok(Args { input, output }) Ok(Args { input, output, root })
} }
/// Print an application-level error (independent from a source file). /// Print an application-level error (independent from a source file).

View File

@ -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 /// If there already exists a source file for this path, it is
/// [replaced](SourceFile::replace). /// [replaced](SourceFile::replace).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -38,11 +38,11 @@
#import a, c, from "target.typ" #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 "" #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" #import * from "lib/0.2.1"
--- ---

View File

@ -6,7 +6,7 @@
= Document = Document
// Include a file // Include a file
#include "importable/chap1.typ" #include "/typ/code/importable/chap1.typ"
// Expression as a file name. // Expression as a file name.
#let chap2 = include "import" + "able/chap" + "2.typ" #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" let x = include "importable/chap3.typ"
} }

View File

@ -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 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. 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%))

View File

@ -4,7 +4,7 @@
// Test loading different image formats. // Test loading different image formats.
// Load an RGBA PNG image. // Load an RGBA PNG image.
#image("../../res/rhino.png") #image("/res/rhino.png")
// Load an RGB JPEG image. // Load an RGB JPEG image.
#set page(height: 60pt) #set page(height: 60pt)
@ -14,14 +14,14 @@
// Test configuring the size and fitting behaviour of images. // Test configuring the size and fitting behaviour of images.
// Set width and height explicitly. // Set width and height explicitly.
#image("../../res/rhino.png", width: 30pt) #image("/res/rhino.png", width: 30pt)
#image("../../res/rhino.png", height: 30pt) #image("/res/rhino.png", height: 30pt)
// Set width and height explicitly and force stretching. // 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. // 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. // Test all three fit modes.
@ -30,9 +30,9 @@
columns: (1fr, 1fr, 1fr), columns: (1fr, 1fr, 1fr),
rows: 100%, rows: 100%,
gutter: 3pt, gutter: 3pt,
image("../../res/tiger.jpg", width: 100%, height: 100%, fit: "contain"), image("/res/tiger.jpg", width: 100%, height: 100%, fit: "contain"),
image("../../res/tiger.jpg", width: 100%, height: 100%, fit: "cover"), image("/res/tiger.jpg", width: 100%, height: 100%, fit: "cover"),
image("../../res/monkey.svg", width: 100%, height: 100%, fit: "stretch"), image("/res/monkey.svg", width: 100%, height: 100%, fit: "stretch"),
) )
--- ---
@ -40,18 +40,18 @@
#set page(height: 60pt) #set page(height: 60pt)
Stuff \ Stuff \
Stuff Stuff
#image("../../res/rhino.png") #image("/res/rhino.png")
--- ---
// Test baseline. // 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. // 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") #image("path/does/not/exist")
--- ---

View File

@ -31,13 +31,13 @@ nor #xetex!
// Test combination of scaling and rotation. // Test combination of scaling and rotation.
#set page(height: 80pt) #set page(height: 80pt)
#align(center + horizon, #align(center + horizon,
rotate(20deg, scale(70%, image("../../res/tiger.jpg"))) rotate(20deg, scale(70%, image("/res/tiger.jpg")))
) )
--- ---
// Test setting rotation origin. // Test setting rotation origin.
#rotate(10deg, origin: top + left, #rotate(10deg, origin: top + left,
image("../../res/tiger.jpg", width: 50%) image("/res/tiger.jpg", width: 50%)
) )
--- ---

View File

@ -23,7 +23,7 @@
columns: 4 * (1fr,), columns: 4 * (1fr,),
row-gutter: 10pt, row-gutter: 10pt,
column-gutter: (0pt, 10%), column-gutter: (0pt, 10%),
align(top, image("../../res/rhino.png")), align(top, image("/res/rhino.png")),
align(top, rect(fill: eastern, align(right)[LoL])), align(top, rect(fill: eastern, align(right)[LoL])),
[rofl], [rofl],
[\ A] * 3, [\ A] * 3,

View File

@ -21,7 +21,7 @@ Hi #box(pad(left: 10pt)[A]) there
// Test that the pad node doesn't consume the whole region. // Test that the pad node doesn't consume the whole region.
#set page(height: 6cm) #set page(height: 6cm)
#align(left)[Before] #align(left)[Before]
#pad(10pt, image("../../res/tiger.jpg")) #pad(10pt, image("/res/tiger.jpg"))
#align(right)[After] #align(right)[After]
--- ---

View File

@ -7,7 +7,7 @@
dx: -10pt, dx: -10pt,
dy: -10pt, dy: -10pt,
image( image(
"../../res/tiger.jpg", "/res/tiger.jpg",
fit: "cover", fit: "cover",
width: 100% + 20pt, width: 100% + 20pt,
height: 100% + 20pt, height: 100% + 20pt,

View File

@ -5,7 +5,7 @@
#place(bottom + center)[© Typst] #place(bottom + center)[© Typst]
= Placement = Placement
#place(right, image("../../res/tiger.jpg", width: 1.8cm)) #place(right, image("/res/tiger.jpg", width: 1.8cm))
Hi there. This is \ Hi there. This is \
a placed node. \ a placed node. \
Unfortunately, \ Unfortunately, \

View File

@ -36,6 +36,18 @@ Hello *{x}*
[Not blue] [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 // Error: 11-25 set is only allowed directly in code and content blocks
{ let x = set text(blue) } { let x = set text(blue) }

View File

@ -56,3 +56,10 @@ Rust is memory-safe and blazingly fast. Let's rewrite everything in rust.
World World
- World - World
---
// Test absolute path in layout phase.
#show "GRAPH" as image("/res/graph.png")
The GRAPH has nodes.

View File

@ -43,7 +43,7 @@ Lריווח #h(1cm) R
--- ---
// Test inline object. // Test inline object.
#set text(lang: "he", "IBM Plex Serif") #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. // Test whether L1 whitespace resetting destroys stuff.

View File

@ -8,10 +8,10 @@ The first paragraph has no indent.
But the second one does. But the second one does.
#image("../../res/tiger.jpg", height: 6pt) #image("/res/tiger.jpg", height: 6pt)
starts a paragraph without indent. starts a paragraph without indent.
#align(center, image("../../res/rhino.png", width: 1cm)) #align(center, image("/res/rhino.png", width: 1cm))
= Headings = Headings
- And lists. - And lists.

View File

@ -31,5 +31,5 @@ My cool #move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink)))
// Link containing a block. // Link containing a block.
#link("https://example.com/", underline: false, block[ #link("https://example.com/", underline: false, block[
My cool rhino My cool rhino
#move(dx: 10pt, image("../../res/rhino.png", width: 1cm)) #move(dx: 10pt, image("/res/rhino.png", width: 1cm))
]) ])