Why are paths always relative to the current file?

Context: I was trying to implement a small function that reads and parses a file with some regex. The function just takes the path to the file and returns the parsed context as dictionary. I wanted to move this function into a package.

/somewhere/else/lib.typ

#let parse-xyz(path) = {
    let data = read(path)
    …
}

main.typ

#let values = import-xyz("my/path.xyz");

After that I found out that the read call resolves the path relative to the file that contains the function (lib.typ) and not relative to the main.typ file that calls it. This is different from python and other scripting languages.

Hello, if you start your path with "/" instead, the path should resolve relative to --root. The documentation should be updated to reflect this soon!

#import "/templates/lib.typ" will load the file templates/lib.typ relative to the --root .

[1] syntax for marking project root · Issue #2608 · typst/typst · GitHub
[2] Clarify the path argument of the image element by kravchenkoloznia · Pull Request #4892 · typst/typst · GitHub

2 Likes

Alternatively, if you need to use the relative path which isn’t necessarily the same as root-relative path, then you would have to use read("my/path.xyz") as an argument to the import-xyz() function.

You can add #let r = read to make import-xyz(r("my/path.xyz")), which will shorten the workaround code but most certainly reduce readability.

I did face this problem in the past and had a few conversations about this (I think with @laurmaedje), but I don’t remember if this is a “limitation” or a “feature”.

The original reason why it is this way is that it’s made for markup and not code: When writing chapters of text, it is very natural to be able to use relative paths to include other chapters. Absolute paths with a leading slash provide an alternative, primarily useful for importing files shared across a whole project.

However, as you’ve noticed, neither of those two properly handle the case of relative paths passed to utility functions or of any kind of path passed into a package. To properly solve this, we need a path type that is separate from a plain string and that remembers where it was created. Ideally, this path would be supplemented by syntax since its much more natural for syntax to be file-dependant than for a path function.

For more discussion on this, see:

1 Like

There actually is a way to work around this (and to be clear: it is hacky and kinda works by accident. it will probably work until the path work mentioned by Laurenz is done, but no guarantees.)

Here it is:

// /somewhere/else/lib.typ
#let parse-xyz(..args) = {
    let data = read(..args)
    …
}

 

// main.typ
#let values = parse-xyz("my/path.xyz");

args is an arguments value, and such a value apparently remembers where it was constructed. So when read() gets all the arguments that parse-xyz() receives, these arguments are still relative to main.typ. Whether the loss in readability due to the arcaneness of this behavior is worth it for you is for you to decide :wink:

1 Like

Nice hack, but to be honest I don’t think I like this :see_no_evil:
Why does this even work?!?

Arguments retain their source location for error messages and the same source location mechanism is used to resolve relative paths.