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.
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.
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)
…
}
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